Skip to content

5.19 Almacenamiento de Archivos en Laravel (Storage)


5.19.1 Introducción

  • En Laravel, los ficheros se pueden almacenar en diferentes ubicaciones, pero las más comunes son public y storage.

  • public: Los ficheros almacenados en esta carpeta son accesibles directamente desde el navegador. Esto es útil cuando necesitamos que los archivos sean accesibles públicamente (por ejemplo, imágenes, documentos, etc.).

  • storage: Los ficheros almacenados aquí no son accesibles directamente desde el navegador, lo que brinda una mayor seguridad para archivos privados o protegidos. Sin embargo, es necesario utilizar un enlace simbólico para poder acceder a ellos desde el navegador.

  • En este tema, vamos a explorar cómo almacenar archivos de forma segura y cómo configurar Laravel para manejar estos ficheros.


5.19.2 Configuración de Laravel para Storage

Laravel proporciona una configuración simple para manejar los archivos. Por defecto, Laravel utiliza el disco local para almacenar los archivos en el directorio storage/app. Sin embargo, Laravel permite configurar otros discos para almacenar los archivos en ubicaciones remotas, como Amazon S3, FTP, Google Cloud Storage, entre otros.

Configuración del disco en config/filesystems.php

Por defecto, Laravel está configurado para usar el disco local para almacenar archivos, pero podemos modificarlo o agregar otros discos. En config/filesystems.php, encontramos algo como esto:

config/filesystems.php

'disks' => [
    'local' => [
        'driver' => 'local',
        'root' => storage_path('app'),
    ],
    'public' => [
        'driver' => 'local',
        'root' => storage_path('app/public'),
        'url' => env('APP_URL') . '/storage',
        'visibility' => 'public',
    ],
    // Otros discos como s3, ftp, etc. pueden configurarse aquí.
],
  • local: Define el almacenamiento local en storage/app.
  • public: Define el almacenamiento en storage/app/public. Laravel lo hace accesible desde el navegador usando un enlace simbólico a través de php artisan storage:link.

5.19.3 Crear la Aplicación para Subir Archivos

Paso 1: Crear la Migración para la Tabla files

Comenzamos creando una migración para la tabla files, donde almacenaremos el nombre, la descripción y la URL del archivo cargado.

Ejecutamos el siguiente comando para crear la migración:

php artisan make:migration create_files_table

Editamos la migración en database/migrations/YYYY_MM_DD_create_files_table.php:

create_files_table.php

public function up()
{
    Schema::create('files', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('description');
        $table->string('url');
        $table->timestamps();
    });
}

Luego, ejecutamos la migración para crear la tabla:

php artisan migrate

Paso 2: Crear el Modelo File

Creamos el modelo File para trabajar con esta tabla:

php artisan make:model File

El modelo File.php quedará así:

File.php

1
2
3
4
5
6
class File extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'description', 'url'];
}

Paso 3: Crear el Controlador para Gestionar Archivos

Ahora, creamos el controlador que gestionará los archivos. Ejecutamos:

php artisan make:controller FileController

En el controlador FileController.php, implementamos los métodos para mostrar el formulario de carga, almacenar el archivo y mostrar los archivos almacenados. En esta primera versión vamos a guardar los archivos en una carpeta pública de manera que sean accesibles desde el navegador.

FileController.php

class FileController extends Controller
{
    public function index()
    {
        $files = File::all();
        return view('files.index', compact('files'));
    }

    public function create()
    {
        return view('files.create');
    }

    public function store(FileRequest $request)
    {

        // Subir el archivo
        $file = $request->file('file');
        $fileName = time() . '_' . uniqId() .  '.' . $file->getClientOriginalExtension();
        $file->move(public_path('files'), $fileName);

        File::create([
            'name' => $request->file('file')->getClientOriginalName(),
            'description' => $request->description,
            'url' => $fileName,
        ]);
        return redirect()->route('files.index');

    }
}

