File uploads are central to dashboards, marketplaces, and document workflows. Laravel abstracts storage behind the Storage facade so you can swap local disks for S3 without rewriting business logic.
Filesystem Configuration
Disks are defined in config/filesystems.php. The default disk comes from FILESYSTEM_DISK in .env, usually local or public.
Each disk specifies a driver (local, s3, ftp) and root path or cloud credentials.
Step 1: Understand default and public disks
The local disk stores private files under storage/app. The public disk uses storage/app/public and is web-accessible after php artisan storage:link.
Never expose storage/app directly through the web server. Use controllers or signed URLs for authorized downloads.
FILESYSTEM_DISK=local
Step 2: Create a symbolic link for public files
Run php artisan storage:link to create public/storage pointing to storage/app/public.
Asset URLs use Storage::url($path) which resolves to /storage/filename when the public disk is configured correctly.
php artisan storage:link
Step 3: Configure an S3 disk
Set AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, and AWS_BUCKET in .env.
Install league/flysystem-aws-s3-v3 via composer if not bundled. Set FILESYSTEM_DISK=s3 in production for scalable object storage.
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
AWS_BUCKET=my-app-uploads
FILESYSTEM_DISK=s3
Step 4: Switch disks in code
Use Storage::disk('s3')->put($path, $contents) to target a specific disk. The default disk respects Storage::put().
Abstract disk choice behind a service so tests can use Storage::fake('s3') without touching real buckets.
Storage::disk('s3')->put('avatars/'.$filename, $fileContents);
Handling HTTP Uploads
Uploaded files arrive as IlluminateHttpUploadedFile instances via $request->file(). Validate before storing to block malicious types.
Laravel validation rules like image, mimes, and max protect your application from oversized or dangerous uploads.
Step 1: Build an upload form and route
Create a form with enctype="multipart/form-data" and a file input named avatar. POST to a controller method that type-hints Request.
For APIs, accept multipart requests from mobile clients using the same $request->file() API.
<input type="file" name="avatar" accept="image/*">
Step 2: Validate file type and size
Add rules: 'avatar' => 'required|image|mimes:jpeg,png,webp|max:2048' for a 2 MB image cap.
Return validation errors to the user before touching storage. Log rejected uploads for security monitoring.
$request->validate([
'avatar' => 'required|image|mimes:jpeg,png,webp|max:2048',
]);
Step 3: Store with a safe filename
Use $path = $request->file('avatar')->store('avatars', 'public') to generate a unique hashed name.
Alternatively storeAs('avatars', $filename) when you control naming. Never use the client-provided filename directly.
$path = $request->file('avatar')->store('avatars', 'public');
Step 4: Persist the path on the model
Save the returned path string on the user record: $user->update(['avatar' => $path]).
Display with Storage::url($user->avatar) in Blade. Delete old files with Storage::delete($oldPath) when replacing avatars.
$user->update(['avatar' => $path]);
Security and Advanced Patterns
Production uploads require virus scanning, access control, and cleanup of orphaned files.
Use temporary URLs for private S3 objects and scheduled jobs to prune unused assets.
Step 1: Generate temporary signed URLs
For private S3 objects, call Storage::temporaryUrl($path, now()->addMinutes(30)) to grant time-limited access.
Local disks do not support temporary URLs. Use controller download endpoints with authorization for private local files.
$url = Storage::disk('s3')->temporaryUrl($path, now()->addMinutes(30));
Step 2: Test uploads with Storage::fake
Storage::fake('public') in tests lets you upload without writing real files. Assert Storage::disk('public')->exists($path).
Combine with UploadedFile::fake()->image('avatar.jpg') for realistic validation tests.
Storage::fake('public');
$file = UploadedFile::fake()->image('photo.jpg');
Step 3: Stream large downloads
Use Storage::download($path) or response()->streamDownload() for CSV exports without loading entire files into memory.
Set appropriate Content-Type and Content-Disposition headers so browsers handle files correctly.
return Storage::download($path, $filename);
Step 4: Schedule cleanup of orphaned files
Create an Artisan command that compares storage paths to database references and deletes unlinked files.
Register the command in app/Console/Kernel.php schedule()->daily() to reclaim disk space automatically.
php artisan make:command PruneOrphanedUploads
Aadyanex builds production-ready Laravel applications with clean architecture, secure APIs, and long-term support.
