Skip to content

Commit 6c0117a

Browse files
committed
Adding global exception management with an ExceptionListener and basic functional tests to check that every error output in our application will have a JSON format
1 parent 480d1c1 commit 6c0117a

File tree

10 files changed

+381
-2
lines changed

10 files changed

+381
-2
lines changed

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"symfony/yaml": "^4.0"
1313
},
1414
"require-dev": {
15+
"symfony/browser-kit": "^4.0",
16+
"symfony/css-selector": "^4.0",
1517
"symfony/dotenv": "^4.0",
1618
"symfony/phpunit-bridge": "^4.0"
1719
},

composer.lock

Lines changed: 168 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/services.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ services:
2525

2626
# add more service definitions when explicit configuration is needed
2727
# please note that last definitions always *replace* previous ones
28+
App\EventListener\ExceptionListener:
29+
arguments: ['%env(APP_ENV)%']
30+
tags:
31+
- { name: kernel.event_listener, event: kernel.exception }
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
namespace App\EventListener;
4+
5+
use App\Exception\AppException;
6+
use App\Exception\ErrorCode\GlobalErrorCodes;
7+
use Symfony\Component\HttpFoundation\JsonResponse;
8+
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
9+
use Symfony\Component\HttpFoundation\Response;
10+
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
11+
12+
/**
13+
* This listener receives ALL errors within the application and will always return the errors in a JSON format
14+
*/
15+
class ExceptionListener
16+
{
17+
/**
18+
* @var string
19+
*/
20+
private $environment;
21+
22+
public function __construct(string $environment)
23+
{
24+
$this->environment = $environment;
25+
}
26+
27+
public function onKernelException(GetResponseForExceptionEvent $event): void
28+
{
29+
$exception = $event->getException();
30+
$response = new JsonResponse();
31+
32+
if ($exception instanceof AppException) {
33+
$response->setStatusCode($exception->getHttpErrorCode());
34+
} elseif ($exception instanceof HttpExceptionInterface) {
35+
$response->setStatusCode($exception->getStatusCode());
36+
} else {
37+
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
38+
}
39+
40+
if (false === $this->isProductionEnvironment()) {
41+
$response->setJson(json_encode($this->buildErrorMessage($exception)));
42+
} else {
43+
$response->setJson(json_encode($this->buildErrorMessageForProductionClient($exception)));
44+
// Here, use your application of preference to write ALL error logs (e.g. rollbar)
45+
}
46+
47+
$event->setResponse($response);
48+
}
49+
50+
private function buildErrorMessage(\Exception $exception): array
51+
{
52+
$errorMessage = [];
53+
if ($exception instanceof AppException) {
54+
$errorMessage['errorCode'] = $exception->getInternalErrorCode();
55+
} else {
56+
$errorMessage['errorCode'] = GlobalErrorCodes::INTERNAL_SERVER_DRAMA;
57+
}
58+
59+
$errorMessage['errorMessage'] = $exception->getMessage();
60+
$errorMessage['errorStackTrace'] = $exception->getTrace();
61+
62+
return $errorMessage;
63+
}
64+
65+
private function buildErrorMessageForProductionClient(\Exception $exception): array
66+
{
67+
$errorMessage = $this->buildErrorMessage($exception);
68+
unset($errorMessage['errorStackTrace']);
69+
70+
return $errorMessage;
71+
}
72+
73+
private function isProductionEnvironment(): bool
74+
{
75+
return in_array($this->environment, ['pre', 'pro']);
76+
}
77+
}

src/Exception/AppException.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace App\Exception;
4+
5+
use Symfony\Component\HttpFoundation\Response;
6+
7+
/**
8+
* Global exception within our system, which will ALWAYS have a well-defined error code
9+
* Such error code is defined in one of the files under ErrorCode/, which will be in this folder
10+
*/
11+
class AppException extends \Exception
12+
{
13+
/**
14+
* @var string
15+
*/
16+
private $internalErrorCode;
17+
18+
/**
19+
* @var int
20+
*/
21+
private $httpErrorCode;
22+
23+
public function __construct(
24+
string $internalErrorCode,
25+
string $httpErrorCode = Response::HTTP_INTERNAL_SERVER_ERROR,
26+
string $message = ''
27+
) {
28+
$this->internalErrorCode = $internalErrorCode;
29+
$this->httpErrorCode = $httpErrorCode;
30+
31+
parent::__construct($message);
32+
}
33+
34+
public function getInternalErrorCode(): string
35+
{
36+
return $this->internalErrorCode;
37+
}
38+
39+
public function getHttpErrorCode(): int
40+
{
41+
return $this->httpErrorCode;
42+
}
43+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace App\Exception\ErrorCode;
4+
5+
final class GlobalErrorCodes
6+
{
7+
const INTERNAL_SERVER_DRAMA = 'GLOB0001';
8+
}

symfony.lock

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444
"ref": "aaddfdf43cdecd4cf91f992052d76c2cadc04543"
4545
}
4646
},
47+
"symfony/browser-kit": {
48+
"version": "v4.0.8"
49+
},
4750
"symfony/cache": {
4851
"version": "v4.0.8"
4952
},
@@ -59,12 +62,18 @@
5962
"ref": "e3868d2f4a5104f19f844fe551099a00c6562527"
6063
}
6164
},
65+
"symfony/css-selector": {
66+
"version": "v4.0.8"
67+
},
6268
"symfony/debug": {
6369
"version": "v4.0.8"
6470
},
6571
"symfony/dependency-injection": {
6672
"version": "v4.0.8"
6773
},
74+
"symfony/dom-crawler": {
75+
"version": "v4.0.8"
76+
},
6877
"symfony/dotenv": {
6978
"version": "v4.0.8"
7079
},

tests/.gitignore

Whitespace-only changes.

0 commit comments

Comments
 (0)