Laravel
File Uploads
Handle single and multiple file uploads using the Storage facade, validate file types and sizes, generate public URLs, and process images with Intervention Image.
Filesystem Disk Configuration: Configure storage disks in
config/filesystems.php. Use local for private files, public for web-accessible files, or s3 for AWS S3.
config/filesystems.php
PHP
<?php
return [
'default' => env('FILESYSTEM_DISK', 'local'),
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'throw' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL') . '/storage',
'visibility' => 'public',
'throw' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
],
// Custom disk example (avatars in public)
'avatars' => [
'driver' => 'local',
'root' => storage_path('app/public/avatars'),
'url' => env('APP_URL') . '/storage/avatars',
'visibility' => 'public',
],
],
];
// Create symlink so public disk is web-accessible:
// php artisan storage:link
File Validation Rules: Validate uploads before storing — check MIME type, size limits, image dimensions, and whether a file was actually provided.
app/Http/Requests/UploadFileRequest.php
PHP
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\File;
class UploadFileRequest extends FormRequest
{
public function authorize(): bool { return true; }
public function rules(): array
{
return [
// Classic image validation
'avatar' => [
'required',
'image', // must be an image
'mimes:jpg,jpeg,png,webp,gif', // allowed MIME types
'max:2048', // max 2 MB (kilobytes)
'dimensions:min_width=100,min_height=100,max_width=2000',
],
// Fluent File rule (Laravel 10+)
'document' => [
'required',
File::types(['pdf', 'doc', 'docx'])
->min(1) // min 1 KB
->max(10 * 1024), // max 10 MB
],
// Image with fluent rule
'thumbnail' => [
'nullable',
File::image()
->min('1kb')
->max('5mb')
->dimensions(
\Illuminate\Validation\Rules\Dimensions::defaults()
->minWidth(200)
->maxWidth(1920)
->ratio(16 / 9)
),
],
// Multiple files
'attachments' => ['required', 'array', 'max:5'],
'attachments.*' => ['file', 'mimes:jpg,png,pdf', 'max:5120'],
];
}
}
Storing Files with the Storage Facade: Use
Storage::putFile() for auto-naming or storeAs() for custom names. Always store the returned path in the database.
app/Http/Controllers/ProfileController.php
PHP
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class ProfileController extends Controller
{
public function updateAvatar(Request $request)
{
$request->validate([
'avatar' => ['required', 'image', 'max:2048'],
]);
$user = $request->user();
// 1. Delete old avatar if exists
if ($user->avatar && Storage::disk('public')->exists($user->avatar)) {
Storage::disk('public')->delete($user->avatar);
}
// 2a. Store with auto-generated unique filename
$path = $request->file('avatar')->store('avatars', 'public');
// Result: 'avatars/AbCdEfGhIj1234.jpg'
// 2b. Store with custom filename
$extension = $request->file('avatar')->getClientOriginalExtension();
$filename = 'avatar_' . $user->id . '_' . time() . '.' . $extension;
$path = $request->file('avatar')->storeAs('avatars', $filename, 'public');
// 2c. Using Storage facade directly
$path = Storage::disk('public')->putFileAs(
'avatars',
$request->file('avatar'),
$filename
);
// 3. Save path to database
$user->update(['avatar' => $path]);
return back()->with('success', 'Avatar updated.');
}
}
Getting the File URL: Retrieve a public URL with
Storage::url() or a temporary signed URL for private files with Storage::temporaryUrl().
file-url-examples.php
PHP
use Illuminate\Support\Facades\Storage;
// Public disk URL (requires storage:link symlink)
$url = Storage::disk('public')->url('avatars/photo.jpg');
// → https://yourdomain.com/storage/avatars/photo.jpg
// Helper using asset() – same result
$url = asset('storage/' . $user->avatar);
// Temporary signed URL for private S3 files (expires in 5 minutes)
$url = Storage::disk('s3')->temporaryUrl(
'private/report.pdf',
now()->addMinutes(5)
);
// Check if file exists
if (Storage::disk('public')->exists($user->avatar)) {
// file is present
}
// Get file size (bytes) and last modified timestamp
$size = Storage::disk('public')->size('avatars/photo.jpg');
$modified = Storage::disk('public')->lastModified('avatars/photo.jpg');
// List files in a directory
$files = Storage::disk('public')->files('avatars');
$dirs = Storage::disk('public')->directories('uploads');
// In Blade template:
// <img src="{{ Storage::disk('public')->url($user->avatar) }}" alt="Avatar">
// or:
// <img src="{{ asset('storage/' . $user->avatar) }}" alt="Avatar">
Deleting Files: Always delete the physical file from storage when the record is removed. Use model events or call
Storage::delete() directly in the controller.
app/Models/Post.php (with boot events)
PHP
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class Post extends Model
{
protected $fillable = ['title', 'body', 'image'];
// Auto-delete file when model is deleted
protected static function boot(): void
{
parent::boot();
static::deleting(function (Post $post) {
if ($post->image) {
Storage::disk('public')->delete($post->image);
}
});
}
}
// Manually in controller:
public function destroy(Post $post): RedirectResponse
{
// Delete associated image first
if ($post->image && Storage::disk('public')->exists($post->image)) {
Storage::disk('public')->delete($post->image);
}
// Delete record
$post->delete();
// Delete multiple files at once
Storage::disk('public')->delete([
'avatars/old_photo.jpg',
'thumbnails/old_thumb.jpg',
]);
return redirect()->route('posts.index')
->with('success', 'Post and associated files deleted.');
}
Image Resizing with Intervention Image: Install the package and use it to resize, crop, watermark, and convert images before saving to disk.
terminal + app/Services/ImageService.php
PHP
# Install Intervention Image v3 (for Laravel 10+)
# composer require intervention/image
<?php
namespace App\Services;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Intervention\Image\Laravel\Facades\Image;
class ImageService
{
/**
* Store an image with optional resize, thumbnail generation.
* Returns array of stored paths.
*/
public function store(UploadedFile $file, string $directory = 'uploads'): array
{
$name = Str::uuid() . '.' . $file->getClientOriginalExtension();
// Read the uploaded file
$image = Image::read($file->getPathname());
// Resize to max 1200px wide, preserving aspect ratio
$image->scaleDown(width: 1200);
// Save full-size image
$fullPath = $directory . '/' . $name;
Storage::disk('public')->put($fullPath, $image->toJpeg(85));
// Generate thumbnail (300x300 crop)
$thumb = Image::read($file->getPathname())
->cover(300, 300); // centered crop
$thumbPath = $directory . '/thumbs/' . $name;
Storage::disk('public')->put($thumbPath, $thumb->toJpeg(75));
// Convert and save as WebP for better compression
$webpPath = $directory . '/webp/' . Str::uuid() . '.webp';
$webp = Image::read($file->getPathname())
->scaleDown(width: 1200);
Storage::disk('public')->put($webpPath, $webp->toWebp(80));
return [
'original' => $fullPath,
'thumbnail' => $thumbPath,
'webp' => $webpPath,
];
}
}
// Usage in controller:
// $paths = app(ImageService::class)->store($request->file('image'), 'posts');
// $post->update(['image' => $paths['original'], 'thumb' => $paths['thumbnail']]);
Multiple File Upload: Accept an array of files from a Blade form, iterate over each, store individually, and record all paths in a related table or JSON column.
app/Http/Controllers/AttachmentController.php
PHP
<?php
namespace App\Http\Controllers;
use App\Models\Attachment;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class AttachmentController extends Controller
{
public function store(Request $request, Post $post): RedirectResponse
{
$request->validate([
'attachments' => ['required', 'array', 'max:10'],
'attachments.*' => ['file', 'mimes:jpg,jpeg,png,pdf,zip', 'max:10240'],
]);
$stored = [];
foreach ($request->file('attachments') as $file) {
$originalName = $file->getClientOriginalName();
$mimeType = $file->getMimeType();
$size = $file->getSize();
// Store each file
$path = $file->store('attachments/' . $post->id, 'public');
// Record in database
$stored[] = $post->attachments()->create([
'original_name' => $originalName,
'path' => $path,
'mime_type' => $mimeType,
'size' => $size,
]);
}
return back()->with('success', count($stored) . ' file(s) uploaded.');
}
}
// Blade form for multiple file upload:
// <form action="{{ route('posts.attachments.store', $post) }}" method="POST"
// enctype="multipart/form-data">
// @csrf
// <input type="file" name="attachments[]" multiple accept=".jpg,.png,.pdf,.zip">
// <button type="submit">Upload Files</button>
// </form>