Skip to content

Commit c409f31

Browse files
committed
v3
1 parent 0de01c5 commit c409f31

File tree

14 files changed

+642
-5
lines changed

14 files changed

+642
-5
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Psr\Container\ContainerInterface;
6+
use Psr\Http\Message\ResponseFactoryInterface;
7+
use Psr\Http\Message\StreamFactoryInterface;
8+
use Psr\Log\LoggerInterface;
9+
use Sunrise\Coder\Codec\JsonCodec;
10+
use Sunrise\Coder\CodecManagerInterface;
11+
use Sunrise\Coder\Dictionary\MediaType;
12+
use Sunrise\Coder\MediaTypeInterface;
13+
use Sunrise\Http\Router\Dictionary\LanguageCode;
14+
use Sunrise\Http\Router\Middleware\ErrorHandlingMiddleware;
15+
use Sunrise\Http\Router\OpenApi\Type;
16+
use Sunrise\Http\Router\View\ErrorView;
17+
use Sunrise\Translator\TranslatorManagerInterface;
18+
19+
use function DI\add;
20+
use function DI\create;
21+
use function DI\factory;
22+
use function DI\get;
23+
24+
return [
25+
'router.error_handling_middleware.codec_context' => [
26+
JsonCodec::CONTEXT_KEY_ENCODING_FLAGS => JSON_PARTIAL_OUTPUT_ON_ERROR,
27+
],
28+
29+
'router.error_handling_middleware.produced_media_types' => [],
30+
'router.error_handling_middleware.default_media_type' => MediaType::JSON,
31+
'router.error_handling_middleware.produced_languages' => [],
32+
'router.error_handling_middleware.default_language' => LanguageCode::English,
33+
'router.error_handling_middleware.fatal_error_status_code' => null,
34+
'router.error_handling_middleware.fatal_error_message' => null,
35+
36+
'router.middlewares' => add([
37+
create(ErrorHandlingMiddleware::class)
38+
->constructor(
39+
responseFactory: get(ResponseFactoryInterface::class),
40+
streamFactory: get(StreamFactoryInterface::class),
41+
codecManager: get(CodecManagerInterface::class),
42+
codecContext: get('router.error_handling_middleware.codec_context'),
43+
producedMediaTypes: get('router.error_handling_middleware.produced_media_types'),
44+
defaultMediaType: get('router.error_handling_middleware.default_media_type'),
45+
translatorManager: get(TranslatorManagerInterface::class),
46+
producedLanguages: get('router.error_handling_middleware.produced_languages'),
47+
defaultLanguage: get('router.error_handling_middleware.default_language'),
48+
logger: get(LoggerInterface::class),
49+
fatalErrorStatusCode: get('router.error_handling_middleware.fatal_error_status_code'),
50+
fatalErrorMessage: get('router.error_handling_middleware.fatal_error_message'),
51+
),
52+
]),
53+
54+
'router.openapi.initial_operation' => add([
55+
'responses' => [
56+
'default' => [
57+
'description' => 'The operation was unsuccessful.',
58+
'content' => factory(
59+
static function (ContainerInterface $container): array {
60+
$mediaTypes = $container->get('router.error_handling_middleware.produced_media_types');
61+
$mediaTypes[] = $container->get('router.error_handling_middleware.default_media_type');
62+
63+
$content = [];
64+
/** @var MediaTypeInterface $mediaType */
65+
foreach ($mediaTypes as $mediaType) {
66+
$content[$mediaType->getIdentifier()] = [
67+
'schema' => new Type(ErrorView::class),
68+
];
69+
}
70+
71+
return $content;
72+
},
73+
),
74+
],
75+
],
76+
]),
77+
];

resources/translations/ru.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@
1515
ErrorMessage::MISSING_COOKIE => 'Отсутствует cookie "{{ cookie_name }}".',
1616
ErrorMessage::INVALID_COOKIE => 'Cookie "{{ cookie_name }}" невалидно.',
1717
ErrorMessage::INVALID_BODY => 'Тело запроса невалидно.',
18+
ErrorMessage::INTERNAL_SERVER_ERROR => 'Что-то пошло не так.',
1819
];

resources/translations/sr.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@
1515
ErrorMessage::MISSING_COOKIE => 'Nedostaje cookie "{{ cookie_name }}".',
1616
ErrorMessage::INVALID_COOKIE => 'Cookie "{{ cookie_name }}" nije validan.',
1717
ErrorMessage::INVALID_BODY => 'Telo zahteva nije validno.',
18+
ErrorMessage::INTERNAL_SERVER_ERROR => 'Nešto nije u redu.',
1819
];

src/Dictionary/ErrorMessage.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ final class ErrorMessage
2929
public const MISSING_COOKIE = 'The cookie "{{ cookie_name }}" is missing.';
3030
public const INVALID_COOKIE = 'The cookie "{{ cookie_name }}" is invalid.';
3131
public const INVALID_BODY = 'The request body is invalid.';
32+
public const INTERNAL_SERVER_ERROR = 'Something went wrong.';
3233
// phpcs:enable Generic.Files.LineLength.TooLong
3334
}

