src/Shared/Infrastructure/Observability/JftReadonlyObservabilityLogger.php line 47

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Shared\Infrastructure\Observability;
  4. use Symfony\Component\HttpKernel\Event\RequestEvent;
  5. use Throwable;
  6. /**
  7.  * FIX-260604-002 — Servicio auxiliar para promover [OBS-JFT-READONLY]
  8.  * de `error_log()` (sin sink visible en dev3 php-fpm 8.x) a Symfony Monolog
  9.  * via el {@see ObservabilityLogger} ya cableado al canal `expedion_audit`.
  10.  *
  11.  * Patron B (servicio auxiliar) elegido por HITL (2026-06-04): la DI directa
  12.  * en {@see \App\Shared\Infrastructure\Form\Type\JsonFieldType} no es trivial
  13.  * porque el guard `isEntityReadonlyByEstado()` es `static` (testabilidad sin
  14.  * DI). Resolvemos exponiendo el servicio via singleton-holder estatico
  15.  * inicializado por el container al boot del kernel.
  16.  *
  17.  * Bootstrap: la clase se etiqueta como `kernel.event_listener` en
  18.  * `kernel.request` (priority 256, mismo patron precedente del proyecto, p.ej.
  19.  * `ExpedienteHttpObservabilityListener`). Al construir el servicio, el
  20.  * container popula automaticamente {@see self::$instance}. El metodo
  21.  * `onKernelRequest` es no-op — solo sirve para forzar la instanciacion temprana.
  22.  *
  23.  * Si por alguna razon el container aun no ha booteado (p.ej. ejecucion CLI o
  24.  * test unitario sin kernel), {@see self::instance()} devuelve un fallback no-op
  25.  * que swallow logs en silencio — garantia FIX-260518-008: observabilidad NUNCA
  26.  * rompe el flujo principal.
  27.  */
  28. final class JftReadonlyObservabilityLogger
  29. {
  30.     private static ?self $instance null;
  31.     public function __construct(private ?ObservabilityLogger $observabilityLogger null)
  32.     {
  33.         // Auto-publica el singleton al construirse por el container.
  34.         self::$instance $this;
  35.     }
  36.     /**
  37.      * Listener no-op. Forza al container a instanciar el servicio en
  38.      * `kernel.request` para que `self::$instance` quede poblado antes
  39.      * de cualquier render de formulario Sonata.
  40.      */
  41.     public function onKernelRequest(RequestEvent $event): void
  42.     {
  43.         // no-op intencional; el side-effect util es el constructor.
  44.     }
  45.     /**
  46.      * Devuelve el singleton activo. Si el container aun no lo ha instanciado
  47.      * (p.ej. test sin kernel boot, llamada muy temprana), devuelve un fallback
  48.      * no-op para no romper el flujo principal.
  49.      */
  50.     public static function instance(): self
  51.     {
  52.         if (self::$instance === null) {
  53.             // Fallback no-op: sin logger inyectado, logReadonlyDecision swallow.
  54.             self::$instance = new self(null);
  55.         }
  56.         return self::$instance;
  57.     }
  58.     /**
  59.      * Permite a tests AAA inyectar/limpiar el singleton explicitamente.
  60.      */
  61.     public static function setInstanceForTesting(?self $instance): void
  62.     {
  63.         self::$instance $instance;
  64.     }
  65.     /**
  66.      * Emite el evento [OBS-JFT-READONLY] al canal Monolog `expedion_audit`
  67.      * via {@see ObservabilityLogger}. Si el observability logger no esta
  68.      * disponible (fallback no-op), swallow silencioso.
  69.      *
  70.      * @param array<string,mixed> $context
  71.      */
  72.     public function logReadonlyDecision(array $context): void
  73.     {
  74.         try {
  75.             if ($this->observabilityLogger !== null) {
  76.                 $this->observabilityLogger->event('[OBS-JFT-READONLY]'$context);
  77.             }
  78.         } catch (Throwable $e) {
  79.             // swallow — observabilidad NUNCA rompe el flujo principal.
  80.         }
  81.     }
  82. }