File upload is one of the common needs in web applications. Laravel provides an easy and secure way to handle file uploads, including uploading multiple files at once. In this tutorial, you'll learn how to build a multiple file upload feature in Laravel from scratch, complete with validation and storage.
Storage Preparation
Laravel uses a filesystem to manage files. First, make sure you've created the symbolic link so uploaded files can be accessed publicly:
php artisan storage:link
This command creates a link from public/storage to storage/app/public. Files stored in storage/app/public/ will be accessible via URL.
Creating the Migration and Model
Create a table to store the uploaded file data:
php artisan make:model UploadedFile -m
Edit the migration file:
public function up(): void
{
Schema::create('uploaded_files', function (Blueprint $table) {
$table->id();
$table->string('original_name');
$table->string('stored_name');
$table->string('path');
$table->string('mime_type');
$table->unsignedBigInteger('size');
$table->timestamps();
});
}
Edit the model app/Models/UploadedFile.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class UploadedFile extends Model
{
protected $fillable = [
'original_name',
'stored_name',
'path',
'mime_type',
'size',
];
}
Run the migration:
php artisan migrate
Creating the HTML Form
Create the upload form view. Note the multiple and enctype attributes:
<!-- resources/views/upload/index.blade.php -->
<form action="{{ route('upload.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<div>
<label for="files">Choose files (more than one allowed):</label>
<input type="file" name="files[]" id="files" multiple accept="image/*,.pdf,.doc,.docx">
@error('files.*')
<p style="color:red">{{ $message }}</p>
@enderror
</div>
<button type="submit">Upload</button>
</form>
The important key: the input name must be files[] (with square brackets) so PHP recognizes it as an array.
Creating the Controller
php artisan make:controller UploadController
Edit app/Http/Controllers/UploadController.php:
<?php
namespace App\Http\Controllers;
use App\Models\UploadedFile;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class UploadController extends Controller
{
public function index()
{
$files = UploadedFile::latest()->get();
return view('upload.index', compact('files'));
}
public function store(Request $request)
{
$request->validate([
'files' => 'required|array|min:1|max:10',
'files.*' => 'file|mimes:jpeg,png,jpg,gif,pdf,doc,docx|max:2048',
]);
$uploadedFiles = [];
foreach ($request->file('files') as $file) {
// Store in storage/app/public/uploads
$path = $file->store('uploads', 'public');
$uploadedFiles[] = UploadedFile::create([
'original_name' => $file->getClientOriginalName(),
'stored_name' => basename($path),
'path' => $path,
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
]);
}
return redirect()->route('upload.index')
->with('success', count($uploadedFiles) . ' files uploaded successfully.');
}
public function destroy(UploadedFile $uploadedFile)
{
// Delete the file from storage
Storage::disk('public')->delete($uploadedFile->path);
$uploadedFile->delete();
return redirect()->route('upload.index')
->with('success', 'File deleted successfully.');
}
}
Defining the Routes
// routes/web.php
use App\Http\Controllers\UploadController;
Route::get('/upload', [UploadController::class, 'index'])->name('upload.index');
Route::post('/upload', [UploadController::class, 'store'])->name('upload.store');
Route::delete('/upload/{uploadedFile}', [UploadController::class, 'destroy'])->name('upload.destroy');
Displaying the Uploaded Files
Add this section in the view to display the file list:
@if(session('success'))
<p style="color:green">{{ session('success') }}</p>
@endif
<ul>
@foreach($files as $file)
<li>
@if(str_starts_with($file->mime_type, 'image/'))
<img src="{{ Storage::url($file->path) }}" width="100">
@endif
{{ $file->original_name }}
({{ number_format($file->size / 1024, 2) }} KB)
<form action="{{ route('upload.destroy', $file) }}" method="POST" style="display:inline">
@csrf @method('DELETE')
<button type="submit">Delete</button>
</form>
</li>
@endforeach
</ul>
File Upload Security Tips
- Always validate the file type with
mimesand limit the size withmax. - Use the random file name Laravel generates, not the original name from the user.
- Don't store executable files in a publicly accessible folder.
- Set the
upload_max_filesizeandpost_max_sizelimits inphp.inias needed.
Conclusion
Building a multiple file upload feature in Laravel is quite easy thanks to built-in helpers like $request->file(), store(), and the Storage system. With proper validation and secure storage, your upload feature is ready for production. You can develop it further by adding an upload progress bar with JavaScript or integrating with a cloud service like AWS S3.