Appearance
ADR-005: Job Handler Registry Auto-Discovery via PHP-DI Tagged Array
Fecha: 2026-02-23 Estado: Aceptado Deciders: Architecture Team, Backend Team
Contexto y Problema
Antes de este cambio, cada nueva implementación de JobHandlerInterface requería llamadas manuales a registerHandler() en TRES lugares distintos:
- La factory de
JobExecutorenshared-definitions.php - El script CLI
background-worker.php - Cualquier otro bootstrap que construya un
JobExecutor
Esto significaba que agregar un handler requería coordinar cambios en archivos no relacionados entre sí. El riesgo principal era olvidar registrar el handler en uno de esos lugares, produciendo un error silencioso que solo se descubría en el momento de ejecutar el job.
Problema: ¿Cómo registrar handlers una sola vez, con fallo inmediato y visible si falta alguno?
Opciones Consideradas
Opción A: JobHandlerRegistry con PHP-DI Named Array (SELECCIONADA)
Descripción:
- Se introduce
JobHandlerRegistry, una clase que recibe todos los handlers via constructor injection - Los handlers se indexan internamente por el valor que retorna su método
getType() - Los handlers se registran una única vez en
shared-definitions.phpbajo un named array'job.handlers' JobExecutorrecibe el registry como dependencia de constructor (autowired)- No existe método
registerHandler()en ninguna clase
Estructura de definiciones:
php
// server/container/shared-definitions.php
'job.handlers' => [
get(BatchInvoicingJobHandler::class),
get(OtroJobHandler::class),
// Agregar nuevos handlers aquí solamente
],
JobHandlerRegistry::class => fn($c) => new JobHandlerRegistry(
$c->get('job.handlers')
),Pros:
- Un único lugar de registro — agregar un handler requiere una sola línea en
shared-definitions.php - El container falla al construirse si cualquier clase handler no existe — error en build time, no en runtime
JobHandlerRegistry::get($type)lanzaNoHandlerExceptioncon el tipo desconocido en el mensaje- Tests unitarios usan mock de
JobHandlerRegistry— no hayregisterHandler()que llamar - El worker CLI obtiene
JobExecutorvia DI container — hereda el registry automáticamente
Contras:
- Requiere que todos los handlers sean definidos en el container DI (no pueden ser instancias ad-hoc)
- El named array
'job.handlers'no es autodescubierto — sigue siendo explícito, solo centralizado
Opción B: Query a ContainerInterface en runtime
Descripción:
- El registry recibe
ContainerInterfacecomo dependencia - Al solicitar un handler, hace
$container->get($type . 'Handler')por convención de nombre
Rechazado: Acopla el registry al DI container. Los errores solo aparecen en el momento de la primera ejecución de un job de ese tipo (runtime), no al construir el container. Además, rompe el principio de dependencias explícitas: es imposible saber qué handlers están disponibles sin inspeccionar el container.
Opción C: PHP-DI #[Tag] attribute autodiscovery
Descripción:
- Cada handler lleva
#[Tag('job.handler')]en su clase - PHP-DI descubre automáticamente todas las implementaciones taggeadas
Rechazado: El proyecto usa definiciones DI explícitas (shared-definitions.php). Mezclar registro por atributos y registro explícito genera inconsistencia en el grafo de dependencias y dificulta razonar sobre qué está disponible en el container. El beneficio de "cero configuración" no compensa la pérdida de trazabilidad.
Decisión
Seleccionamos Opción A: JobHandlerRegistry con PHP-DI Named Array
Justificación:
- Centraliza el registro en un único archivo sin introducir magia de autodiscovery
- Consistente con el estilo explícito existente del container (
shared-definitions.php) - Fallo en build time: el container no se construye si falta una clase handler
NoHandlerExceptioncon tipo en el mensaje habilita debugging inmediatobackground-worker.phpno necesita cambios cuando se agrega un nuevo handler
Consecuencias
Positivas
- Agregar un nuevo handler requiere cambios en UN solo lugar: añadir
get(NuevoHandler::class)al array'job.handlers'enshared-definitions.php - El container falla inmediatamente con error claro si la clase handler no existe — no en runtime
NoHandlerExceptiones lanzada porJobHandlerRegistry::get()con el tipo desconocido en el mensaje, permitiendo debugging rápido- Tests unitarios usan mock de
JobHandlerRegistry— no es necesario llamarregisterHandler()en tests - El método
registerHandler()ya no existe enJobExecutor
Negativas
- Los handlers deben estar definidos en el container DI — no se pueden instanciar fuera del container sin configuración explícita
- Si el array
'job.handlers'no se actualiza al agregar un handler, el job fallará conNoHandlerExceptionen lugar de un error de container (aunque este caso es equivalente en visibilidad)
Archivos Afectados
server/container/shared-definitions.php— define el array'job.handlers'y la factory deJobHandlerRegistryserver/Core/Services/JobExecutor.php— eliminado métodoregisterHandler(), recibeJobHandlerRegistryvia constructorserver/Core/Services/JobHandlerRegistry.php(nuevo) — clase que indexa handlers porgetType()y exponeget(string $type): JobHandlerInterface
Referencias
- ADR-004: Wrapper Pattern — patrón para implementar handlers
- ADR-006: Two-Phase CLI Bootstrap — cómo el worker CLI obtiene
JobExecutor - Backend: Arquitectura de Componentes