Appearance
Guard 42P01 — Tablas Opcionales en Modelos PHP
⚠️ DOCUMENTACION RETROSPECTIVA — Generada a partir de codigo implementado el 2026-05-18
Modulo: backend (patron transversal) Tipo: Patron tecnico Estado: Implementado Aplica a: Cualquier modelo PHP que acceda a tablas que no existen en todos los tenants
Contexto
El sistema Bautista es multi-tenant con schemas PostgreSQL por sucursal. Las migraciones usan shouldExecute() para decidir si corren en cada tenant. Esto significa que tablas como budget_prefa, preformul o prefa pueden no existir en schemas donde el modulo correspondiente no fue migrado.
Cuando un modelo hace un JOIN o SELECT contra una tabla inexistente, PostgreSQL retorna SQLSTATE 42P01 (undefined table), lo que provoca un crash del endpoint afectado.
Solucion: Guard con try/catch en la capa Model
La responsabilidad del guard es exclusiva del Model. El Service no necesita saber si la tabla existe o no: recibe el valor seguro que el Model le retorna (false / null / []).
Valores de retorno seguros por tipo de metodo
| Tipo de metodo | Retorna si tabla no existe |
|---|---|
existsBy*() | false |
getBy*() (uno) | null |
getBy*() (varios) | [] |
Implementacion del guard (referencia: BudgetPrefa)
El patron esta implementado en Modules/Crm/Infrastructure/Persistence/Models/BudgetPrefa.php. Los tres metodos publicos ilustran las tres variantes:
existsByBudgetId()— captura 42P01, retornafalsegetByBudgetId()— captura 42P01, retornanullgetByBudgetIds()— captura 42P01, retorna[]
La deteccion usa str_contains($e->getMessage(), '42P01'). Cualquier otro PDOException se re-lanza sin modificar.
Regla: nunca hacer JOIN a tablas opcionales en el Model principal
El modelo de la entidad principal (ej: Budget) no debe hacer JOIN directo a tablas opcionales. Antes del fix, Budget::getByCrmId() hacia LEFT JOIN budget_prefa y LEFT JOIN prefa, lo que causaba 42P01 en tenants sin esas tablas.
Patron correcto: los datos de tablas opcionales se cargan en la capa Service mediante batch loading, usando el Model de la tabla opcional con su guard propio.
Batch loading en el Service (referencia: BudgetService)
Implementado en Modules/Crm/Application/Services/BudgetService.php, metodo mapBudgetsWithBatchLoad().
El Service hace UNA query por relacion para todos los IDs del batch, incluyendo la tabla opcional:
$prefaByBudget = $this->budgetPrefaModel->getByBudgetIds($ids);Si la tabla no existe, getByBudgetIds() retorna [] gracias al guard, y todos los presupuestos del batch reciben prefa_id = null y factura_id = null sin error.
mapBudgetToDTO() acepta ?array $prefaData como parametro opcional con default null. Cuando viene null (llamada individual, no batch), hace la query individual con el mismo guard.
Como agregar este patron a una nueva tabla opcional
- Crear el Model de la tabla opcional con guard en todos los metodos publicos de lectura.
- No agregar JOINs a esa tabla en el Model de la entidad principal.
- En el Service, agregar la llamada batch al metodo
getBy*Ids()del Model opcional dentro del metodo de batch loading existente. - Pasar el resultado como parametro opcional al metodo
mapToDTO().
Tablas opcionales conocidas
| Tabla | Modulo | Guard implementado |
|---|---|---|
budget_prefa | CRM / Ventas (prefa) | Si — BudgetPrefa |
prefa | Ventas | Si — via JOIN en BudgetPrefa::getByBudgetIds() |
Relacion con el endpoint de capacidades
El frontend no deberia intentar acceder a funcionalidades que dependen de tablas opcionales sin saber si existen. Para eso existe el endpoint GET /mod-crm/features.
⚠️ NOTA IMPORTANTE: Validar con stakeholders antes de considerar final. Este documento fue generado retrospectivamente a partir del codigo implementado. Pueden existir tablas opcionales no listadas aqui.