<?php
namespace Products\SchoolNowBundle\Subscriber;
use App\Component\ViewLayer\Views\JsonView;
use Cms\CoreBundle\Service\ContextManager;
use Cms\TenantBundle\Model\SimpleTenantableInterface;
use Products\SchoolNowBundle\Controller\AbstractAdminApiController;
use Products\SchoolNowBundle\Controller\AbstractApiController;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Security;
/**
*
*/
final class ApiControllerSubscriber implements EventSubscriberInterface
{
// DI
protected Security $security;
protected ContextManager $cm;
/**
* {@inheritDoc}
*/
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => [
['onKernelRequest', 0],
],
KernelEvents::CONTROLLER => [
['onKernelController', 0],
],
KernelEvents::EXCEPTION => [
['onKernelException', 0],
],
KernelEvents::VIEW => [
['onKernelView', 0],
],
];
}
/**
* @param Security $security
* @param ContextManager $cm
*/
public function __construct(
Security $security,
ContextManager $cm
)
{
$this->security = $security;
$this->cm = $cm;
}
/**
* @param string|null $route
* @return bool
*/
protected function isApiRoute(?string $route): bool
{
if ( ! $route) {
return false;
}
return str_starts_with($route, AbstractApiController::ROUTING__PREFIX)
|| str_starts_with($route, AbstractAdminApiController::ROUTING__PREFIX);
}
/**
* @param string|null $route
* @return bool
*/
protected function isAdminApiRoute(?string $route): bool
{
if ( ! $route) {
return false;
}
return str_starts_with($route, AbstractAdminApiController::ROUTING__PREFIX);
}
/**
* @param RequestEvent $event
* @return void
*/
public function onKernelRequest(RequestEvent $event): void
{
// get controller
$route = $event->getRequest()->attributes->get('_route');
// make sure it is us
if ($this->isApiRoute($route) || $this->isAdminApiRoute($route)) {
// if the user is a profile, set the tenant in the context
$user = $this->security->getUser();
if ($user instanceof SimpleTenantableInterface && ! $this->cm->getGlobalContext()->getTenant()) {
$this->cm->getGlobalContext()->setTenant($user->getTenant());
}
}
}
/**
* @param ControllerEvent $event
* @return void
*/
public function onKernelController(ControllerEvent $event): void
{
// get controller
$route = $event->getRequest()->attributes->get('_route');
// make sure it is us
if ($this->isApiRoute($route)) {
// set timer
$event->getRequest()->attributes->set('_api_start', hrtime(true));
}
}
/**
* @param ExceptionEvent $event
* @return void
*/
public function onKernelException(ExceptionEvent $event): void
{
// get controller
$route = $event->getRequest()->attributes->get('_route');
// make sure it is us
if ($this->isApiRoute($route)) {
// determine the most appropriate http status code
$status = Response::HTTP_INTERNAL_SERVER_ERROR;
switch (true) {
case $event->getThrowable() instanceof AuthenticationException:
$status = Response::HTTP_UNAUTHORIZED;
break;
case $event->getThrowable() instanceof AccessDeniedHttpException:
$status = Response::HTTP_FORBIDDEN;
break;
}
// assemble json response and attach to event
$event->setResponse(new JsonResponse(
[
'error' => [
'status' => $status,
'code' => $event->getThrowable()->getCode(),
'message' => $event->getThrowable()->getMessage(),
'file' => $event->getThrowable()->getFile(),
'line' => $event->getThrowable()->getLine(),
'stack' => $event->getThrowable()->getTraceAsString(),
'previous' => $event->getThrowable()->getPrevious() ? [
'code' => $event->getThrowable()->getPrevious()->getCode(),
'message' => $event->getThrowable()->getPrevious()->getMessage(),
'file' => $event->getThrowable()->getPrevious()->getFile(),
'line' => $event->getThrowable()->getPrevious()->getLine(),
'stack' => $event->getThrowable()->getPrevious()->getTraceAsString(),
] : null,
],
'request' => [
'uri' => $event->getRequest()->getUri(),
'scheme' => $event->getRequest()->getScheme(),
'host' => $event->getRequest()->getHost(),
'path_info' => $event->getRequest()->getPathInfo(),
'query_string' => $event->getRequest()->getQueryString(),
'method' => $event->getRequest()->getMethod(),
'headers' => $event->getRequest()->headers->all(),
'request' => $event->getRequest()->request->all(),
'query' => $event->getRequest()->query->all(),
'body' => $event->getRequest()->getContent(),
],
],
$status,
));
}
}
/**
* @param ViewEvent $event
* @return void
*/
public function onKernelView(ViewEvent $event): void
{
// get controller
$route = $event->getRequest()->attributes->get('_route');
// make sure it is us
if ($this->isApiRoute($route)) {
// set timer
$event->getRequest()->attributes->set('_api_stop', $stop = hrtime(true));
// calc span
$event->getRequest()->attributes->set(
'_api_span',
$span = ($stop - $event->getRequest()->attributes->get('_api_start'))
);
// set in the meta
$view = $event->getControllerResult();
if ($view instanceof JsonView) {
// attach metadata for api timing
$view->setData(array_merge(
$view->getData(),
(isset($view->getData()['meta'])) ? [
'meta' => array_merge(
$view->getData()['meta'],
[
'tenant' => $this->cm->getGlobalContext()->getTenant()
? $this->cm->getGlobalContext()->getTenant()->getSlug()
: null,
'runtime' => ceil($span/1e+6),//nanoseconds to milliseconds
]
),
] : []
));
}
}
}
}