File Storage: Como manejar archivos y discos en Laravel

File Storage: Como manejar archivos y discos en Laravel

Introducción

Más de una vez he tenido problemas para almacenar y compartir archivos en mis aplicaciones. Ya sea actualizando fotos de perfil, subiendo imágenes de productos, compartiendo facturas generadas y más. Verás, siempre necesitaremos interactuar con archivos.

Cuando te animas a hacerlo y lo intentas, te surgen varias preguntas:

  • ¿Dónde almaceno mis archivos?
  • ¿Cómo decirle a Laravel donde buscarlos?
  • ¿Cómo saber si Laravel puede encontrar mis archivos?
  • ¿Cómo compartirlos en la web?
  • ¿Qué información debo almacenar en mi base de datos?
  • ¿Cómo mostrar mis archivos almacenados, leyendo desde la base de datos?
  • ¿Cómo almacenar los archivos subidos por usuarios?

Cuando comprendes cómo funciona, es pan comido. En este artículo aprenderás a responder cada una de estas preguntas.

Base

Para mayor facilidad, haremos una analogía con un apartado más conocido de Laravel: la definición de rutas.

Cuando definimos una ruta, podemos asignarle un nombre mediante el método name(). Esto nos ayuda a evitar especificar la ruta directamente:

// web.php
Route::get('/products', ProductsController::class)->name('products.index');

// products.blade.php
<a href="{{ route('products.index') }}" alt="...">Listado de productos</a>

Asignándole un nombre (y utilizándolo), el esfuerzo se simplifica cuando quieres modificar la ruta. Digamos que queremos cambiarla de myapp.com/all-products a myapp.com/products:

// Cambiar de:
Route::get('/all-products', [ProductsController::class, 'index'])
    ->name('products.index');
// a:
Route::get('/products', [ProductsController::class, 'index'])
    ->name('products.index');

Pues listo. No tenemos que reemplazar manualmente los lugares en donde hayamos utilizado esta ruta, puesto que es identificada a través de su nombre. El nombre le da un alias a la ruta que encapsula cuál sea que sea la definición por detrás de esta.

¿Útil cierto? 😉

El mismo concepto se maneja para el File Storage y sus discos. Los discos son un concepto importante en el manejo de archivos en Laravel. Un disk no es nada de otro mundo, es solo un “alias” que representa una ubicación de almacenamiento y su configuración. Funcionan igual que las rutas nombradas que vimos hace un instante. De este modo podemos utilizarlos y, si lo queremos, modificar la implementación de estos sin necesidad de editar los lugares en los que lo utilicemos:

$file = Storage::disk('mi-disco-x')->get($filename);

Podemos definir tantos discos como queramos: AWS S3, Digital Ocean, Dropbox, etc, así como también para nuestra unidad local. Este último es el que nos interesa ahora mismo. Podemos tener, de hecho, varios discos que utilicen el mismo driver, tal como lo hace Laravel por defecto.