src/Dictionary/LanguageCode.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/**
4+
* It's free open-source software released under the MIT License.
5+
*
6+
* @author Anatoly Nekhay <[email protected]>
7+
* @copyright Copyright (c) 2018, Anatoly Nekhay
8+
* @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
9+
* @link https://github.com/sunrise-php/http-router
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sunrise\Http\Router\Dictionary;
15+
16+
use Sunrise\Http\Router\LanguageInterface;
17+
18+
/**
19+
* @since 3.0.0
20+
*/
21+
enum LanguageCode: string implements LanguageInterface
22+
{
23+
case English = 'en';
24+
25+
public function getCode(): string
26+
{
27+
return $this->value;
28+
}
29+
}

src/Exception/HttpExceptionFactory.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,16 @@ public static function invalidBody(
141141
$previous,
142142
);
143143
}
144+
145+
public static function internalServerError(
146+
?string $message = null,
147+
?int $code = null,
148+
?Throwable $previous = null,
149+
): HttpException {
150+
return new HttpException(
151+
$message ?? ErrorMessage::INTERNAL_SERVER_ERROR,
152+
$code ?? StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR,
153+
$previous,
154+
);
155+
}
144156
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<?php
2+
3+
/**
4+
* It's free open-source software released under the MIT License.
5+
*
6+
* @author Anatoly Nekhay <[email protected]>
7+
* @copyright Copyright (c) 2018, Anatoly Nekhay
8+
* @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
9+
* @link https://github.com/sunrise-php/http-router
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sunrise\Http\Router\Middleware;
15+
16+
use Psr\Http\Message\ResponseFactoryInterface;
17+
use Psr\Http\Message\ResponseInterface;
18+
use Psr\Http\Message\ServerRequestInterface;
19+
use Psr\Http\Message\StreamFactoryInterface;
20+
use Psr\Http\Server\MiddlewareInterface;
21+
use Psr\Http\Server\RequestHandlerInterface;
22+
use Psr\Log\LoggerInterface;
23+
use Sunrise\Coder\CodecManagerInterface;
24+
use Sunrise\Coder\MediaTypeInterface;
25+
use Sunrise\Http\Router\Dictionary\HeaderName;
26+
use Sunrise\Http\Router\Exception\HttpException;
27+
use Sunrise\Http\Router\Exception\HttpExceptionFactory;
28+
use Sunrise\Http\Router\LanguageInterface;
29+
use Sunrise\Http\Router\ServerRequest;
30+
use Sunrise\Http\Router\Validation\ConstraintViolationInterface;
31+
use Sunrise\Http\Router\View\ErrorView;
32+
use Sunrise\Http\Router\View\ViolationView;
33+
use Sunrise\Translator\TranslatorManagerInterface;
34+
use Throwable;
35+
36+
use function sprintf;
37+
38+
final class ErrorHandlingMiddleware implements MiddlewareInterface
39+
{
40+
public function __construct(
41+
private readonly ResponseFactoryInterface $responseFactory,
42+
private readonly StreamFactoryInterface $streamFactory,
43+
private readonly CodecManagerInterface $codecManager,
44+
/** @var array<array-key, mixed> */
45+
private readonly array $codecContext,
46+
/** @var list<MediaTypeInterface> */
47+
private readonly array $producedMediaTypes,
48+
private readonly MediaTypeInterface $defaultMediaType,
49+
private readonly TranslatorManagerInterface $translatorManager,
50+
/** @var list<LanguageInterface> */
51+
private readonly array $producedLanguages,
52+
private readonly LanguageInterface $defaultLanguage,
53+
private readonly LoggerInterface $logger,
54+
private readonly ?int $fatalErrorStatusCode = null,
55+
private readonly ?string $fatalErrorMessage = null,
56+
) {
57+
}
58+
59+
/**
60+
* @inheritDoc
61+
*/
62+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
63+
{
64+
try {
65+
return $handler->handle($request);
66+
} catch (HttpException $e) {
67+
return $this->handleHttpError($e, $request);
68+
} catch (Throwable $e) {
69+
return $this->handleFatalError($e, $request);
70+
}
71+
}
72+
73+
private function handleHttpError(HttpException $error, ServerRequestInterface $request): ResponseInterface
74+
{
75+
$clientPreferredMediaType = ServerRequest::create($request)
76+
->getClientPreferredMediaType(...$this->producedMediaTypes)
77+
?? $this->defaultMediaType;
78+
79+
$clientPreferredLanguage = ServerRequest::create($request)
80+
->getClientPreferredLanguage(...$this->producedLanguages)
81+
?? $this->defaultLanguage;
82+
83+
return $this->createErrorResponse($error, $clientPreferredMediaType, $clientPreferredLanguage);
84+
}
85+
86+
private function handleFatalError(Throwable $error, ServerRequestInterface $request): ResponseInterface
87+
{
88+
$this->logger->error($error->getMessage(), [
89+
'error' => $error,
90+
'request' => $request,
91+
]);
92+
93+
$httpError = HttpExceptionFactory::internalServerError(
94+
message: $this->fatalErrorMessage,
95+
code: $this->fatalErrorStatusCode,
96+
previous: $error,
97+
);
98+
99+
return $this->handleHttpError($httpError, $request);
100+
}
101+
102+
private function createErrorResponse(
103+
HttpException $error,
104+
MediaTypeInterface $mediaType,
105+
LanguageInterface $language,
106+
): ResponseInterface {
107+
$response = $this->responseFactory->createResponse($error->getCode());
108+
foreach ($error->getHeaderFields() as [$fieldName, $fieldValue]) {
109+
$response = $response->withHeader($fieldName, $fieldValue);
110+
}
111+
112+
$errorView = $this->createErrorView($error, $language);
113+
$responseContent = $this->codecManager->encode($mediaType, $errorView, $this->codecContext);
114+
$responseContentType = sprintf('%s; charset=UTF-8', $mediaType->getIdentifier());
115+
116+
return $response->withHeader(HeaderName::CONTENT_TYPE, $responseContentType)
117+
->withBody($this->streamFactory->createStream($responseContent));
118+
}
119+
120+
private function createErrorView(
121+
HttpException $error,
122+
LanguageInterface $language,
123+
): ErrorView {
124+
$message = $this->translatorManager->translate(
125+
domain: $error->getTranslationDomain(),
126+
locale: $language->getCode(),
127+
template: $error->getMessageTemplate(),
128+
placeholders: $error->getMessagePlaceholders(),
129+
);
130+
131+
$violationViews = [];
132+
foreach ($error->getConstraintViolations() as $violation) {
133+
$violationViews[] = $this->createViolationView($violation, $language);
134+
}
135+
136+
return new ErrorView(
137+
message: $message,
138+
violations: $violationViews,
139+
);
140+
}
141+
142+
private function createViolationView(
143+
ConstraintViolationInterface $violation,
144+
LanguageInterface $language,
145+
): ViolationView {
146+
$message = $this->translatorManager->translate(
147+
domain: $violation->getTranslationDomain(),
148+
locale: $language->getCode(),
149+
template: $violation->getMessageTemplate(),
150+
placeholders: $violation->getMessagePlaceholders(),
151+
);
152+
153+
return new ViolationView(
154+
source: $violation->getPropertyPath(),
155+
message: $message,
156+
code: $violation->getCode(),
157+
);
158+
}
159+
}