Creamos un Request para validar los datos de entrada. Ejecutamos:

php artisan make:request FileRequest
En app/Http/Requests/FileRequest.php, definimos las reglas de validación:

FileRequest.php

class FileRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'file' => 'required|file|mimes:jpg,png,jpeg,pdf|max:2048',
            'description' => 'required|string|max:255',
        ];
    }
}

En este caso, estamos validando que el archivo sea de tipo jpg, png, jpeg o pdf y que no supere los 2MB. También validamos que la descripción sea un texto con un máximo de 255 caracteres.


Paso 4: Crear las Rutas para las Operaciones

En routes/web.php, agregamos las rutas necesarias para index, create y store:

1
2
3
Route::get('/myfiles', [FileController::class, 'index'])->name('files.index');
Route::get('/myfiles/create', [FileController::class, 'create'])->name('files.create');
Route::post('/myfiles', [FileController::class, 'store'])->name('files.store');

Paso 5: Crear las Vistas para Subir y Ver Archivos

Layout layouts/app.blade.php

Comencemos creando un layout básico para nuestras vistas. En resources/views/layouts/app.blade.php, añadimos:

app.blade.php

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Laravel File Upload</title>
    <link rel="stylesheet" href="{{ asset('css/app.css') }}">
</head>
<body>
    <div class="container">
        <nav>
            <a href="{{ route('files.index') }}">Archivos</a>
            <a href="{{ route('files.create') }}">Subir Archivo</a>
        </nav>
        @yield('content')
    </div>
</body>
</html>

Vista files/create.blade.php

En resources/views/files/create.blade.php, creamos el formulario para cargar archivos:

create.blade.php

@extends('layouts.app')

@section('content')
    <h1>Subir un Archivo</h1>
    <form action="{{ route('files.store') }}" method="POST" enctype="multipart/form-data">
        @csrf
        <input type="file" name="file" />
        <input type="text" name="description" placeholder="Descripción" required />
        <button type="submit">Subir</button>
    </form>
@endsection

Vista files/index.blade.php

En resources/views/files/index.blade.php, mostramos los archivos cargados:

index.blade.php

@extends('layouts.app')

@section('content')
    <h1>Archivos Subidos</h1>
    <ul>
        @forelse ($files as $file)
            <li>
                <p>{{ $file->name }} - {{ $file->description }} - Localización: {{ $file->url }}</p>
                <img src="{{ asset('files/'.$file->url)}}" alt="{{ $file->name }}" width="100" />
            </li>
        @empty
            <li>
                <p>No hay archivos subidos.</p>
            </li>
        @endforelse
    </ul>
@endsection

Paso 6: Probar la Carga de Archivos

  1. Accede a http://localhost/myfiles/create y sube un archivo.
  2. Accede a http://localhost/files para ver los archivos cargados.

5.19.4 Cambiar de public a storage

Por defecto, los archivos cargados se almacenan en la carpeta public. Para evitar que los archivos sean accesibles directamente desde el navegador, podemos moverlos a la carpeta storage.

Paso 1: Crear un enlace simbólico

Para crear un enlace simbólico entre la carpeta storage y public (para poder acceder a los archivos de forma controlada), ejecuta:

php artisan storage:link

Paso 2: Cambiar la ruta en la migración y el código

A diferencia de la práctica anterior ahora vamos a almacenar los archivos en storage/app/private. Para ello, tenemos que modificar el controlador, ya que ahora no moveremos los archivos a public, sino que los almacenaremos directamente en storage.

Modifica el método store en el controlador para almacenar los archivos en storage en lugar de public:

$request->file('file')->storeAs('files', $fileName);

Esto guardará los archivos en storage/app/private/files en lugar de en public/files. Por lo tanto ya no tenemos acceso a él desde la vista y si la cargamos veremos que este último fichero no se carga.


5.19.5 Permitir acceso a archivos almacenados en storage