Similar a cuando definimos nuestras rutas en los archivos del directorio ./routes/*.php , los discos se definen en el archivo ./config/filesystems.php. Veamos los que trae Laravel por defecto.

    'disks' => [

        'local' => [
            'driver' => 'local',
            'root' => storage_path('app'),
        ],

        'public' => [
            'driver' => 'local',
            'root' => storage_path('app/public'),
            'url' => env('APP_URL').'/storage',
            'visibility' => 'public',
        ],

        '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),
        ],

    ],

Cada una de las entradas del arreglo 'disks' corresponde a un disco. Como ves, tenemos los discos local, public y s3.

Si nos centramos en los primeros 2, podemos notar que ambos utilizan el mismo driver: local. Por tanto describamos la configuración que comparten y notemos como es que se diferencian:

  • driver: Como mencioné, esto indica el driver del disco
  • root: Esto le indica la ubicación absoluta del directorio en donde se ubican los archivos del driver. Esto es, la ubicación física de dónde están los archivos.
  • url: Esto le dice a Laravel cuál será la url a utilizar para este driver en especial. Entonces, es una ubicación virtual. Más adelante lo utilizaremos.
  • visibility: Por defecto, todos los archivos son privados. Si se especifica esta llave con valor public, se le está diciendo a Laravel que la visibilidad de los archivos de este driver es pública.

Si te preguntas como manejar directorios con visibilidad privada, revisa este otro artículo que publiqué hace poco.

Entonces, una vez entendido esto vayamos con un ejemplo para responder las preguntas que listamos al inicio.

Para fines prácticos, creé una app en la cual definiremos un disco para almacenar los avatares de nuestros usuarios. Ya tenemos 2 archivos en nuestro directorio y pasaremos a leerlos.

¿Dónde almaceno mis archivos?

La respuesta simple es: Donde queramos. ¿Es nuestra app cierto? Pues, nosotros podemos colocar nuestros archivos donde nos parezca 😎. Lo único que tenemos que hacer es decirle a Laravel dónde encontrarlos al momento de configurar nuestro disco.

Como recomendación, tus archivos deberían estar almacenados en la carpeta ./storage. Ese directorio tiene especialmente esa función.

¿Cómo decirle a Laravel dónde buscarlos?

Aclarado lo anterior, vayamos a configurar nuestro disco. Para hacerlo más fácil, utilizaré valores distintos para el (1) nombre del disco, (2) el directorio en donde se almacenan los archivos y (3) la url por donde accederemos a nuestros archivos.

'disks' => [
    // ...
    'avatars' => [
        'driver' => 'local',
        'root' => storage_path('app/user-avatars'),
        'url' => env('APP_URL').'/fotos-de-usuarios',
        'visibility' => 'public',
    ],
    // ...
],

Analicemos nuestra configuración:

  • Aquí le estamos diciendo a Laravel que este disco se llama avatars y que manejará el driver local. Por tanto, quiere decir que estarán almacenados localmente.
  • Le estamos diciendo que la ubicación real de nuestros archivos están en ./storage/app/user-avatars
  • Además, le estamos diciendo que las URL de los archivos relativos a este directorio será bajo mycoolapp.test/fotos-de-usuarios
  • Por último, le indicamos que la visibilidad es pública

Notarás que estamos utilizando la variable de entorno env('APP_URL'). Para que los links sean correctos, no olvides modificar esa entrada en tu .env.

Una vez almacenados, ¿cómo puedo acceder a ellos?

Probemos nuestro nuevo disco. Tal como comenté al inicio, ya tenemos el directorio ./storage/app/user-avatars , el cual tiene dos imágenes: martinelli.webp y saka.jpg

structure.png

Mediante Tinker (php artisan tinker) comprobemos si Laravel puede leer las imágenes de nuestro nuevo disco:

php artisan tinker
>>> Storage::disk('avatars')->allFiles();
=> [
     "martinelli.webp",
     "saka.jpg",
   ]

¡Genial! Nuestros archivos están siendo leídos correctamente. Podemos ver cual es la dirección física de estos de la mano de método path($file):

>>> Storage::disk('avatars')->path('saka.jpg');
=> "/Users/hck/Code/mycoolapp/storage/app/user-avatars/saka.jpg"

Lo cual es cierto (esa es la ubicación absoluta en mi computadora).

¿Cómo compartir mis archivos en la web?

Este punto probablemente sea el que más te interese. ¿Recuerdas que en la configuración de nuestro disco especificamos la entrada 'url'? Bueno, Laravel la utilizará.. pues para generar urls. Para hacerlo, tan solo debemos utilizar el método url($filename):

>>> Storage::disk('avatars')->url('saka.jpg');
=> "https://mycoolapp.test/fotos-de-usuarios/saka.jpg"

¿Super simple cierto? Ahora vayamos al navegador para probar nuestra url:

img-not-found.png

Nos retorna 404 😟.

sheldon-hiperventilando.gif

Calma, no te asustes. Entendamos lo sucedido.

¿Qué pasó? Por seguridad, los navegadores web solo tiene acceso al directorio /public de nuestra aplicación.
Cuando especificamos la ruta mi-app.com/mi/directorio/archivo.pdf, nuestra app tratará de encontrar el elemento respecto del directorio público:
./public/mi/directorio/archivo.pdf.

Entonces, lo que está haciendo en nuestro caso es tratar de resolver el archivo ./public/fotos-de-usuarios/saka.jpg. Si nos fijamos en nuestra app, ni el directorio ni el archivo existen dentro de /public 🤔. ¿Cómo podemos resolverlo? Simple: Mediante enlaces simbólicos.

Quizás lo notaste o quizás no, pero si te fijas bien verás que en nuestro config/filesystems.php hay una segunda sección llamada 'links':

'links' => [
    public_path('storage') => storage_path('app/public'),
],

Es aquí donde podemos especificar todos los enlaces simbólicos de nuestra app. Como llave se especifica el directorio virtual, y como valor, el directorio real que queremos enlazar.

Podemos ver que ya existe uno el cual vincula el directorio virtual ./public/storage hacia el directorio real ./storage/app/public.

Reemplazaré ese con el que necesitamos nosotros. Dado que queremos que nuestras imagen esté disponible bajo mycoolapp.test/fotos-de-usuarios/XXXX.jpg y esta realmente existe en ./storage/app/user-avatars/XXXX.jpg, debemos hacer lo siguiente.

'links' => [
    public_path('fotos-de-usuarios') => storage_path('app/user-avatars'),
],

Luego de hacer esto, debemos de /realmente/ crear los enlaces simbólicos. Para ello corremos el conveniente comando php artisan storage:link.

Si ya ejecutaste este comando en el pasado, puedes volver a hacerlo sin problemas.

➜ php artisan storage:link
The [/Users/hck/Code/mycoolapp/public/fotos-de-usuarios] link has been connected to [/Users/hck/Code/mycoolapp/storage/app/user-avatars].
The links have been created.

Ahora, sí. Podemos probar nuevamente en el navegador y esta vez sí veremos nuestra imagen:

img-found.png
celebrating.gif

¿Qué información debo almacenar en mi base de datos?

En lo personal, yo siempre recomiendo almacenar tan solo el nombre de los archivos. ¿Por qué? Es simple.

Para empezar, no estamos amarrando la ubicación de nuestros archivos en la BD. Si quisiéramos mover los archivos a otro directorio pues tendríamos que actualizar todos los registros de la base de datos. El mismo problema tendremos cuando deseemos moverlos a un servicio/proveedor distinto (Amazon S3, Dropbox, Google Drive, etc).

¿Para qué complicarnos? No vale la pena. Dejemos que Laravel se encargue de ello 😎.

¿Cómo mostrar mis archivos almacenados, leyendo desde la base de datos?

Dado que tenemos nuestros registros en la base de datos con tan solo el nombre, ¿cómo obtener el enlace completo para mostrarlo en nuestras vistas Blade o retornarlo mediante nuestra API? Para ello, yo hago uso de Eloquent Accessors.

Los accessors no son más que métodos que nos permiten modificar los valores que tenemos en nuestro modelo. Podemos incluso crear campos virtuales (lo hago bastante para el campo $user->full_name por poner un ejemplo).

En nuestra app, imaginemos que estamos almacenando el nombre de las fotos de perfil en la columna profile_picture de nuestra tabla users. Entonces, para nuestro primer usuario tenemos el nombre del archivo del siguiente modo:

$user = User::find($id);
echo $user->profile_picture;
=> "saka.jpg"

Lo que haremos ahora será crear un accessor virtual el cual tendrá el enlace público hacia esta imagen. Le llamaremos $user->profile_picture_url. Para esto, en nuestro modelo User hacemos:

public function getProfilePictureUrlAttribute(): string
{
    return Storage::disk('avatars')->url($this->profile_picture);
}

De este modo, tendremos a nuestra disposición nuestro nuevo campo virtual. Probemos nuevamente en Tinker:

$user = User::find($id);
echo $user->profile_picture;
=> "saka.jpg"

echo $user->profile_picture_url;
=> "https://mycoolapp.test/fotos-de-usuarios/saka.jpg"

Esto además es muy conveniente puesto que, si en el futuro, modificamos el driver y la ubicación de nuestros archivos solo tendremos que actualizar tanto el disco, como nuestro accessor y listo. No tendremos que hacer más cambios.

Para mostrar nuestros archivos en Blade, podemos hacer lo siguiente:

// UsersController.php
$user = User::find($id);
return view('users.show')->withUser($user);

// Blade
<img src="{{ $user->profile_picture_url }}" alt="{{ $user->name }}" />

¿Cómo almacenar los archivos subidos por usuarios?

Es muy fácil. En nuestros controladores, podemos hacer uso del método store(…), el cual está disponible para todos los archivos que se reciban a través del request. Cuando queremos utilizar un disco en particular, debemos de pasarle dos parámetros: el directorio del archivo y el nombre del disco. Por ejemplo:

public function update(Request $request, User $user)
{
    $validated = $request->validate([
        // SIEMPRE valida tus datos
        'profile_picture' => ['image', 'max:2000'],
    ]);

    $filename = $request->file('profile_picture')->store('/', 'avatars');
    $user->profile_picture = $filename;
    // ...
    $user->save();
}

Puedes darte cuenta de que no le asigné un nombre manualmente. Esto es debido a que, si lo hacemos de este modo, Laravel le asignará un hash auto generado al nombre del archivo. Algo es muy útil pues nos ahorra tener que lidiar con nombres duplicados, en caso varios usuarios suban archivos con el mismo nombre. Como bonus, de este modo nos olvidamos de extraer la extensión del archivo para luego concatenarlo al nombre 😉.

No especifiqué tampoco ningún directorio en particular, pues de esto ya se encarga nuestro disco configurado. Por último, puedes notar que el nombre auto-asignado se lo pasamos al usuario bajo $user->profile_picture, por tanto ya tenemos el nombre del archivo vinculado al usuario correcto.

Cierre

¡Y listo! Así terminamos esta guía. Espero que te haya traído más claridad sobre como manejar los archivos en tus apps. Puedes crear tantos discos como desees, tan solo tienes que configurarlo y crear el enlace simbólico 😉👌.

2019-2025 © kennyhorna.com | @kennyhorna