Subida de archivos
Subir archivos es una funcionalidad común en aplicaciones web: imágenes de perfil, documentos PDF, hojas de cálculo. Laravel simplifica este proceso con el facade Storage y validaciones específicas para archivos.
El formulario HTML
Para subir archivos, el formulario debe tener el atributo
enctype="multipart/form-data". Sin él, los archivos
no se enviarán:
<form action="{{ route('photos.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<label for="photo">Selecciona una imagen:</label>
<input type="file" name="photo" id="photo">
@error('photo')
<span class="error">{{ $message }}</span>
@enderror
<button type="submit">Subir imagen</button>
</form>
Sin enctype="multipart/form-data", el archivo
no se enviará y $request->file('photo')
devolverá null.
Recibir el archivo en el controlador
Usa el método file() del request para obtener el
archivo subido:
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PhotoController extends Controller
{
public function store(Request $request): RedirectResponse
{
// Verificar si se subió un archivo
if ($request->hasFile('photo')) {
$file = $request->file('photo');
// Información del archivo
$name = $file->getClientOriginalName(); // foto.jpg
$extension = $file->extension(); // jpg
$size = $file->getSize(); // bytes
$mimeType = $file->getMimeType(); // image/jpeg
}
return redirect()->route('photos.index');
}
}
Validar archivos
Laravel incluye reglas de validación específicas para archivos:
<?php
$request->validate([
// Archivo obligatorio, imagen, máximo 2MB
'photo' => 'required|image|max:2048',
// Archivo obligatorio con extensiones específicas
'document' => 'required|file|mimes:pdf,doc,docx|max:5120',
// Imagen con dimensiones mínimas
'avatar' => 'required|image|dimensions:min_width=100,min_height=100',
]);
Las reglas más comunes para archivos:
file: debe ser un archivo subidoimage: debe ser una imagen (jpeg, png, bmp, gif, svg, webp)mimes:jpg,png,pdf: extensiones permitidasmimetypes:image/jpeg,image/png: tipos MIME permitidosmax:2048: tamaño máximo en KB (2048 = 2MB)dimensions:min_width=100,min_height=100: dimensiones de imagen
Almacenar archivos con Storage
Laravel usa el facade Storage para gestionar archivos.
El método más simple es store():
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Photo;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PhotoController extends Controller
{
public function store(Request $request): RedirectResponse
{
$request->validate([
'photo' => 'required|image|max:2048',
]);
// Guardar en storage/app/photos con nombre único
$path = $request->file('photo')->store('photos');
// Resultado: photos/aB3xY9kL2mN4pQ7r.jpg
// Guardar la ruta en la base de datos
Photo::create([
'path' => $path,
'user_id' => $request->user()->id,
]);
return redirect()->route('photos.index');
}
}
El método store() genera automáticamente un nombre
único para evitar colisiones. Si quieres especificar el nombre:
<?php
// Guardar con nombre específico
$path = $request->file('photo')->storeAs(
'photos',
'perfil-' . $user->id . '.jpg'
);
// Resultado: photos/perfil-42.jpg
Discos de almacenamiento
Laravel soporta múltiples "discos" configurados en
config/filesystems.php. Los más comunes:
local: almacena enstorage/app(privado)public: almacena enstorage/app/public(accesible vía web)s3: Amazon S3 u otros servicios compatibles
<?php
// Guardar en el disco público (accesible vía web)
$path = $request->file('photo')->store('photos', 'public');
// O con storeAs
$path = $request->file('photo')->storeAs('photos', 'avatar.jpg', 'public');
Para que el disco public sea accesible desde el
navegador, ejecuta php artisan storage:link.
Esto crea un enlace de public/storage a
storage/app/public.
Mostrar archivos públicos
Para archivos en el disco público, usa el helper asset()
con la ruta del storage:
{{-- Si guardaste en disco 'public' con ruta 'photos/avatar.jpg' --}}
<img src="{{ asset('storage/' . $photo->path) }}" alt="Foto">
{{-- O usando el helper Storage::url() --}}
<img src="{{ Storage::url($photo->path) }}" alt="Foto">
Eliminar archivos
Usa el facade Storage para eliminar archivos:
<?php
use Illuminate\Support\Facades\Storage;
// Eliminar del disco por defecto
Storage::delete('photos/avatar.jpg');
// Eliminar del disco público
Storage::disk('public')->delete('photos/avatar.jpg');
// Eliminar múltiples archivos
Storage::delete(['file1.jpg', 'file2.jpg']);
Ejemplo completo: Avatar de usuario
Veamos un ejemplo práctico de subida de avatar de usuario:
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class ProfileController extends Controller
{
public function updateAvatar(Request $request): RedirectResponse
{
$request->validate([
'avatar' => 'required|image|mimes:jpg,png,webp|max:1024',
]);
$user = $request->user();
// Eliminar avatar anterior si existe
if ($user->avatar) {
Storage::disk('public')->delete($user->avatar);
}
// Guardar nuevo avatar
$path = $request->file('avatar')->store('avatars', 'public');
// Actualizar usuario
$user->update(['avatar' => $path]);
return redirect()->route('profile.edit');
}
}
{{-- resources/views/profile/edit.blade.php --}}
{{-- Mostrar avatar actual --}}
@if($user->avatar)
<img src="{{ asset('storage/' . $user->avatar) }}" alt="Avatar" width="100">
@else
<img src="{{ asset('images/default-avatar.png') }}" alt="Avatar" width="100">
@endif
{{-- Formulario para cambiar avatar --}}
<form action="{{ route('profile.avatar') }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PATCH')
<label for="avatar">Cambiar avatar:</label>
<input type="file" name="avatar" id="avatar" accept="image/*">
@error('avatar')
<span class="error">{{ $message }}</span>
@enderror
<button type="submit">Actualizar</button>
</form>
Subir múltiples archivos
Para subir varios archivos a la vez, usa un array en el campo:
<form action="{{ route('gallery.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<label for="photos">Selecciona varias imágenes:</label>
<input type="file" name="photos[]" id="photos" multiple>
@error('photos.*')
<span class="error">{{ $message }}</span>
@enderror
<button type="submit">Subir imágenes</button>
</form>
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Photo;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class GalleryController extends Controller
{
public function store(Request $request): RedirectResponse
{
$request->validate([
'photos' => 'required|array|max:10',
'photos.*' => 'image|mimes:jpg,png,webp|max:2048',
]);
foreach ($request->file('photos') as $photo) {
$path = $photo->store('gallery', 'public');
Photo::create([
'path' => $path,
'user_id' => $request->user()->id,
]);
}
return redirect()->route('gallery.index');
}
}
Validación con Form Request
Para validaciones complejas, usa un Form Request:
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreDocumentRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'document' => 'required|file|mimes:pdf,doc,docx|max:10240',
];
}
public function messages(): array
{
return [
'document.required' => 'Debes seleccionar un documento.',
'document.mimes' => 'Solo se permiten archivos PDF, DOC o DOCX.',
'document.max' => 'El documento no puede superar los 10MB.',
];
}
}
Resumen
- Usa
enctype="multipart/form-data"en el formulario $request->file('campo')obtiene el archivo subido$request->hasFile('campo')verifica si existe- Valida con
image,mimes,max $file->store('carpeta')guarda con nombre único$file->storeAs('carpeta', 'nombre.ext')guarda con nombre específico- Usa disco
publicpara archivos accesibles vía web php artisan storage:linkcrea el enlace simbólicoStorage::delete($path)elimina archivos
Ejercicios
Ejercicio 1: Subir imagen de producto
Crea un controlador ProductController con un método
store que valide y guarde una imagen de producto.
La imagen debe ser obligatoria, máximo 2MB, y solo aceptar
formatos jpg, png y webp. Guárdala en el disco público en la
carpeta products.
Ver solución
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0.01',
'image' => 'required|image|mimes:jpg,png,webp|max:2048',
]);
$path = $request->file('image')->store('products', 'public');
Product::create([
'name' => $request->name,
'price' => $request->price,
'image' => $path,
]);
return redirect()->route('products.index');
}
}
Ejercicio 2: Actualizar documento con eliminación del anterior
Crea un método updateDocument que permita actualizar
un documento asociado a un modelo Report. El documento
puede ser PDF o DOCX (máximo 5MB). Antes de guardar el nuevo
documento, elimina el anterior si existe.
Ver solución
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Report;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class ReportController extends Controller
{
public function updateDocument(Request $request, Report $report): RedirectResponse
{
$request->validate([
'document' => 'required|file|mimes:pdf,docx|max:5120',
]);
// Eliminar documento anterior si existe
if ($report->document) {
Storage::disk('public')->delete($report->document);
}
// Guardar nuevo documento
$path = $request->file('document')->store('reports', 'public');
$report->update(['document' => $path]);
return redirect()->route('reports.show', $report);
}
}
Ejercicio 3: Galería con múltiples imágenes
Crea un Form Request StoreGalleryRequest para subir
una galería con título (obligatorio, máximo 100 caracteres) y
hasta 5 imágenes. Cada imagen debe ser JPG o PNG y máximo 1MB.
Incluye mensajes personalizados en español.
Ver solución
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreGalleryRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'required|string|max:100',
'images' => 'required|array|max:5',
'images.*' => 'image|mimes:jpg,png|max:1024',
];
}
public function messages(): array
{
return [
'title.required' => 'El título de la galería es obligatorio.',
'title.max' => 'El título no puede superar los 100 caracteres.',
'images.required' => 'Debes seleccionar al menos una imagen.',
'images.max' => 'Solo puedes subir un máximo de 5 imágenes.',
'images.*.image' => 'Todos los archivos deben ser imágenes.',
'images.*.mimes' => 'Solo se permiten imágenes JPG y PNG.',
'images.*.max' => 'Cada imagen no puede superar 1MB.',
];
}
}
¿Has encontrado un error o tienes una sugerencia para mejorar esta lección?
Escríbenos¿Te está gustando el curso?
Tenemos cursos premium con proyectos reales, soporte personalizado y certificado.
Descubrir cursos premium