From 469c8f29f0710adaf18b57dc47066326e6614f2a Mon Sep 17 00:00:00 2001 From: Kevin Traini Date: Mon, 27 May 2024 18:11:48 +0200 Subject: [PATCH 1/4] doc: Add doc for exception management Part of #18 --- README.md | 33 +++++++++++++++++++++++++++++++++ config/foo.yml | 1 - 2 files changed, 33 insertions(+), 1 deletion(-) delete mode 100644 config/foo.yml diff --git a/README.md b/README.md index b109a54..4a3f071 100644 --- a/README.md +++ b/README.md @@ -130,3 +130,36 @@ class MyMiddleware implements Middleware You can do whatever you want in your middleware. If something went wrong, the procedure is the same as for RequestHandler. + +### Special response handling + +By special, we mean 404, 500, ... In short HTTP code different from 2XX. By default, Archict will use `ResponseHandler` +which just set the corresponding headers. Via the config file of this Brick you change this behavior: + +```yaml +error_handling: + 404: \MyHandler + 501: 'Oops! Something went wrong' +``` + +You have 2 choices: + +1. Pass a string, Archict will use it as response body +2. Pass a class string of a class implementing interface `ResponseHandler`, Archict will call it. For example: + +```php +withBody($factory->createStream("Page '{$request->getUri()->getPath()}' not found!")); + } +} +``` diff --git a/config/foo.yml b/config/foo.yml deleted file mode 100644 index c17f189..0000000 --- a/config/foo.yml +++ /dev/null @@ -1 +0,0 @@ -name: 'hello' \ No newline at end of file From eca776b4d4d5d9dd6561ab146196e2190cd26b5d Mon Sep 17 00:00:00 2001 From: Kevin Traini Date: Mon, 27 May 2024 18:18:16 +0200 Subject: [PATCH 2/4] feat: Router now also handle its response Part of #18 Instead of returning the response object and handle it externally, Router keeps it, and then handle it in a second method --- ...seHandler.php => FinalResponseHandler.php} | 5 ++- include/ResponseHandler.php | 36 +++++++++++++++++++ include/Router.php | 22 +++++++++--- tests/unit/RouterTest.php | 26 ++------------ 4 files changed, 61 insertions(+), 28 deletions(-) rename include/HTTP/{ResponseHandler.php => FinalResponseHandler.php} (97%) create mode 100644 include/ResponseHandler.php diff --git a/include/HTTP/ResponseHandler.php b/include/HTTP/FinalResponseHandler.php similarity index 97% rename from include/HTTP/ResponseHandler.php rename to include/HTTP/FinalResponseHandler.php index c9b2f32..4b86393 100644 --- a/include/HTTP/ResponseHandler.php +++ b/include/HTTP/FinalResponseHandler.php @@ -29,7 +29,10 @@ use Psr\Http\Message\ResponseInterface; -final class ResponseHandler +/** + * @internal + */ +final class FinalResponseHandler { public function writeResponse(ResponseInterface $response): void { diff --git a/include/ResponseHandler.php b/include/ResponseHandler.php new file mode 100644 index 0000000..049d3bd --- /dev/null +++ b/include/ResponseHandler.php @@ -0,0 +1,36 @@ +loadRoutes(); @@ -58,12 +62,22 @@ public function route(ServerRequestInterface $request): ResponseInterface $request, ); - $response = $this->handleRoute($route, $request); + $this->response = $this->handleRoute($route, $request); } catch (HTTPException $exception) { - $response = $exception->toResponse(); + $this->response = $exception->toResponse(); + } catch (Throwable $throwable) { + $this->response = HTTPExceptionFactory::ServerError($throwable->getMessage())->toResponse(); } + } - return $response; + public function response(): void + { + if ($this->response === null) { + throw new LogicException('You should call Router::route() before'); + } + + $final_handler = new FinalResponseHandler(); + $final_handler->writeResponse($this->response); } private function handleMiddleware(MiddlewareInformation $middleware, ServerRequestInterface $request): ServerRequestInterface diff --git a/tests/unit/RouterTest.php b/tests/unit/RouterTest.php index 24137df..02db71a 100644 --- a/tests/unit/RouterTest.php +++ b/tests/unit/RouterTest.php @@ -5,35 +5,15 @@ namespace Archict\Router; use Archict\Core\Core; -use Archict\Core\Services\ServiceManager; -use GuzzleHttp\Psr7\ServerRequest; use PHPUnit\Framework\TestCase; final class RouterTest extends TestCase { - private ServiceManager $service_manager; - private Router $router; - - protected function setUp(): void + public function testRouterIsLoaded(): void { $core = Core::build(); $core->load(); - $this->service_manager = $core->service_manager; - - $router = $this->service_manager->get(Router::class); - self::assertNotNull($router); - $this->router = $router; - } - - public function testRouterIsLoaded(): void - { - self::assertTrue($this->service_manager->has(Router::class)); - self::assertInstanceOf(Router::class, $this->service_manager->get(Router::class)); - } - - public function testItReturns404(): void - { - $response = $this->router->route(new ServerRequest('GET', 'route')); - self::assertSame(404, $response->getStatusCode()); + self::assertTrue($core->service_manager->has(Router::class)); + self::assertInstanceOf(Router::class, $core->service_manager->get(Router::class)); } } From d7c710be4b960e0d69ab2602a102e6442efa5a07 Mon Sep 17 00:00:00 2001 From: Kevin Traini Date: Mon, 27 May 2024 18:35:31 +0200 Subject: [PATCH 3/4] feat: Add config file for Router Service Part of #18 And validate it --- composer.lock | 12 ++-- config/router.yml | 0 include/Config/ConfigurationValidator.php | 59 +++++++++++++++++++ include/Config/RouterConfiguration.php | 42 +++++++++++++ ...ndlerShouldImplementInterfaceException.php | 38 ++++++++++++ .../Exception/HTTPCodeNotHandledException.php | 36 +++++++++++ include/Router.php | 12 +++- 7 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 config/router.yml create mode 100644 include/Config/ConfigurationValidator.php create mode 100644 include/Config/RouterConfiguration.php create mode 100644 include/Exception/ErrorHandlerShouldImplementInterfaceException.php create mode 100644 include/Exception/HTTPCodeNotHandledException.php diff --git a/composer.lock b/composer.lock index aa90f59..9f11b57 100644 --- a/composer.lock +++ b/composer.lock @@ -48,16 +48,16 @@ }, { "name": "archict/core", - "version": "v0.2.0", + "version": "v0.2.1", "source": { "type": "git", "url": "https://github.com/Archict/core.git", - "reference": "ccbc5d5453c5d4ce07eb7e4772f656b9f680c91d" + "reference": "8c18791403f1596a5762753e89fa3c2d99ed5a1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Archict/core/zipball/ccbc5d5453c5d4ce07eb7e4772f656b9f680c91d", - "reference": "ccbc5d5453c5d4ce07eb7e4772f656b9f680c91d", + "url": "https://api.github.com/repos/Archict/core/zipball/8c18791403f1596a5762753e89fa3c2d99ed5a1d", + "reference": "8c18791403f1596a5762753e89fa3c2d99ed5a1d", "shasum": "" }, "require": { @@ -91,9 +91,9 @@ "description": "Heart of Archict, this library load and manage Bricks", "support": { "issues": "https://github.com/Archict/core/issues", - "source": "https://github.com/Archict/core/tree/v0.2.0" + "source": "https://github.com/Archict/core/tree/v0.2.1" }, - "time": "2024-04-25T16:32:59+00:00" + "time": "2024-05-14T06:54:08+00:00" }, { "name": "azjezz/psl", diff --git a/config/router.yml b/config/router.yml new file mode 100644 index 0000000..e69de29 diff --git a/include/Config/ConfigurationValidator.php b/include/Config/ConfigurationValidator.php new file mode 100644 index 0000000..77dcfab --- /dev/null +++ b/include/Config/ConfigurationValidator.php @@ -0,0 +1,59 @@ +error_handling as $code => $handler) { + if ($code < 400 || $code >= 600) { + throw new HTTPCodeNotHandledException($code); + } + + if (class_exists($handler)) { + $reflection = new ReflectionClass($handler); + if (!$reflection->implementsInterface(ResponseHandler::class)) { + throw new ErrorHandlerShouldImplementInterfaceException($code, $handler); + } + } + } + } +} diff --git a/include/Config/RouterConfiguration.php b/include/Config/RouterConfiguration.php new file mode 100644 index 0000000..22c03bc --- /dev/null +++ b/include/Config/RouterConfiguration.php @@ -0,0 +1,42 @@ + $error_handling + */ + public function __construct( + public array $error_handling = [], + ) { + } +} diff --git a/include/Exception/ErrorHandlerShouldImplementInterfaceException.php b/include/Exception/ErrorHandlerShouldImplementInterfaceException.php new file mode 100644 index 0000000..f389a2f --- /dev/null +++ b/include/Exception/ErrorHandlerShouldImplementInterfaceException.php @@ -0,0 +1,38 @@ +route_collection'; @@ -35,12 +39,18 @@ final class Router private RouteCollection $route_collection; private ?ResponseInterface $response; + /** + * @throws HTTPCodeNotHandledException + * @throws ErrorHandlerShouldImplementInterfaceException + */ public function __construct( private readonly EventDispatcher $event_dispatcher, private readonly CacheInterface $cache, + private readonly RouterConfiguration $configuration, ) { $this->mapper = (new MapperBuilder())->allowPermissiveTypes()->mapper(); $this->normalizer = (new MapperBuilder())->normalizer(Format::json()); + (new ConfigurationValidator())->validate($this->configuration); } /** From 94a360f4ce074d6b0d0a7214f0b53a43b4f67a13 Mon Sep 17 00:00:00 2001 From: Kevin Traini Date: Mon, 27 May 2024 18:42:30 +0200 Subject: [PATCH 4/4] feat: Router uses configuration to call config error handlers Part of #18 --- include/Router.php | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/include/Router.php b/include/Router.php index 424f0fa..69bd363 100644 --- a/include/Router.php +++ b/include/Router.php @@ -38,6 +38,7 @@ final class Router private readonly Normalizer $normalizer; private RouteCollection $route_collection; private ?ResponseInterface $response; + private ?ServerRequestInterface $request; /** * @throws HTTPCodeNotHandledException @@ -61,18 +62,19 @@ public function route(ServerRequestInterface $request): void { $this->loadRoutes(); + $this->request = $request; try { - $path = $request->getUri()->getPath(); - $route = $this->route_collection->getMatchingRoute($path, $request->getMethod()); - $middlewares = $this->route_collection->getMatchingMiddlewares($path, $request->getMethod()); + $path = $this->request->getUri()->getPath(); + $route = $this->route_collection->getMatchingRoute($path, $this->request->getMethod()); + $middlewares = $this->route_collection->getMatchingMiddlewares($path, $this->request->getMethod()); - $request = array_reduce( + $this->request = array_reduce( $middlewares, - fn(ServerRequestInterface $request, MiddlewareInformation $middleware) => $this->handleMiddleware($middleware, $request), - $request, + fn(ServerRequestInterface $mid_request, MiddlewareInformation $middleware) => $this->handleMiddleware($middleware, $mid_request), + $this->request, ); - $this->response = $this->handleRoute($route, $request); + $this->response = $this->handleRoute($route, $this->request); } catch (HTTPException $exception) { $this->response = $exception->toResponse(); } catch (Throwable $throwable) { @@ -82,12 +84,26 @@ public function route(ServerRequestInterface $request): void public function response(): void { - if ($this->response === null) { + if ($this->response === null || $this->request === null) { throw new LogicException('You should call Router::route() before'); } + $response = $this->response; + $code = $this->response->getStatusCode(); + if (isset($this->configuration->error_handling[$code])) { + $handler = $this->configuration->error_handling[$code]; + if (class_exists($handler)) { + $object = new $handler(); + assert($object instanceof ResponseHandler); + $response = $object->handleResponse($response, $this->request); + } else { + $factory = new HttpFactory(); + $response = $response->withBody($factory->createStream($handler)); + } + } + $final_handler = new FinalResponseHandler(); - $final_handler->writeResponse($this->response); + $final_handler->writeResponse($response); } private function handleMiddleware(MiddlewareInformation $middleware, ServerRequestInterface $request): ServerRequestInterface