src/ResponseResolver/EncodableResponseResolver.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,8 @@ public function resolveResponse(
6262
}
6363

6464
$serverRequest = ServerRequest::create($request);
65-
$clientPreferredMediaType = $serverRequest->getClientPreferredMediaType(
66-
...$serverRequest->getRoute()->getProducedMediaTypes()
67-
);
65+
$serverProducedMediaTypes = $serverRequest->getRoute()->getProducedMediaTypes();
66+
$clientPreferredMediaType = $serverRequest->getClientPreferredMediaType(...$serverProducedMediaTypes);
6867

6968
$processParams = $annotations[0]->newInstance();
7069
$codecMediaType = $clientPreferredMediaType ?? $processParams->defaultMediaType ?? $this->defaultMediaType;

src/View/ErrorView.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/**
4+
* It's free open-source software released under the MIT License.
5+
*
6+
* @author Anatoly Nekhay <[email protected]>
7+
* @copyright Copyright (c) 2018, Anatoly Nekhay
8+
* @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
9+
* @link https://github.com/sunrise-php/http-router
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sunrise\Http\Router\View;
15+
16+
use Sunrise\Hydrator\Annotation\Subtype;
17+
18+
/**
19+
* @since 3.0.0
20+
*/
21+
final class ErrorView
22+
{
23+
public function __construct(
24+
public readonly string $message,
25+
/** @var array<array-key, ViolationView> */
26+
#[Subtype(ViolationView::class)]
27+
public readonly array $violations,
28+
) {
29+
}
30+
}

src/View/ViolationView.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/**
4+
* It's free open-source software released under the MIT License.
5+
*
6+
* @author Anatoly Nekhay <[email protected]>
7+
* @copyright Copyright (c) 2018, Anatoly Nekhay
8+
* @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
9+
* @link https://github.com/sunrise-php/http-router
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sunrise\Http\Router\View;
15+
16+
/**
17+
* @since 3.0.0
18+
*/
19+
final class ViolationView
20+
{
21+
public function __construct(
22+
public readonly string $source,
23+
public readonly string $message,
24+
public readonly ?string $code,
25+
) {
26+
}
27+
}

0 commit comments

Comments
 (0)