Skip to content

Handlers: Implementación de Nuevos Jobs

◄ Anterior: API Endpoints | Índice | Siguiente: Multi-Tenant ►


Tabla de Contenidos


Interface JobHandlerInterface

Ubicación: Core/Interfaces/JobHandlerInterface.php

Namespace: App\Core\Interfaces

Propósito: Contrato que deben cumplir todos los handlers de jobs

Contrato:

php
interface JobHandlerInterface
{
    /**
     * Obtener tipo de job que maneja este handler
     *
     * @return string Tipo de job (ej: 'batch_invoicing')
     */
    public function getType(): string;

    /**
     * Ejecutar job con payload dado
     *
     * @param array $payload Datos necesarios para ejecutar
     * @return array Resultado del job
     * @throws Exception Si falla la ejecución
     */
    public function handle(array $payload): array;
}

Ejemplo: BatchInvoicingJobHandler

Ubicación: Ventas/Handlers/BatchInvoicingJobHandler.php

Namespace: App\Ventas\Handlers

Propósito: Handler para facturación masiva (ejemplo de implementación)

Type: 'batch_invoicing'

Payload esperado:

php
[
    'cliente_ids' => [1, 2, 3, 4, 5],
    'fecha' => '2026-02-05',
    'concepto' => 'Facturación mensual',
    'monto_base' => 1000.00
]

Result retornado:

php
[
    'facturas_creadas' => 5,
    'monto_total' => 5000.00,
    'factura_ids' => [101, 102, 103, 104, 105],
    'errores' => [] // Clientes que fallaron
]

Implementación:

php
class BatchInvoicingJobHandler implements JobHandlerInterface
{
    private FacturaService $facturaService;

    public function __construct(FacturaService $facturaService)
    {
        $this->facturaService = $facturaService;
    }

    public function getType(): string
    {
        return 'batch_invoicing';
    }

    public function handle(array $payload): array
    {
        // 1. Validar payload
        $this->validatePayload($payload);

        // 2. Extraer datos
        $clienteIds = $payload['cliente_ids'];
        $fecha = $payload['fecha'];
        $concepto = $payload['concepto'];
        $montoBase = $payload['monto_base'];

        // 3. Procesar batch
        $facturasCreadas = [];
        $errores = [];

        foreach ($clienteIds as $clienteId) {
            try {
                // 4. Reconstruir DTO que espera FacturaService::insert()
                $facturaDTO = new CreateFacturaDTO(
                    cliente_id: $clienteId,
                    fecha: $fecha,
                    items: [
                        [
                            'concepto' => $concepto,
                            'monto' => $montoBase
                        ]
                    ]
                );

                // 5. Delegar a service existente (NO modificado)
                $factura = $this->facturaService->insert($facturaDTO);

                $facturasCreadas[] = $factura->id;

            } catch (Exception $e) {
                $errores[] = [
                    'cliente_id' => $clienteId,
                    'error' => $e->getMessage()
                ];
            }
        }

        // 6. Retornar resultado consolidado
        return [
            'facturas_creadas' => count($facturasCreadas),
            'monto_total' => $montoBase * count($facturasCreadas),
            'factura_ids' => $facturasCreadas,
            'errores' => $errores
        ];
    }

    private function validatePayload(array $payload): void
    {
        $required = ['cliente_ids', 'fecha', 'concepto', 'monto_base'];
        foreach ($required as $field) {
            if (!isset($payload[$field])) {
                throw new InvalidArgumentException("Campo requerido: {$field}");
            }
        }
    }
}

Patrón Wrapper

Concepto: El handler NO modifica servicios existentes. En su lugar, envuelve (wraps) el service existente.

Puntos clave:

  1. NO modifica service existente: FacturaService queda intacto
  2. Reconstruye request DTO: Crea el mismo DTO que usaría el controller síncrono
  3. Delega lógica compleja: El service hace TODO el trabajo (validaciones, transacciones, etc.)
  4. Acumula resultados: Handler solo itera y consolida
  5. Manejo de errores: Captura exceptions por item, NO falla todo el batch

