cles systematiquement strippees du payload `changes` */ private const array SENSITIVE_KEYS = ['password', 'plainPassword', 'token', 'secret']; public function __construct( #[Autowire(service: 'doctrine.dbal.audit_connection')] private readonly Connection $connection, private readonly Security $security, private readonly RequestStack $requestStack, private readonly RequestIdProvider $requestIdProvider, ) {} /** * Ecrit une ligne d'audit. * * @param string $entityType Format "module.Entity" (ex: "core.User") * @param string $entityId ID de l'entite (int ou UUID serialise) * @param string $action create|update|delete * @param array $changes Payload JSON (filtre des cles sensibles) */ public function log( string $entityType, string $entityId, string $action, array $changes, ): void { $filteredChanges = $this->stripSensitive($changes); $this->connection->insert('audit_log', [ 'id' => Uuid::v7()->toRfc4122(), 'entity_type' => $entityType, 'entity_id' => $entityId, 'action' => $action, 'changes' => $filteredChanges, 'performed_by' => $this->security->getUser()?->getUserIdentifier() ?? 'system', 'performed_at' => new DateTimeImmutable('now', new DateTimeZone('UTC')), 'ip_address' => $this->requestStack->getCurrentRequest()?->getClientIp(), 'request_id' => $this->requestIdProvider->getRequestId(), ], [ // Types de conversion DBAL : UUID natif PG + jsonb + datetimetz. // Sans 'id' => GUID, DBAL passerait un varchar et Postgres ferait // un cast implicite — ca marche mais l'intention est floue. 'id' => Types::GUID, 'changes' => Types::JSON, 'performed_at' => Types::DATETIMETZ_IMMUTABLE, ]); } /** * Supprime recursivement les cles sensibles du payload. * * Utile pour les snapshots complets (create/delete) ou les changes * d'update : le listener prefiltre deja mais on garde cette garde * en defense-in-depth si un appelant direct oublie `#[AuditIgnore]`. * * Recursion : parcourt les sous-tableaux (ex: changes structures * `{field: {old, new}}`, snapshots avec relations imbriquees, ou * payload arbitraire pousse par un appelant direct). * * @param array $data * * @return array */ private function stripSensitive(array $data): array { foreach ($data as $key => $value) { if (in_array($key, self::SENSITIVE_KEYS, true)) { unset($data[$key]); continue; } if (is_array($value)) { $data[$key] = $this->stripSensitive($value); } } return $data; } }