Skip to content

Commit 9058a9f

Browse files
committed
Add renderer providers
1 parent 51bd058 commit 9058a9f

13 files changed

+297
-18
lines changed

composer.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,16 @@
4747
"yiisoft/injector": "^1.0"
4848
},
4949
"require-dev": {
50-
"bamarni/composer-bin-plugin": "^1.8",
50+
"bamarni/composer-bin-plugin": "^1.8.2",
5151
"httpsoft/http-message": "^1.1.6",
52-
"phpunit/phpunit": "^10.5.44",
52+
"phpunit/phpunit": "^10.5.45",
5353
"psr/event-dispatcher": "^1.0",
54-
"rector/rector": "^2.0.7",
54+
"rector/rector": "^2.0.11",
5555
"roave/infection-static-analysis-plugin": "^1.35",
5656
"spatie/phpunit-watcher": "^1.24",
5757
"vimeo/psalm": "^5.26.1 || ^6.9.1",
5858
"yiisoft/di": "^1.3",
59-
"yiisoft/test-support": "^3.0.1"
59+
"yiisoft/test-support": "^3.0.2"
6060
},
6161
"autoload": {
6262
"psr-4": {

src/Factory/ThrowableResponseFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
/**
3535
* `ThrowableResponseFactory` renders `Throwable` object
3636
* and produces a response according to the content type provided by the client.
37+
*
38+
* @deprecated Use {@see \Yiisoft\ErrorHandler\ThrowableResponseFactory} instead.
3739
*/
3840
final class ThrowableResponseFactory implements ThrowableResponseFactoryInterface
3941
{

src/Renderer/HeaderRenderer.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,21 @@
88
use Throwable;
99
use Yiisoft\ErrorHandler\ErrorData;
1010
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
11+
use Yiisoft\Http\Header;
1112

1213
/**
1314
* Formats throwable into HTTP headers.
1415
*/
1516
final class HeaderRenderer implements ThrowableRendererInterface
1617
{
18+
private const CONTENT_TYPE = '*/*';
19+
1720
public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
1821
{
19-
return new ErrorData('', ['X-Error-Message' => self::DEFAULT_ERROR_MESSAGE]);
22+
return new ErrorData('', [
23+
'X-Error-Message' => self::DEFAULT_ERROR_MESSAGE,
24+
Header::CONTENT_TYPE => self::CONTENT_TYPE,
25+
]);
2026
}
2127

2228
public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
@@ -27,6 +33,7 @@ public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = n
2733
'X-Error-Code' => (string) $t->getCode(),
2834
'X-Error-File' => $t->getFile(),
2935
'X-Error-Line' => (string) $t->getLine(),
36+
Header::CONTENT_TYPE => self::CONTENT_TYPE,
3037
]);
3138
}
3239
}

src/Renderer/HtmlRenderer.php

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
1616
use Yiisoft\FriendlyException\FriendlyExceptionInterface;
1717

18+
use Yiisoft\Http\Header;
19+
1820
use function array_values;
1921
use function dirname;
2022
use function extract;
@@ -51,6 +53,8 @@
5153
*/
5254
final class HtmlRenderer implements ThrowableRendererInterface
5355
{
56+
private const CONTENT_TYPE = 'text/html';
57+
5458
private readonly GithubMarkdown $markdownParser;
5559

5660
/**
@@ -158,18 +162,24 @@ public function __construct(
158162

159163
public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
160164
{
161-
return new ErrorData($this->renderTemplate($this->template, [
165+
return new ErrorData(
166+
$this->renderTemplate($this->template, [
162167
'request' => $request,
163-
'throwable' => $t,
164-
]));
168+
'throwable' => $t,
169+
]),
170+
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
171+
);
165172
}
166173

167174
public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
168175
{
169-
return new ErrorData($this->renderTemplate($this->verboseTemplate, [
170-
'request' => $request,
171-
'throwable' => $t,
172-
]));
176+
return new ErrorData(
177+
$this->renderTemplate($this->verboseTemplate, [
178+
'request' => $request,
179+
'throwable' => $t,
180+
]),
181+
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
182+
);
173183
}
174184

175185
/**

src/Renderer/JsonRenderer.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@
99
use Yiisoft\ErrorHandler\ErrorData;
1010
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
1111

12+
use Yiisoft\Http\Header;
13+
1214
use function json_encode;
1315

1416
/**
1517
* Formats throwable into JSON string.
1618
*/
1719
final class JsonRenderer implements ThrowableRendererInterface
1820
{
21+
private const CONTENT_TYPE = 'application/json';
22+
1923
public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
2024
{
2125
return new ErrorData(
@@ -24,7 +28,8 @@ public function render(Throwable $t, ?ServerRequestInterface $request = null): E
2428
'message' => self::DEFAULT_ERROR_MESSAGE,
2529
],
2630
JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
27-
)
31+
),
32+
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
2833
);
2934
}
3035