Ventajas del patrón:

  • ✅ CERO impacto en código existente
  • ✅ Service puede usarse sincrónicamente (original) o asincrónicamente (via handler)
  • ✅ Feature flag controlado (rollback instantáneo)
  • ✅ Fácil testing (unit tests del handler con mock del service)

Flujo visual:

Controller (síncrono)               JobHandler (asíncrono)
      ↓                                      ↓
  CreateDTO                              CreateDTO (x N)
      ↓                                      ↓
  Service::insert()  ← SHARED →    Service::insert() (x N)
      ↓                                      ↓
  Return DTO                          Accumulate results

Guía para Agregar Nuevos Handlers

Paso 1: Crear Clase Handler

Ubicación: {Modulo}/Handlers/{NombreJobHandler}.php

php
namespace App\{Modulo}\Handlers;

use App\Core\Interfaces\JobHandlerInterface;

class {NombreJobHandler} implements JobHandlerInterface
{
    public function __construct(
        // Inyectar services necesarios
        private {RelevantService} $service
    ) {}

    public function getType(): string
    {
        return '{tipo_job}'; // Ej: 'generate_report'
    }

    public function handle(array $payload): array
    {
        // 1. Validar payload
        // 2. Extraer datos
        // 3. Procesar (delegar a service)
        // 4. Retornar resultado
    }
}

Paso 2: Registrar Handler en DI Container

Ubicación: config/dependencies.php

php
$container->set(JobExecutor::class, function (ContainerInterface $c) {
    $executor = new JobExecutor(
        $c->get(JobRepository::class),
        $c->get(NotificationRepository::class),
        $c->get(ConnectionManager::class)
    );

    // Registrar handlers existentes
    $executor->registerHandler($c->get(BatchInvoicingJobHandler::class));

    // Registrar NUEVO handler
    $executor->registerHandler($c->get({NombreJobHandler}::class));

    return $executor;
});

Paso 3: Testing

Ubicación: Tests/Unit/{Modulo}/{NombreJobHandler}Test.php

php
public function testHandleProcessesPayloadCorrectly(): void
{
    // Arrange
    $mockService = $this->createMock({RelevantService}::class);
    $mockService->expects($this->once())
        ->method('processItem')
        ->willReturn($expectedResult);

    $handler = new {NombreJobHandler}($mockService);

    $payload = ['test' => 'data'];

    // Act
    $result = $handler->handle($payload);

    // Assert
    $this->assertEquals($expectedResult, $result);
}

Paso 4: Documentar Tipo de Job

Agregar a: docs/backend/background-jobs-handlers.md

markdown
### {tipo_job}

**Handler**: `{NombreJobHandler}`

**Payload**:
- `campo1` (tipo): Descripción
- `campo2` (tipo): Descripción

**Result**:
- `resultado1` (tipo): Descripción
- `resultado2` (tipo): Descripción

**Ejemplo**:
POST /api/jobs/{tipo_job}
{
  "payload": {
    "campo1": "valor"
  }
}

Checklist de Implementación

  • [ ] Crear clase handler que implemente JobHandlerInterface
  • [ ] Inyectar services necesarios via constructor
  • [ ] Implementar getType() retornando tipo único
  • [ ] Implementar handle(array $payload): array
  • [ ] Validar payload con método privado
  • [ ] Delegar procesamiento a service existente (patrón wrapper)
  • [ ] Acumular resultados y errores
  • [ ] Retornar array con resultado consolidado
  • [ ] Registrar handler en JobExecutor (dependencies.php)
  • [ ] Crear unit test mockeando service
  • [ ] Documentar tipo de job con payload y result
  • [ ] Probar flujo end-to-end (dispatch → execute → notify)

◄ Anterior: API Endpoints | Índice | Siguiente: Multi-Tenant ►