La primera manera de poder acceder a los archivos es almacenarlos en la carpeta storage\app\public. Para ello, modificamos el método store en el controlador para almacenar los archivos en storage en lugar de public:

$file->storeAs('files', $fileName);

Si volvemos a subir el fichero ahora veremos que se encuentra en la carpeta storage/app/public/files. Pero, es cierto, que seguimos sin ver el fichero en la web. Esto es por dos motivos:

  • El primero es que no hemos creado el enlace simbólico entre storage y public.
  • El segundo es que no hemos modificado la vista para que cargue el fichero desde storage.

Vamos a ver cómo solucionarlo.

Paso 1: Crear el enlace simbólico

Ejecuta el siguiente comando para crear un enlace simbólico que permita acceder a los archivos almacenados en storage desde public:

php artisan storage:link
Esto creará un enlace simbólico en la carpeta public que apunta a la carpeta storage/app/public.

Modificamos el archivo .env para cambiar el disco de almacenamiento por defecto a public:

FILESYSTEM_DISK=public
Esto hará que todos los archivos que se suban se almacenen en storage/app/public. Además a esta carpeta le hemos dado un enlace simbólico en public/storage. Para que sean accesibles desde el navegador.

Paso 2: Modificar la vista para cargar el fichero

Ahora, en la vista files/index.blade.php, modificamos la forma de cargar el fichero:

<img src="{{ asset('storage/files/'.$file->url)}}" alt="{{ $file->name }}" width="100" /> 

Ahora los ficheros que subamos se almacenarán en storage/app/public/files y serán accesibles desde el navegador a través de public/storage/files. De esta forma estarán almacenados de forma segura y no serán accesibles directamente desde el navegador.

5.19.6 Almacenarminto de archivos en storage

Comenzamos por hacer un reset a la base de datos y crear la tabla files de nuevo:

php artisan migrate:reset
php artisan migrate

Y vaciamos las carpetas storage/app/public y storage/app, para asegurarnos de que no hay archivos duplicados y poder verexactamente lo que subimos.

Comenzamos por subir un par de imagenes y veremos que gracias al enlace simbólico que hemos creado, los archivos que subamos a storage/app/public serán accesibles desde el navegador a través de public/storage.

Ahora por ejemplo si queremos guardar un archivo en un disco diferente al public, podemos hacerlo de la siguiente manera:

Storage::disk('local')->putFileAs('files', $file, $fileName);

Esto guardará el archivo en storage/app/files. Si queremos acceder a este archivo desde el navegador, no podremos hacerlo directamente, ya que no hemos creado un enlace simbólico para esta carpeta.

Para poder ofrecer la descarga en el controlador podemos hacer lo siguiente:

Esto nos dará la ruta completa del archivo en el sistema de archivos.

Con todo esto, vamos a eliminar la etiqueta img que hemos añadido antes y vamos a añadir un enlace para descargar el archivo. Para ello, en la vista files/index.blade.php, añadimos lo siguiente:

<a href="{{ route('files.download', $file->id) }}">Descargar</a>
Esto generará un enlace que permitirá al usuario descargar el archivo. En el controlador, añadimos la siguiente función:

Necesitamos la ruta para descargar el archivo. Para ello, añadimos la siguiente función en el controlador:

Route::get('/myfiles/download/{id}', [FileController::class, 'download'])->name('files.download');

Y por último, añadimos la función download en el controlador:

download

public function download($id)
    {
        $file = File::findOrFail($id);

        // Ruta relativa dentro del disco 'local'
        $filePath = 'files/' . $file->url;

        // Verifica que el archivo existe antes de intentar descargarlo
        if (!Storage::disk('local')->exists($filePath)) {
            abort(404, 'Archivo no encontrado.');
        }

        // Devuelve el archivo como descarga
        $absolutePath = Storage::disk('local')->path($filePath);
        return response()->download($absolutePath, $file->name);
    }

5.19.7 Opción para eliminar el Archivo

Para eliminar el archivo, podemos añadir un botón de eliminar en la vista files/index.blade.php:

