Skip to content

Commit fd2cbf5

Browse files
authored
IBX-8784: Added AbstractExceptionVisitor template to allow fine-grained control of output
1 parent a3f7066 commit fd2cbf5

File tree

4 files changed

+173
-123
lines changed

4 files changed

+173
-123
lines changed

.php-cs-fixer.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
$configFactory = new InternalConfigFactory();
1212
$configFactory->withRules([
1313
'declare_strict_types' => false,
14+
'phpdoc_no_empty_return' => false,
1415
]);
1516

1617
return $configFactory

phpstan-baseline.neon

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2890,11 +2890,6 @@ parameters:
28902890
count: 1
28912891
path: src/lib/Server/Output/ValueObjectVisitor/ContentFieldValidationException.php
28922892

2893-
-
2894-
message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Output\\\\ValueObjectVisitor\\\\ContentFieldValidationException\\:\\:visit\\(\\) has no return type specified\\.$#"
2895-
count: 1
2896-
path: src/lib/Server/Output/ValueObjectVisitor/ContentFieldValidationException.php
2897-
28982893
-
28992894
message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Output\\\\ValueObjectVisitor\\\\ContentList\\:\\:visit\\(\\) has no return type specified\\.$#"
29002895
count: 1
@@ -3175,16 +3170,6 @@ parameters:
31753170
count: 1
31763171
path: src/lib/Server/Output/ValueObjectVisitor/DeletedUserSession.php
31773172

