Appearance
ADR-006: Two-Phase CLI Bootstrap
Fecha: 2026-02-19 Estado: Aceptado Deciders: Architecture Team, Backend Team
Contexto y Problema
El worker CLI (cli/background-worker.php) ejecuta jobs en background sin contexto HTTP. No hay JWT token, no hay request, no hay headers.
Sin embargo, BatchInvoicingOrchestrator tiene una cadena de dependencias que requiere datos del JWT:
BatchInvoicingOrchestrator
→ BatchInvoicingAuditService → necesita Payload (cuit, nro_cliente)
→ ArcaClientFactory → necesita Payload (cuit)El objeto Payload normalmente se construye desde el JWT token en contexto HTTP. En CLI, este token no existe.
Problema: ¿Cómo proporcionar contexto de ejecución (cuit, nro_cliente, id_usuario, etc.) al worker CLI sin JWT?
Decisión
Two-Phase CLI Bootstrap: El worker ejecuta en dos fases separadas.
Fase 1 — Lectura minima del job (sin DI container)
El worker recibe {job_id} {schema} {db} como argumentos CLI (pasados por JobDispatcher). Luego hace una conexion PDO directa y minima a DB_INI para leer solo el payload del job:
php
// cli/background-worker.php - Fase 1
$jobId = (int) $argv[1];
$schema = trim($argv[2]);
$db = trim($argv[3]);
// Conexion minima a DB_INI (sin DI container)
$pdo = new PDO($dsn, $user, $password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$stmt = $pdo->prepare('SELECT payload FROM background_jobs WHERE id = :id LIMIT 1');
$stmt->execute(['id' => $jobId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
// Extraer contexto del payload
$jobPayload = json_decode($row['payload'], true);
$context = $jobPayload['_context'] ?? [];
// $context = ['cuit' => '...', 'nro_cliente' => '...', 'id_usuario' => 1,
// 'id' => 1, 'sistema' => '...', 'schema_level' => '...', 'prueba' => bool]
// Propagar contexto al bootstrap via variables de entorno
putenv("CLI_JOB_DB={$db}");
putenv("CLI_JOB_SCHEMA={$schema}");
putenv('CLI_ARCA_CUIT=' . ($context['cuit'] ?? '0'));
putenv('CLI_ARCA_NRO_CLIENTE=' . ($context['nro_cliente'] ?? '0'));
putenv('CLI_PAYLOAD_ID_USUARIO='. ($context['id_usuario'] ?? '0'));
putenv('CLI_PAYLOAD_ID=' . ($context['id'] ?? '0'));
putenv('CLI_PAYLOAD_SISTEMA=' . ($context['sistema'] ?? '0'));
putenv('CLI_JOB_PRUEBA=' . (!empty($context['prueba']) ? '1' : '0'));
$pdo = null; // Cerrar conexion de Fase 1Fase 2 — Bootstrap completo con Payload sintetico
bootstrap-cli.php lee las variables de entorno establecidas en Fase 1 y construye:
- ConnectionManager con conexion
oficialapuntando a$dby conexioniniaDB_INI - Payload sintetico con los datos del
_context - DI container con shared-definitions + overrides CLI
php
// cli/bootstrap-cli.php
$cliJobDb = getenv('CLI_JOB_DB');
$cliJobSchema = getenv('CLI_JOB_SCHEMA') ?: 'public';
// ... demas env vars ...
$connectionManager = new ConnectionManager();
$connectionManager->setConfig('oficial', ['database' => $cliJobDb, /* ... */]);
$connectionManager->setConfig('ini', ['database' => DB_INI, 'skip_schema_context' => true]);
$connectionManager->setAlias('principal', 'oficial');
$syntheticPayload = new Payload(
id: $payloadId,
db: $cliJobDb,
schema: $cliJobSchema,
sistema: $payloadSistema,
nro_cliente: $arcaNroCliente,
id_usuario: $payloadIdUsuario,
cuit: $arcaCuit,
// ... timestamps JWT sinteticos ...
);
// DI container inyecta Payload, ConnectionManager y Logger como overrides CLI
$container = $containerBuilder->build();
return $container;De vuelta en background-worker.php, Fase 2 configura el schema y ejecuta:
php
// cli/background-worker.php - Fase 2
$container = require __DIR__ . '/bootstrap-cli.php';
$connectionManager = $container->get(ConnectionManager::class);
$connectionManager->setSchemaContext($schema);
$executor = $container->get(JobExecutor::class);
$executor->execute($jobId);JobController almacena _context en el payload
Cuando JobController despacha un job, copia los datos relevantes del JWT Payload al payload del job bajo la clave _context:
php
// JobController::dispatch()
$jobPayload = $body['payload'] ?? [];
// Guardar contexto JWT para que el worker CLI pueda reconstruir Payload
$jobPayload['_context'] = [
'cuit' => $this->payload->cuit,
'nro_cliente' => $this->payload->nro_cliente,
'id_usuario' => $this->payload->id_usuario,
'id' => $this->payload->id,
'sistema' => $this->payload->sistema,
'schema_level' => $schemaLevel,
'prueba' => $prueba,
];
$jobId = $this->dispatcher->dispatch($type, $jobPayload, $userId, $schema, $db, $nroSistema, $prueba);Consecuencias
Positivas
- ✅ Workers son completamente autosuficientes (no necesitan JWT al iniciar)
- ✅ El contexto viaja con el job payload (serializado en BD)
- ✅ No requiere modificar
BatchInvoicingOrchestratorni sus dependencias - ✅ Consistente con ADR-004 (Wrapper Pattern): no se modifica código existente
- ✅ Fase 1 es extremadamente liviana (solo PDO, sin framework)
- ✅ Cualquier servicio que dependa de Payload funciona transparentemente
Negativas
- ❌ Datos de contexto duplicados (JWT + payload
_context) - ❌ Si Payload agrega campos nuevos, hay que actualizar
_context - ❌ Dos fases de bootstrap aumentan complejidad del worker
Alternativas Rechazadas
Alternativa A: Pasar credenciales como argumentos CLI
bash
php cli/background-worker.php 123 --cuit=20123456789 --nro_cliente=1 --id_usuario=5Rechazado: Los argumentos CLI son visibles en la lista de procesos (ps aux). Datos como CUIT y credenciales quedarían expuestos. Riesgo de seguridad inaceptable.
Alternativa B: Almacenar JWT token en la BD
sql
INSERT INTO background_jobs (... jwt_token ...) VALUES (... $token ...)Rechazado: Almacenar JWT completo en BD es un riesgo de seguridad. Si la BD es comprometida, los tokens permiten suplantar usuarios. Además, tokens JWT expiran, y el worker podría ejecutar después de la expiración.
Alternativa C: Bootstrap único con lazy-loading de Payload
Rechazado: Requeriría que todos los servicios soporten Payload nullable o lazy, lo cual implicaría refactorizar BatchInvoicingAuditService, ArcaClientFactory, etc. Viola ADR-004 (no modificar código existente).