Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Yii Error Handler Change Log

## 4.0.1 under development
## 4.1.0 under development

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

## 4.0.0 February 05, 2025

Expand All @@ -11,7 +15,7 @@
- Chg #139: Change PHP constraint in `composer.json` to `~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0` (@vjik)
- Enh #125: Add error code & show function arguments (@xepozz)
- Enh #130: Pass exception message instead of rendered exception to logger in `ErrorHandler` (@olegbaturin)
- Enh #133: Extract response generator from `ErrorCatcher` middleware into separate `ThrowableResponseFactory`
- Enh #133: Extract response generator from `ErrorCatcher` middleware into separate `ThrowableResponseFactory`
class (@olegbaturin)
- Enh #138, #139: Raise the minimum PHP version to 8.1 and minor refactoring (@vjik)
- Bug #139: Explicitly mark nullable parameters (@vjik)
Expand Down
35 changes: 18 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,11 @@ For more information about creating your own renders and examples of rendering e

### Using a factory to create a response

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

```php
use Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory;
use Yiisoft\ErrorHandler\RendererProvider;
use Yiisoft\ErrorHandler\ThrowableResponseFactory;

/**
* @var \Throwable $throwable
Expand All @@ -135,27 +136,27 @@ use Yiisoft\ErrorHandler\Factory\ThrowableResponseFactory;
* @var \Yiisoft\ErrorHandler\ErrorHandler $errorHandler
*/

$throwableResponseFactory = new ThrowableResponseFactory($responseFactory, $errorHandler, $container);
$throwableResponseFactory = new ThrowableResponseFactory(
$responseFactory,
$errorHandler,
new RendererProvider\CompositeRendererProvider(
new RendererProvider\HeadRendererProvider(),
new RendererProvider\ContentTypeRendererProvider($container),
),
);

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

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

```php
// Returns a new instance without renderers by the specified content types.
$throwableResponseFactory = $throwableResponseFactory->withoutRenderers('application/xml', 'text/xml');

// Returns a new instance with the specified content type and renderer class.
$throwableResponseFactory = $throwableResponseFactory->withRenderer('my/format', new MyRenderer());