3178-
-
3179-
message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Output\\\\ValueObjectVisitor\\\\Exception\\:\\:visit\\(\\) has no return type specified\\.$#"
3180-
count: 1
3181-
path: src/lib/Server/Output/ValueObjectVisitor/Exception.php
3182-
3183-
-
3184-
message: "#^Property Ibexa\\\\Rest\\\\Server\\\\Output\\\\ValueObjectVisitor\\\\Exception\\:\\:\\$httpStatusCodes type has no value type specified in iterable type array\\.$#"
3185-
count: 1
3186-
path: src/lib/Server/Output/ValueObjectVisitor/Exception.php
3187-
31883173
-
31893174
message: "#^Property Ibexa\\\\Rest\\\\Server\\\\Output\\\\ValueObjectVisitor\\\\Exception\\:\\:\\$translator \\(Symfony\\\\Contracts\\\\Translation\\\\TranslatorInterface\\) does not accept Symfony\\\\Contracts\\\\Translation\\\\TranslatorInterface\\|null\\.$#"
31903175
count: 1
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
namespace Ibexa\Contracts\Rest\Output\Exceptions;
8+
9+
use Ibexa\Contracts\Rest\Output\Generator;
10+
use Ibexa\Contracts\Rest\Output\ValueObjectVisitor;
11+
use Ibexa\Contracts\Rest\Output\Visitor;
12+
use Ibexa\Core\Base\Translatable;
13+
use JMS\TranslationBundle\Annotation\Desc;
14+
use JMS\TranslationBundle\Annotation\Ignore;
15+
use Symfony\Contracts\Translation\TranslatorInterface;
16+
17+
abstract class AbstractExceptionVisitor extends ValueObjectVisitor
18+
{
19+
/**
20+
* Mapping of HTTP status codes to their respective error messages.
21+
*
22+
* @var array<int, string>
23+
*/
24+
protected static $httpStatusCodes = [
25+
400 => 'Bad Request',
26+
401 => 'Unauthorized',
27+
402 => 'Payment Required',
28+
403 => 'Forbidden',
29+
404 => 'Not Found',
30+
405 => 'Method Not Allowed',
31+
406 => 'Not Acceptable',
32+
407 => 'Proxy Authentication Required',
33+
408 => 'Request Time-out',
34+
409 => 'Conflict',
35+
410 => 'Gone',
36+
411 => 'Length Required',
37+
412 => 'Precondition Failed',
38+
413 => 'Request Entity Too Large',
39+
414 => 'Request-URI Too Long',
40+
415 => 'Unsupported Media Type',
41+
416 => 'Requested range not satisfiable',
42+
417 => 'Expectation Failed',
43+
418 => "I'm a teapot",
44+
421 => 'There are too many connections from your internet address',
45+
422 => 'Unprocessable Entity',
46+
423 => 'Locked',
47+
424 => 'Failed Dependency',
48+
425 => 'Unordered Collection',
49+
426 => 'Upgrade Required',
50+
500 => 'Internal Server Error',
51+
501 => 'Not Implemented',
52+
502 => 'Bad Gateway',
53+
503 => 'Service Unavailable',
54+
504 => 'Gateway Time-out',
55+
505 => 'HTTP Version not supported',
56+
506 => 'Variant Also Negotiates',
57+
507 => 'Insufficient Storage',
58+
509 => 'Bandwidth Limit Exceeded',
59+
510 => 'Not Extended',
60+
];
61+
62+
/**
63+
* Returns HTTP status code.
64+
*
65+
* @return int
66+
*/
67+
protected function getStatus()
68+
{
69+
return 500;
70+
}
71+
72+
/**
73+
* @param \Exception $data
74+
*
75+
* @return void
76+
*/
77+
public function visit(Visitor $visitor, Generator $generator, $data)
78+
{
79+
$generator->startObjectElement('ErrorMessage');
80+
81+
$visitor->setHeader('Content-Type', $generator->getMediaType('ErrorMessage'));
82+
83+
$statusCode = $this->generateErrorCode($generator, $visitor, $data);
84+
85+
$errorMessage = $this->getErrorMessage($data, $statusCode);
86+
$generator->valueElement('errorMessage', $errorMessage);
87+
88+
$errorDescription = $this->getErrorDescription($data, $statusCode);
89+
$generator->valueElement('errorDescription', $errorDescription);
90+
91+
if ($this->canDisplayExceptionTrace()) {
92+
$generator->valueElement('trace', $data->getTraceAsString());
93+
$generator->valueElement('file', $data->getFile());
94+
$generator->valueElement('line', $data->getLine());
95+
}
96+
97+
$previous = $data->getPrevious();
98+
if ($previous !== null && $this->canDisplayPreviousException()) {
99+
$generator->startObjectElement('Previous', 'ErrorMessage');
100+
$visitor->visitValueObject($previous);
101+
$generator->endObjectElement('Previous');
102+
}
103+
104+
$generator->endObjectElement('ErrorMessage');
105+
}
106+
107+
protected function generateErrorCode(Generator $generator, Visitor $visitor, \Exception $e): int
108+
{
109+
$statusCode = $this->getStatus();
110+
$visitor->setStatus($statusCode);
111+
112+
$generator->valueElement('errorCode', $statusCode);
113+
114+
return $statusCode;
115+
}
116+
117+
protected function getErrorMessage(\Exception $data, int $statusCode): string
118+
{
119+
return static::$httpStatusCodes[$statusCode] ?? static::$httpStatusCodes[500];
120+
}
121+
122+
protected function getErrorDescription(\Exception $data, int $statusCode): string
123+
{
124+
$translator = $this->getTranslator();
125+
if ($statusCode < 500 || $this->canDisplayExceptionMessage()) {
126+
$errorDescription = $data instanceof Translatable && $translator
127+
? /** @Ignore */
128+
$translator->trans($data->getMessageTemplate(), $data->getParameters(), 'ibexa_repository_exceptions')
129+
: $data->getMessage();
130+
} else {
131+
// Do not leak any file paths and sensitive data on production environments
132+
$errorDescription = $translator
133+
? /** @Desc("An error has occurred. Please try again later or contact your Administrator.") */
134+
$translator->trans('non_verbose_error', [], 'ibexa_repository_exceptions')
135+
: 'An error has occurred. Please try again later or contact your Administrator.';
136+
}
137+
138+
return $errorDescription;
139+
}
140+
141+
protected function getTranslator(): ?TranslatorInterface
142+
{
143+
return null;
144+
}
145+
146+
protected function canDisplayExceptionTrace(): bool
147+
{
148+
return false;
149+
}
150+
151+
protected function canDisplayPreviousException(): bool
152+
{
153+
return false;
154+
}
155+
156+
protected function canDisplayExceptionMessage(): bool
157+
{
158+
return false;
159+
}
160+
}

src/lib/Server/Output/ValueObjectVisitor/Exception.php

Lines changed: 12 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,13 @@
66
*/
77
namespace Ibexa\Rest\Server\Output\ValueObjectVisitor;
88

9-
use Ibexa\Contracts\Rest\Output\Generator;
10-
use Ibexa\Contracts\Rest\Output\ValueObjectVisitor;
11-
use Ibexa\Contracts\Rest\Output\Visitor;
12-
use Ibexa\Core\Base\Translatable;
13-
use JMS\TranslationBundle\Annotation\Desc;
14-
use JMS\TranslationBundle\Annotation\Ignore;
9+
use Ibexa\Contracts\Rest\Output\Exceptions\AbstractExceptionVisitor;
1510
use Symfony\Contracts\Translation\TranslatorInterface;
1611

