Archivos, imágenes y almacenamiento
A dónde van los archivos subidos, por qué S3 es el valor por omisión, y el trait que convierte una subida de Livewire en una imagen redimensionada con miniaturas.
Esta guía cubre el disco en el que escribe la aplicación, las variables que necesita, cómo trabajar con el disco local en su lugar, y el proceso de subida detrás de la foto de perfil, que reutilizas para cualquier imagen que guarde tu producto. La necesitas cuando configuras un entorno nuevo y la primera vez que un modelo tuyo tiene foto.
El disco por omisión es S3
config/filesystems.php pone el disco por omisión en env('FILESYSTEM_DISK', 's3'), y .env.example escribe FILESYSTEM_DISK=s3. El kit asume un bucket en todas partes, incluida tu máquina: toda subida y toda llamada a Storage:: sin nombre de disco va a él.
| Variable | En .env.example |
Qué hace |
|---|---|---|
FILESYSTEM_DISK |
s3 |
El disco por omisión: s3, public o local |
AWS_ACCESS_KEY_ID |
relleno | Credenciales, compartidas con el mailer ses y el servicio de SMS |
AWS_SECRET_ACCESS_KEY |
relleno | Igual |
AWS_DEFAULT_REGION |
us-west-1 |
La región del bucket |
AWS_BUCKET |
weblabor-test |
El bucket. Pon el tuyo |
AWS_USE_PATH_STYLE_ENDPOINT |
false |
true para servicios que direccionan los buckets por ruta, como MinIO |
AWS_URL |
no está | URL pública base de los archivos, cuando un CDN o un dominio propio sirve el bucket |
AWS_ENDPOINT |
no está | El endpoint de API de un servicio compatible con S3; déjalo vacío para AWS |
El disco s3 tiene visibility en public, así que todo archivo que se escribe en él se puede leer en su URL directa, y throw en false, así que una escritura fallida regresa false y no registra nada en vez de lanzar un error. Cuando una subida parece no hacer nada, revisa las credenciales y la política del bucket antes que el código.
Usar un disco local en su lugar
Hay dos discos locales definidos. local escribe en storage/app/private, no es público y sirve los archivos con URL temporales firmadas. public escribe en storage/app/public, es público y arma las URL como APP_URL/storage/{ruta}. Para una máquina sin bucket, usa public:
FILESYSTEM_DISK=public
php artisan storage:link
storage:link crea el enlace simbólico public/storage apuntando a storage/app/public, la única entrada de links en config/filesystems.php. Sin él la URL se arma pero el servidor web no encuentra nada ahí, así que cada imagen es un enlace roto. Córrelo una vez por máquina y en cada despliegue que arranque de un clon nuevo.
Las URL de imagen salen de getImageUrl($path, $default = null, $disk = null), el helper detrás de $user->avatar: regresa $default cuando la ruta está vacía o el archivo no está en el disco, la URL directa cuando el visibility del disco es public, y si no, una URL temporal válida por cinco minutos, guardada en caché ese mismo tiempo. El disco local también funciona, entonces, a cambio de una URL firmada por imagen.
Subir una imagen
App\Livewire\Traits\UploadPhotos envuelve el WithFileUploads de Livewire y agrega un método:
$this->savePhoto($property, $folder = 'photos', $oldValue = null);
Llamado con una propiedad con punto como user.photo:
- Lee el archivo subido desde la propiedad.
- Refresca el modelo (
user), lee el valor actual del atributo (photo) y borra ese archivo y sus miniaturas del disco por omisión. - Nombra el archivo nuevo
{id del usuario con sesión}-{timestamp unix}.{extensión}. - Ejecuta
saveImagesWithThumbs($file->temporaryUrl(), $folder, $name, 1200, 1200). - Escribe la ruta que regresa (
avatars/7-1725360000.jpg) en el atributo y guarda el modelo.
Llamado con una propiedad simple (logo), pone la ruta en la propiedad y se detiene: tú la guardas donde quieras, y no se borra nada. El parámetro $oldValue se acepta pero nada lo lee.
Enlazar una propiedad de Livewire a un atributo de modelo con un punto necesita legacy_model_binding, que config/livewire.php deja en true.
Qué produce saveImagesWithThumbs
El helper está en el paquete weblabormx/laravel-front y redimensiona con intervention/image sobre su driver Imagick, así que PHP necesita la extensión imagick donde corran las subidas. Por cada subida escribe, en la carpeta que pasaste:
| Sufijo | Tamaño | Ajuste |
|---|---|---|
| ninguno | 1200×1200 como máximo, reducida sólo cuando es mayor | Conserva proporciones |
s |
90×90 | Cuadrado recortado |
b |
160×160 | Cuadrado recortado |
t |
160×160 | Conserva proporciones |
m |
320×320 | Conserva proporciones |
l |
640×640 | Conserva proporciones |
h |
1024×1024 | Conserva proporciones |
El sufijo va antes de la extensión: avatars/7-1725360000.jpg tiene al lado avatars/7-1725360000b.jpg. Los seis tamaños son config('front.thumbnails') del paquete; publica config/front.php para cambiarlos. Una subida heic, heif o avif se convierte a JPEG con calidad 90 y se guarda con extensión .jpg. Todo archivo se escribe con visibilidad public.
Lee una miniatura con getThumb($rutaOUrl, 'b'), que inserta el sufijo en el último segmento de la cadena que le des, sea ruta o URL completa. Primero llama a validateGetThumb() de app/Helpers/base.php, que regresa la cadena intacta cuando empieza con https://api.dicebear.com o https://www.gravatar.com: son avatares generados sin miniaturas a las que apuntar. Agrega ahí tus propios hosts cuando un modelo pueda guardar una imagen externa.
Qué hace Livewire antes de que corra tu código
Livewire guarda primero la subida en una ubicación temporal; config/livewire.php la gobierna bajo temporary_file_upload:
| Llave | Valor | Efecto |
|---|---|---|
disk |
LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK, vacío |
El disco de los archivos temporales; vacío significa el disco por omisión, o sea S3 de fábrica |
rules |
null |
El valor por omisión de Livewire: required, file, max:12288, 12 MB. Tu propia regla puede ser más estricta, nunca más laxa |
directory |
null |
livewire-tmp en ese disco |
middleware |
null |
throttle:60,1 en el endpoint de subida |
preview_mimes |
lista | Extensiones a las que se les permite una URL temporal de vista previa; incluye heic y heif |
max_upload_time |
5 |
Minutos antes de que una subida inconclusa se invalide |
cleanup |
true |
Los archivos temporales con más de 24 horas se eliminan |
Dos consecuencias importan. Con el disco temporal en S3, el navegador sube directo al bucket con una URL prefirmada, así que las reglas CORS del bucket deben permitir PUT desde tu dominio; en un disco local el archivo pasa por tu servidor y no hay CORS de por medio. Y savePhoto() lee el archivo a través de temporaryUrl(), que sólo existe para las extensiones de preview_mimes: avif no está en esa lista, así que una imagen AVIF pasa el validador del perfil y luego falla con una excepción de "no previsualizable". Agrega 'avif' a preview_mimes si quieres aceptarla. En un disco local esa URL es una ruta firmada sobre APP_URL, que el propio servidor descarga, así que APP_URL debe resolverse desde la máquina que corre PHP.
Un componente que guarda una foto
La foto de perfil es el ejemplo que viene: App\Livewire\Auth\MyProfile valida user.photo como required|mimes:jpeg,png,jpg,gif,webp,heic,heif,avif|max:5120 y llama a savePhoto('user.photo', 'avatars'). El logotipo de una empresa sigue la misma forma:
<?php
namespace App\Livewire\App\Companies;
use App\Livewire\Traits\UploadPhotos;
use App\Models\Company;
use Livewire\Component;
class EditLogo extends Component
{
use UploadPhotos;
public Company $company;
public function updatedCompanyLogo()
{
$this->validate([
'company.logo' => 'required|mimes:jpeg,png,jpg,webp|max:5120',
]);
$this->savePhoto('company.logo', 'logos');
}
public function render()
{
return view('livewire.app.companies.edit-logo');
}
}
<div>
@if ($company->logo)
<img src="{{ getThumb(getImageUrl($company->logo), 'm') }}" alt="{{ $company->name }}" class="h-28 w-28 rounded-lg">
@endif
<input type="file" wire:model.live="company.logo" id="logo" class="hidden" />
<x-button type="button" onclick="document.getElementById('logo').click();" :label="__('Change logo')" primary />
@error('company.logo')
<span class="text-sm text-negative-600">{{ $message }}</span>
@enderror
</div>
wire:model.live envía el archivo en cuanto se elige, Livewire llama a updatedCompanyLogo(), y el método valida y guarda en un solo paso; no hay botón Guardar que pulsar. La carpeta logos se crea en el disco la primera vez. La columna logo guarda logos/7-1725360000.png, una ruta relativa al disco, nunca una URL: la URL se arma al renderizar, así que pasar de public a s3 más adelante necesita copiar los archivos y no reescribir nada en la base de datos.