<?php
namespace App\EventSubscriber;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ApiRequestLogSubscriber implements EventSubscriberInterface
{
private const IGNORED_ROUTES = [
'app_api_overall_saleorder_getsaleorders',
];
public function __construct(private readonly LoggerInterface $logger)
{
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::CONTROLLER => 'onKernelController',
KernelEvents::RESPONSE => 'onKernelResponse',
];
}
public function onKernelController(ControllerEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$controller = $event->getController();
if (is_array($controller)) {
$controllerClass = get_class($controller[0]);
$controllerMethod = $controller[1];
} elseif (is_object($controller)) {
$controllerClass = get_class($controller);
$controllerMethod = '__invoke';
} else {
return;
}
if (!str_starts_with($controllerClass, 'App\\Controller\\API\\')) {
return;
}
$request = $event->getRequest();
if ($this->shouldIgnoreRoute($request)) {
return;
}
$this->logger->info('API call', [
'route' => $request->attributes->get('_route'),
'method' => $request->getMethod(),
'path' => $request->getPathInfo(),
'query' => $request->query->all(),
'controller' => $controllerClass . '::' . $controllerMethod
]);
}
public function onKernelResponse(ResponseEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
if ($this->shouldIgnoreRoute($request)) {
return;
}
if (!$this->isApiController($request)) {
return;
}
$controller = $request->attributes->get('_controller');
if (!is_string($controller)) {
return;
}
$response = $event->getResponse();
$statusCode = $response->getStatusCode();
$context = [
'route' => $request->attributes->get('_route'),
'method' => $request->getMethod(),
'path' => $request->getPathInfo(),
'controller' => $controller,
'status_code' => $statusCode,
];
if ($statusCode >= 400) {
$errorMessage = $this->extractErrorMessage($response);
if ($errorMessage !== null) {
$context['error_message'] = $errorMessage;
}
$this->logger->error('API response', $context);
return;
}
$this->logger->info('API response', $context);
}
private function isApiController(Request $request): bool
{
$controller = $request->attributes->get('_controller');
if (is_array($controller)) {
$controller = get_class($controller[0]) . '::' . $controller[1];
} elseif (is_object($controller)) {
$controller = get_class($controller) . '::__invoke';
}
return is_string($controller) && str_starts_with($controller, 'App\\Controller\\API\\');
}
private function extractErrorMessage(Response $response): ?string
{
$content = trim($response->getContent());
if ($content === '') {
return Response::$statusTexts[$response->getStatusCode()] ?? null;
}
$contentType = strtolower((string) $response->headers->get('content-type'));
if (str_contains($contentType, 'application/json') || str_contains($contentType, '+json')) {
$decoded = json_decode($content, true);
if (json_last_error() === JSON_ERROR_NONE) {
if (is_string($decoded)) {
return $decoded;
}
if (is_array($decoded)) {
foreach (['message', 'error_description', 'detail', 'title', 'error'] as $key) {
$value = $decoded[$key] ?? null;
if (is_string($value) && trim($value) !== '') {
return $value;
}
}
if (isset($decoded['error']) && is_array($decoded['error'])) {
foreach (['message', 'error_description', 'detail', 'title', 'error'] as $key) {
$value = $decoded['error'][$key] ?? null;
if (is_string($value) && trim($value) !== '') {
return $value;
}
}
}
}
}
}
if (strlen($content) > 500) {
return substr($content, 0, 500) . '...[truncated]';
}
return $content;
}
private function shouldIgnoreRoute(Request $request): bool
{
return in_array((string) $request->attributes->get('_route'), self::IGNORED_ROUTES, true);
}
}