From 358dec6cc1725d9057155661d6f1cc5802aee7d2 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Sat, 9 Nov 2024 16:16:42 +0000 Subject: [PATCH] Updated caching router to accept a custom cache key --- docs/6.x/cached-router.md | 76 ++++++++ docs/6.x/controllers.md | 269 ++++++++++++++++++++++++++++ docs/6.x/dependency-injection.md | 81 +++++++++ docs/6.x/http.md | 94 ++++++++++ docs/6.x/index.md | 89 ++++++++++ docs/6.x/middleware.md | 151 ++++++++++++++++ docs/6.x/routes.md | 240 +++++++++++++++++++++++++ docs/6.x/strategies.md | 294 +++++++++++++++++++++++++++++++ docs/6.x/usage.md | 118 +++++++++++++ src/Cache/Router.php | 11 +- 10 files changed, 1417 insertions(+), 6 deletions(-) create mode 100644 docs/6.x/cached-router.md create mode 100644 docs/6.x/controllers.md create mode 100644 docs/6.x/dependency-injection.md create mode 100644 docs/6.x/http.md create mode 100644 docs/6.x/index.md create mode 100644 docs/6.x/middleware.md create mode 100644 docs/6.x/routes.md create mode 100644 docs/6.x/strategies.md create mode 100644 docs/6.x/usage.md diff --git a/docs/6.x/cached-router.md b/docs/6.x/cached-router.md new file mode 100644 index 0000000..ad636cb --- /dev/null +++ b/docs/6.x/cached-router.md @@ -0,0 +1,76 @@ +--- +layout: post +title: Cached Router (BETA) +sections: + Introduction: introduction + BETA Notes: beta-notes + To-do: to-do + Usage: usage + Cache Stores: cache-stores +--- +> **Note:** The cached router implementation is currently in BETA and is not recommended for production applications without thorough testing. + +## Introduction + +Route provides a way to improve performance on larger applications by caching a serialised, fully configured router, minimising the amount of bootstrap code that is executed on each request. + +## BETA Notes + +A cached router is essentially a fully configured router object, serialised, and stored in a simple cache store. While this works well in test scenarios, depending on the controllers you add to the router, it is actually possible that the cache will attempt to serialise your entire application and cause side effects to your code depending on any custom magic methods you may be implementing. + +It is recommended that when using a cached router, you lazy load your controllers. This way, they will not be instantiated/invoked until they are used. + +Please report any issues you via an issue on the repository. + +Why is this BETA feature included? To encourage higher rates of testing to make the feature as stable as possible. + +## To-do + +- ✓ Provide a way to create a router, build it and cache the resulting object. +- ✓ Have cached router accept any implementation of PSR-16 simple cache interface. + - ✓ Provide file based implementation. +- Test test test test test. +- Suggestions? Open tickets on repository. + +## Usage + +Usage of the cached router is very similar to usage of the standard route, but rather than instantiating and configuring `League\Route\Router`, you instead instantiate a cached router with a `callable` that will be used to configure the router. + +~~~php +map('GET', '/', function (ServerRequestInterface $request): ResponseInterface { + $response = new Laminas\Diactoros\Response; + $response->getBody()->write('

Hello, World!

