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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"require": {
"php": ">=8.5",
"psr/container": "^2.0",
"respect/fluent": "^2.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^2.0",
"psr/http-server-handler": "^1.0",
Expand Down
18 changes: 13 additions & 5 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -546,24 +546,32 @@ You can use any combination of the above but also need to implement the `Routina

## Error Handling

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

```php
$r3->exceptionRoute('InvalidArgumentException', function (InvalidArgumentException $e) {
$r3->onException('InvalidArgumentException', function (InvalidArgumentException $e) {
return 'Sorry, this error happened: ' . $e->getMessage();
});
```

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

```php
$r3->errorRoute(function (array $err) {
$r3->onError(function (array $err) {
return 'Sorry, these errors happened: ' . var_export($err, true);
});
```

You can also handle specific HTTP status codes:

```php
$r3->onStatus(404, function () {
return 'Page not found';
});
```

***

See also:
Expand Down
4 changes: 2 additions & 2 deletions example/full.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,11 @@ public function get(string $id): string
throw new RuntimeException('Something went wrong!');
});

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

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

Expand Down
105 changes: 56 additions & 49 deletions src/DispatchContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Respect\Parameter\Resolver;
use Respect\Rest\Handlers\ErrorHandler;
use Respect\Rest\Handlers\ExceptionHandler;
use Respect\Rest\Handlers\StatusHandler;
use Respect\Rest\Routes\AbstractRoute;
use Throwable;

use function in_array;
use function is_a;
use function rawurldecode;
use function rtrim;
Expand Down Expand Up @@ -50,7 +54,7 @@ final class DispatchContext implements ContainerInterface
private string $effectivePath = '';

/** @var array<int, AbstractRoute> */
private array $sideRoutes = [];
private array $handlers = [];

private Resolver|null $resolver = null;

Expand Down Expand Up @@ -138,23 +142,22 @@ public function response(): ResponseInterface|null
}

$route = $this->route;
$isHandler = in_array($route, $this->handlers, true);
$previousErrorHandler = $isHandler ? null : $this->installErrorHandler();

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

if ($preRoutineResult !== null) {
if ($preRoutineResult instanceof AbstractRoute) {
return $this->forward($preRoutineResult);
}
if ($preRoutineResult instanceof AbstractRoute) {
return $this->forward($preRoutineResult);
}

if ($preRoutineResult instanceof ResponseInterface) {
return $this->finalizeResponse($preRoutineResult);
}
if ($preRoutineResult instanceof ResponseInterface) {
return $this->finalizeResponse($preRoutineResult);
}

if ($preRoutineResult === false) {
return $this->finalizeResponse('');
}
if ($preRoutineResult === false) {
return $this->finalizeResponse('');
}

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

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

if ($errorResponse !== null) {
return $errorResponse;
if (!$isHandler) {
$errorResponse = $this->forwardCollectedErrors();
if ($errorResponse !== null) {
return $errorResponse;
}
}

return $this->finalizeResponse($processedResult);
} catch (Throwable $e) {
$exceptionResponse = $this->catchExceptions($e, $route);
if ($exceptionResponse === null) {
throw $e;
if (!$isHandler) {
$exceptionResponse = $this->catchExceptions($e);
if ($exceptionResponse !== null) {
return $exceptionResponse;
}
}

return $exceptionResponse;
throw $e;
} finally {
if ($previousErrorHandler !== null) {
set_error_handler($previousErrorHandler);
}
}
}

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

/** @param array<int, AbstractRoute> $sideRoutes */
public function setSideRoutes(array $sideRoutes): void
/** @param array<int, AbstractRoute> $handlers */
public function setHandlers(array $handlers): void
{
$this->sideRoutes = $sideRoutes;
$this->handlers = $handlers;
}

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

/** @return callable|null The previous error handler, or null */
protected function prepareForErrorForwards(AbstractRoute $route): callable|null
/** @return callable|null The previous error handler, or null if no ErrorHandler is registered */
private function installErrorHandler(): callable|null
{
foreach ($route->sideRoutes as $sideRoute) {
if ($sideRoute instanceof Routes\Error) {
foreach ($this->handlers as $handler) {
if ($handler instanceof ErrorHandler) {
return set_error_handler(
static function (
int $errno,
string $errstr,
string $errfile = '',
int $errline = 0,
) use ($sideRoute): bool {
$sideRoute->errors[] = [$errno, $errstr, $errfile, $errline];
) use ($handler): bool {
$handler->errors[] = [$errno, $errstr, $errfile, $errline];

return true;
},
Expand All @@ -251,54 +262,50 @@ static function (
return null;
}

protected function forwardErrors(callable|null $errorHandler, AbstractRoute $route): ResponseInterface|null
private function forwardCollectedErrors(): ResponseInterface|null
{
if ($errorHandler !== null) {
set_error_handler($errorHandler);
}

foreach ($route->sideRoutes as $sideRoute) {
if ($sideRoute instanceof Routes\Error && $sideRoute->errors) {
return $this->forward($sideRoute);
foreach ($this->handlers as $handler) {
if ($handler instanceof ErrorHandler && $handler->errors) {
return $this->forward($handler);
}
}

return null;
}

protected function catchExceptions(Throwable $e, AbstractRoute $route): ResponseInterface|null
private function catchExceptions(Throwable $e): ResponseInterface|null
{
foreach ($route->sideRoutes as $sideRoute) {
if (!$sideRoute instanceof Routes\Exception) {
foreach ($this->handlers as $handler) {
if (!$handler instanceof ExceptionHandler) {
continue;
}

if (is_a($e, $sideRoute->class)) {
$sideRoute->exception = $e;
if (is_a($e, $handler->class)) {
$handler->exception = $e;

return $this->forward($sideRoute);
return $this->forward($handler);
}
}

return null;
}

protected function forwardToStatusRoute(ResponseInterface $preparedResponse): ResponseInterface|null
private function forwardToStatusRoute(ResponseInterface $preparedResponse): ResponseInterface|null
{
$statusCode = $preparedResponse->getStatusCode();

foreach ($this->sideRoutes as $sideRoute) {
foreach ($this->handlers as $handler) {
if (
$sideRoute instanceof Routes\Status
&& ($sideRoute->statusCode === $statusCode || $sideRoute->statusCode === null)
$handler instanceof StatusHandler
&& ($handler->statusCode === $statusCode || $handler->statusCode === null)
) {
$this->hasStatusOverride = true;

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

$result = $this->forward($sideRoute);
$result = $this->forward($handler);

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

protected function finalizeResponse(mixed $response): ResponseInterface
private function finalizeResponse(mixed $response): ResponseInterface
{
return $this->responder()->finalize(
$response,
Expand Down
2 changes: 1 addition & 1 deletion src/DispatchEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public function dispatchContext(DispatchContext $context): DispatchContext
}

$context->setRoutinePipeline($this->routinePipeline);
$context->setSideRoutes($this->routeProvider->getSideRoutes());
$context->setHandlers($this->routeProvider->getHandlers());

if (!$this->isRoutelessDispatch($context) && $context->route === null) {
$this->routeDispatch($context);
Expand Down
5 changes: 3 additions & 2 deletions src/Routes/Error.php → src/Handlers/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

declare(strict_types=1);

namespace Respect\Rest\Routes;
namespace Respect\Rest\Handlers;

use Respect\Rest\DispatchContext;
use Respect\Rest\Routes\Callback;

final class Error extends Callback
final class ErrorHandler extends Callback
{
/** @var callable */
public $callback;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

declare(strict_types=1);

namespace Respect\Rest\Routes;
namespace Respect\Rest\Handlers;

use Respect\Rest\DispatchContext;
use Respect\Rest\Routes\Callback;
use Throwable;

final class Exception extends Callback
final class ExceptionHandler extends Callback
{
/** @var callable */
public $callback;
Expand Down
6 changes: 4 additions & 2 deletions src/Routes/Status.php → src/Handlers/StatusHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

declare(strict_types=1);

namespace Respect\Rest\Routes;
namespace Respect\Rest\Handlers;

final class Status extends Callback
use Respect\Rest\Routes\Callback;

final class StatusHandler extends Callback
{
/** @var callable */
public $callback;
Expand Down
2 changes: 1 addition & 1 deletion src/RouteProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface RouteProvider
public function getRoutes(): array;

/** @return array<int, AbstractRoute> */
public function getSideRoutes(): array;
public function getHandlers(): array;

public function getBasePath(): string;
}
Loading