Reader Stacks

Uploading and Storing Images in Laravel 10 and 11

The image upload API itself hasn't meaningfully changed across recent Laravel versions — what genuinely differs in Laravel 10 and 11 is scaffolding and folder structure, not the store() call itself.

Uploading and Storing Images in Laravel 10 and 11

The core image upload API in Laravel — validating, storing, and referencing the file — has stayed essentially unchanged across Laravel 10 and 11; what's actually worth knowing is the storage setup and a couple of version-specific project structure details.

Creating the storage symlink (a required one-time step)

php artisan storage:link

This creates a symbolic link from public/storage to storage/app/public — without it, files stored via the public disk are saved correctly but aren't actually accessible through a public URL at all, a common point of confusion for a fresh Laravel install where this step is easy to forget.

The upload form and validation

public function store(Request $request)
{
    $request->validate([
        'image' => 'required|image|mimes:jpeg,png,jpg,webp|max:2048',
    ]);

    $path = $request->file('image')->store('products', 'public');

    Product::create([
        'name' => $request->name,
        'image' => $path,
    ]);

    return redirect()->route('products.index')->with('success', 'Product created.');
}

Generating a custom filename instead of a random hash

$image = $request->file('image');
$filename = time().'_'.$image->getClientOriginalName();
$path = $image->storeAs('products', $filename, 'public');

storeAs(), rather than plain store(), allows specifying the exact filename — prefixing it with time() avoids overwriting an existing file with the same original name, since two different users uploading a file both named photo.jpg would otherwise collide.

Displaying the stored image

{{ $product->name }}

Laravel 11's slimmer default project structure

Laravel 11 significantly streamlined the default application skeleton (fewer default service provider files, a leaner bootstrap/app.php replacing much of the old Kernel.php configuration) — this affects where middleware and some configuration live by default, but the actual file storage and upload APIs (Storage, UploadedFile) are entirely unaffected by this structural change.

Using the Storage facade directly, as an alternative to the instance method

use Illuminate\Support\Facades\Storage;

$path = Storage::disk('public')->putFile('products', $request->file('image'));

Storage::putFile() is functionally equivalent to calling ->store() directly on the uploaded file instance — both ultimately call the same underlying filesystem logic, and the choice between them is mostly a matter of code style and consistency with the rest of a given codebase.

Deleting an image when its owning record is deleted

protected static function booted(): void
{
    static::deleting(function (Product $product) {
        if ($product->image) {
            Storage::disk('public')->delete($product->image);
        }
    });
}

Adding this to the model's deleting event ensures the stored image file is cleaned up automatically whenever a product is deleted through any code path — more reliable than remembering to delete the image manually in every controller method that might delete a product.