Appearance
Patrón de Storage Local — Convención de Rutas
Tipo: Decisión Arquitectónica
Estado: En Uso (desde abril 2026)
Alcance: Todos los módulos que gestionen archivos/imágenes
Problema
El almacenamiento de archivos necesita una estructura predecible que:
- Aísle archivos por tenant (
nro_sistema) - Sea extensible a múltiples recursos (
productos,crm, etc.) - No dependa del schema de sucursal en el path físico — el UUID garantiza unicidad
Convención de Ruta
storage/images/{nro_sistema}/{recurso}/{id_entidad}/{uuid}.{ext}| Segmento | Descripción | Ejemplo |
|---|---|---|
storage/images/ | Raíz fija del storage de imágenes | — |
{nro_sistema} | Identificador del tenant | emp001 |
{recurso} | Nombre del módulo/entidad | productos, crm |
{id_entidad} | ID de la entidad dueña del archivo | 42 |
{uuid}.webp | Nombre único generado — garantiza unicidad sin schema | a3f2...webp |
Ejemplo concreto:
storage/images/emp001/productos/42/a3f2c1d8-...webp
storage/images/emp001/crm/15/b9e4a2f1-...webp ← futuroPrincipio clave: LocalImageStorage es genérico
LocalImageStorage no sabe de productos. No tiene el recurso hardcodeado. Solo conoce la raíz storage/images/. El sub-path del recurso es responsabilidad del caller.
LocalImageStorage StorageContext
───────────────── vs. ─────────────────
Sabe: storage/images/ Provee: {nroSistema}
No sabe: productos/ Provee: {resource} ← "productos", "crm", etc.
Provee: {entityId}StorageContext
El contexto que se pasa al storage encapsula los tres componentes variables del path:
php
final class StorageContext
{
public function __construct(
public readonly string $nroSistema, // tenant
public readonly string $resource, // 'productos', 'crm', etc.
public readonly int $entityId, // id de la entidad
) {}
}El path resultante que construye LocalImageStorage:
php
sprintf(
'storage/images/%s/%s/%d/%s.webp',
$ctx->nroSistema,
$ctx->resource,
$ctx->entityId,
$uuid
)Por qué no incluir el schema de sucursal
Los archivos físicos no se duplican por sucursal. Si suc0001 y suc0002 ambas tienen el producto #42, cada imagen subida genera su propio UUID. El schema en el path no aportaba aislamiento real — solo añadía un segmento redundante.
❌ storage/images/productos/emp001/suc0001/42/uuid.webp ← schema en el medio
✅ storage/images/emp001/productos/42/uuid.webp ← tenant al tope, uuid diferenciaEl schema de sucursal sigue siendo relevante a nivel de tabla (producto_imagen existe en cada schema). El aislamiento de datos ocurre en la base de datos, no en el filesystem.
Registrar un nuevo recurso con imágenes
Al agregar soporte de imágenes a un nuevo módulo (ejemplo: CRM):
StorageContextno cambia — solo pasarresource: 'crm'LocalImageStorageno cambia — es genérico- El controller del módulo construye el contexto con el resource correcto:
php
$ctx = new StorageContext(
nroSistema: (string) ($payload['sistema'] ?? ''),
resource: 'crm', // ← único cambio
entityId: $idContacto,
);- Crear la tabla
{entidad}_imagenequivalente aproducto_imagen - El service del módulo usa
ProductoImagenServicecomo referencia
Módulos con imágenes
| Módulo | Resource | Estado |
|---|---|---|
| Ventas — Productos | productos | ✅ Implementado |
| CRM | crm | Planificado |
Referencia de implementación
Modules/Ventas/Infrastructure/Storage/LocalImageStorage.phpModules/Ventas/Infrastructure/Storage/StorageContext.phpModules/Ventas/Infrastructure/Storage/ImageStorageStrategyInterface.php