src/EventSubscriber/ApiRequestLoggingSubscriber.php line 34

Open in your IDE?
  1. <?php
  2. namespace App\EventSubscriber;
  3. use App\Entity\ApiClient;
  4. use App\Entity\ApiRequestLog;
  5. use App\Entity\User;
  6. use App\Security\ApiTokenAuthenticator;
  7. use Doctrine\ORM\EntityManagerInterface;
  8. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  9. use Symfony\Component\HttpKernel\Event\RequestEvent;
  10. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  11. use Symfony\Component\HttpKernel\KernelEvents;
  12. class ApiRequestLoggingSubscriber implements EventSubscriberInterface
  13. {
  14.     private const START_ATTR '_api_start_time';
  15.     private EntityManagerInterface $em;
  16.     public function __construct(EntityManagerInterface $em)
  17.     {
  18.         $this->em $em;
  19.     }
  20.     public static function getSubscribedEvents(): array
  21.     {
  22.         return [
  23.             KernelEvents::REQUEST => ['onRequest'1024],
  24.             KernelEvents::RESPONSE => ['onResponse', -1024],
  25.         ];
  26.     }
  27.     public function onRequest(RequestEvent $event): void
  28.     {
  29.         $request $event->getRequest();
  30.         if (!str_starts_with($request->getPathInfo(), '/api/v1')) {
  31.             return;
  32.         }
  33.         $request->attributes->set(self::START_ATTRmicrotime(true));
  34.     }
  35.     public function onResponse(ResponseEvent $event): void
  36.     {
  37.         $request $event->getRequest();
  38.         if (!str_starts_with($request->getPathInfo(), '/api/v1')) {
  39.             return;
  40.         }
  41.         $response $event->getResponse();
  42.         $start $request->attributes->get(self::START_ATTR);
  43.         $duration $start ? (int) ((microtime(true) - $start) * 1000) : null;
  44.         $this->attachRateLimitHeaders($request$response);
  45.         $log = new ApiRequestLog();
  46.         $log->setMethod($request->getMethod());
  47.         $log->setPath($request->getPathInfo());
  48.         $log->setStatusCode($response->getStatusCode());
  49.         $log->setIp($request->getClientIp());
  50.         $log->setUserAgent(substr((string) $request->headers->get('User-Agent'''), 0255));
  51.         $log->setDurationMs($duration);
  52.         $client $request->attributes->get(ApiTokenAuthenticator::ATTR_CLIENT);
  53.         if ($client instanceof ApiClient) {
  54.             $log->setApiClient($client);
  55.         }
  56.         $impersonated $request->attributes->get(ApiTokenAuthenticator::ATTR_IMPERSONATED_BY);
  57.         if ($impersonated instanceof User) {
  58.             $log->setImpersonatedByUser($impersonated);
  59.         }
  60.         $errorCode $this->extractErrorCode($response);
  61.         if ($errorCode !== null) {
  62.             $log->setErrorCode($errorCode);
  63.         }
  64.         try {
  65.             $this->em->persist($log);
  66.             $this->em->flush();
  67.         } catch (\Throwable $e) {
  68.             // logging must never break the response
  69.         }
  70.     }
  71.     private function attachRateLimitHeaders($request$response): void
  72.     {
  73.         $minute $request->attributes->get(ApiTokenAuthenticator::ATTR_RATE_MINUTE);
  74.         $hour $request->attributes->get(ApiTokenAuthenticator::ATTR_RATE_HOUR);
  75.         if (is_array($minute)) {
  76.             $response->headers->set('X-RateLimit-Limit', (string) $minute['limit']);
  77.             $response->headers->set('X-RateLimit-Remaining', (string) $minute['remaining']);
  78.             $response->headers->set('X-RateLimit-Reset', (string) $minute['reset']);
  79.         }
  80.         if (is_array($hour)) {
  81.             $response->headers->set('X-RateLimit-Hour-Limit', (string) $hour['limit']);
  82.             $response->headers->set('X-RateLimit-Hour-Remaining', (string) $hour['remaining']);
  83.         }
  84.     }
  85.     private function extractErrorCode($response): ?string
  86.     {
  87.         if ($response->getStatusCode() < 400) {
  88.             return null;
  89.         }
  90.         $content $response->getContent();
  91.         if (!is_string($content) || $content === '') {
  92.             return null;
  93.         }
  94.         $data json_decode($contenttrue);
  95.         return is_array($data) && isset($data['error']['code']) ? (string) $data['error']['code'] : null;
  96.     }
  97. }