// Returns a new instance with the specified force content type to respond with regardless of request.
$throwableResponseFactory = $throwableResponseFactory->forceContentType('application/json');
```
- `HeadRendererProvider` - renders error into HTTP headers. It is used for HEAD requests.
- `ContentTypeRendererProvider` - renders error based on accept HTTP header. By default, JSON, XML and plain text are
supported.
- `ClosureRendererProvider` - allows you to create your own renderer provider using closures.
- `CompositeRendererProvider` - allows you to combine several renderer providers.

### Using a middleware for catching unhandled errors

Expand Down
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,16 @@
"yiisoft/injector": "^1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8",
"bamarni/composer-bin-plugin": "^1.8.2",
"httpsoft/http-message": "^1.1.6",
"phpunit/phpunit": "^10.5.44",
"phpunit/phpunit": "^10.5.45",
"psr/event-dispatcher": "^1.0",
"rector/rector": "^2.0.7",
"rector/rector": "^2.0.11",
"roave/infection-static-analysis-plugin": "^1.35",
"spatie/phpunit-watcher": "^1.24",
"vimeo/psalm": "^5.26.1 || ^6.9.1",
"yiisoft/di": "^1.3",
"yiisoft/test-support": "^3.0.1"
"yiisoft/test-support": "^3.0.2"
},
"autoload": {
"psr-4": {
Expand Down
2 changes: 2 additions & 0 deletions src/Factory/ThrowableResponseFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
/**
* `ThrowableResponseFactory` renders `Throwable` object
* and produces a response according to the content type provided by the client.
*
* @deprecated Use {@see \Yiisoft\ErrorHandler\ThrowableResponseFactory} instead.
*/
final class ThrowableResponseFactory implements ThrowableResponseFactoryInterface
{
Expand Down
45 changes: 37 additions & 8 deletions src/Renderer/HeaderRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,54 @@
use Throwable;
use Yiisoft\ErrorHandler\ErrorData;
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
use Yiisoft\Http\Header;

/**
* Formats throwable into HTTP headers.
*/
final class HeaderRenderer implements ThrowableRendererInterface
{
/**
* @param string|null $contentType The content type to be set in the response header.
*/
public function __construct(
private readonly ?string $contentType = null,
) {
}

public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
{
return new ErrorData('', ['X-Error-Message' => self::DEFAULT_ERROR_MESSAGE]);
return new ErrorData(
'',
$this->addContentTypeHeader([
'X-Error-Message' => self::DEFAULT_ERROR_MESSAGE,
]),
);
}

public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
{
return new ErrorData('', [
'X-Error-Type' => $t::class,
'X-Error-Message' => $t->getMessage(),
'X-Error-Code' => (string) $t->getCode(),
'X-Error-File' => $t->getFile(),
'X-Error-Line' => (string) $t->getLine(),
]);
return new ErrorData(
'',
$this->addContentTypeHeader([
'X-Error-Type' => $t::class,
'X-Error-Message' => $t->getMessage(),
'X-Error-Code' => (string) $t->getCode(),
'X-Error-File' => $t->getFile(),
'X-Error-Line' => (string) $t->getLine(),
]),
);
}

/**
* @param array<string, string|string[]> $headers
* @return array<string, string|string[]>
*/
private function addContentTypeHeader(array $headers): array
{
if ($this->contentType !== null) {
$headers[Header::CONTENT_TYPE] = $this->contentType;
}
return $headers;
}
}
25 changes: 17 additions & 8 deletions src/Renderer/HtmlRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Yiisoft\ErrorHandler\Exception\ErrorException;
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
use Yiisoft\FriendlyException\FriendlyExceptionInterface;
use Yiisoft\Http\Header;

use function array_values;
use function dirname;
Expand Down Expand Up @@ -51,6 +52,8 @@
*/
final class HtmlRenderer implements ThrowableRendererInterface
{
private const CONTENT_TYPE = 'text/html';

private readonly GithubMarkdown $markdownParser;

/**
Expand Down Expand Up @@ -158,18 +161,24 @@ public function __construct(

public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
{
return new ErrorData($this->renderTemplate($this->template, [
'request' => $request,
'throwable' => $t,
]));
return new ErrorData(
$this->renderTemplate($this->template, [
'request' => $request,
'throwable' => $t,
]),
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
);
}

public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
{
return new ErrorData($this->renderTemplate($this->verboseTemplate, [
'request' => $request,
'throwable' => $t,
]));
return new ErrorData(
$this->renderTemplate($this->verboseTemplate, [
'request' => $request,
'throwable' => $t,
]),
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
);
}

/**
Expand Down
9 changes: 7 additions & 2 deletions src/Renderer/JsonRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Throwable;
use Yiisoft\ErrorHandler\ErrorData;
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
use Yiisoft\Http\Header;

use function json_encode;

Expand All @@ -16,6 +17,8 @@
*/
final class JsonRenderer implements ThrowableRendererInterface
{
private const CONTENT_TYPE = 'application/json';

public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
{
return new ErrorData(
Expand All @@ -24,7 +27,8 @@ public function render(Throwable $t, ?ServerRequestInterface $request = null): E
'message' => self::DEFAULT_ERROR_MESSAGE,
],
JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
)
),
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
);
}

Expand All @@ -41,7 +45,8 @@ public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = n
'trace' => $t->getTrace(),
],
JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR
)
),
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
);
}
}
16 changes: 14 additions & 2 deletions src/Renderer/PlainTextRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,33 @@
use Throwable;
use Yiisoft\ErrorHandler\ErrorData;
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
use Yiisoft\Http\Header;

use function sprintf;

/**
* Formats throwable into plain text string.
*/
final class PlainTextRenderer implements ThrowableRendererInterface
{
public function __construct(
private readonly string $contentType = 'text/plain',
) {
}

public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
{
return new ErrorData(self::DEFAULT_ERROR_MESSAGE);
return new ErrorData(
self::DEFAULT_ERROR_MESSAGE,
[Header::CONTENT_TYPE => $this->contentType],
);
}

public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
{
return new ErrorData(
self::throwableToString($t)
self::throwableToString($t),
[Header::CONTENT_TYPE => $this->contentType],
);
}

Expand Down
13 changes: 11 additions & 2 deletions src/Renderer/XmlRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Throwable;
use Yiisoft\ErrorHandler\ErrorData;
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
use Yiisoft\Http\Header;

use function str_replace;

Expand All @@ -16,13 +17,18 @@
*/
final class XmlRenderer implements ThrowableRendererInterface
{
private const CONTENT_TYPE = 'application/xml';

public function render(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
{
$content = '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>';
$content .= "\n<error>\n";
$content .= $this->tag('message', self::DEFAULT_ERROR_MESSAGE);
$content .= '</error>';
return new ErrorData($content);
return new ErrorData(
$content,
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
);
}

public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData
Expand All @@ -36,7 +42,10 @@ public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = n
$content .= $this->tag('line', (string) $t->getLine());
$content .= $this->tag('trace', $t->getTraceAsString());
$content .= '</error>';
return new ErrorData($content);
return new ErrorData(
$content,
[Header::CONTENT_TYPE => self::CONTENT_TYPE],
);
}

private function tag(string $name, string $value): string
Expand Down
41 changes: 41 additions & 0 deletions src/RendererProvider/ClosureRendererProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Yiisoft\ErrorHandler\RendererProvider;

use Closure;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Yiisoft\ErrorHandler\ThrowableRendererInterface;

use function is_string;

/**
* Provides a renderer based on a closure that returns a `ThrowableRendererInterface` or its class name.
*
* @psalm-type TClosure = Closure(ServerRequestInterface $request): (class-string<ThrowableRendererInterface>|ThrowableRendererInterface|null)
*/
final class ClosureRendererProvider implements RendererProviderInterface
{
/**
* @psalm-param TClosure $closure
*/
public function __construct(
private readonly Closure $closure,
private readonly ContainerInterface $container,
) {
}

public function get(ServerRequestInterface $request): ?ThrowableRendererInterface
{
$result = ($this->closure)($request);

if (is_string($result)) {
/** @var ThrowableRendererInterface */
return $this->container->get($result);
}

return $result;
}
}
Loading
Loading