1712
/**
1813
* Exception value object visitor.
1914
*/
20-
class Exception extends ValueObjectVisitor
15+
class Exception extends AbstractExceptionVisitor
2116
{
2217
/**
2318
* Is debug mode enabled?
@@ -26,49 +21,6 @@ class Exception extends ValueObjectVisitor
2621
*/
2722
protected $debug = false;
2823

29-
/**
30-
* Mapping of HTTP status codes to their respective error messages.
31-
*
32-
* @var array
33-
*/
34-
protected static $httpStatusCodes = [
35-
400 => 'Bad Request',
36-
401 => 'Unauthorized',
37-
402 => 'Payment Required',
38-
403 => 'Forbidden',
39-
404 => 'Not Found',
40-
405 => 'Method Not Allowed',
41-
406 => 'Not Acceptable',
42-
407 => 'Proxy Authentication Required',
43-
408 => 'Request Time-out',
44-
409 => 'Conflict',
45-
410 => 'Gone',
46-
411 => 'Length Required',
47-
412 => 'Precondition Failed',
48-
413 => 'Request Entity Too Large',
49-
414 => 'Request-URI Too Long',
50-
415 => 'Unsupported Media Type',
51-
416 => 'Requested range not satisfiable',
52-
417 => 'Expectation Failed',
53-
418 => "I'm a teapot",
54-
421 => 'There are too many connections from your internet address',
55-
422 => 'Unprocessable Entity',
56-
423 => 'Locked',
57-
424 => 'Failed Dependency',
58-
425 => 'Unordered Collection',
59-
426 => 'Upgrade Required',
60-
500 => 'Internal Server Error',
61-
501 => 'Not Implemented',
62-
502 => 'Bad Gateway',
63-
503 => 'Service Unavailable',
64-
504 => 'Gateway Time-out',
65-
505 => 'HTTP Version not supported',
66-
506 => 'Variant Also Negotiates',
67-
507 => 'Insufficient Storage',
68-
509 => 'Bandwidth Limit Exceeded',
69-
510 => 'Not Extended',
70-
];
71-
7224
/** @var \Symfony\Contracts\Translation\TranslatorInterface */
7325
protected $translator;
7426

@@ -84,72 +36,24 @@ public function __construct($debug = false, ?TranslatorInterface $translator = n
8436
$this->translator = $translator;
8537
}
8638

87-
/**
88-
* Returns HTTP status code.
89-
*
90-
* @return int
91-
*/
92-
protected function getStatus()
39+
protected function getTranslator(): ?TranslatorInterface
9340
{
94-
return 500;
41+
return $this->translator;
9542
}
9643

97-
/**
98-
* Visit struct returned by controllers.
99-
*
100-
* @param \Ibexa\Contracts\Rest\Output\Visitor $visitor
101-
* @param \Ibexa\Contracts\Rest\Output\Generator $generator
102-
* @param \Exception $data
103-
*/
104-
public function visit(Visitor $visitor, Generator $generator, $data)
44+
protected function canDisplayExceptionMessage(): bool
10545
{
106-
$generator->startObjectElement('ErrorMessage');
107-
108-
$visitor->setHeader('Content-Type', $generator->getMediaType('ErrorMessage'));
109-
110-
$statusCode = $this->generateErrorCode($generator, $visitor, $data);
111-
112-
$generator->valueElement(
113-
'errorMessage',
114-
static::$httpStatusCodes[$statusCode] ?? static::$httpStatusCodes[500]
115-
);
116-
117-
if ($this->debug || $statusCode < 500) {
118-
$errorDescription = $data instanceof Translatable && $this->translator
119-
? /** @Ignore */ $this->translator->trans($data->getMessageTemplate(), $data->getParameters(), 'ibexa_repository_exceptions')
120-
: $data->getMessage();
121-
} else {
122-
// Do not leak any file paths and sensitive data on production environments
123-
$errorDescription = $this->translator
124-
? /** @Desc("An error has occurred. Please try again later or contact your Administrator.") */ $this->translator->trans('non_verbose_error', [], 'ibexa_repository_exceptions')
125-
: 'An error has occurred. Please try again later or contact your Administrator.';
126-
}
127-
128-
$generator->valueElement('errorDescription', $errorDescription);
129-
130-
if ($this->debug) {
131-
$generator->valueElement('trace', $data->getTraceAsString());
132-
$generator->valueElement('file', $data->getFile());
133-
$generator->valueElement('line', $data->getLine());
134-
}
135-
136-
if ($previous = $data->getPrevious()) {
137-
$generator->startObjectElement('Previous', 'ErrorMessage');
138-
$visitor->visitValueObject($previous);
139-
$generator->endObjectElement('Previous');
140-
}
141-
142-
$generator->endObjectElement('ErrorMessage');
46+
return $this->debug;
14347
}
14448

145-
protected function generateErrorCode(Generator $generator, Visitor $visitor, \Exception $e): int
49+
protected function canDisplayExceptionTrace(): bool
14650
{
147-
$statusCode = $this->getStatus();
148-
$visitor->setStatus($statusCode);
149-
150-
$generator->valueElement('errorCode', $statusCode);
51+
return $this->debug;
52+
}
15153

152-
return $statusCode;
54+
protected function canDisplayPreviousException(): bool
55+
{
56+
return true;
15357
}
15458
}
15559

0 commit comments

Comments
 (0)