Skip to content

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:

  1. La factory de JobExecutor en shared-definitions.php
  2. El script CLI background-worker.php
  3. 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.php bajo un named array 'job.handlers'
  • JobExecutor recibe 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) lanza NoHandlerException con el tipo desconocido en el mensaje
  • Tests unitarios usan mock de JobHandlerRegistry — no hay registerHandler() que llamar
  • El worker CLI obtiene JobExecutor via 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 ContainerInterface como 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
  • NoHandlerException con tipo en el mensaje habilita debugging inmediato
  • background-worker.php no 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' en shared-definitions.php
  • El container falla inmediatamente con error claro si la clase handler no existe — no en runtime
  • NoHandlerException es lanzada por JobHandlerRegistry::get() con el tipo desconocido en el mensaje, permitiendo debugging rápido
  • Tests unitarios usan mock de JobHandlerRegistry — no es necesario llamar registerHandler() en tests
  • El método registerHandler() ya no existe en JobExecutor

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á con NoHandlerException en 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 de JobHandlerRegistry
  • server/Core/Services/JobExecutor.php — eliminado método registerHandler(), recibe JobHandlerRegistry via constructor
  • server/Core/Services/JobHandlerRegistry.php (nuevo) — clase que indexa handlers por getType() y expone get(string $type): JobHandlerInterface

Referencias