To upload a file in Laravel 13, use a form with enctype="multipart/form-data", validate it with rules such as file, mimes and max, store it with $request->file('document')->store('documents') and save the returned path in your database. This tutorial builds a complete document upload with a private disk and an authorized download.
Targets: Laravel 13 (PHP 8.3 to 8.5) and the default local filesystem disk. For images and multiple files, see the related tutorials on image upload and multiple file upload.
Step 1: Create the model and migration
php artisan make:model Document -m Schema::create('documents', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->string('original_name'); $table->string('path'); $table->unsignedBigInteger('size'); $table->timestamps(); }); // app/Models/Document.php protected $fillable = ['user_id', 'original_name', 'path', 'size']; Run php artisan migrate. Storing the original name lets you show a friendly file name later, while the stored file gets a random name.
Step 2: Build the upload form
<form method="POST" action="{{ route('documents.store') }}" enctype="multipart/form-data"> @csrf <input type="file" name="document"> @error('document') <p>{{ $message }}</p> @enderror <button type="submit">Upload</button> </form> Without enctype="multipart/form-data" the browser sends only the file name and Laravel receives no file.
Step 3: Validate and store the file
<?php namespace App\Http\Controllers; use App\Models\Document; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; class DocumentController extends Controller { public function store(Request $request): RedirectResponse { $request->validate([ 'document' => ['required', 'file', 'mimes:pdf,doc,docx,xls,xlsx,txt', 'max:5120'], ]); $file = $request->file('document'); $path = $file->store('documents'); $request->user()->documents()->create([ 'original_name' => $file->getClientOriginalName(), 'path' => $path, 'size' => $file->getSize(), ]); return back()->with('success', 'File uploaded.'); } } Add public function documents(): HasMany { return $this->hasMany(Document::class); } to the User model so the relationship call works.
store('documents') saves the file on the default disk (the local disk, which keeps files private under storage/app/private) in a documents folder with a generated unique name and returns the relative path. The size rule max:5120 is in kilobytes, so 5 MB.
Register the route: Route::post('/documents', [DocumentController::class, 'store'])->middleware('auth')->name('documents.store');
Which validation rules should I use?
-
filechecks that a successful upload exists. -
mimeschecks the file type from its contents, not only the extension. -
maxlimits the size in kilobytes.minsets a lower bound. -
extensionsrestricts the file extension list when you also need that check. -
image,dimensionsand similar rules apply to pictures.
Always validate on the server. The accept attribute on the input is only a convenience for the user.
Step 4: Download the file securely
Files on the private disk have no public URL, so create a route that checks permissions and streams the file:
use App\Models\Document; use Illuminate\Support\Facades\Storage; Route::get('/documents/{document}/download', function (Document $document) { abort_unless(auth()->id() === $document->user_id, 403); return Storage::download($document->path, $document->original_name); })->middleware('auth')->name('documents.download'); The second argument of Storage::download() sets the file name the browser suggests. The 403 check stops users from opening other people's files by guessing IDs.
Public or private disk?
Use the private default disk for documents that need access control. Use the public disk (store('files', 'public') plus php artisan storage:link) only for files that anyone may open, such as product images. Never put sensitive documents on the public disk.
Why does my upload fail with no error?
PHP has its own limits that apply before Laravel runs. Check these php.ini values:
upload_max_filesize = 20M post_max_size = 25M max_file_uploads = 20 If post_max_size is smaller than the request, the whole request is empty and validation says the field is required. Nginx also has client_max_body_size, which must be larger than your biggest file.
How do I check that it works?
- Upload a small PDF and confirm a file appears in
storage/app/private/documents. - Check the
documentstable row: path, original name and size. - Open the download route and confirm the file downloads with its original name.
- Log in as another user and confirm the same URL returns 403.
Common mistakes
- Missing enctype: no file reaches the server.
- Trusting the extension: use
mimes, and never store files in a place where the web server may execute them. - Using the client file name for storage: store with a generated name and keep the original only as text.
- Ignoring PHP and Nginx limits: they block large uploads before validation.
- Public disk for private files: anyone with the URL can download them.
FAQ
How do I delete the file when the record is deleted?
Call Storage::delete($document->path) before or after deleting the record, ideally in the controller or a model event.
How do I store files on S3?
Set up the S3 disk in .env and use store('documents', 's3').
Can I rename the stored file?
Yes. Use storeAs('documents', $name) with a name that you generate, for example with Str::uuid().
References
- Laravel 13 file uploads
- Laravel 13 validation: mimes rule
- Laravel 13: downloading files
- PHP: upload_max_filesize
Originally published on yasiramin.dev.
