<?php
declare(strict_types=1);
namespace App\Shared\Infrastructure\Observability;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Throwable;
/**
* FIX-260604-002 — Servicio auxiliar para promover [OBS-JFT-READONLY]
* de `error_log()` (sin sink visible en dev3 php-fpm 8.x) a Symfony Monolog
* via el {@see ObservabilityLogger} ya cableado al canal `expedion_audit`.
*
* Patron B (servicio auxiliar) elegido por HITL (2026-06-04): la DI directa
* en {@see \App\Shared\Infrastructure\Form\Type\JsonFieldType} no es trivial
* porque el guard `isEntityReadonlyByEstado()` es `static` (testabilidad sin
* DI). Resolvemos exponiendo el servicio via singleton-holder estatico
* inicializado por el container al boot del kernel.
*
* Bootstrap: la clase se etiqueta como `kernel.event_listener` en
* `kernel.request` (priority 256, mismo patron precedente del proyecto, p.ej.
* `ExpedienteHttpObservabilityListener`). Al construir el servicio, el
* container popula automaticamente {@see self::$instance}. El metodo
* `onKernelRequest` es no-op — solo sirve para forzar la instanciacion temprana.
*
* Si por alguna razon el container aun no ha booteado (p.ej. ejecucion CLI o
* test unitario sin kernel), {@see self::instance()} devuelve un fallback no-op
* que swallow logs en silencio — garantia FIX-260518-008: observabilidad NUNCA
* rompe el flujo principal.
*/
final class JftReadonlyObservabilityLogger
{
private static ?self $instance = null;
public function __construct(private ?ObservabilityLogger $observabilityLogger = null)
{
// Auto-publica el singleton al construirse por el container.
self::$instance = $this;
}
/**
* Listener no-op. Forza al container a instanciar el servicio en
* `kernel.request` para que `self::$instance` quede poblado antes
* de cualquier render de formulario Sonata.
*/
public function onKernelRequest(RequestEvent $event): void
{
// no-op intencional; el side-effect util es el constructor.
}
/**
* Devuelve el singleton activo. Si el container aun no lo ha instanciado
* (p.ej. test sin kernel boot, llamada muy temprana), devuelve un fallback
* no-op para no romper el flujo principal.
*/
public static function instance(): self
{
if (self::$instance === null) {
// Fallback no-op: sin logger inyectado, logReadonlyDecision swallow.
self::$instance = new self(null);
}
return self::$instance;
}
/**
* Permite a tests AAA inyectar/limpiar el singleton explicitamente.
*/
public static function setInstanceForTesting(?self $instance): void
{
self::$instance = $instance;
}
/**
* Emite el evento [OBS-JFT-READONLY] al canal Monolog `expedion_audit`
* via {@see ObservabilityLogger}. Si el observability logger no esta
* disponible (fallback no-op), swallow silencioso.
*
* @param array<string,mixed> $context
*/
public function logReadonlyDecision(array $context): void
{
try {
if ($this->observabilityLogger !== null) {
$this->observabilityLogger->event('[OBS-JFT-READONLY]', $context);
}
} catch (Throwable $e) {
// swallow — observabilidad NUNCA rompe el flujo principal.
}
}
}