Passed
Pull Request — main (#465)
by MusikAnimal
03:37 queued 27s
created

ExceptionListener::onKernelException()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 39
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 25
nc 5
nop 1
dl 0
loc 39
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\EventSubscriber;
6
7
use App\Controller\XtoolsController;
8
use App\Exception\XtoolsHttpException;
9
use App\Helper\I18nHelper;
10
use Psr\Log\LoggerInterface;
11
use Symfony\Component\ErrorHandler\Exception\FlattenException;
12
use Symfony\Component\HttpFoundation\JsonResponse;
13
use Symfony\Component\HttpFoundation\RedirectResponse;
14
use Symfony\Component\HttpFoundation\Response;
15
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
16
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
17
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
18
use Symfony\Component\Serializer\Normalizer\ProblemNormalizer;
19
use Throwable;
20
use Twig\Environment;
21
use Twig\Error\RuntimeError;
22
23
/**
24
 * A ExceptionListener ensures Twig exceptions are properly
25
 * handled, so that a friendly error page is shown to the user.
26
 */
27
class ExceptionListener
28
{
29
    protected Environment $templateEngine;
30
    protected FlashBagInterface $flashBag;
31
    protected I18nHelper $i18n;
32
    protected LoggerInterface $logger;
33
34
    /** @var string The environment. */
35
    protected string $environment;
36
37
    /**
38
     * Constructor for the ExceptionListener.
39
     * @param Environment $templateEngine
40
     * @param LoggerInterface $logger
41
     * @param FlashBagInterface $flashBag
42
     * @param I18nHelper $i18n
43
     * @param string $environment
44
     */
45
    public function __construct(
46
        Environment $templateEngine,
47
        LoggerInterface $logger,
48
        FlashBagInterface $flashBag,
49
        I18nHelper $i18n,
50
        string $environment = 'prod'
51
    ) {
52
        $this->templateEngine = $templateEngine;
53
        $this->logger = $logger;
54
        $this->flashBag = $flashBag;
55
        $this->i18n = $i18n;
56
        $this->environment = $environment;
57
    }
58
59
    /**
60
     * Capture the exception, check if it's a Twig error and if so
61
     * throw the previous exception, which should be more meaningful.
62
     * @param ExceptionEvent $event
63
     */
64
    public function onKernelException(ExceptionEvent $event): void
65
    {
66
        $exception = $event->getThrowable();
67
68
        // We only care about the previous (original) exception, not the one Twig put on top of it.
69
        $prevException = $exception->getPrevious();
70
71
        $isApi = '/api/' === substr($event->getRequest()->getRequestUri(), 0, 5);
72
73
        if ($exception instanceof XtoolsHttpException && !$isApi) {
74
            $response = $this->getXtoolsHttpResponse($exception);
75
        } elseif ($exception instanceof RuntimeError && null !== $prevException) {
76
            $response = $this->getTwigErrorResponse($prevException);
77
        } elseif ($exception instanceof AccessDeniedHttpException) {
78
            // FIXME: For some reason the automatic error page rendering doesn't work for 403 responses...
79
            $response = new Response(
80
                $this->templateEngine->render('bundles/TwigBundle/Exception/error.html.twig', [
81
                    'status_code' => $exception->getStatusCode(),
82
                    'status_text' => 'Forbidden',
83
                    'exception' => $exception,
84
                ])
85
            );
86
        } elseif ($isApi && 'json' === $event->getRequest()->get('format', 'json')) {
87
            $normalizer = new ProblemNormalizer('prod' !== $this->environment);
88
            $params = array_merge(
89
                $normalizer->normalize(FlattenException::createFromThrowable($exception)),
90
                $event->getRequest()->attributes->get('_route_params') ?? [],
91
            );
92
            $params['title'] = $params['detail'];
93
            $params['detail'] = $this->i18n->msgIfExists($exception->getMessage(), [$exception->getCode()]);
94
            $response = new JsonResponse(
95
                XtoolsController::normalizeApiProperties($params)
96
            );
97
        } else {
98
            return;
99
        }
100
101
        // sends the modified response object to the event
102
        $event->setResponse($response);
103
    }
104
105
    /**
106
     * Handle an XtoolsHttpException, either redirecting back to the configured URL,
107
     * or in the case of API requests, return the error in a JsonResponse.
108
     * @param XtoolsHttpException $exception
109
     * @return JsonResponse|RedirectResponse
110
     */
111
    private function getXtoolsHttpResponse(XtoolsHttpException $exception)
112
    {
113
        if ($exception->isApi()) {
114
            $this->flashBag->add('error', $exception->getMessage());
115
            $flashes = $this->flashBag->peekAll();
116
            $this->flashBag->clear();
117
            return new JsonResponse(array_merge(
118
                array_merge($flashes, FlattenException::createFromThrowable($exception)->toArray()),
119
                $exception->getParams()
120
            ), $exception->getStatusCode());
121
        }
122
123
        return new RedirectResponse($exception->getRedirectUrl());
124
    }
125
126
    /**
127
     * Handle a Twig runtime exception.
128
     * @param Throwable $exception
129
     * @return Response
130
     * @throws Throwable
131
     */
132
    private function getTwigErrorResponse(Throwable $exception): Response
133
    {
134
        if ('prod' !== $this->environment) {
135
            throw $exception;
136
        }
137
138
        // Log the exception, since we're handling it and it won't automatically be logged.
139
        $file = explode('/', $exception->getFile());
140
        $this->logger->error(
141
            '>>> CRITICAL (\''.$exception->getMessage().'\' - '.
142
            end($file).' - line '.$exception->getLine().')'
143
        );
144
145
        return new Response(
146
            $this->templateEngine->render('bundles/TwigBundle/Exception/error.html.twig', [
147
                'status_code' => Response::HTTP_INTERNAL_SERVER_ERROR,
148
                'status_text' => 'Internal Server Error',
149
                'exception' => $exception,
150
            ]),
151
            Response::HTTP_INTERNAL_SERVER_ERROR
152
        );
153
    }
154
}
155