Eliminar archivo

1
2
3
4
5
<form action="{{ route('files.destroy', $file->id) }}" method="POST" onsubmit="return confirm('¿Estás seguro de que deseas eliminar este archivo?');">
    @csrf
    @method('DELETE')
    <button type="submit">Eliminar</button>
</form>

Esto generará un formulario que enviará una solicitud DELETE al controlador para eliminar el archivo. En el controlador, añadimos la siguiente ruta:

Route::delete('/myfiles/{id}', [FileController::class, 'destroy'])->name('files.destroy');

Y en el controlador, añadimos la siguiente función:

destroy

public function destroy($id)
{
    $file = File::findOrFail($id);
    $filePath = 'files/' . $file->url;

    // Verifica que el archivo existe antes de intentar eliminarlo
    if (Storage::disk('local')->exists($filePath)) {
        Storage::disk('local')->delete($filePath);
    }

    $file->delete();
    return redirect()->route('files.index');
}

Prueba la funcionalidad de eliminar archivos y asegúrate de que se eliminan correctamente tanto de la base de datos como del sistema de archivos.


5.19.7 Ejercicio Propuesto: Clientes

Objetivo: Crear una tabla clientes y permitir que el usuario suba un PDF (DNI) y una imagen (perfil) al crear el cliente.

Pasos:

  1. Crear la migración para la tabla clientes.
  2. Crear un formulario que permita subir tanto un PDF como una imagen.
  3. Modificar la vista para mostrar las imágenes y enlaces de descarga del PDF.
  4. Implementar la funcionalidad de edición y actualización de estos archivos.

Paso 1: Crear la migración para la tabla clientes

Primero, creamos la migración para la tabla clientes, que almacenará el DNI (PDF) y la imagen de perfil.

Ejecutamos el siguiente comando para crear la migración:

php artisan make:migration create_clients_table

En la migración generada, editamos database/migrations/YYYY_MM_DD_create_clients_table.php para definir los campos de la tabla:

create_clients_table.php

public function up()
{
    Schema::create('clients', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('email')->unique();
        $table->string('dni')->nullable();
        $table->string('profile_image')->nullable();
        $table->timestamps();
    });
}

Después, ejecutamos la migración para crear la tabla en la base de datos:

php artisan migrate

Paso 2: Crear el Modelo Client

Ahora, vamos a crear el modelo para interactuar con la tabla clients.

Ejecutamos:

php artisan make:model Client

En el archivo generado app/Models/Client.php, agregamos lo siguiente:

Client.php

```php {linenums="1"} class Client extends Model { use HasFactory;

// Definir los campos asignables en masa
protected $fillable = ['name', 'email', 'dni', 'profile_image'];

}

```


Paso 3: Crear el Controlador para gestionar los clientes

Creamos el controlador que manejará las operaciones de creación y almacenamiento de los archivos. Ejecutamos:

php artisan make:controller ClientController

En app/Http/Controllers/ClientController.php, agregamos lo siguiente:

ClientController.php

class ClientController extends Controller
{
    public function create()
    {
        return view('clients.create');
    }

    public function store(Request $request)
    {
        // Validación de los archivos y datos
        $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:clients,email',
            'dni' => 'required|file|mimes:pdf|max:2048',
            'profile_image' => 'required|image|mimes:jpg,png,jpeg|max:2048',
        ]);

        // Subir el archivo PDF del DNI
        $dniPath = $request->file('dni')->store('public/dni');

        // Subir la imagen de perfil
        $profileImagePath = $request->file('profile_image')->store('public/profile_images');

        // Crear el cliente y almacenar la ruta de los archivos
        Client::create([
            'name' => $request->name,
            'email' => $request->email,
            'dni' => $dniPath,
            'profile_image' => $profileImagePath,
        ]);

        return redirect()->route('clients.index');
    }

    public function index()
    {
        $clients = Client::all();
        return view('clients.index', compact('clients'));
    }
}

