Skip to content

Commit 44f49fa

Browse files
authored
Add renderer providers (#145)
1 parent 51bd058 commit 44f49fa

23 files changed

+793
-57
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
# Yii Error Handler Change Log
22

3-
## 4.0.1 under development
3+
## 4.1.0 under development
44

55
- Bug #142: Fix dark mode argument display issues (@pamparam83)
6+
- Enh #145: Set content type header in renderers (@vjik)
7+
- New #145: Add `Yiisoft\ErrorHandler\ThrowableResponseFactory` that provides a response for `Throwable` object with
8+
renderer provider usage (@vjik)
9+
- Chg #145: Mark `Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory` as deprecated (@vjik)
610

711
## 4.0.0 February 05, 2025
812

@@ -11,7 +15,7 @@
1115
- Chg #139: Change PHP constraint in `composer.json` to `~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0` (@vjik)
1216
- Enh #125: Add error code & show function arguments (@xepozz)
1317
- Enh #130: Pass exception message instead of rendered exception to logger in `ErrorHandler` (@olegbaturin)
14-
- Enh #133: Extract response generator from `ErrorCatcher` middleware into separate `ThrowableResponseFactory`
18+
- Enh #133: Extract response generator from `ErrorCatcher` middleware into separate `ThrowableResponseFactory`
1519
class (@olegbaturin)
1620
- Enh #138, #139: Raise the minimum PHP version to 8.1 and minor refactoring (@vjik)
1721
- Bug #139: Explicitly mark nullable parameters (@vjik)

README.md

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,11 @@ For more information about creating your own renders and examples of rendering e
122122

123123
### Using a factory to create a response
124124

125-
`Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory` renders `Throwable` object and produces a response according to the content type provided by the client.
125+
`Yiisoft\ErrorHandler\ThrowableResponseFactory` renders `Throwable` object and produces a response according to the content type provided by the client.
126126

127127
```php
128-
use Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory;
128+
use Yiisoft\ErrorHandler\RendererProvider;
129+
use Yiisoft\ErrorHandler\ThrowableResponseFactory;
129130

130131
/**
131132
* @var \Throwable $throwable
@@ -135,27 +136,27 @@ use Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory;
135136
* @var \Yiisoft\ErrorHandler\ErrorHandler $errorHandler
136137
*/
137138

138-
$throwableResponseFactory = new ThrowableResponseFactory($responseFactory, $errorHandler, $container);
139+
$throwableResponseFactory = new ThrowableResponseFactory(
140+
$responseFactory,
141+
$errorHandler,
142+
new RendererProvider\CompositeRendererProvider(
143+
new RendererProvider\HeadRendererProvider(),
144+
new RendererProvider\ContentTypeRendererProvider($container),
145+
),
146+
);
139147

140148
// Creating an instance of the `Psr\Http\Message\ResponseInterface` with error information.
141149
$response = $throwableResponseFactory->create($throwable, $request);
142150
```
143151

144-
`Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory` chooses how to render an exception based on accept HTTP header.
145-
If it's `text/html` or any unknown content type, it will use the error or exception HTML template to display errors.
146-
For other mime types, the error handler will choose different renderer that is registered within the error catcher.
147-
By default, JSON, XML and plain text are supported. You can change this behavior as follows:
152+
`Yiisoft\ErrorHandler\ThrowableResponseFactory` chooses how to render an exception by renderer provider. Providers
153+
available out of the box:
148154

149-
```php
150-
// Returns a new instance without renderers by the specified content types.
151-
$throwableResponseFactory = $throwableResponseFactory->withoutRenderers('application/xml', 'text/xml');
152-
153-
// Returns a new instance with the specified content type and renderer class.
154-
$throwableResponseFactory = $throwableResponseFactory->withRenderer('my/format', new MyRenderer());
155-
156-
// Returns a new instance with the specified force content type to respond with regardless of request.
157-
$throwableResponseFactory = $throwableResponseFactory->forceContentType('application/json');
158-
```
155+
- `HeadRendererProvider` - renders error into HTTP headers. It is used for HEAD requests.
156+
- `ContentTypeRendererProvider` - renders error based on accept HTTP header. By default, JSON, XML and plain text are
157+
supported.
158+
- `ClosureRendererProvider` - allows you to create your own renderer provider using closures.
159+
- `CompositeRendererProvider` - allows you to combine several renderer providers.
159160

160161
### Using a middleware for catching unhandled errors
161162

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: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,54 @@
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+
/**
19+
* @param string|null $contentType The content type to be set in the response header.
20+
*/
21+
public function __construct(
22+
private readonly ?string $contentType = null,
23+
) {
24+
}
25+
1726
public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
1827
{
19-
return new ErrorData('', ['X-Error-Message' => self::DEFAULT_ERROR_MESSAGE]);
28+
return new ErrorData(
29+
'',
30+
$this->addContentTypeHeader([
31+
'X-Error-Message' => self::DEFAULT_ERROR_MESSAGE,
32+
]),
33+
);
2034
}
2135

2236
public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
2337
{
24-
return new ErrorData('', [
25-
'X-Error-Type' => $t::class,
26-
'X-Error-Message' => $t->getMessage(),
27-
'X-Error-Code' => (string) $t->getCode(),
28-
'X-Error-File' => $t->getFile(),
29-
'X-Error-Line' => (string) $t->getLine(),
30-
]);
38+
return new ErrorData(
39+
'',
40+
$this->addContentTypeHeader([
41+
'X-Error-Type' => $t::class,
42+
'X-Error-Message' => $t->getMessage(),
43+
'X-Error-Code' => (string) $t->getCode(),
44+
'X-Error-File' => $t->getFile(),
45+
'X-Error-Line' => (string) $t->getLine(),
46+
]),
47+
);
48+
}
49+
50+
/**
51+
* @param array<string, string|string[]> $headers
52+
* @return array<string, string|string[]>
53+
*/
54+
private function addContentTypeHeader(array $headers): array
55+
{
56+
if ($this->contentType !== null) {
57+
$headers[Header::CONTENT_TYPE] = $this->contentType;
58+
}
59+
return $headers;
3160
}
3261
}

