$ lexprog.com

// notes from an old coder -- php, databases, and the occasional rant

[April 07, 2025] Laravel

Laravel Filesystem and Storage

Laravel Filesystem: Tips & Tricks

────────────────────────────────────────────────────────

Laravel Filesystem: Tips & Tricks

Tip: Use storage:link for Public Files

php artisan storage:link

Creates a symlink from public/storage to storage/app/public.

Gotcha: Local Driver is NOT for Production

The local driver stores files on the same server. If you scale horizontally, files won't be shared. Use S3 or similar.

Tip: Stream Large Files

return response()->stream(function () {
    $stream = Storage::readStream('large-file.pdf');
    fpassthru($stream);
}, 200, [
    'Content-Type' => 'application/pdf',
]);

Don't load the entire file into memory.

Tip: Temporary URLs for Private Files

$url = Storage::disk('s3')->temporaryUrl(
    'private-file.pdf',
    now()->addMinutes(5)
);

Generates a time-limited signed URL.

Gotcha: File Visibility

When uploading to S3, set visibility:

Storage::disk('s3')->put('file.txt', $content, 'public');

Default is private.

Tip: Use assertExists() in Tests

Storage::fake('avatars');
$response = $this->post('/avatar', ['file' => UploadedFile::fake()->image('avatar.jpg')]);
Storage::disk('avatars')->assertExists('avatars/avatar.jpg');

Tip: Multiple Disks

$localPath = Storage::disk('local')->put('file.txt', 'content');
$s3Path = Storage::disk('s3')->put('file.txt', 'content');

Switch disks easily with the disk() method.

Gotcha: Missing Files Don't Throw Errors

Storage::get('nonexistent.txt') returns null, not an exception. Check with exists() first.

Tip: Use route:cache Carefully

php artisan route:cache is fast, but it doesn't work with closure-based routes. Every time you cache routes, Laravel serializes them. If you have Route::redirect() or closure callbacks, the cache breaks. Stick to controller-based routes in production.

Tip: Model APP_KEY Rotation

Rotating APP_KEY invalidates all encrypted data — cookies, encrypted DB columns, and password reset tokens. If you must rotate (e.g., after a leak), plan a migration that re-encrypts existing data with the new key.

Gotcha: Local Scope Leaks

Global scopes defined in booted() apply to ALL queries on that model — including relationships. An innocent User::all() in admin panel might exclude soft-deleted users if a global scope is active.

Senior Insight

File storage seems straightforward until you hit the 2MB upload limit in production and nobody knows why. In my experience, the three things most teams get wrong are: (1) not configuring temporary URLs for private disks, (2) forgetting to set visibility explicitly when using S3, and (3) not cleaning up orphaned files when models are deleted. I always add a Model::deleted event that cleans up associated files — future-proofing from day one.

Source: Laravel News (https://laravel-news.com/), Freek.dev (https://freek.dev/tags/laravel), Spatie Blog (https://spatie.be/blog)

────────────────────────────────────────────────────────
<-- back to posts