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:
| Disk | Where files live |
|---|---|
local | The storage/app folder — not directly reachable from a browser |
public | The storage/app/public folder, made reachable from a browser through a symlink |
s3 | Amazon 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.
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.