src/Renderer/HtmlRenderer.php

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Yiisoft\ErrorHandler\Exception\ErrorException;
1515
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
1616
use Yiisoft\FriendlyException\FriendlyExceptionInterface;
17+
use Yiisoft\Http\Header;
1718

1819
use function array_values;
1920
use function dirname;
@@ -51,6 +52,8 @@
5152
*/
5253
final class HtmlRenderer implements ThrowableRendererInterface
5354
{
55+
private const CONTENT_TYPE = 'text/html';
56+
5457
private readonly GithubMarkdown $markdownParser;
5558

5659
/**
@@ -158,18 +161,24 @@ public function __construct(
158161

159162
public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
160163
{
161-
return new ErrorData($this->renderTemplate($this->template, [
162-
'request' => $request,
163-
'throwable' => $t,
164-
]));
164+
return new ErrorData(
165+
$this->renderTemplate($this->template, [
166+
'request' => $request,
167+
'throwable' => $t,
168+
]),
169+
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
170+
);
165171
}
166172

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

175184
/**

src/Renderer/JsonRenderer.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Throwable;
99
use Yiisoft\ErrorHandler\ErrorData;
1010
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
11+
use Yiisoft\Http\Header;
1112

1213
use function json_encode;
1314

@@ -16,6 +17,8 @@
1617
*/
1718
final class JsonRenderer implements ThrowableRendererInterface
1819
{
20+
private const CONTENT_TYPE = 'application/json';
21+
1922
public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
2023
{
2124
return new ErrorData(
@@ -24,7 +27,8 @@ public function render(Throwable $t, ?ServerRequestInterface $request = null): E
2427
'message' => self::DEFAULT_ERROR_MESSAGE,
2528
],
2629
JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
27-
)
30+
),
31+
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
2832
);
2933
}
3034

@@ -41,7 +45,8 @@ public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = n
4145
'trace' => $t->getTrace(),
4246
],
4347
JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR
44-
)
48+
),
49+
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
4550
);
4651
}
4752
}

src/Renderer/PlainTextRenderer.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,33 @@
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+
public function __construct(
21+
private readonly string $contentType = 'text/plain',
22+
) {
23+
}
24+
1725
public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
1826
{
19-
return new ErrorData(self::DEFAULT_ERROR_MESSAGE);
27+
return new ErrorData(
28+
self::DEFAULT_ERROR_MESSAGE,
29+
[Header::CONTENT_TYPE => $this->contentType],
30+
);
2031
}
2132

2233
public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
2334
{
2435
return new ErrorData(
25-
self::throwableToString($t)
36+
self::throwableToString($t),
37+
[Header::CONTENT_TYPE => $this->contentType],
2638
);
2739
}
2840

src/Renderer/XmlRenderer.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Throwable;
99
use Yiisoft\ErrorHandler\ErrorData;
1010
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
11+
use Yiisoft\Http\Header;
1112

1213
use function str_replace;
1314

@@ -16,13 +17,18 @@
1617
*/
1718
final class XmlRenderer implements ThrowableRendererInterface
1819
{
20+
private const CONTENT_TYPE = 'application/xml';
21+
1922
public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
2023
{
2124
$content = '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>';
2225
$content .= "\n<error>\n";
2326
$content .= $this->tag('message', self::DEFAULT_ERROR_MESSAGE);
2427
$content .= '</error>';
25-
return new ErrorData($content);
28+
return new ErrorData(
29+
$content,
30+
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
31+
);
2632
}
2733

2834
public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
@@ -36,7 +42,10 @@ public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = n
3642
$content .= $this->tag('line', (string) $t->getLine());
3743
$content .= $this->tag('trace', $t->getTraceAsString());
3844
$content .= '</error>';
39-
return new ErrorData($content);
45+
return new ErrorData(
46+
$content,
47+
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
48+
);
4049
}
4150

4251
private function tag(string $name, string $value): string
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
* Provides a renderer based on a closure that returns a `ThrowableRendererInterface` or its class name.
16+
*
17+
* @psalm-type TClosure = Closure(ServerRequestInterface $request): (class-string<ThrowableRendererInterface>|ThrowableRendererInterface|null)
18+
*/
19+
final class ClosureRendererProvider implements RendererProviderInterface
20+
{
21+
/**
22+
* @psalm-param TClosure $closure
23+
*/
24+
public function __construct(
25+
private readonly Closure $closure,
26+
private readonly ContainerInterface $container,
27+
) {
28+
}
29+
30+
public function get(ServerRequestInterface $request): ?ThrowableRendererInterface
31+
{
32+
$result = ($this->closure)($request);
33+
34+
if (is_string($result)) {
35+
/** @var ThrowableRendererInterface */
36+
return $this->container->get($result);
37+
}
38+
39+
return $result;
40+
}
41+
}

0 commit comments

Comments
 (0)