Skip to content

Commit 5a67c19

Browse files
committed
feat: add ability to customize error rendering
1 parent 1bb88f6 commit 5a67c19

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+895
-344
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@
249249
"lint:fix": "vendor/bin/mago lint --fix --format-after-fix",
250250
"style": "composer fmt && composer lint:fix",
251251
"test": "composer phpunit",
252+
"test:stop": "composer phpunit --stop-on-error --stop-on-failure",
252253
"lint": "vendor/bin/mago lint --potentially-unsafe --minimum-fail-level=note",
253254
"phpstan": "vendor/bin/phpstan analyse src tests --memory-limit=1G",
254255
"rector": "vendor/bin/rector process --no-ansi",
Lines changed: 75 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,60 @@
11
---
22
title: Exception handling
3-
description: "Learn how to gracefully handle exceptions in your application by writing exception processors."
3+
description: "Learn how exception handling works, how to manually report exceptions, and how to customize exception rendering for HTTP responses."
44
---
55

66
## Overview
77

8-
Tempest comes with its own exception handler, which provides a simple way to catch and process exceptions. During local development, Tempest uses [Whoops](https://github.com/filp/whoops) to display detailed error pages. In production, it will show a generic error page.
8+
Tempest comes with its own exception handler, which provides a simple way to catch and process exceptions. During local development, Tempest uses [Whoops](https://github.com/filp/whoops) to display detailed error pages. In production, it displays a generic error page.
99

10-
When an exception is thrown, it will be caught and piped through the registered exception processors. By default, the only registered exception processor, {b`Tempest\Core\LogExceptionProcessor`}, will simply log the exception.
10+
When an exception is thrown, it is caught and piped through the registered exception reporters. By default, the only registered exception reporter, {b`Tempest\Core\LoggingExceptionReporter`}, logs the exception.
1111

12-
Of course, you may create your own exception processors. This is done by creating a class that implements the {`Tempest\Core\ExceptionProcessor`} interface. Classes implementing this interface are automatically [discovered](../4-internals/02-discovery.md), so you don't need to register them manually.
12+
Custom exception reporters can be created by implementing the {b`Tempest\Core\Exceptions\ExceptionReporter`} interface. Classes implementing this interface are automatically [discovered](../4-internals/02-discovery.md) and do not require manual registration.
1313

14-
## Reporting exceptions
14+
## Processing exceptions
1515

16-
Sometimes, you may want to report an exception without necessarily throwing it. For example, you may want to log an exception, but not stop the execution of the application. To do this, you can use the `Tempest\report()` function.
16+
Exceptions can be reported without throwing them using the `process()` method of the {b`Tempest\Core\Exceptions\ExceptionProcessor`} interface. This allows putting exceptions through the reporting process without stopping the application's execution.
1717

18-
```php
19-
use function Tempest\report;
20-
21-
try {
22-
// Some code that may throw an exception
23-
} catch (SomethingFailed $e) {
24-
report($e);
25-
}
26-
```
27-
28-
## Disabling default logging
29-
30-
Exception processors are discovered when Tempest boots, then stored in the `exceptionProcessors` property of {`Tempest\Core\AppConfig`}. The default logging processor, {b`Tempest\Core\LogExceptionProcessor`}, is automatically added to the list of processors.
31-
32-
To disable exception logging, you may remove it in a `KernelEvent::BOOTED` event handler:
18+
```php app/CreateUser.php
19+
use Tempest\Core\Exceptions\ExceptionProcessor;
3320

34-
```php
35-
use Tempest\Core\AppConfig;
36-
use Tempest\Core\KernelEvent;
37-
use Tempest\Core\LogExceptionProcessor;
38-
use Tempest\EventBus\EventHandler;
39-
use Tempest\Support\Arr;
40-
41-
final readonly class DisableExceptionLogging
21+
final readonly class CreateUser
4222
{
4323
public function __construct(
44-
private AppConfig $appConfig,
45-
) {
46-
}
24+
private ExceptionProcessor $exceptions
25+
) {}
4726

48-
#[EventHandler(KernelEvent::BOOTED)]
4927
public function __invoke(): void
5028
{
51-
Arr\forget_values($this->appConfig->exceptionProcessors, LogExceptionProcessor::class);
29+
try {
30+
// Some code that may throw an exception
31+
} catch (SomethingFailed $somethingFailed) {
32+
$this->exceptions->process($somethingFailed);
33+
}
5234
}
5335
}
5436
```
5537

38+
## Disabling exception logging
39+
40+
The default logging reporter, {b`Tempest\Core\Exceptions\ExceptionReporter`}, is automatically added to the list of reporters. To disable it, create an {b`Tempest\Core\Exceptions\ExceptionsConfig`} [configuration file](../1-essentials/06-configuration.md#configuration-files) and set `logging` to `false`:
41+
42+
```php app/exceptions.config.php
43+
use Tempest\Core\Exceptions\ExceptionsConfig;
44+
45+
return new ExceptionsConfig(
46+
logging: false,
47+
);
48+
```
49+
5650
## Adding context to exceptions
5751

58-
Sometimes, an exception may have information that you would like to be logged. By implementing the {`Tempest\Core\HasContext`} interface on an exception class, you can provide additional context that will be logged—and available to other processors.
52+
Exceptions can provide additional information for logging by implementing the {`Tempest\Core\ProvidesContext`} interface. The context data becomes available to exception processors.
5953

6054
```php
61-
use Tempest\Core\HasContext;
55+
use Tempest\Core\ProvidesContext;
6256

63-
final readonly class UserWasNotFound extends Exception implements HasContext
57+
final readonly class UserWasNotFound extends Exception implements ProvidesContext
6458
{
6559
public function __construct(private string $userId)
6660
{
@@ -76,47 +70,68 @@ final readonly class UserWasNotFound extends Exception implements HasContext
7670
}
7771
```
7872

79-
## Customizing the error page
73+
## Customizing exception rendering
8074

81-
In production, when an uncaught exception occurs, Tempest displays a minimalistic, generic error page. You may customize this behavior by adding a middleware dedicated to catching {b`Tempest\Http\HttpRequestFailed`} exceptions.
75+
Exception renderers provide control over how exceptions are rendered in HTTP responses. Custom renderers can be used to display specialized error pages for specific exception types, format errors differently based on content type (JSON, HTML, XML), or provide user-friendly error messages for common scenarios like 404 or validation failures.
8276

83-
For instance, you may display a branded error page by providing a view:
77+
To create a custom renderer, implement the {b`Tempest\Router\Exceptions\ExceptionRenderer`} interface. It requires a `canRender()` method to determine if the renderer can handle the given exception and request, and a `render()` method to produce the response:
8478

85-
```php
79+
```php app/NotFoundExceptionRenderer.php
80+
use Tempest\Http\ContentType;
8681
use Tempest\Http\HttpRequestFailed;
87-
use Tempest\Router\HttpMiddleware;
82+
use Tempest\Http\Request;
83+
use Tempest\Http\Response;
84+
use Tempest\Http\Responses\NotFound;
85+
use Tempest\Http\Status;
86+
use Tempest\Router\Exceptions\ExceptionRenderer;
87+
use Throwable;
88+
8889
use function Tempest\view;
8990

90-
final class CatchHttpRequestFailuresMiddleware implements HttpMiddleware
91+
final class NotFoundExceptionRenderer implements ExceptionRenderer
9192
{
92-
public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
93+
public function canRender(Throwable $throwable, Request $request): bool
9394
{
94-
try {
95-
return $next($request);
96-
} catch (HttpRequestFailed $failure) {
97-
return new GenericResponse(
98-
status: $failure->status,
99-
body: view('./error.view.php', failure: $failure),
100-
);
95+
if (! $request->accepts(ContentType::HTML)) {
96+
return false;
97+
}
98+
99+
if (! $throwable instanceof HttpRequestFailed) {
100+
return false;
101101
}
102+
103+
return $throwable->status === Status::NOT_FOUND;
104+
}
105+
106+
public function render(Throwable $throwable): Response
107+
{
108+
return new NotFound(
109+
body: view('./404.view.php'),
110+
);
102111
}
103112
}
104113
```
105114

115+
:::info
116+
Exception renderers are automatically [discovered](../4-internals/02-discovery.md) and checked in {b`#[Tempest\Core\Priority]`} order.
117+
:::
118+
106119
## Testing
107120

108-
By extending {`Tempest\Framework\Testing\IntegrationTest`} from your test case, you gain access to the exception testing utilities, which allow you to make assertions about reported exceptions.
121+
By extending {`Tempest\Framework\Testing\IntegrationTest`} from a test case, exception testing utilities may be accessed for making assertions about processed exceptions.
109122

110123
```php
111-
// Prevents exceptions from being actually processed
112-
$this->exceptions->preventReporting();
124+
// Allows exceptions to be processed during tests
125+
$this->exceptions->allowProcessing();
113126

114-
// Asserts that the exception was reported
115-
$this->exceptions->assertReported(UserNotFound::class);
127+
// Assert that the exception was processed
128+
$this->exceptions->assertProcessed(UserNotFound::class);
116129

117-
// Asserts that the exception was not reported
118-
$this->exceptions->assertNotReported(UserNotFound::class);
130+
// Assert that the exception was not processed
131+
$this->exceptions->assertNotProcessed(UserNotFound::class);
119132

120-
// Asserts that no exceptions were reported
121-
$this->exceptions->assertNothingReported();
133+
// Assert that no exceptions were processed
134+
$this->exceptions->assertNothingProcessed();
122135
```
136+
137+
By default, Tempest disables exception processing during tests. It is recommended to unit-test your own {b`Tempest\Core\Exceptions\ExceptionReporter`} implementations.

packages/console/src/Exceptions/ConsoleExceptionHandler.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use Tempest\Console\Input\ConsoleArgumentBag;
1212
use Tempest\Container\Tag;
1313
use Tempest\Core\ExceptionHandler;
14-
use Tempest\Core\ExceptionReporter;
14+
use Tempest\Core\Exceptions\ExceptionProcessor;
1515
use Tempest\Core\Kernel;
1616
use Tempest\Highlight\Escape;
1717
use Tempest\Highlight\Highlighter;
@@ -28,13 +28,13 @@ public function __construct(
2828
private Highlighter $highlighter,
2929
private Console $console,
3030
private ConsoleArgumentBag $argumentBag,
31-
private ExceptionReporter $exceptionReporter,
31+
private ExceptionProcessor $exceptionProcessor,
3232
) {}
3333

3434
public function handle(Throwable $throwable): void
3535
{
3636
try {
37-
$this->exceptionReporter->report($throwable);
37+
$this->exceptionProcessor->process($throwable);
3838

3939
$this->console
4040
->writeln()

packages/core/src/AppConfig.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ public function __construct(
1919

2020
?string $baseUri = null,
2121

22-
/** @var class-string<\Tempest\Core\ExceptionProcessor>[] */
23-
public array $exceptionProcessors = [],
24-
2522
/**
2623
* @var array<class-string<\Tempest\Core\InsightsProvider>>
2724
*/

packages/core/src/ExceptionProcessor.php

Lines changed: 0 additions & 13 deletions
This file was deleted.

packages/core/src/ExceptionReporter.php

Lines changed: 0 additions & 38 deletions
This file was deleted.

packages/core/src/ExceptionTester.php

Lines changed: 0 additions & 90 deletions
This file was deleted.

packages/core/src/ExceptionHandlerInitializer.php renamed to packages/core/src/Exceptions/ExceptionHandlerInitializer.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
<?php
22

3-
namespace Tempest\Core;
3+
namespace Tempest\Core\Exceptions;
44

55
use Tempest\Console\Exceptions\ConsoleExceptionHandler;
66
use Tempest\Container\Container;
77
use Tempest\Container\Initializer;
88
use Tempest\Container\Singleton;
9+
use Tempest\Core\ExceptionHandler;
910
use Tempest\Router\Exceptions\HttpExceptionHandler;
1011

1112
final class ExceptionHandlerInitializer implements Initializer
1213
{
1314
#[Singleton]
1415
public function initialize(Container $container): ExceptionHandler
1516
{
16-
$config = $container->get(AppConfig::class);
17-
1817
return match (true) {
1918
PHP_SAPI === 'cli' => $container->get(ConsoleExceptionHandler::class),
2019
default => $container->get(HttpExceptionHandler::class),

0 commit comments

Comments
 (0)