'); + return $response; + }); + + return $router; +}, $cacheStore, cacheEnabled: true, cacheKey: 'my-router'); + +$request = Laminas\Diactoros\ServerRequestFactory::fromGlobals( + $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES +); + +$response = $cachedRouter->dispatch($request); + +// send the response to the browser +(new Laminas\HttpHandlerRunner\Emitter\SapiEmitter)->emit($response); +~~~ + +In the example above, if the file `/path/to/cache/file.cache` does not exist, the `callable` passed to the cached router will be invoked, and the returned router will be serialised and cached. + +On subsequent requests, the router will be resolved from the cache file instead. + +## Cache Stores + +The cached router can use any [PSR-16](https://www.php-fig.org/psr/psr-16/) simple cache implementation, serialisation happens before it is passed to be stored in the cache, so the implementation will always only use one namespaced key, with the value being the serialised router. diff --git a/docs/6.x/controllers.md b/docs/6.x/controllers.md new file mode 100644 index 0000000..0ca17fd --- /dev/null +++ b/docs/6.x/controllers.md @@ -0,0 +1,269 @@ +--- +layout: post +title: Controllers +sections: + Introduction: introduction + Defining Controllers: defining-controllers + Types of Controllers: types-of-controllers + Dependency Injection: dependency-injection +--- +## Introduction + +Every defined route requires a `callable` to invoke when dispatched, something that could be described as a controller in MVC. By default, Route only imposes that the callable is defined with a specific signature, it is given a request object as the first argument, an associative array of wildcard route arguments as the second argument, and expects a response object to be returned. Read more about this in [HTTP](/5.x/http). + +This behaviour can be changed by creating/using a different strategy, read more about strategies [here](/5.x/strategies). + +## Defining Controllers + +Defining what controller is invoked when a route is matched is as easy as padding a callable as the the third argument of the `map` method or the second argument of the proxy methods for different request verbs, `get`, `post` etc. + +~~~php +map('GET', '/route', function (ServerRequestInterface $request): ResponseInterface { + // ... +}); + +$router->get('/another-route', function (ServerRequestInterface $request): ResponseInterface { + // ... +}); +~~~ + +## Types of Controllers + +As mentioned above, Route will dispatch any `callable` when a route is matched. + +For performance reasons, Route also allows you to define controllers as a type of proxy, there are two of these proxies that will allow you to define strings and the actually callable will be built when Route dispatches it. + +### Closure + +A controller can be defined as a simple `\Closure` anonymous function. + +~~~php +map('GET', '/', function (ServerRequestInterface $request): ResponseInterface { + // ... +}); +~~~ + +### Lazy Loaded Class Method (Proxy) + +You can define a class method as a controller where the callable is to be lazy loaded when it is dispatched by defining a string and separating the class name and method name like so `ClassName::methodName`. + +~~~php +map('GET', '/', 'Acme\SomeController::someMethod'); +~~~ + +### Lazy Loaded Class Implementing `__invoke` (Proxy) + +You can define the name of a class that implements the magic `__invoke` method and the object will not be instantiated until it is dispatched. + +~~~php +map('GET', '/', Acme\SomeController::class); +~~~ + +### Lazy Loaded Array Based Callable (Proxy) + +A controller can be defined as an array based callable where the class element will not be instantiated until it is dispatched. + +~~~php +map('GET', '/', [Acme\SomeController::class, 'someMethod']); +~~~ + +### Object Implementing `__invoke` + +~~~php +map('GET', '/', new Acme\SomeController); +~~~ + +### Array Based Callable + +~~~php +map('GET', '/', [new Acme\SomeController, 'someMethod']); +~~~ + +### Named Function + +~~~php +map('GET', '/', 'Acme\controller'); +~~~ + +## Dependency Injection + +Where Route is instantiating the objects for your defined controller, a dependency injection container can be used to resolve those objects. Read more on dependency injection [here](/5.x/dependency-injection/). diff --git a/docs/6.x/dependency-injection.md b/docs/6.x/dependency-injection.md new file mode 100644 index 0000000..9aa7d0a --- /dev/null +++ b/docs/6.x/dependency-injection.md @@ -0,0 +1,81 @@ +--- +layout: post +title: Dependency Injection +sections: + Introduction: introduction + Recommended Reading: recommended-reading + Using a Container: using-a-container +--- +## Introduction + +Route has the ability to use a [PSR-11](https://www.php-fig.org/psr/psr-11/) dependency injection container to resolve any classes it needs to instantiate. Using a dependency injection container is no longer forced with route, however, it is very much recommended. + +## Recommended Reading + +It is recommended that if you have limited or no knowledge of dependency injection you should read about the concepts before you attempt to implement it. A good place to get started is with the [Dependency Injection chapter](https://www.phptherightway.com/#dependency_injection) of PHP The Right Way. + +## Using a Container + +In these examples, we will be using [league/container](https://container.thephpleague.com/) to demonstrate how to easily implement a dependency injection container with route. + +Consider that we have a controller class that needs a template renderer to load and render our HTML templates. + +~~~php +templateRenderer = $templateRenderer; + } + + /** + * Controller. + * + * @param \Psr\Http\Message\ServerRequestInterface $request + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function __invoke(ServerRequestInterface $request): ResponseInterface + { + $body = $this->templateRenderer->render('some-template'); + $response = new Response; + + $response->getBody()->write($body); + return $response->withStatus(200); + } +} +~~~ + +We can now build a container, define our controller and set it on the strategy, when the route is matched, the controller will be resolved via the container with the template renderer passed to it. + +~~~php +add(Acme\SomeController::class)->addArgument(Acme\TemplateRenderer::class); +$container->add(Acme\TemplateRenderer::class); + +$strategy = (new League\Route\Strategy\ApplicationStrategy)->setContainer($container); +$router = (new League\Route\Router)->setStrategy($strategy); + +$router->map('GET', '/', Acme\SomeController::class); +~~~ diff --git a/docs/6.x/http.md b/docs/6.x/http.md new file mode 100644 index 0000000..6cff1fc --- /dev/null +++ b/docs/6.x/http.md @@ -0,0 +1,94 @@ +--- +layout: post +title: HTTP +sections: + Introduction: introduction + The Request: the-request + The Response: the-response +--- +## Introduction + +HTTP messages form the core of any modern web application. Route is built with this in mind, so it dispatches a [PSR-7](https://www.php-fig.org/psr/psr-7/) request object and expects a [PSR-7](https://www.php-fig.org/psr/psr-7/) response object to be returned by your controller or middleware. + +We also make use of [PSR-15](https://www.php-fig.org/psr/psr-15/) request handlers and middleware. + +Throughout this documentation, we will be using [laminas-diactoros](https://docs.laminas.dev/laminas-diactoros/) to provide our HTTP messages but any implementation is supported. + +## The Request + +Route dispatches a `Psr\Http\Message\ServerRequestInterface` implementations, passes it through your middleware and finally to your controller as the first argument of the controller callable. + +### Middleware Signature + +Middlewares should be implementations of `Psr\Http\Server\MiddlewareInterface`, the request implementation will be passed as the first argument and an implementation of `Psr\Http\Server\RequestHandlerInterface` will be passed as the second argument so that you can trigger the next middleware in the stack. + +An example middleware would look something like this. + +~~~php +handle($request); + } +} +~~~ + +Read more about middleware [here](/5.x/middleware). + +### Controller Signature + +A basic controller signature should look something like this. + +~~~php +getBody()->write('

