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>