src/EventSubscriber/CrmAiExceptionSubscriber.php line 39

  1. <?php
  2. declare(strict_types=1);
  3. namespace App\EventSubscriber;
  4. use Psr\Log\LoggerInterface;
  5. use Symfony\Component\DependencyInjection\Attribute\Autowire;
  6. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  7. use Symfony\Component\HttpFoundation\JsonResponse;
  8. use Symfony\Component\HttpFoundation\Request;
  9. use Symfony\Component\HttpFoundation\Response;
  10. use Symfony\Component\HttpKernel\Event\ExceptionEvent;
  11. use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
  12. use Symfony\Component\HttpKernel\KernelEvents;
  13. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  14. use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  15. use Symfony\Component\Security\Core\User\UserInterface;
  16. final class CrmAiExceptionSubscriber implements EventSubscriberInterface
  17. {
  18.     public function __construct(
  19.         #[Autowire(service'monolog.logger.crm_ai')]
  20.         private readonly LoggerInterface $crmAiLogger,
  21.         private readonly TokenStorageInterface $tokenStorage,
  22.     ) {
  23.     }
  24.     /**
  25.      * @return array<string, array{0: string, 1: int}>
  26.      */
  27.     public static function getSubscribedEvents(): array
  28.     {
  29.         return [
  30.             KernelEvents::EXCEPTION => ['onKernelException'64],
  31.         ];
  32.     }
  33.     public function onKernelException(ExceptionEvent $event): void
  34.     {
  35.         $request $event->getRequest();
  36.         if (!$this->isCrmAiRequest($request)) {
  37.             return;
  38.         }
  39.         $exception $event->getThrowable();
  40.         $diagnosticId $this->createDiagnosticId();
  41.         $statusCode $this->resolveStatusCode($exception);
  42.         $this->logException($request$exception$diagnosticId$statusCode);
  43.         $message $statusCode === Response::HTTP_FORBIDDEN
  44.             'Nie udało się uruchomić rozmowy z asystentem. Brak dostępu albo sesja/token bezpieczeństwa są nieaktualne.'
  45.             'Nie udało się wykonać akcji asystenta CRM.';
  46.         $event->setResponse(new JsonResponse([
  47.             'error' => $message ' Kod diagnostyczny: ' $diagnosticId '.',
  48.             'diagnostic_id' => $diagnosticId,
  49.             'diagnostic_reason' => $statusCode === Response::HTTP_FORBIDDEN 'access_denied' 'crm_ai_exception',
  50.             'support_message' => 'Podaj ten kod administratorowi CRM.',
  51.         ], $statusCode));
  52.     }
  53.     private function isCrmAiRequest(Request $request): bool
  54.     {
  55.         $path $request->getPathInfo();
  56.         return str_starts_with($path'/admin/ai/')
  57.             || str_starts_with($path'/consultant/ai/');
  58.     }
  59.     private function resolveStatusCode(\Throwable $exception): int
  60.     {
  61.         if ($exception instanceof AccessDeniedException) {
  62.             return Response::HTTP_FORBIDDEN;
  63.         }
  64.         if ($exception instanceof HttpExceptionInterface) {
  65.             return $exception->getStatusCode();
  66.         }
  67.         return Response::HTTP_INTERNAL_SERVER_ERROR;
  68.     }
  69.     private function createDiagnosticId(): string
  70.     {
  71.         return 'crm-ai-exception-' date('Ymd-His') . '-' bin2hex(random_bytes(4));
  72.     }
  73.     private function logException(Request $request\Throwable $exceptionstring $diagnosticIdint $statusCode): void
  74.     {
  75.         $user $this->tokenStorage->getToken()?->getUser();
  76.         $roles $user instanceof UserInterface $user->getRoles() : [];
  77.         $context = [
  78.             'diagnostic_id' => $diagnosticId,
  79.             'status_code' => $statusCode,
  80.             'path' => $request->getPathInfo(),
  81.             'route' => $request->attributes->get('_route'),
  82.             'method' => $request->getMethod(),
  83.             'user_id' => $user !== null && method_exists($user'getId') ? $user->getId() : null,
  84.             'user_identifier' => $user instanceof UserInterface $user->getUserIdentifier() : null,
  85.             'user_roles' => array_values(array_unique($roles)),
  86.             'csrf_header_present' => $request->headers->has('X-CSRF-TOKEN'),
  87.             'csrf_header_length' => strlen((string) $request->headers->get('X-CSRF-TOKEN''')),
  88.             'ajax_header' => $request->headers->get('X-Requested-With'),
  89.             'client_ip' => $request->getClientIp(),
  90.             'user_agent' => $request->headers->get('User-Agent'),
  91.             'exception_class' => $exception::class,
  92.             'exception_message' => $exception->getMessage(),
  93.             'exception_file' => $exception->getFile(),
  94.             'exception_line' => $exception->getLine(),
  95.         ];
  96.         if ($statusCode >= Response::HTTP_INTERNAL_SERVER_ERROR) {
  97.             $this->crmAiLogger->error('CRM AI request failed before controller response.'$context);
  98.             return;
  99.         }
  100.         $this->crmAiLogger->warning('CRM AI request was blocked before controller response.'$context);
  101.     }
  102. }