Passed
Push — swagger-docs ( 6837d7 )
by MusikAnimal
11:33
created

ExceptionListener::onKernelException()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 39
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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