File Storage

Nearly every real application eventually needs to handle files — user avatars, uploaded documents, generated PDFs or exports. Laravel's Storage facade gives you one unified API for reading, writing, and deleting files, regardless of where those files actually physically live — the local disk during development, or a cloud service like Amazon S3 in production — without rewriting any of the code that uses it.

Disks: Configured Storage Locations

A disk is a named storage location, configured in config/filesystems.php, and every Storage call is made against a specific disk by name:

DiskWhere files live
localThe storage/app folder — not directly reachable from a browser
publicThe storage/app/public folder, made reachable from a browser through a symlink
s3Amazon S3, or any S3-compatible cloud storage service — swap-in-compatible with the exact same Storage API

Storing an Uploaded File

When a form submits a file, Laravel's request object exposes it through file(), and the uploaded file itself has a convenient store() method that saves it to a given disk under a given directory, generating a unique filename automatically:

<?php
$path = $request->file('avatar')->store('avatars', 'public');
// e.g. "avatars/6mF3kLp2qZ.jpg"

store() returns the path the file was saved under, relative to the disk's root — worth saving into a database column so the application knows where to find this particular file again later, without needing to guess or reconstruct the generated filename.

Retrieving a Public URL

A stored path on its own isn't a usable web address. For any file on the public disk, Storage::url() turns a stored path into a real, browser-loadable URL:

<?php
$url = Storage::url($path);
// e.g. "/storage/avatars/6mF3kLp2qZ.jpg"

Reading, Writing, and Deleting Programmatically

Beyond handling uploads, the Storage facade offers a small, consistent set of methods for working with files directly — the same four methods regardless of which disk you're targeting:

<?php
Storage::disk('public')->put('notes/hello.txt', 'Hello, world!');

$contents = Storage::disk('public')->get('notes/hello.txt');

if (Storage::disk('public')->exists('notes/hello.txt')) {
    Storage::disk('public')->delete('notes/hello.txt');
}

put() writes raw content to a path, get() reads a file's contents back as a string, exists() checks whether a given path is actually present before acting on it, and delete() removes it. When no disk() call is made, Laravel simply uses whichever disk is configured as the application's default.

storage:link: Why the Symlink Matters

Files saved to the public disk physically live in storage/app/public — a folder that sits outside the project's actual web root (public/), and is therefore not reachable by a browser on its own, no matter how correct the saved path is. Laravel bridges this gap with a single command, normally run once when a project is first set up:

php artisan storage:link

This creates a symbolic link at public/storage pointing back to storage/app/public, so anything saved on the public disk becomes reachable through a URL. Skipping this step is a very common source of confusion for developers new to Laravel — uploads appear to succeed, the path gets saved to the database correctly, and yet every image renders as a broken link, purely because the symlink was never created.

Note: storage:link only needs to run once per environment (once on your local machine, once again on a production server) — not on every deploy, and not once per file uploaded.

Full Worked Example: Handling an Avatar Upload

Putting every piece together — validating that the upload really is an image, storing it on the public disk, and saving the resulting path onto the currently authenticated user's model:

<?php
// app/Http/Controllers/ProfileController.php
public function updateAvatar(Request $request)
{
    $request->validate([
        'avatar' => 'required|image|max:2048',
    ]);

    $path = $request->file('avatar')->store('avatars', 'public');

    $request->user()->update([
        'avatar_path' => $path,
    ]);

    return back()->with('status', 'Avatar updated!');
}

The image validation rule rejects anything that isn't actually an image file before store() ever runs, and max:2048 caps the upload at 2 MB. Later, rendering the saved avatar anywhere in the application just needs Storage::url($user->avatar_path) to turn the stored path back into a real, browser-loadable image URL.

Next up: With uploads handled, the next lesson covers reaching out to users directly — sending them emails and other alerts — with Mail & Notifications.