• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

systemsdk / docker-symfony-api / #83

pending completion
#83

push

DKravtsov
Updated composer dependencies, refactoring.

45 of 45 new or added lines in 12 files covered. (100.0%)

1483 of 2844 relevant lines covered (52.14%)

22.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.66
/src/General/Transport/EventSubscriber/ExceptionSubscriber.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace App\General\Transport\EventSubscriber;
6

7
use App\General\Application\Exception\Interfaces\ClientErrorInterface;
8
use App\General\Domain\Utils\JSON;
9
use App\User\Application\Security\UserTypeIdentification;
10
use Doctrine\DBAL\Exception;
11
use Doctrine\ORM\Exception\ORMException;
12
use JsonException;
13
use Psr\Log\LoggerInterface;
14
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15
use Symfony\Component\HttpFoundation\Response;
16
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
17
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
18
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
19
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
20
use Symfony\Component\Security\Core\Exception\AuthenticationException;
21
use Throwable;
22

23
use function array_intersect;
24
use function array_key_exists;
25
use function class_implements;
26
use function in_array;
27
use function spl_object_hash;
28

29
/**
30
 * Class ExceptionSubscriber
31
 *
32
 * @package App\General
33
 */
34
class ExceptionSubscriber implements EventSubscriberInterface
35
{
36
    /**
37
     * @var array<string, bool>
38
     */
39
    private static array $cache = [];
40

41
    /**
42
     * @var array<int, class-string>
43
     */
44
    private static array $clientExceptions = [
45
        HttpExceptionInterface::class,
46
        ClientErrorInterface::class,
47
    ];
48

49
    public function __construct(
50
        private readonly LoggerInterface $logger,
51
        private readonly UserTypeIdentification $userService,
52
        private readonly string $environment,
53
    ) {
54
    }
148✔
55

56
    /**
57
     * {@inheritdoc}
58
     */
59
    public static function getSubscribedEvents(): array
60
    {
61
        return [
×
62
            ExceptionEvent::class => [
×
63
                'onKernelException',
×
64
                -100,
×
65
            ],
×
66
        ];
×
67
    }
68

69
    /**
70
     * Method to handle kernel exception.
71
     *
72
     * @throws JsonException
73
     */
74
    public function onKernelException(ExceptionEvent $event): void
75
    {
76
        // Get exception from current event
77
        $exception = $event->getThrowable();
71✔
78
        // Log  error
79
        $this->logger->error((string)$exception);
71✔
80
        // Create new response
81
        $response = new Response();
71✔
82
        $response->headers->set('Content-Type', 'application/json');
71✔
83
        $response->setStatusCode($this->getStatusCode($exception));
71✔
84
        $response->setContent(JSON::encode($this->getErrorMessage($exception, $response)));
71✔
85
        // Send the modified response object to the event
86
        $event->setResponse($response);
71✔
87
    }
88

89
    /**
90
     * Method to get "proper" status code for exception response.
91
     */
92
    private function getStatusCode(Throwable $exception): int
93
    {
94
        return $this->determineStatusCode($exception, $this->userService->getSecurityUser() !== null);
71✔
95
    }
96

97
    /**
98
     * Method to get actual error message.
99
     *
100
     * @return array<string, mixed>
101
     */
102
    private function getErrorMessage(Throwable $exception, Response $response): array
103
    {
104
        // Set base of error message
105
        $error = [
71✔
106
            'message' => $this->getExceptionMessage($exception),
71✔
107
            'code' => $exception->getCode(),
71✔
108
            'status' => $response->getStatusCode(),
71✔
109
        ];
71✔
110

111
        // Attach more info to error response in dev environment
112
        if ($this->environment === 'dev') {
71✔
113
            $error += [
×
114
                'debug' => [
×
115
                    'exception' => $exception::class,
×
116
                    'file' => $exception->getFile(),
×
117
                    'line' => $exception->getLine(),
×
118
                    'message' => $exception->getMessage(),
×
119
                    'trace' => $exception->getTrace(),
×
120
                    'traceString' => $exception->getTraceAsString(),
×
121
                ],
×
122
            ];
×
123
        }
124

125
        return $error;
71✔
126
    }
127

128
    /**
129
     * Helper method to convert exception message for user. This method is used in 'production' environment so, that
130
     * application won't reveal any sensitive error data to users.
131
     */
132
    private function getExceptionMessage(Throwable $exception): string
133
    {
134
        return $this->environment === 'dev'
71✔
135
            ? $exception->getMessage()
×
136
            : $this->getMessageForProductionEnvironment($exception);
71✔
137
    }
138

139
    private function getMessageForProductionEnvironment(Throwable $exception): string
140
    {
141
        $message = $exception->getMessage();
71✔
142

143
        $accessDeniedClasses = [
71✔
144
            AccessDeniedHttpException::class,
71✔
145
            AccessDeniedException::class,
71✔
146
            AuthenticationException::class,
71✔
147
        ];
71✔
148

149
        if (in_array($exception::class, $accessDeniedClasses, true)) {
71✔
150
            $message = 'Access denied.';
64✔
151
        } elseif ($exception instanceof Exception || $exception instanceof ORMException) {
7✔
152
            // Database errors
153
            $message = 'Database error.';
×
154
        } elseif (!$this->isClientExceptions($exception)) {
7✔
155
            $message = 'Internal server error.';
×
156
        }
157

158
        return $message;
71✔
159
    }
160

161
    /**
162
     * Method to determine status code for specified exception.
163
     */
164
    private function determineStatusCode(Throwable $exception, bool $isUser): int
165
    {
166
        $accessDeniedException = static fn (bool $isUser): int => $isUser
71✔
167
            ? Response::HTTP_FORBIDDEN
×
168
            : Response::HTTP_UNAUTHORIZED;
×
169

170
        $clientException = static fn (HttpExceptionInterface|ClientErrorInterface|Throwable $exception): int =>
71✔
171
            $exception instanceof HttpExceptionInterface || $exception instanceof ClientErrorInterface
71✔
172
                ? $exception->getStatusCode()
71✔
173
                : (int)$exception->getCode();
71✔
174

175
        $statusCode = match (true) {
71✔
176
            $exception instanceof AuthenticationException => Response::HTTP_UNAUTHORIZED,
71✔
177
            $exception instanceof AccessDeniedException => $accessDeniedException($isUser),
71✔
178
            $this->isClientExceptions($exception) => $clientException($exception),
71✔
179
            default => 0,
71✔
180
        };
71✔
181

182
        return $statusCode > 0 ? $statusCode : Response::HTTP_INTERNAL_SERVER_ERROR;
71✔
183
    }
184

185
    /**
186
     * Method to check if exception is ok to show to user (client) or not. Note
187
     * that if this returns true exception message is shown as-is to user.
188
     */
189
    private function isClientExceptions(Throwable $exception): bool
190
    {
191
        $cacheKey = spl_object_hash($exception);
71✔
192

193
        if (!array_key_exists($cacheKey, self::$cache)) {
71✔
194
            $intersect = array_intersect((array)class_implements($exception), self::$clientExceptions);
71✔
195

196
            self::$cache[$cacheKey] = $intersect !== [];
71✔
197
        }
198

199
        return self::$cache[$cacheKey];
71✔
200
    }
201
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc