Skip to content

Commit 4e7f75d

Browse files
committed
Extract handlers, adopt Fluent NamespaceLookup, update docs
Move Error, Exception and Status from Routes/ to Handlers/ as standalone handler classes. Replace the internal sideRoutes array with appendHandler() on Router. Rename exceptionRoute()/errorRoute()/ statusRoute() to onException()/onError()/onStatus(). Use Fluent's NamespaceLookup for routine instantiation in AbstractRoute, replacing manual reflection-based class resolution. Update docs/README.md error handling section to reflect the new handler API and add onStatus() example.
1 parent 0f1843b commit 4e7f75d

15 files changed

Lines changed: 160 additions & 124 deletions

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"require": {
2424
"php": ">=8.5",
2525
"psr/container": "^2.0",
26+
"respect/fluent": "^2.0",
2627
"psr/http-factory": "^1.0",
2728
"psr/http-message": "^2.0",
2829
"psr/http-server-handler": "^1.0",

docs/README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -546,24 +546,32 @@ You can use any combination of the above but also need to implement the `Routina
546546

547547
## Error Handling
548548

549-
Respect\Rest provides two special ways to handle errors. The first one is using exception
550-
routes:
549+
Respect\Rest provides handlers for exceptions and errors. Register an exception
550+
handler with `onException`:
551551

552552
```php
553-
$r3->exceptionRoute('InvalidArgumentException', function (InvalidArgumentException $e) {
553+
$r3->onException('InvalidArgumentException', function (InvalidArgumentException $e) {
554554
return 'Sorry, this error happened: ' . $e->getMessage();
555555
});
556556
```
557557

558558
Whenever an uncaught exception appears on any route, it will be caught and forwarded to
559-
this side route. Similarly, there is a route for PHP errors:
559+
this handler. Similarly, there is a handler for PHP errors:
560560

561561
```php
562-
$r3->errorRoute(function (array $err) {
562+
$r3->onError(function (array $err) {
563563
return 'Sorry, these errors happened: ' . var_export($err, true);
564564
});
565565
```
566566

567+
You can also handle specific HTTP status codes:
568+
569+
```php
570+
$r3->onStatus(404, function () {
571+
return 'Page not found';
572+
});
573+
```
574+
567575
***
568576

569577
See also:

example/full.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,11 @@ public function get(string $id): string
184184
throw new RuntimeException('Something went wrong!');
185185
});
186186

187-
$r3->exceptionRoute('RuntimeException', function (RuntimeException $e) {
187+
$r3->onException('RuntimeException', function (RuntimeException $e) {
188188
return 'Caught exception: ' . $e->getMessage();
189189
});
190190

191-
$r3->errorRoute(function (array $err) {
191+
$r3->onError(function (array $err) {
192192
return 'Error occurred: ' . ($err[0]['message'] ?? 'unknown');
193193
});
194194

src/DispatchContext.php

Lines changed: 56 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010
use Psr\Http\Message\ServerRequestInterface;
1111
use Psr\Http\Message\StreamFactoryInterface;
1212
use Respect\Parameter\Resolver;
13+
use Respect\Rest\Handlers\ErrorHandler;
14+
use Respect\Rest\Handlers\ExceptionHandler;
15+
use Respect\Rest\Handlers\StatusHandler;
1316
use Respect\Rest\Routes\AbstractRoute;
1417
use Throwable;
1518

19+
use function in_array;
1620
use function is_a;
1721
use function rawurldecode;
1822
use function rtrim;
@@ -50,7 +54,7 @@ final class DispatchContext implements ContainerInterface
5054
private string $effectivePath = '';
5155

5256
/** @var array<int, AbstractRoute> */
53-
private array $sideRoutes = [];
57+
private array $handlers = [];
5458

5559
private Resolver|null $resolver = null;
5660

@@ -138,23 +142,22 @@ public function response(): ResponseInterface|null
138142
}
139143

140144
$route = $this->route;
145+
$isHandler = in_array($route, $this->handlers, true);
146+
$previousErrorHandler = $isHandler ? null : $this->installErrorHandler();
141147

142148
try {
143-
$errorHandler = $this->prepareForErrorForwards($route);
144149
$preRoutineResult = $this->routinePipeline()->processBy($this, $route);
145150

146-
if ($preRoutineResult !== null) {
147-
if ($preRoutineResult instanceof AbstractRoute) {
148-
return $this->forward($preRoutineResult);
149-
}
151+
if ($preRoutineResult instanceof AbstractRoute) {
152+
return $this->forward($preRoutineResult);
153+
}
150154

151-
if ($preRoutineResult instanceof ResponseInterface) {
152-
return $this->finalizeResponse($preRoutineResult);
153-
}
155+
if ($preRoutineResult instanceof ResponseInterface) {
156+
return $this->finalizeResponse($preRoutineResult);
157+
}
154158

155-
if ($preRoutineResult === false) {
156-
return $this->finalizeResponse('');
157-
}
159+
if ($preRoutineResult === false) {
160+
return $this->finalizeResponse('');
158161
}
159162

160163
$rawResult = $route->dispatchTarget($this->method(), $this->params, $this);
@@ -164,20 +167,28 @@ public function response(): ResponseInterface|null
164167
}
165168

166169
$processedResult = $this->routinePipeline()->processThrough($this, $route, $rawResult);
167-
$errorResponse = $this->forwardErrors($errorHandler, $route);
168170

169-
if ($errorResponse !== null) {
170-
return $errorResponse;
171+
if (!$isHandler) {
172+
$errorResponse = $this->forwardCollectedErrors();
173+
if ($errorResponse !== null) {
174+
return $errorResponse;
175+
}
171176
}
172177

173178
return $this->finalizeResponse($processedResult);
174179
} catch (Throwable $e) {
175-
$exceptionResponse = $this->catchExceptions($e, $route);
176-
if ($exceptionResponse === null) {
177-
throw $e;
180+
if (!$isHandler) {
181+
$exceptionResponse = $this->catchExceptions($e);
182+
if ($exceptionResponse !== null) {
183+
return $exceptionResponse;
184+
}
178185
}
179186

180-
return $exceptionResponse;
187+
throw $e;
188+
} finally {
189+
if ($previousErrorHandler !== null) {
190+
set_error_handler($previousErrorHandler);
191+
}
181192
}
182193
}
183194

@@ -193,10 +204,10 @@ public function setRoutinePipeline(RoutinePipeline $routinePipeline): void
193204
$this->routinePipeline = $routinePipeline;
194205
}
195206

196-
/** @param array<int, AbstractRoute> $sideRoutes */
197-
public function setSideRoutes(array $sideRoutes): void
207+
/** @param array<int, AbstractRoute> $handlers */
208+
public function setHandlers(array $handlers): void
198209
{
199-
$this->sideRoutes = $sideRoutes;
210+
$this->handlers = $handlers;
200211
}
201212

202213
public function setResponder(Responder $responder): void
@@ -228,19 +239,19 @@ public function get(string $id): mixed
228239
throw new NotFoundException(sprintf('No entry found for "%s"', $id));
229240
}
230241

231-
/** @return callable|null The previous error handler, or null */
232-
protected function prepareForErrorForwards(AbstractRoute $route): callable|null
242+
/** @return callable|null The previous error handler, or null if no ErrorHandler is registered */
243+
private function installErrorHandler(): callable|null
233244
{
234-
foreach ($route->sideRoutes as $sideRoute) {
235-
if ($sideRoute instanceof Routes\Error) {
245+
foreach ($this->handlers as $handler) {
246+
if ($handler instanceof ErrorHandler) {
236247
return set_error_handler(
237248
static function (
238249
int $errno,
239250
string $errstr,
240251
string $errfile = '',
241252
int $errline = 0,
242-
) use ($sideRoute): bool {
243-
$sideRoute->errors[] = [$errno, $errstr, $errfile, $errline];
253+
) use ($handler): bool {
254+
$handler->errors[] = [$errno, $errstr, $errfile, $errline];
244255

245256
return true;
246257
},
@@ -251,54 +262,50 @@ static function (
251262
return null;
252263
}
253264

254-
protected function forwardErrors(callable|null $errorHandler, AbstractRoute $route): ResponseInterface|null
265+
private function forwardCollectedErrors(): ResponseInterface|null
255266
{
256-
if ($errorHandler !== null) {
257-
set_error_handler($errorHandler);
258-
}
259-
260-
foreach ($route->sideRoutes as $sideRoute) {
261-
if ($sideRoute instanceof Routes\Error && $sideRoute->errors) {
262-
return $this->forward($sideRoute);
267+
foreach ($this->handlers as $handler) {
268+
if ($handler instanceof ErrorHandler && $handler->errors) {
269+
return $this->forward($handler);
263270
}
264271
}
265272

266273
return null;
267274
}
268275

269-
protected function catchExceptions(Throwable $e, AbstractRoute $route): ResponseInterface|null
276+
private function catchExceptions(Throwable $e): ResponseInterface|null
270277
{
271-
foreach ($route->sideRoutes as $sideRoute) {
272-
if (!$sideRoute instanceof Routes\Exception) {
278+
foreach ($this->handlers as $handler) {
279+
if (!$handler instanceof ExceptionHandler) {
273280
continue;
274281
}
275282

276-
if (is_a($e, $sideRoute->class)) {
277-
$sideRoute->exception = $e;
283+
if (is_a($e, $handler->class)) {
284+
$handler->exception = $e;
278285

279-
return $this->forward($sideRoute);
286+
return $this->forward($handler);
280287
}
281288
}
282289

283290
return null;
284291
}
285292

286-
protected function forwardToStatusRoute(ResponseInterface $preparedResponse): ResponseInterface|null
293+
private function forwardToStatusRoute(ResponseInterface $preparedResponse): ResponseInterface|null
287294
{
288295
$statusCode = $preparedResponse->getStatusCode();
289296

290-
foreach ($this->sideRoutes as $sideRoute) {
297+
foreach ($this->handlers as $handler) {
291298
if (
292-
$sideRoute instanceof Routes\Status
293-
&& ($sideRoute->statusCode === $statusCode || $sideRoute->statusCode === null)
299+
$handler instanceof StatusHandler
300+
&& ($handler->statusCode === $statusCode || $handler->statusCode === null)
294301
) {
295302
$this->hasStatusOverride = true;
296303

297304
// Run routine negotiation (e.g. Accept) before forwarding,
298305
// since the normal route-selection phase was skipped
299-
$this->routinePipeline()->matches($this, $sideRoute, $this->params);
306+
$this->routinePipeline()->matches($this, $handler, $this->params);
300307

301-
$result = $this->forward($sideRoute);
308+
$result = $this->forward($handler);
302309

303310
// Preserve the original status code on the forwarded response
304311
return $result?->withStatus($statusCode);
@@ -308,7 +315,7 @@ protected function forwardToStatusRoute(ResponseInterface $preparedResponse): Re
308315
return null;
309316
}
310317

311-
protected function finalizeResponse(mixed $response): ResponseInterface
318+
private function finalizeResponse(mixed $response): ResponseInterface
312319
{
313320
return $this->responder()->finalize(
314321
$response,

src/DispatchEngine.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public function dispatchContext(DispatchContext $context): DispatchContext
6060
}
6161

6262
$context->setRoutinePipeline($this->routinePipeline);
63-
$context->setSideRoutes($this->routeProvider->getSideRoutes());
63+
$context->setHandlers($this->routeProvider->getHandlers());
6464

6565
if (!$this->isRoutelessDispatch($context) && $context->route === null) {
6666
$this->routeDispatch($context);
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
declare(strict_types=1);
44

5-
namespace Respect\Rest\Routes;
5+
namespace Respect\Rest\Handlers;
66

77
use Respect\Rest\DispatchContext;
8+
use Respect\Rest\Routes\Callback;
89

9-
final class Error extends Callback
10+
final class ErrorHandler extends Callback
1011
{
1112
/** @var callable */
1213
public $callback;
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
declare(strict_types=1);
44

5-
namespace Respect\Rest\Routes;
5+
namespace Respect\Rest\Handlers;
66

77
use Respect\Rest\DispatchContext;
8+
use Respect\Rest\Routes\Callback;
89
use Throwable;
910

10-
final class Exception extends Callback
11+
final class ExceptionHandler extends Callback
1112
{
1213
/** @var callable */
1314
public $callback;
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
declare(strict_types=1);
44

5-
namespace Respect\Rest\Routes;
5+
namespace Respect\Rest\Handlers;
66

7-
final class Status extends Callback
7+
use Respect\Rest\Routes\Callback;
8+
9+
final class StatusHandler extends Callback
810
{
911
/** @var callable */
1012
public $callback;

src/RouteProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ interface RouteProvider
1212
public function getRoutes(): array;
1313

1414
/** @return array<int, AbstractRoute> */
15-
public function getSideRoutes(): array;
15+
public function getHandlers(): array;
1616

1717
public function getBasePath(): string;
1818
}

0 commit comments

Comments
 (0)