@@ -41,7 +46,8 @@ public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = n
4146
'trace' => $t->getTrace(),
4247
],
4348
JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR
44-
)
49+
),
50+
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
4551
);
4652
}
4753
}

src/Renderer/PlainTextRenderer.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,30 @@
88
use Throwable;
99
use Yiisoft\ErrorHandler\ErrorData;
1010
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
11+
use Yiisoft\Http\Header;
12+
13+
use function sprintf;
1114

1215
/**
1316
* Formats throwable into plain text string.
1417
*/
1518
final class PlainTextRenderer implements ThrowableRendererInterface
1619
{
20+
private const CONTENT_TYPE = 'text/plain';
21+
1722
public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
1823
{
19-
return new ErrorData(self::DEFAULT_ERROR_MESSAGE);
24+
return new ErrorData(
25+
self::DEFAULT_ERROR_MESSAGE,
26+
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
27+
);
2028
}
2129

2230
public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
2331
{
2432
return new ErrorData(
25-
self::throwableToString($t)
33+
self::throwableToString($t),
34+
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
2635
);
2736
}
2837

src/Renderer/XmlRenderer.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,27 @@
99
use Yiisoft\ErrorHandler\ErrorData;
1010
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
1111

12+
use Yiisoft\Http\Header;
13+
1214
use function str_replace;
1315

1416
/**
1517
* Formats throwable into XML string.
1618
*/
1719
final class XmlRenderer implements ThrowableRendererInterface
1820
{
21+
private const CONTENT_TYPE = 'application/xml';
22+
1923
public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
2024
{
2125
$content = '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>';
2226
$content .= "\n<error>\n";
2327
$content .= $this->tag('message', self::DEFAULT_ERROR_MESSAGE);
2428
$content .= '</error>';
25-
return new ErrorData($content);
29+
return new ErrorData(
30+
$content,
31+
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
32+
);
2633
}
2734

2835
public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
@@ -36,7 +43,10 @@ public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = n
3643
$content .= $this->tag('line', (string) $t->getLine());
3744
$content .= $this->tag('trace', $t->getTraceAsString());
3845
$content .= '</error>';
39-
return new ErrorData($content);
46+
return new ErrorData(
47+
$content,
48+
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
49+
);
4050
}
4151

4252
private function tag(string $name, string $value): string
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\ErrorHandler\RendererProvider;
6+
7+
use Closure;
8+
use Psr\Container\ContainerInterface;
9+
use Psr\Http\Message\ServerRequestInterface;
10+
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
11+
12+
use function is_string;
13+
14+
/**
15+
* @psalm-type TClosure = Closure(ServerRequestInterface $request): (class-string<ThrowableRendererInterface>|ThrowableRendererInterface|null)
16+
*/
17+
final class ClosureRendererProvider implements RendererProviderInterface
18+
{
19+
/**
20+
* @psalm-param TClosure $closure
21+
*/
22+
public function __construct(
23+
private readonly Closure $closure,
24+
private readonly ContainerInterface $container,
25+
) {
26+
}
27+
28+
public function get(ServerRequestInterface $request): ?ThrowableRendererInterface
29+
{
30+
$result = ($this->closure)($request);
31+
32+
if (is_string($result)) {
33+
/** @var ThrowableRendererInterface */
34+
return $this->container->get($result);
35+
}
36+
37+
return $result;
38+
}
39+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\ErrorHandler\RendererProvider;
6+
7+
use Psr\Http\Message\ServerRequestInterface;
8+
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
9+
10+
final class CompositeRendererProvider implements RendererProviderInterface
11+
{
12+
/**
13+
* @psalm-var list<RendererProviderInterface>
14+
*/
15+
private array $providers;
16+
17+
/**
18+
* @no-named-arguments
19+
*/
20+
public function __construct(RendererProviderInterface ...$providers)
21+
{
22+
$this->providers = $providers;
23+
}
24+
25+
public function get(ServerRequestInterface $request): ?ThrowableRendererInterface
26+
{
27+
foreach ($this->providers as $provider) {
28+
$renderer = $provider->get($request);
29+
if ($renderer !== null) {
30+
return $renderer;
31+
}
32+
}
33+
34+
return null;
35+
}
36+
}
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+
namespace Yiisoft\ErrorHandler\RendererProvider;
6+
7+
use InvalidArgumentException;
8+
use Psr\Container\ContainerInterface;
9+
use Psr\Http\Message\ServerRequestInterface;
10+
use Yiisoft\ErrorHandler\Renderer\HtmlRenderer;
11+
use Yiisoft\ErrorHandler\Renderer\JsonRenderer;
12+
use Yiisoft\ErrorHandler\Renderer\PlainTextRenderer;
13+
use Yiisoft\ErrorHandler\Renderer\XmlRenderer;
14+
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
15+
use Yiisoft\Http\Header;
16+
use Yiisoft\Http\HeaderValueHelper;
17+
18+
use function array_key_exists;
19+
20+
final class ContentTypeRendererProvider implements RendererProviderInterface
21+
{
22+
/**
23+
* @psalm-var array<string, class-string<ThrowableRendererInterface>>
24+
*/
25+
private readonly array $renderers;
26+
27+
/**
28+
* @psalm-param array<string, class-string<ThrowableRendererInterface>>|null $renderers
29+
*/
30+
public function __construct(
31+
private readonly ContainerInterface $container,
32+
?array $renderers = null,
33+
) {
34+
$this->renderers = $renderers ?? [
35+
'application/json' => JsonRenderer::class,
36+
'application/xml' => XmlRenderer::class,
37+
'text/xml' => XmlRenderer::class,
38+
'text/plain' => PlainTextRenderer::class,
39+
'text/html' => HtmlRenderer::class,
40+
'*/*' => HtmlRenderer::class,
41+
];
42+
}
43+
44+
public function get(ServerRequestInterface $request): ?ThrowableRendererInterface
45+
{
46+
$rendererClass = $this->selectRendererClass($request);
47+
if ($rendererClass === null) {
48+
return null;
49+
}
50+
51+
/** @var ThrowableRendererInterface */
52+
return $this->container->get($rendererClass);
53+
}
54+
55+
/**
56+
* @psalm-return class-string<ThrowableRendererInterface>|null
57+
*/
58+
private function selectRendererClass(ServerRequestInterface $request): ?string
59+
{
60+
$acceptHeader = $request->getHeader(Header::ACCEPT);
61+
62+
try {
63+
$contentTypes = HeaderValueHelper::getSortedAcceptTypes($acceptHeader);
64+
} catch (InvalidArgumentException) {
65+
// The "Accept" header contains an invalid "q" factor.
66+
return null;
67+
}
68+
69+
foreach ($contentTypes as $contentType) {
70+
if (array_key_exists($contentType, $this->renderers)) {
71+
return $this->renderers[$contentType];
72+
}
73+
}
74+
75+
return null;
76+
}
77+
}

0 commit comments

Comments
 (0)