Paso 4: Crear las rutas para la creación y listado de clientes

En routes/web.php, agregamos las rutas para crear y listar los clientes:

1
2
3
Route::get('/clients', [ClientController::class, 'index'])->name('clients.index');
Route::get('/clients/create', [ClientController::class, 'create'])->name('clients.create');
Route::post('/clients', [ClientController::class, 'store'])->name('clients.store');

Paso 5: Crear las vistas para crear y listar clientes

Vista clients/create.blade.php (Formulario para crear un cliente y subir archivos)

En resources/views/clients/create.blade.php:

create.blade.php

@extends('layouts.app')

@section('content')
    <h1>Crear Cliente</h1>
    <form action="{{ route('clients.store') }}" method="POST" enctype="multipart/form-data">
        @csrf
        <input type="text" name="name" placeholder="Nombre" required>
        <input type="email" name="email" placeholder="Correo electrónico" required>

        <label for="dni">Subir DNI (PDF)</label>
        <input type="file" name="dni" accept="application/pdf" required>

        <label for="profile_image">Subir Imagen de Perfil (JPG, PNG)</label>
        <input type="file" name="profile_image" accept="image/*" required>

        <button type="submit">Crear Cliente</button>
    </form>
@endsection

Vista clients/index.blade.php (Mostrar clientes y sus archivos)

En resources/views/clients/index.blade.php:

index.blade.php

@extends('layouts.app')

@section('content')
    <h1>Clientes</h1>
    <ul>
        @foreach ($clients as $client)
            <li>
                <strong>{{ $client->name }}</strong> - {{ $client->email }}
                <br>
                <a href="{{ Storage::url($client->dni) }}" target="_blank">Ver DNI (PDF)</a>
                <br>
                <img src="{{ Storage::url($client->profile_image) }}" alt="Profile Image" width="100">
            </li>
        @endforeach
    </ul>
@endsection

Paso 6: Probar la funcionalidad

  1. Visita http://localhost/clients/create y crea un nuevo cliente, subiendo el archivo DNI (PDF) y la imagen de perfil.
  2. Después de crear el cliente, será redirigido a http://localhost/clients donde podrá ver el cliente creado junto con los enlaces para ver su DNI (PDF) y su imagen de perfil.

5.19.7 Cambiar de public a storage

Por defecto, los archivos se guardan en storage/app/public, lo que los hace accesibles desde el navegador. Sin embargo, para mejorar la seguridad, podemos moverlos a storage y crear un enlace simbólico.

Paso 1: Crear el enlace simbólico

Ejecuta el siguiente comando para crear un enlace simbólico que permita acceder a los archivos almacenados en storage desde public:

php artisan storage:link

Paso 2: Cambiar las rutas de acceso a los archivos

En el controlador, cambia la ruta de los archivos de esta manera:

$profileImagePath = $request->file('profile_image')->store('profile_images', 'public');
$client->profile_image = $profileImagePath;

Y en la vista clients/index.blade.php, usa el método Storage::url() para generar la URL correcta de los archivos:

<a href="{{ Storage::url($client->dni) }}" target="_blank">Ver DNI (PDF)</a>
<img src="{{ Storage::url($client->profile_image) }}" alt="Profile Image" width="100">

Paso 3: Descargar el archivo

Para permitir que el usuario descargue los archivos, podemos agregar un enlace de descarga en la vista:

<a href="{{ Storage::url($client->dni) }}" download>Descargar DNI</a>

5.19.8 Resumen

  • Hemos creado una tabla de clientes con campos para DNI (PDF) y imagen de perfil.
  • Hemos implementado un formulario que permite subir ambos tipos de archivo y guardarlos en el almacenamiento de Laravel.
  • Se ha utilizado Storage::url() para generar las URLs de acceso a los archivos, tanto para visualizarlos como para permitir su descarga.
  • Se ha cambiado el almacenamiento de los archivos de public a storage para mejorar la seguridad.