Storage
Com gestionar fitxers a Laravel: sistema de fitxers, discs locals i al núvol, pujada, descàrrega i streaming.
El sistema de fitxers de Laravel#
Gestionar fitxers és una necessitat fonamental en gairebé qualsevol aplicació web: avatars d'usuari, documents PDF, imatges de productes, exportacions CSV, backups de bases de dades. La complexitat apareix quan l'aplicació creix i necessites moure fitxers d'un disc local a Amazon S3, o quan vols suportar múltiples proveïdors de storage simultàniament. Sense una abstracció adequada, el codi es plena de condicionals per cada proveïdor i canviar d'un a l'altre implica reescriure tot el codi de gestió de fitxers.
Laravel resol aquest problema amb una capa d'abstracció construïda sobre Flysystem, una biblioteca PHP de Frank de Jonge que proporciona una API unificada per interactuar amb qualsevol sistema de fitxers. Gràcies a Flysystem, pots escriure codi que funciona exactament igual tant si els fitxers es guarden al disc local del servidor com si es pugen a Amazon S3, Google Cloud Storage, o qualsevol altre proveïdor. La facade Storage és el punt d'entrada principal, i tots els mètodes funcionen de la mateixa manera independentment del disc subjacent.
Aquesta abstracció segueix el patró de driver que Laravel utilitza en molts altres components (cache, sessions, cues, correu). Defineixes discs al fitxer de configuració, cadascun amb el seu driver, i el codi de l'aplicació simplement referencia el nom del disc. Si un dia decideixes migrar d'emmagatzematge local a S3, només cal canviar la configuració: el codi de l'aplicació no es toca.
Configuració#
Tota la configuració dels discs es troba al fitxer config/filesystems.php. Aquest fitxer defineix el disc per defecte (default), els discs disponibles (disks) i els enllaços simbòlics (links). Laravel ve amb tres discs preconfigurats: local, public i s3.
El disc local guarda fitxers al directori storage/app/private i és el disc per defecte. Els fitxers guardats aquí no són accessibles des del navegador, cosa que el fa ideal per a documents privats, backups i fitxers temporals. El disc public guarda fitxers a storage/app/public i està pensat per a fitxers que han de ser accessibles des del web, com avatars i imatges de productes. El disc s3 connecta amb Amazon S3 o qualsevol servei compatible amb l'API S3 (com MinIO o DigitalOcean Spaces).
// config/filesystems.php
return [
'default' => env('FILESYSTEM_DISK', 'local'),
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'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,
],
],
'links' => [
public_path('storage') => storage_path('app/public'),
],
];Les credencials d'S3 es configuren amb variables d'entorn al fitxer .env. Això permet tenir configuracions diferents per a cada entorn: un bucket de desenvolupament, un de staging i un de producció:
AWS_ACCESS_KEY_ID=la-teva-clau
AWS_SECRET_ACCESS_KEY=el-teu-secret
AWS_DEFAULT_REGION=eu-west-1
AWS_BUCKET=la-teva-app-production
AWS_URL=
AWS_ENDPOINT=
AWS_USE_PATH_STYLE_ENDPOINT=falsePer canviar el disc per defecte, simplement modifica la variable FILESYSTEM_DISK al fitxer .env. Per exemple, per fer servir S3 com a disc per defecte en producció:
FILESYSTEM_DISK=s3Operacions bàsiques#
La facade Storage proporciona mètodes per a totes les operacions habituals amb fitxers. Per defecte, els mètodes operen sobre el disc configurat com a default, però pots especificar un disc diferent amb el mètode disk().
Escriure fitxers#
El mètode put és la manera més directa de guardar contingut en un fitxer. Accepta el camí del fitxer i el contingut com a cadena o com a resource PHP. Si el directori no existeix, Laravel el crea automàticament. Si el fitxer ja existeix, el sobreescriu:
use Illuminate\Support\Facades\Storage;
// Guardar text
Storage::put('documents/informe.txt', 'Contingut de l\'informe');
// Guardar en un disc específic
Storage::disk('s3')->put('backups/db.sql', $sqlDump);
// Guardar amb un resource PHP (eficient per a fitxers grans)
$resource = fopen('/tmp/fitxer-gran.zip', 'r');
Storage::put('arxius/fitxer-gran.zip', $resource);
fclose($resource);Per afegir contingut al final d'un fitxer existent, utilitza append. Per escriure al principi, utilitza prepend. Aquests mètodes són útils per a fitxers de log o fitxers que creixen incrementalment:
// Afegir al final
Storage::append('logs/activitat.log', '[2024-01-15] Usuari ha iniciat sessió');
// Escriure al principi
Storage::prepend('logs/activitat.log', '[CAPÇALERA] Log d\'activitat');Llegir fitxers#
El mètode get retorna el contingut del fitxer com a cadena. Si necessites comprovar si un fitxer existeix abans de llegir-lo, utilitza exists o missing. Aquests mètodes són molt importants per evitar errors quan treballes amb fitxers que poden haver estat eliminats o que encara no s'han creat:
// Llegir contingut
$contingut = Storage::get('documents/informe.txt');
// Comprovar existència
if (Storage::exists('documents/informe.txt')) {
$contingut = Storage::get('documents/informe.txt');
}
// Comprovar absència
if (Storage::missing('documents/informe.txt')) {
// El fitxer no existeix
}Informació de fitxers#
Laravel proporciona mètodes per obtenir metadades dels fitxers: la mida en bytes, la data de l'última modificació i el tipus MIME. Aquesta informació és útil per mostrar detalls als usuaris, validar fitxers o implementar lògica de cache:
// Mida en bytes
$mida = Storage::size('documents/informe.pdf');
// Darrera modificació (timestamp Unix)
$timestamp = Storage::lastModified('documents/informe.pdf');
// Tipus MIME
$mime = Storage::mimeType('documents/informe.pdf');Eliminar, copiar i moure#
Les operacions d'eliminació, còpia i moviment funcionen de manera consistent en tots els discs. El mètode delete accepta un sol camí o un array de camins per eliminar múltiples fitxers alhora:
// Eliminar un fitxer
Storage::delete('temporal/fitxer.tmp');
// Eliminar múltiples fitxers
Storage::delete([
'temporal/fitxer1.tmp',
'temporal/fitxer2.tmp',
'temporal/fitxer3.tmp',
]);
// Copiar un fitxer
Storage::copy('originals/foto.jpg', 'copies/foto-backup.jpg');
// Moure un fitxer (reanomenar)
Storage::move('uploads/temp.jpg', 'avatars/usuari-42.jpg');Directoris#
Treballar amb directoris és tan senzill com treballar amb fitxers. Laravel proporciona mètodes per crear i eliminar directoris, i per llistar els fitxers i subdirectoris que contenen. Aquests mètodes són especialment útils quan necessites processar tots els fitxers d'una carpeta, per exemple per generar un llistat de documents o per netejar fitxers temporals.
El mètode files retorna un array amb els fitxers del directori indicat (sense recursió). Si necessites incloure els fitxers de tots els subdirectoris, utilitza allFiles. De manera similar, directories retorna els subdirectoris immediats i allDirectories els retorna recursivament:
// Fitxers d'un directori (sense recursió)
$fitxers = Storage::files('documents');
// Tots els fitxers recursivament
$totsFitxers = Storage::allFiles('documents');
// Subdirectoris immediats
$dirs = Storage::directories('uploads');
// Tots els subdirectoris recursivament
$totsDirs = Storage::allDirectories('uploads');
// Crear un directori
Storage::makeDirectory('exports/2024');
// Eliminar un directori i tot el seu contingut
Storage::deleteDirectory('temporal');Visibilitat dels fitxers#
La visibilitat dels fitxers controla els permisos d'accés. Laravel defineix dos nivells de visibilitat: public i private. En un disc local, public correspon a permisos 0755 per a directoris i 0644 per a fitxers, mentre que private correspon a 0700 i 0600 respectivament. En discs al núvol com S3, la visibilitat determina si el fitxer és accessible públicament via URL o si requereix una URL temporal signada.
La visibilitat és un concepte important perquè determina qui pot accedir als fitxers. Un avatar d'usuari hauria de ser públic perquè es mostra a la pàgina de perfil i qualsevol visitant l'ha de poder veure. En canvi, un document fiscal privat hauria de tenir visibilitat privada i servir-se a través d'una ruta autenticada:
// Guardar amb visibilitat específica
Storage::put('fitxer.jpg', $contingut, 'public');
// Canviar la visibilitat
Storage::setVisibility('fitxer.jpg', 'public');
Storage::setVisibility('fitxer.jpg', 'private');
// Consultar la visibilitat
$visibilitat = Storage::getVisibility('fitxer.jpg');El disc públic i storage:link#
El disc public està pensat per a fitxers que han de ser accessibles directament des del navegador: imatges, documents PDF descarregables, fitxers CSS o JavaScript generats dinàmicament. El problema és que el directori storage no és accessible des del web per defecte (i no hauria de ser-ho, perquè conté fitxers sensibles com logs, sessions i claus de xifrat).
La solució de Laravel és crear un enllaç simbòlic (symlink) des de public/storage cap a storage/app/public. Això permet que els fitxers del disc públic siguin accessibles via URL sense exposar la resta del directori storage. L'Artisan command storage:link crea aquest enllaç:
php artisan storage:linkUn cop creat l'enllaç, els fitxers guardats al disc public són accessibles via URL. Per exemple, un fitxer guardat a storage/app/public/avatars/usuari-42.jpg serà accessible a https://la-teva-app.com/storage/avatars/usuari-42.jpg:
// Obtenir la URL d'un fitxer al disc públic
$url = Storage::disk('public')->url('avatars/usuari-42.jpg');
// Resultat: /storage/avatars/usuari-42.jpgA les plantilles Blade, pots construir la URL directament amb el helper asset:
<img src="{{ asset('storage/' . $user->avatar) }}" alt="Avatar de {{ $user->name }}">
{{-- O amb el mètode url del disc --}}
<img src="{{ Storage::disk('public')->url($user->avatar) }}" alt="Avatar">Pujada de fitxers#
La pujada de fitxers és una de les operacions més habituals en una aplicació web. Laravel simplifica el procés amb mètodes integrats a l'objecte UploadedFile que retorna el request. El mètode store guarda el fitxer amb un nom únic generat automàticament (un UUID), cosa que evita conflictes de noms. El mètode storeAs permet especificar el nom del fitxer manualment.
Abans de guardar qualsevol fitxer, és important validar-lo. Laravel proporciona regles de validació específiques per a fitxers: file comprova que sigui un fitxer vàlid, image comprova que sigui una imatge (JPEG, PNG, GIF, BMP, SVG, WebP), mimes valida l'extensió i max limita la mida en kilobytes. Validar correctament protegeix el servidor contra fitxers maliciosos i evita que s'ompli el disc amb fitxers massa grans:
use Illuminate\Http\Request;
public function store(Request $request)
{
$request->validate([
'avatar' => ['required', 'image', 'mimes:jpg,png,webp', 'max:2048'],
]);
// Guardar amb nom únic automàtic (UUID)
$path = $request->file('avatar')->store('avatars', 'public');
// Resultat: avatars/a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg
// Guardar amb nom específic
$path = $request->file('avatar')->storeAs(
'avatars',
'usuari-' . auth()->id() . '.jpg',
'public'
);
// Resultat: avatars/usuari-42.jpg
auth()->user()->update(['avatar' => $path]);
return back()->with('success', 'Avatar actualitzat correctament.');
}Pujar amb visibilitat pública#
Si necessites que el fitxer sigui públic immediatament (especialment important en discs S3 on la visibilitat per defecte és privada), utilitza storePublicly o storePubliclyAs:
// Guardar amb visibilitat pública
$path = $request->file('document')->storePublicly('documents', 's3');
// Guardar amb nom específic i visibilitat pública
$path = $request->file('document')->storePubliclyAs(
'documents',
'informe-2024.pdf',
's3'
);Pujada de múltiples fitxers#
Quan l'usuari puja diversos fitxers alhora, el camp del formulari ha de tenir l'atribut multiple i el nom ha d'acabar amb []. La validació utilitza .* per validar cada element de l'array individualment:
public function store(Request $request)
{
$request->validate([
'fotos' => ['required', 'array', 'max:10'],
'fotos.*' => ['image', 'mimes:jpg,png,webp', 'max:5120'],
]);
$paths = [];
foreach ($request->file('fotos') as $foto) {
$paths[] = $foto->store('galeria', 'public');
}
// Guardar els camins a la base de dades
foreach ($paths as $path) {
auth()->user()->photos()->create(['path' => $path]);
}
return back()->with('success', count($paths) . ' fotos pujades.');
}Descàrregues i streaming#
De vegades necessites servir fitxers als usuaris de maneres diferents: com a descàrrega directa, com a resposta inline (per exemple, un PDF que es mostra al navegador) o com a streaming per a fitxers grans. Laravel proporciona mètodes per a cada cas.
El mètode download força el navegador a descarregar el fitxer amb un diàleg de "Desar com". Pots especificar el nom del fitxer que veurà l'usuari i capçaleres HTTP addicionals. El mètode response retorna el fitxer inline, cosa que és útil per a imatges i PDFs que vols mostrar directament al navegador:
use Illuminate\Support\Facades\Storage;
// Forçar descàrrega
return Storage::download('documents/informe.pdf');
// Descàrrega amb nom personalitzat
return Storage::download(
'documents/abc123.pdf',
'informe-mensual-gener-2024.pdf'
);
// Descàrrega amb capçaleres addicionals
return Storage::download(
'documents/informe.pdf',
'informe.pdf',
['X-Custom-Header' => 'valor']
);
// Resposta inline (mostrar al navegador)
return Storage::response('imatges/foto.jpg');Servir fitxers privats#
Un patró molt comú és servir fitxers privats a través d'una ruta autenticada. El fitxer es guarda al disc local (que no és accessible des del web) i es serveix només si l'usuari té permís. Això és essencial per a documents confidencials com factures, contractes o informes mèdics:
// routes/web.php
Route::get('/documents/{document}/download', [DocumentController::class, 'download'])
->middleware('auth');
// app/Http/Controllers/DocumentController.php
public function download(Document $document)
{
// Comprovar que l'usuari pot accedir al document
$this->authorize('view', $document);
if (Storage::missing($document->path)) {
abort(404, 'El document no existeix.');
}
return Storage::download($document->path, $document->original_name);
}URLs temporals per a S3#
Quan treballes amb S3 i fitxers privats, les URLs temporals (pre-signed URLs) són la solució més elegant. Generen una URL que inclou una signatura criptogràfica i una data d'expiració. L'usuari pot accedir al fitxer directament des d'S3 sense passar pel servidor de l'aplicació, cosa que redueix la càrrega del servidor i millora el rendiment:
// URL temporal vàlida durant 5 minuts
$url = Storage::disk('s3')->temporaryUrl(
'documents/informe.pdf',
now()->addMinutes(5)
);
// URL temporal amb capçaleres de resposta personalitzades
$url = Storage::disk('s3')->temporaryUrl(
'documents/informe.pdf',
now()->addMinutes(5),
[
'ResponseContentType' => 'application/pdf',
'ResponseContentDisposition' => 'attachment; filename="informe.pdf"',
]
);Les URLs temporals són especialment útils per a fitxers grans: en lloc de descarregar el fitxer d'S3 al servidor i després servir-lo a l'usuari (cosa que consumeix memòria i ample de banda del servidor), l'usuari descarrega directament d'S3. El servidor només genera la URL, una operació que triga mil·lisegons.
URLs de fitxers#
Cada disc té un mètode url que retorna la URL pública del fitxer. Per al disc public, retorna una URL relativa basada en l'enllaç simbòlic. Per al disc s3, retorna la URL completa del fitxer al bucket. Per al disc local, el mètode url no funciona directament perquè els fitxers locals no són accessibles des del web:
// Disc public: /storage/avatars/usuari-42.jpg
$url = Storage::disk('public')->url('avatars/usuari-42.jpg');
// Disc s3: https://bucket.s3.amazonaws.com/avatars/usuari-42.jpg
$url = Storage::disk('s3')->url('avatars/usuari-42.jpg');Si necessites personalitzar la generació d'URLs, pots definir el host del disc a la configuració. Això és útil quan fas servir un CDN davant d'S3:
// config/filesystems.php
's3' => [
'driver' => 's3',
'url' => 'https://cdn.la-teva-app.com',
// ...
],Discs personalitzats#
Laravel inclou drivers per a local i s3 de sèrie. Si necessites connectar amb un sistema de fitxers diferent (SFTP, FTP, Dropbox, Google Cloud Storage), pots instal·lar l'adaptador Flysystem corresponent i registrar un driver personalitzat.
Per exemple, per afegir suport SFTP, instal·la el paquet league/flysystem-sftp-v3 i registra el driver al AppServiceProvider. Flysystem manté adaptadors oficials per a molts sistemes de fitxers, i la comunitat n'ha creat molts més. Un cop registrat el driver, pots configurar discs amb ell com si fos qualsevol altre driver:
composer require league/flysystem-sftp-v3// config/filesystems.php
'disks' => [
'sftp' => [
'driver' => 'sftp',
'host' => env('SFTP_HOST'),
'username' => env('SFTP_USERNAME'),
'password' => env('SFTP_PASSWORD'),
'port' => env('SFTP_PORT', 22),
'root' => env('SFTP_ROOT', '/'),
'timeout' => 30,
],
],Un cop configurat, el disc SFTP es fa servir exactament igual que qualsevol altre disc. Aquesta és la gràcia de l'abstracció: el codi de l'aplicació no sap ni li importa si el fitxer es guarda localment, a S3 o a un servidor SFTP remot:
Storage::disk('sftp')->put('backups/db-2024-01-15.sql', $sqlDump);
$fitxers = Storage::disk('sftp')->allFiles('backups');Discs amb àmbit (scoped disks)#
De vegades vols que un disc operi dins d'un subdirectori específic sense haver de prefixar tots els camins manualment. Els discs amb àmbit (scoped disks) et permeten definir un disc que treballa dins d'un prefix concret d'un altre disc. Totes les operacions es resolen relativament a aquest prefix.
Això és especialment útil quan tens un sol bucket S3 però vols que cada mòdul de l'aplicació treballi dins del seu propi "directori". Per exemple, un disc per a avatars, un per a documents i un per a exportacions, tots dins del mateix bucket però aïllats entre ells:
// config/filesystems.php
'disks' => [
's3' => [
'driver' => 's3',
// ...configuració S3
],
's3-avatars' => [
'driver' => 'scoped',
'disk' => 's3',
'prefix' => 'avatars',
],
's3-documents' => [
'driver' => 'scoped',
'disk' => 's3',
'prefix' => 'documents',
],
],// Guarda a s3://bucket/avatars/usuari-42.jpg
Storage::disk('s3-avatars')->put('usuari-42.jpg', $contingut);
// Guarda a s3://bucket/documents/informe.pdf
Storage::disk('s3-documents')->put('informe.pdf', $contingut);
// Llistar fitxers dins d'avatars
$fitxers = Storage::disk('s3-avatars')->allFiles();Testing#
Laravel proporciona el mètode Storage::fake() que substitueix el disc real per un sistema de fitxers virtual en memòria. Això permet escriure tests que pugen, llegeixen i eliminen fitxers sense tocar el disc real ni S3. Després del test, el sistema de fitxers virtual es descarta automàticament.
El mètode fake() és fonamental per testejar pujades de fitxers. Combinat amb UploadedFile::fake(), pots simular tot el procés de pujada sense necessitat de fitxers reals. Les assercions assertExists i assertMissing permeten verificar que els fitxers s'han guardat (o eliminat) correctament:
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
test('un usuari pot pujar un avatar', function () {
Storage::fake('public');
$fitxer = UploadedFile::fake()->image('avatar.jpg', 200, 200);
$response = $this->actingAs($user)->post('/perfil/avatar', [
'avatar' => $fitxer,
]);
$response->assertRedirect();
// Verificar que el fitxer existeix
Storage::disk('public')->assertExists('avatars/' . $fitxer->hashName());
});
test('un avatar ha de ser una imatge vàlida', function () {
Storage::fake('public');
$fitxer = UploadedFile::fake()->create('document.pdf', 1024);
$response = $this->actingAs($user)->post('/perfil/avatar', [
'avatar' => $fitxer,
]);
$response->assertSessionHasErrors('avatar');
// Verificar que no s'ha guardat cap fitxer
Storage::disk('public')->assertDirectoryEmpty('avatars');
});Pots simular fitxers de qualsevol tipus i mida. El mètode fake()->image() crea una imatge real (amb les dimensions especificades), mentre que fake()->create() crea un fitxer buit amb l'extensió i mida indicades:
// Imatge de 500x300 píxels
$imatge = UploadedFile::fake()->image('foto.jpg', 500, 300);
// Imatge que pesa 500 KB
$imatgeGran = UploadedFile::fake()->image('foto.jpg')->size(500);
// PDF de 2 MB
$pdf = UploadedFile::fake()->create('informe.pdf', 2048, 'application/pdf');
// Fitxer CSV de 100 KB
$csv = UploadedFile::fake()->create('dades.csv', 100, 'text/csv');Verificar eliminació de fitxers#
Quan la lògica de l'aplicació inclou eliminació de fitxers (per exemple, quan un usuari canvia el seu avatar i l'antic s'ha d'esborrar), pots verificar-ho amb assertMissing:
test('canviar avatar elimina l\'anterior', function () {
Storage::fake('public');
// Simular un avatar existent
Storage::disk('public')->put('avatars/antic.jpg', 'contingut');
$user->update(['avatar' => 'avatars/antic.jpg']);
// Pujar un nou avatar
$nouAvatar = UploadedFile::fake()->image('nou.jpg');
$this->actingAs($user)->post('/perfil/avatar', [
'avatar' => $nouAvatar,
]);
// L'antic s'ha eliminat
Storage::disk('public')->assertMissing('avatars/antic.jpg');
// El nou existeix
Storage::disk('public')->assertExists('avatars/' . $nouAvatar->hashName());
});El Storage::fake() és un dels mètodes de testing més útils de Laravel perquè elimina completament la dependència del sistema de fitxers real. No cal netejar fitxers després dels tests, no cal configurar buckets S3 de test, i els tests s'executen molt més ràpid perquè tot passa en memòria. A més, com que el sistema de fitxers virtual es crea nou per a cada test, no hi ha contaminació entre tests: cada test comença amb un disc buit i net.