Hello, World!

'); + return $response; +} +~~~ + +Route does not provide any functionality for creating or interacting with a response object. For more information, please refer to the documentation of the [PSR-7](https://www.php-fig.org/psr/psr-7/) implementation that you have chosen to use diff --git a/docs/6.x/index.md b/docs/6.x/index.md new file mode 100644 index 0000000..4355e47 --- /dev/null +++ b/docs/6.x/index.md @@ -0,0 +1,89 @@ +--- +layout: post +title: Getting Started +sections: + What is Route?: what-is-route + What isn't Route?: what-isnt-route + Goals: goals + Questions?: questions + Installation: installation +--- +[![Author](https://img.shields.io/badge/author-@philipobenito-blue.svg?style=flat-square)](https://twitter.com/philipobenito) +[![Latest Version](https://img.shields.io/github/release/thephpleague/route.svg?style=flat-square)](https://github.com/thephpleague/route/releases) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](https://github.com/thephpleague/route/blob/master/LICENSE.md) +[![Build Status](https://img.shields.io/travis/thephpleague/route/master.svg?style=flat-square)](https://travis-ci.org/thephpleague/route) +[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/thephpleague/route.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/route/code-structure) +[![Quality Score](https://img.shields.io/scrutinizer/g/thephpleague/route.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/route) +[![Total Downloads](https://img.shields.io/packagist/dt/league/route.svg?style=flat-square)](https://packagist.org/packages/league/route) + +## What is Route? + +Route is a fast PSR-7 routing/dispatcher package including PSR-15 middleware implementation that enables you to build well designed performant web apps. + +At its core is Nikita Popov's [FastRoute](https://github.com/nikic/FastRoute) package allowing this package to concentrate on the dispatch of your controllers. + +[Route on Packagist](https://packagist.org/packages/league/route) + +## What isn't Route? + +Route is not a framework, it will not allow you to build an application out of the box. + +## Goals + +- To provide a "friendlier" API on top of [FastRoute](https://github.com/nikic/FastRoute). +- To provide an easy interface to implement PSR-7 HTTP messages in to your applications. +- To enable you to implement PSR-15 middleware in to your applications. +- To provide convenience in building web applications and APIs. + +## Questions? + +Route was created by Phil Bennett. Find him on Twitter at [@philipobenito](https://twitter.com/philipobenito). + +## Installation + +## System Requirements + +You need `PHP >= 7.2.0` to use `League\Route` but the latest stable version of PHP is recommended. + +You will also require an implementation of PSR-7 HTTP Message. Throughout the documentation we will be using the [Laminas Diactoros Project](https://github.com/laminas/laminas-diactoros/), however, there are many implementations to choose from on [Packagist](https://packagist.org/providers/psr/http-message-implementation). + +### Composer + +Route is available on [Packagist](https://packagist.org/packages/league/route) and can be installed using [Composer](https://getcomposer.org/): + +~~~ +composer require league/route +~~~ + +Most modern frameworks will include Composer out of the box, but ensure the following file is included: + +~~~php + A middleware component is an individual component participating, often together +> with other middleware components, in the processing of an incoming request and +> the creation of a resulting response, as defined by PSR-7. +> +> A middleware component MAY create and return a response without delegating to +> a request handler, if sufficient conditions are met. + +Route is a [PSR-15](https://www.php-fig.org/psr/psr-15/) server request handler, and as such can handle the invocation of a stack of middlewares. + +## Example Middleware + +A good example of middleware is using one to determine user auth. + +~~~php +handle($request); + } + + // if user does not have auth, possibly return a redirect response, + // this will not continue to any further middleware and will never + // reach your route callable + return new RedirectResponse(/* .. */); + } +} +~~~ + +## Defining Middleware + +Middleware can be defined to run in 3 ways: + +1. On the router - will run for every matched route. +2. On a route group - will run for any matched route in that group. +3. On a specific route - will run only when that route is matched. + +Using the example middleware above, we can lock down the entire application by adding the middleware to the router. + +~~~php +middleware(new Acme\AuthMiddleware); + +// ... add routes +~~~ + +If we only want to lock down a group, such as an admin area, we can just add the middleware to the group. + +~~~php +group('/admin', function ($router) { + // ... add routes + }) + ->middleware(new Acme\AuthMiddleware) +; +~~~ + +Or finally, we can lock down a specific route by only adding the middleware to that route. + +~~~php +map('GET', '/private', 'Acme\SomeController::someMethod') + ->middleware(new Acme\AuthMiddleware) +; +~~~ + +## Middleware Order + +Middleware is invoked in a specific order but depending on the logic contained in a middleware, you can control whether your code is run before or after your controller is invoked. + +The invocation order is as follows: + +1. Exception handler defined by the active strategy. This middleware should wrap the rest of the application and catch any exceptions to be gracefully handled. +2. Middleware added to the router. +3. Middleware added to a matched route group. +4. Middleware added to a specific matched route. + +To control whether your logic runs before or after your controller, you can have the request handler run as the first thing you do in your middleware, it will return a response, you can then do whatever you need to with the response and return it. + +~~~php +handle($request); + + // ... + // do something with the response + return $response; + } +} +~~~ + +## Route as a Middleware + +League\Route is itself a Request Handler, so an instance of `League\Route\Router` can be added to any existing middleware stack. diff --git a/docs/6.x/routes.md b/docs/6.x/routes.md new file mode 100644 index 0000000..b17c664 --- /dev/null +++ b/docs/6.x/routes.md @@ -0,0 +1,240 @@ +--- +layout: post +title: Routes +sections: + Request Verbs: request-verbs + Route Conditions: route-conditions + Route Groups: route-groups + Wildcard Routes: wildcard-routes +--- +## Request Verbs + +Route has convenience methods for setting routes that will respond differently depending on the HTTP request method. + +~~~php +get('/acme/route', 'Acme\Controller::getMethod'); +$router->post('/acme/route', 'Acme\Controller::postMethod'); +$router->put('/acme/route', 'Acme\Controller::putMethod'); +$router->patch('/acme/route', 'Acme\Controller::patchMethod'); +$router->delete('/acme/route', 'Acme\Controller::deleteMethod'); +$router->head('/acme/route', 'Acme\Controller::headMethod'); +$router->options('/acme/route', 'Acme\Controller::optionsMethod'); +~~~ + +Each of the above routes will respond to the same URI but will invoke a different callable based on the HTTP request method. + +## Route Conditions + +There are times when you may wish to add further conditions to route matching other than the request verb and URI. Route allows this by chaining further conditions to the route definition. + +### Host + +You can limit a route to match only if the host is a match as well as the request verb and URI. + +~~~php +map('GET', '/acme/route', 'Acme\Controller::getMethod') + ->setHost('example.com') +; +~~~ + +The route above will only match if the request is for `GET //example.com/acme/route`. + +### Scheme + +You can limit a route to match only if the scheme is a match as well as the request verb and URI. + +~~~php +map('GET', '/acme/route', 'Acme\Controller::getMethod') + ->setHost('example.com') + ->setScheme('https') +; +~~~ + +The route above will only match if the request is for `GET https://example.com/acme/route`. + +### Port + +You can limit a route to match only if the port is a match as well as the request verb and URI. + +~~~php +map('GET', '/acme/route', 'Acme\Controller::getMethod') + ->setHost('example.com') + ->setScheme('https') + ->setPort(8080) +; +~~~ + +The route above will only match if the request is for `GET https://example.com:8080/acme/route`. + +As you can see above, these conditions are chainable. You can also apply the conditions to a route group so that they will be applied to all routes defined in that group, or individually on any of the routes defined within a group. For more on this, see below. + +## Route Groups + +Route groups are a way of organising your route definitions, they allow us to provide conditions and a prefix across multiple routes. As an example, this would be useful for an admin area of a website. + +~~~php +group('/admin', function (\League\Route\RouteGroup $route) { + $route->map('GET', '/acme/route1', 'AcmeController::actionOne'); + $route->map('GET', '/acme/route2', 'AcmeController::actionTwo'); + $route->map('GET', '/acme/route3', 'AcmeController::actionThree'); +}); +~~~ + +The above code will define routes that will respond to the following. + +~~~shell +GET /admin/acme/route1 +GET /admin/acme/route2 +GET /admin/acme/route3 +~~~ + +### Named Routes + +Named routes helps when you want to retrieve a Route by a human friendly label. + +~~~php +group('/admin', function (\League\Route\RouteGroup $route) { + $route->map('GET', '/acme/route1', 'AcmeController::actionOne')->setName('actionOne'); + $route->map('GET', '/acme/route2', 'AcmeController::actionTwo')->setName('actionTwo'); +}); + +$route = $router->getNamedRoute('actionOne'); +$route->getPath(); // "/admin/acme/route1" +~~~ + +### Conditions + +As mentioned above, route conditions can be applied to a group and will be matched across all routes contained in that group, specific routes within the group can override this functionality as displayed below. + +~~~php +group('/admin', function (\League\Route\RouteGroup $route) { + $route->map('GET', '/acme/route1', 'AcmeController::actionOne'); + $route->map('GET', '/acme/route2', 'AcmeController::actionTwo')->setScheme('https'); + $route->map('GET', '/acme/route3', 'AcmeController::actionThree'); +}) + ->setScheme('http') + ->setHost('example.com') +; +~~~ + +The above code will define routes that will respond to the following. + +~~~shell +GET http://example.com/admin/acme/route1 +GET https://example.com/admin/acme/route2 +GET http://example.com/admin/acme/route3 +~~~ + +## Wildcard Routes + +Wildcard routes allow a route to respond to dynamic segments of a URI. If a route has dynamic URI segments, they will be passed in to the controller as an associative array of arguments. + +~~~php +map('GET', '/user/{id}/{name}', function (ServerRequestInterface $request, array $args): ResponseInterface { + // $args = [ + // 'id' => {id}, // the actual value of {id} + // 'name' => {name} // the actual value of {name} + // ]; + + // ... +}); +~~~ + +Dynamic URI segments can also be limited to match certain requirements. + +~~~php +map('GET', '/user/{id:number}/{name:word}', function (ServerRequestInterface $request, array $args): ResponseInterface { + // $args = [ + // 'id' => {id}, // the actual value of {id} + // 'name' => {name} // the actual value of {name} + // ]; + + // ... +}); +~~~ + +There are several built in conditions for dynamic segments of a URI. + +- number +- word +- alphanum_dash +- slug +- uuid + +Dynamic segments can also be set as any regular expression such as {id:[0-9]+}. + +For convenience, you can also register your own aliases for a particular regular expression using the `addPatternMatcher` method on `League\Route\Router`. + +For example: + +~~~php +addPatternMatcher('wordStartsWithM', '(m|M)[a-zA-Z]+'); + +$router->map('GET', 'user/mTeam/{name:wordStartsWithM}', function ( + ServerRequestInterface $request, + array $args +): ResponseInterface { + // $args = [ + // 'id' => {id}, // the actual value of {id} + // 'name' => {name} // the actual value of {name} + // ]; + + // ... +}); +~~~ + +The above pattern matcher will create an internal regular expression string: `{$1:(m|M)[a-zA-Z]+}`, where `$1` will interpret to `name`, the variable listed before the colon. diff --git a/docs/6.x/strategies.md b/docs/6.x/strategies.md new file mode 100644 index 0000000..853e86a --- /dev/null +++ b/docs/6.x/strategies.md @@ -0,0 +1,294 @@ +--- +layout: post +title: Strategies +sections: + Introduction: introduction + Applying Strategies: applying-strategies + Application Strategy: application-strategy + JSON Strategy: json-strategy + Response Decorators: response-decorators + Custom Strategies: custom-strategies +--- +## Introduction + +Strategies are a way of defining how a route callable is dispatched. A strategy defines what to do if a route is matched, if no route is found and what to do in certain error conditions. + +Route provides two strategies out of the box, one aimed at standard web apps and one aimed at JSON APIs. + +- `League\Route\Strategy\ApplicationStrategy` (Default) +- `League\Route\Strategy\JsonStrategy` (Requires a HTTP Response Factory) + +> It is strongly recommended that these strategies are only used as a base for you to build your own custom strategy. + +## Applying Strategies + +Strategies can be applied in three ways, each takes precedence over the previous. + +### Globally + +Will apply to all routes defined by the router unless the route or its parent group has a different strategy applied. + +~~~php +setStrategy(new ApplicationStrategy); +~~~ + +### Per Group + +Applying a strategy to a group will apply it to all routes defined within that group as well as any errors that occur when a request is within the group prefix. In these cases, any globally applied strategy will be ignored. + +~~~php +group('/group', function ($router) { + $router->map('GET', '/acme/route', 'Acme\Controller::action'); + $router->put('/acme/route', 'Acme\Controller::action'); + }) + ->setStrategy(new ApplicationStrategy) +; +~~~ + +### Per Route + +A strategy can be applied to any specific route, at top level or within a group, this will take precedence over any strategy applied to its parent group or globally. + +~~~php +map('GET', '/acme/route', 'Acme\Controller::action')->setStrategy(new CustomStrategy); + +$router + ->group('/group', function ($router) { + $router + ->map('GET', '/acme/route', 'Acme\Controller::action') + ->setStrategy(new CustomStrategy) // will ignore the strategy applied to the group + ; + }) + ->setStrategy(new ApplicationStrategy) +; +~~~ + +## Application Strategy + +`League\Route\Strategy\ApplicationStrategy` is used by default, it provides the controller with a PSR-7 `Psr\Http\Message\ServerRequestInterface` implementation and any route arguments. It expects your controller to build and return an implementation of `Psr\Http\Message\ResponseInterface`. + +### Controller Signature + +~~~php +getBody()->write(/* $content */); + return $response->withStatus(200); +}); +~~~ + +### Throwable Decorators + +The application strategy simply allows any `Throwable` to bubble out, you can catch them in your bootstrap process or you have the option to extend this strategy and overload the exception/throwable decorator methods. See [Custom Strategies](#custom-strategies). + +## JSON Strategy + +`League\Route\Strategy\JsonStrategy` aims to make building JSON APIs a little easier. It provides a PSR-7 `Psr\Http\Message\ServerRequestInterface` implementation and any route arguments to the controller as with the application strategy, the difference being that you can either build and return a response yourself or return an array or object, and a JSON response will be built for you. + +To make use of the JSON strategy, you will need to provide it with a [PSR-17](https://www.php-fig.org/psr/psr-17/) response factory implementation. Some examples of HTTP Factory packages can be found [here](https://github.com/http-interop?utf8=%E2%9C%93&q=http-factory&type=&language=). We will use the `laminas-diactoros` factory as an example. + +~~~php +setStrategy($strategy); +~~~ + +### Controller Signature + +~~~php +getBody()->write(json_encode(/* $content */)); + return $response->withAddedHeader('content-type', 'application/json')->withStatus(200); +}); + +function arrayController(ServerRequestInterface $request, array $args): array { + // ... + return [ + // ... + ]; +}); +~~~ + +### JSON Flags + +You can pass an optional second argument to the `JsonStrategy` to define the JSON flags to use when encoding the response. + +~~~php +setStrategy($strategy); +~~~ + + +### Exception Decorators + +`League\Route\Strategy\JsonStrategy` will decorate all exceptions, `NotFound`, `MethodNotAllowed`, and any 4xx or 5xx exceptions as a JSON Response, setting the correct HTTP status code and content type header in the process. + +~~~json +{ + "status_code": 404, + "message": "Not Found" +} +~~~ + +#### HTTP 4xx Exceptions + +In a RESTful API, covering all outcomes and returning the correct 4xx response can become quite verbose. Therefore, the dispatcher provides a convenient way to ensure you can return the correct response without the need for a conditional being created for every outcome. + +Simply throw one of the HTTP exceptions from within your application layer and the strategy will catch the exception and build the appropriate response. + +~~~php +post('/acme', function (ServerRequestInterface $request): ResponseInterface { + throw new BadRequestException; +}); +~~~ + +##### Available HTTP Exceptions + +| Status Code | Exception | Description | +| ----------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 400 | `League\Route\Http\Exception\BadRequestException` | The request cannot be fulfilled due to bad syntax. | +| 401 | `League\Route\Http\Exception\UnauthorizedException` | Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. | +| 403 | `League\Route\Http\Exception\ForbiddenException` | The request was a valid request, but the server is refusing to respond to it. | +| 404 | `League\Route\Http\Exception\NotFoundException` | The requested resource could not be found but may be available again in the future. | +| 405 | `League\Route\Http\Exception\MethodNotAllowedException` | A request was made of a resource using a request method not supported by that resource; for example, using GET on a form which requires data to be presented via POST, or using PUT on a read-only resource. | +| 406 | `League\Route\Http\Exception\NotAcceptableException` | The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request. | +| 409 | `League\Route\Http\Exception\ConflictException` | Indicates that the request could not be processed because of conflict in the request, such as an edit conflict in the case of multiple updates. | +| 410 | `League\Route\Http\Exception\GoneException` | Indicates that the resource requested is no longer available and will not be available again. | +| 411 | `League\Route\Http\Exception\LengthRequiredException` | The request did not specify the length of its content, which is required by the requested resource. | +| 412 | `League\Route\Http\Exception\PreconditionFailedException` | The server does not meet one of the preconditions that the requester put on the request. | +| 415 | `League\Route\Http\Exception\UnsupportedMediaException` | The request entity has a media type which the server or resource does not support. | +| 417 | `League\Route\Http\Exception\ExpectationFailedException` | The server cannot meet the requirements of the Expect request-header field. | +| 418 | `League\Route\Http\Exception\ImATeapotException` | [I'm a teapot](http://en.wikipedia.org/wiki/April_Fools%27_Day_RFC). | +| 428 | `League\Route\Http\Exception\PreconditionRequiredException` | The origin server requires the request to be conditional. | +| 429 | `League\Route\Http\Exception\TooManyRequestsException` | The user has sent too many requests in a given amount of time. | +| 451 | `League\Route\Http\Exception\UnavailableForLegalReasonsException` | The resource is unavailable for legal reasons. | + +## Response Decorators + +Response decorators allow you to add one, or many callables to a strategy that will be invoked on successful a route match for every response. + +This can be useful for simple things like adding a header to every successful response, although it is recommended for anything complex, to use a middleware instead. + +~~~php +addResponseDecorator(function (Psr\Http\Message\ResponseInterface $response): Psr\Http\Message\ResponseInterface { + return $response->withAddedHeader('content-type', 'acme-app/json'); +}); + +$router = (new League\Route\Router)->setStrategy($strategy); +~~~ + +## Custom Strategies + +You can build your own custom strategy to use in your application as long as it is an implementation of `League\Route\Strategy\StrategyInterface`. A strategy is tasked with: + +1. Providing a middleware that invokes your controller then decorates and returns your controllers response. +2. Providing a middleware that will decorate a 404 `NotFoundException` and return a response. +3. Providing a middleware that will decorate a 405 `MethodNotAllowedException` and return a response. +4. Providing a middleware that will decorate any other exception and return a response. + +~~~php +map('GET', '/', function (ServerRequestInterface $request): ResponseInterface { + $response = new Laminas\Diactoros\Response; + $response->getBody()->write('

Hello, World!

'); + return $response; +}); + +$response = $router->dispatch($request); + +// send the response to the browser +(new Laminas\HttpHandlerRunner\Emitter\SapiEmitter)->emit($response); +~~~ + +## APIs + +Only a few changes are needed to create a simple JSON API. We have to change the strategy that the router uses to dispatch a controller, as well as providing a response factory to ensure the JSON Strategy can build the response it needs to. + +To provide a response factory, we will need to install a package that provides a response factory, such as Laminas Diactoros. + +~~~php +setStrategy($strategy); + +// map a route +$router->map('GET', '/', function (ServerRequestInterface $request): array { + return [ + 'title' => 'My New Simple API', + 'version' => 1, + ]; +}); + +$response = $router->dispatch($request); + +// send the response to the browser +(new Laminas\HttpHandlerRunner\Emitter\SapiEmitter)->emit($response); +~~~ + +The code above will convert your returned array into a JSON response. + +~~~json +{ + "title": "My New Simple API", + "version": 1 +} +~~~ + +[composer]: https://getcomposer.org/ +[dependencies]: https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies +[psr7]: https://www.php-fig.org/psr/psr-7/ +[diactoros]:https://github.com/laminas/laminas-diactoros/ diff --git a/src/Cache/Router.php b/src/Cache/Router.php index f3a9185..f1c0df5 100644 --- a/src/Cache/Router.php +++ b/src/Cache/Router.php @@ -3,7 +3,7 @@ /** * The cached router is currently in BETA and not recommended for production code. * - * Please feel free to heavily test and report any issues as an issue on the Github repository. + * Please feel free to heavily test and report any issues as an issue on the GitHub repository. */ declare(strict_types=1); @@ -19,8 +19,6 @@ class Router { - protected const CACHE_KEY = 'league/route/cache'; - /** * @var callable */ @@ -34,7 +32,8 @@ class Router public function __construct( callable $builder, protected CacheInterface $cache, - protected bool $cacheEnabled = true + protected bool $cacheEnabled = true, + protected string $cacheKey = 'league/route/cache' ) { $this->builder = $builder; } @@ -47,7 +46,7 @@ public function dispatch(ServerRequestInterface $request): ResponseInterface protected function buildRouter(ServerRequestInterface $request): MainRouter { - if (true === $this->cacheEnabled && $cache = $this->cache->get(static::CACHE_KEY)) { + if (true === $this->cacheEnabled && $cache = $this->cache->get($this->cacheKey)) { $router = u($cache, ['allowed_classes' => true]); if ($router instanceof MainRouter) { @@ -64,7 +63,7 @@ protected function buildRouter(ServerRequestInterface $request): MainRouter if ($router instanceof MainRouter) { $router->prepareRoutes($request); - $this->cache->set(static::CACHE_KEY, s($router)); + $this->cache->set($this->cacheKey, s($router)); return $router; }