|
| 1 | +# Using the ResourceGenerator in path-segregated middleware |
| 2 | + |
| 3 | +- Since 1.1.0. |
| 4 | + |
| 5 | +You may want to develop your API as a separate module that you can then drop in |
| 6 | +to an existing application; you may even want to [path-segregate](https://docs.zendframework.com/zend-expressive/v3/features/router/piping/#path-segregation) it. |
| 7 | + |
| 8 | +In such cases, you will want to use a different router instance, which has a |
| 9 | +huge number of ramifications: |
| 10 | + |
| 11 | +- You'll need separate routing middleware. |
| 12 | +- You'll need a separate [UrlHelper](https://docs.zendframework.com/zend-expressive/v3/features/helpers/url-helper/) instance, as well as its related middleware. |
| 13 | +- You'll need a separate URL generator for HAL that consumes the separate |
| 14 | + `UrlHelper` instance. |
| 15 | +- You'll need a separate `LinkGenerator` for HAL that consumes the separate URL |
| 16 | + generator. |
| 17 | +- You'll need a separate `ResourceGenerator` for HAL that consumes the separate |
| 18 | + `LinkGenerator`. |
| 19 | + |
| 20 | +This can be accomplished by writing your own factories, but that means a lot of |
| 21 | +extra code, and the potential for it to go out-of-sync with the official |
| 22 | +factories for these services. What should you do? |
| 23 | + |
| 24 | +## Virtual services |
| 25 | + |
| 26 | +Since version 1.1.0 of this package, and versions 3.1.0 of |
| 27 | +zend-expressive-router and 5.1.0 of zend-expressive-helpers, you can now pass |
| 28 | +additional constructor arguments to a number of factories to allow varying the |
| 29 | +service dependencies they look for. |
| 30 | + |
| 31 | +In our example below, we will create an `Api` module. This module will have its |
| 32 | +own router, and be segregated in the path `/api`; all routes we create will be |
| 33 | +relative to that path, and not include it in their definitions. The handler we |
| 34 | +create will return HAL-JSON, and thus need to generate links using the |
| 35 | +configured router and base path. |
| 36 | + |
| 37 | +To begin, we will alter the `ConfigProvider` for our module to add the |
| 38 | +definitions noted below: |
| 39 | + |
| 40 | +```php |
| 41 | +// in src/Api/ConfigProvider.php: |
| 42 | +namespace Api; |
| 43 | + |
| 44 | +use Zend\Expressive\Hal\LinkGeneratorFactory; |
| 45 | +use Zend\Expressive\Hal\LinkGenerator\ExpressiveUrlGeneratorFactory; |
| 46 | +use Zend\Expressive\Hal\Metadata\MetadataMap; |
| 47 | +use Zend\Expressive\Hal\ResourceGeneratorFactory; |
| 48 | +use Zend\Expressive\Helper\UrlHelperFactory; |
| 49 | +use Zend\Expressive\Helper\UrlHelperMiddlewareFactory; |
| 50 | +use Zend\Expressive\Router\FastRouteRouter; |
| 51 | +use Zend\Expressive\Router\Middleware\RouteMiddlewareFactory; |
| 52 | + |
| 53 | +class ConfigProvider |
| 54 | +{ |
| 55 | + public function __invoke() : array |
| 56 | + { |
| 57 | + return [ |
| 58 | + 'dependencies' => $this->getDependencies(), |
| 59 | + MetadataMap::class => $this->getMetadataMap(), |
| 60 | + ]; |
| 61 | + } |
| 62 | + |
| 63 | + public function getDependencies() : array |
| 64 | + { |
| 65 | + return [ |
| 66 | + 'delegators' => [ |
| 67 | + // module class name => delegators |
| 68 | + Router::class => [ |
| 69 | + RoutesDelegatorFactory::class, |
| 70 | + ], |
| 71 | + ], |
| 72 | + 'factories' => [ |
| 73 | + // module class name => factory |
| 74 | + LinkGenerator::class => new LinkGeneratorFactory(UrlGenerator::class), |
| 75 | + ResourceGenerator::class => new ResourceGeneratorFactory(LinkGenerator::class), |
| 76 | + Router::class => FastRouteRouterFactory::class, |
| 77 | + UrlHelper::class => new UrlHelperFactory('/api', Router::class), |
| 78 | + UrlHelperMiddleware::class => new UrlHelperMiddlewareFactory(UrlHelper::class), |
| 79 | + UrlGenerator::class => new ExpressiveUrlGeneratorFactory(UrlHelper::class), |
| 80 | + |
| 81 | + // Our handler: |
| 82 | + CreateBookHandler::class => CreateBookHandlerFactory::class, |
| 83 | + ], |
| 84 | + ]; |
| 85 | + } |
| 86 | + |
| 87 | + public function getMetadataMap() : array |
| 88 | + { |
| 89 | + return [ |
| 90 | + // ... |
| 91 | + ]; |
| 92 | + } |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +Note that the majority of these service names are _virtual_; they do not resolve |
| 97 | +to actual classes. PHP allows usage of the `::class` pseudo-constant anywhere, |
| 98 | +and will resolve the value based on the current namespace. This gives us virtual |
| 99 | +services such as `Api\Router`, `Api\UrlHelper`, etc. |
| 100 | + |
| 101 | +Also note that we are creating factory _instances_. Normally, we recommend not |
| 102 | +using closures or instances for factories due to potential problems with |
| 103 | +configuration caching. Fortunately, we have provided functionality in each of |
| 104 | +these factories that allows them to be safely cached, retaining the |
| 105 | +context-specific configuration required. |
| 106 | + |
| 107 | +> ### What about the hard-coded path? |
| 108 | +> |
| 109 | +> You'll note that the above example hard-codes the base path for the |
| 110 | +> `UrlHelper`. What if you want to use a different path? |
| 111 | +> |
| 112 | +> You can override the service in an application-specific configuration under |
| 113 | +> `config/autoload/`, specifying a different path! |
| 114 | +> |
| 115 | +> ```php |
| 116 | +> \Api\UrlHelper::class => new UrlHelperFactory('/different/path', \Api\Router::class), |
| 117 | +> ``` |
| 118 | +
|
| 119 | +## Using virtual services with a handler |
| 120 | +
|
| 121 | +Now let's turn to our `CreateBookHandler`. We'll define it as follows: |
| 122 | +
|
| 123 | +```php |
| 124 | +// in src/Api/CreateBookHandler.php: |
| 125 | +namespace Api; |
| 126 | +
|
| 127 | +use Psr\Http\Message\ResponseInterface; |
| 128 | +use Psr\Http\Message\ServerRequestInterface; |
| 129 | +use Psr\Http\Server\RequestHandlerInterface; |
| 130 | +use Zend\Expressive\Hal\HalResponseFactory; |
| 131 | +use Zend\Expressive\Hal\ResourceGenerator; |
| 132 | +
|
| 133 | +class CreateBookHandler implements RequestHandlerInterface |
| 134 | +{ |
| 135 | + private $resourceGenerator; |
| 136 | +
|
| 137 | + private $responseFactory; |
| 138 | +
|
| 139 | + public function __construct(ResourceGenerator $resourceGenerator, HalResponseFactory $responseFactory) |
| 140 | + { |
| 141 | + $this->resourceGenerator = $resourceGenerator; |
| 142 | + $this->responseFactory = $responseFactory; |
| 143 | + } |
| 144 | +
|
| 145 | + public function handle(ServerRequestInterface $request) : ResponseInterface |
| 146 | + { |
| 147 | + // do some work ... |
| 148 | +
|
| 149 | + $resource = $this->resourceGenerator->fromObject($book, $request); |
| 150 | + return $this->responseFactory->createResponse($request, $book); |
| 151 | + } |
| 152 | +} |
| 153 | +``` |
| 154 | +
|
| 155 | +This handler needs a HAL resource generator. More specifically, it needs the one |
| 156 | +specific to our module. As such, we'll define our factory as follows: |
| 157 | + |
| 158 | +```php |
| 159 | +// in src/Api/CreateBookHandlerFactory.php: |
| 160 | +namespace Api; |
| 161 | + |
| 162 | +use Psr\Container\ContainerInterface; |
| 163 | +use Zend\Expressive\Hal\HalResponseFactory; |
| 164 | + |
| 165 | +class CreateBookHandlerFactory |
| 166 | +{ |
| 167 | + public function __invoke(ContainerInterface $container) : CreateBookHandler |
| 168 | + { |
| 169 | + return new CreateBookHandler( |
| 170 | + ResourceGenerator::class, // module-specific service name! |
| 171 | + HalResponseFactory::class |
| 172 | + ); |
| 173 | + } |
| 174 | +} |
| 175 | +``` |
| 176 | + |
| 177 | +## Creating path-segregated routes |
| 178 | + |
| 179 | +Finally, we need to create a route to it. We can do that by creating a delegator |
| 180 | +factory (which we have already referenced above): |
| 181 | + |
| 182 | +```php |
| 183 | +// In src/Api/RoutesDelegatorFactory.php: |
| 184 | +namespace Api; |
| 185 | + |
| 186 | +use Psr\Container\ContainerInterface; |
| 187 | +use Zend\Expressive\MiddlewareFactory; |
| 188 | +use Zend\Expressive\Router\RouteCollector; |
| 189 | +use Zend\Expressive\Router\RouterInterface; |
| 190 | + |
| 191 | +/** |
| 192 | + * Add routes to the router. |
| 193 | + * |
| 194 | + * This delegator decorates creation of the router, and is used to |
| 195 | + * inject routes into it via a `RouteCollector` instance, using a combination of |
| 196 | + * the HTTP method name as the instance method, a path, a middleware/handler, and |
| 197 | + * optionally a name. |
| 198 | + * |
| 199 | + * You will need to use the MiddlewareFactory to prepare your middleware, |
| 200 | + * as the `RouteCollector` expects valid middleware instances. |
| 201 | + */ |
| 202 | +class RoutesDelegatorFactory |
| 203 | +{ |
| 204 | + public function __invoke(ContainerInterface $container, string $serviceName, callable $routerFactory) : RouterInterface |
| 205 | + { |
| 206 | + $router = $routerFactory(); |
| 207 | + $routes = new RouteCollector($router); |
| 208 | + $factory = $container->get(MiddlewareFactory::class); |
| 209 | + |
| 210 | + // Add routing here: |
| 211 | + $routes->post('/books', $factory->lazy(CreateBookHandler::class)); |
| 212 | + |
| 213 | + // Return the router at the end! |
| 214 | + return $router; |
| 215 | + } |
| 216 | +} |
| 217 | +``` |
| 218 | + |
| 219 | +Note that the routing does **not** include the string `/api`; this is because |
| 220 | +that string will be stripped when we path-segregate our API middleware pipeline. |
| 221 | +All routing will be _relative_ to that path. |
| 222 | + |
| 223 | +## Creating a path-segregated pipeline |
| 224 | + |
| 225 | +Finally, we will create our path-segregated middleware pipeline: |
| 226 | + |
| 227 | +```php |
| 228 | +// in config/pipeline.php: |
| 229 | +$app->pipe('/api', [ |
| 230 | + \Zend\ProblemDetails\ProblemDetailsMiddleware::class, |
| 231 | + \Api\RouteMiddleware::class, // module-specific routing middleware! |
| 232 | + ImplicitHeadMiddleware::class, |
| 233 | + ImplicitOptionsMiddleware::class, |
| 234 | + MethodNotAllowedMiddleware::class, |
| 235 | + \Api\UrlHelperMiddleware::class, // module-specific URL helper middleware! |
| 236 | + DispatchMiddleware::class, |
| 237 | + \Zend\ProblemDetails\ProblemDetailsNotFoundHandler::class, |
| 238 | +]); |
| 239 | +``` |
| 240 | + |
| 241 | +> You might want to create the above as a middleware pipeline _service_ via a |
| 242 | +> factory: |
| 243 | +> |
| 244 | +> ```php |
| 245 | +> namespace Api; |
| 246 | +> |
| 247 | +> use Psr\Container\ContainerInterface; |
| 248 | +> use Zend\Expressive\MiddlewareFactory; |
| 249 | +> use Zend\Expressive\Router\Middleware as RouterMiddleware; |
| 250 | +> use Zend\ProblemDetails\ProblemDetailsMiddleware; |
| 251 | +> use Zend\ProblemDetails\ProblemDetailsNotFoundHandler; |
| 252 | +> use Zend\Stratigility\MiddlewarePipe; |
| 253 | +> |
| 254 | +> class PipelineFactory |
| 255 | +> { |
| 256 | +> public function __invoke(ContainerInterface $container) : MiddlewarePipe |
| 257 | +> { |
| 258 | +> $factory = $container->get(MiddlewareFactory::class); |
| 259 | +> $pipeline = new MiddlewarePipe(); |
| 260 | +> $pipeline->pipe($factory->lazy(ProblemDetailsMiddleware::class)); |
| 261 | +> $pipeline->pipe($factory->lazy(RouteMiddleware::class)); // module-specific! |
| 262 | +> $pipeline->pipe($factory->lazy(RouterMiddleware\ImplicitHeadMiddleware::class)); |
| 263 | +> $pipeline->pipe($factory->lazy(RouterMiddleware\ImplicitOptionsMiddleware::class)); |
| 264 | +> $pipeline->pipe($factory->lazy(RouterMiddleware\MethodNotAllowedMiddleware::class)); |
| 265 | +> $pipeline->pipe($factory->lazy(UrlHelperMiddlweare::class)); // module-specific! |
| 266 | +> $pipeline->pipe($factory->lazy(RouterMiddleware\DispatchMiddleware::class)); |
| 267 | +> $pipeline->pipe($factory->lazy(ProblemDetailsNotFoundHandler::class)); |
| 268 | +> return $pipeline; |
| 269 | +> } |
| 270 | +> } |
| 271 | +> ``` |
| 272 | +> |
| 273 | +> Such an approach keeps the pipeline definition in the module, which allows you |
| 274 | +> to better re-use it later. |
| 275 | +
|
| 276 | +The above approach will allow you to create a custom pipeline that can be |
| 277 | +dropped into an existing application, and allows defining per-module routing and |
| 278 | +dispatch relative to a given path. |
0 commit comments