|
| 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 | + 'factories' => [ |
| 67 | + // module-specific class name => factory |
| 68 | + LinkGenerator::class => new LinkGeneratorFactory(UrlGenerator::class), |
| 69 | + ResourceGenerator::class => new ResourceGeneratorFactory(LinkGenerator::class), |
| 70 | + Router::class => FastRouteRouterFactory::class, |
| 71 | + UrlHelper::class => new UrlHelperFactory('/api', Router::class), |
| 72 | + UrlHelperMiddleware::class => new UrlHelperMiddlewareFactory(UrlHelper::class), |
| 73 | + UrlGenerator::class => new ExpressiveUrlGeneratorFactory(UrlHelper::class), |
| 74 | + |
| 75 | + // Our handler: |
| 76 | + CreateBookHandler::class => CreateBookHandlerFactory::class, |
| 77 | + |
| 78 | + // And our pipeline: |
| 79 | + Pipeline::class => PipelineFactory::class, |
| 80 | + ], |
| 81 | + ]; |
| 82 | + } |
| 83 | + |
| 84 | + public function getMetadataMap() : array |
| 85 | + { |
| 86 | + return [ |
| 87 | + // ... |
| 88 | + ]; |
| 89 | + } |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +Note that the majority of these service names are _virtual_; they do not resolve |
| 94 | +to actual classes. PHP allows usage of the `::class` pseudo-constant anywhere, |
| 95 | +and will resolve the value based on the current namespace. This gives us virtual |
| 96 | +services such as `Api\Router`, `Api\UrlHelper`, etc. |
| 97 | + |
| 98 | +Also note that we are creating factory _instances_. Normally, we recommend not |
| 99 | +using closures or instances for factories due to potential problems with |
| 100 | +configuration caching. Fortunately, we have provided functionality in each of |
| 101 | +these factories that allows them to be safely cached, retaining the |
| 102 | +context-specific configuration required. |
| 103 | + |
| 104 | +> ### What about the hard-coded path? |
| 105 | +> |
| 106 | +> You'll note that the above example hard-codes the base path for the |
| 107 | +> `UrlHelper`. What if you want to use a different path? |
| 108 | +> |
| 109 | +> You can override the service in an application-specific configuration under |
| 110 | +> `config/autoload/`, specifying a different path! |
| 111 | +> |
| 112 | +> ```php |
| 113 | +> \Api\UrlHelper::class => new UrlHelperFactory('/different/path', \Api\Router::class), |
| 114 | +> ``` |
| 115 | +
|
| 116 | +## Using virtual services with a handler |
| 117 | +
|
| 118 | +Now let's turn to our `CreateBookHandler`. We'll define it as follows: |
| 119 | +
|
| 120 | +```php |
| 121 | +// in src/Api/CreateBookHandler.php: |
| 122 | +namespace Api; |
| 123 | +
|
| 124 | +use Psr\Http\Message\ResponseInterface; |
| 125 | +use Psr\Http\Message\ServerRequestInterface; |
| 126 | +use Psr\Http\Server\RequestHandlerInterface; |
| 127 | +use Zend\Expressive\Hal\HalResponseFactory; |
| 128 | +use Zend\Expressive\Hal\ResourceGenerator; |
| 129 | +
|
| 130 | +class CreateBookHandler implements RequestHandlerInterface |
| 131 | +{ |
| 132 | + private $resourceGenerator; |
| 133 | +
|
| 134 | + private $responseFactory; |
| 135 | +
|
| 136 | + public function __construct(ResourceGenerator $resourceGenerator, HalResponseFactory $responseFactory) |
| 137 | + { |
| 138 | + $this->resourceGenerator = $resourceGenerator; |
| 139 | + $this->responseFactory = $responseFactory; |
| 140 | + } |
| 141 | +
|
| 142 | + public function handle(ServerRequestInterface $request) : ResponseInterface |
| 143 | + { |
| 144 | + // do some work ... |
| 145 | +
|
| 146 | + $resource = $this->resourceGenerator->fromObject($book, $request); |
| 147 | + return $this->responseFactory->createResponse($request, $book); |
| 148 | + } |
| 149 | +} |
| 150 | +``` |
| 151 | +
|
| 152 | +This handler needs a HAL resource generator. More specifically, it needs the one |
| 153 | +specific to our module. As such, we'll define our factory as follows: |
| 154 | + |
| 155 | +```php |
| 156 | +// in src/Api/CreateBookHandlerFactory.php: |
| 157 | +namespace Api; |
| 158 | + |
| 159 | +use Psr\Container\ContainerInterface; |
| 160 | +use Zend\Expressive\Hal\HalResponseFactory; |
| 161 | + |
| 162 | +class CreateBookHandlerFactory |
| 163 | +{ |
| 164 | + public function __invoke(ContainerInterface $container) : CreateBookHandler |
| 165 | + { |
| 166 | + return new CreateBookHandler( |
| 167 | + ResourceGenerator::class, // module-specific service name! |
| 168 | + HalResponseFactory::class |
| 169 | + ); |
| 170 | + } |
| 171 | +} |
| 172 | +``` |
| 173 | + |
| 174 | +You can create any number of such handlers for your module; the above |
| 175 | +demonstrates how and where injection of the alternate resource generator occurs. |
| 176 | + |
| 177 | +## Creating our pipeline and routes |
| 178 | + |
| 179 | +Now we can create our pipeline and routes. |
| 180 | + |
| 181 | +Generally when piping to an application instance, we can specify a class name of |
| 182 | +middleware to pipe, or an array of middleware: |
| 183 | + |
| 184 | +```php |
| 185 | +// in config/pipeline.php: |
| 186 | +$app->pipe('/api', [ |
| 187 | + \Zend\ProblemDetails\ProblemDetailsMiddleware::class, |
| 188 | + \Api\RouteMiddleware::class, // module-specific routing middleware! |
| 189 | + ImplicitHeadMiddleware::class, |
| 190 | + ImplicitOptionsMiddleware::class, |
| 191 | + MethodNotAllowedMiddleware::class, |
| 192 | + \Api\UrlHelperMiddleware::class, // module-specific URL helper middleware! |
| 193 | + DispatchMiddleware::class, |
| 194 | + \Zend\ProblemDetails\ProblemDetailsNotFoundHandler::class, |
| 195 | +]); |
| 196 | +``` |
| 197 | + |
| 198 | +However, we have both the pipeline _and_ routes, and we likely want to indicate |
| 199 | +the exact behavior of this pipeline. Additionally, we may want to re-use this |
| 200 | +pipeline in other applications; pushing this into the application configuration |
| 201 | +makes that more error-prone. |
| 202 | + |
| 203 | +As such, we will create a factory that generates and returns a |
| 204 | +`Zend\Stratigility\MiddlewarePipe` instance that is fully configured for our |
| 205 | +module. As part of this functionality, we will also add our module-specific |
| 206 | +routing. |
| 207 | + |
| 208 | +```php |
| 209 | +// In src/Api/PipelineFactory.php: |
| 210 | +namespace Api; |
| 211 | + |
| 212 | +use Psr\Container\ContainerInterface; |
| 213 | +use Zend\Expressive\MiddlewareFactory; |
| 214 | +use Zend\Expressive\Router\Middleware as RouterMiddleware; |
| 215 | +use Zend\ProblemDetails\ProblemDetailsMiddleware; |
| 216 | +use Zend\ProblemDetails\ProblemDetailsNotFoundHandler; |
| 217 | +use Zend\Stratigility\MiddlewarePipe; |
| 218 | + |
| 219 | +class PipelineFactory |
| 220 | +{ |
| 221 | + public function __invoke(ContainerInterface $container) : MiddlewarePipe |
| 222 | + { |
| 223 | + $factory = $container->get(MiddlewareFactory::class); |
| 224 | + |
| 225 | + // First, create our middleware pipeline |
| 226 | + $pipeline = new MiddlewarePipe(); |
| 227 | + $pipeline->pipe($factory->lazy(ProblemDetailsMiddleware::class)); |
| 228 | + $pipeline->pipe($factory->lazy(RouteMiddleware::class)); // module-specific! |
| 229 | + $pipeline->pipe($factory->lazy(RouterMiddleware\ImplicitHeadMiddleware::class)); |
| 230 | + $pipeline->pipe($factory->lazy(RouterMiddleware\ImplicitOptionsMiddleware::class)); |
| 231 | + $pipeline->pipe($factory->lazy(RouterMiddleware\MethodNotAllowedMiddleware::class)); |
| 232 | + $pipeline->pipe($factory->lazy(UrlHelperMiddlweare::class)); // module-specific! |
| 233 | + $pipeline->pipe($factory->lazy(RouterMiddleware\DispatchMiddleware::class)); |
| 234 | + $pipeline->pipe($factory->lazy(ProblemDetailsNotFoundHandler::class)); |
| 235 | + |
| 236 | + // Second, we'll create our routes |
| 237 | + $router = $container->get(Router::class); // Retrieve our module-specific router |
| 238 | + $routes = new RouteCollector($router); // Create a route collector to simplify routing |
| 239 | + |
| 240 | + // Start routing: |
| 241 | + $routes->post('/books', $factory->lazy(CreateBookHandler::class)); |
| 242 | + |
| 243 | + // Return the pipeline now that we're done! |
| 244 | + return $pipeline; |
| 245 | + } |
| 246 | +} |
| 247 | +``` |
| 248 | + |
| 249 | +Note that the routing definitions do **not** include the prefix `/api`; this is |
| 250 | +because that prefix will be stripped when we path-segregate our API middleware |
| 251 | +pipeline. All routing will be _relative_ to that path. |
| 252 | + |
| 253 | +## Creating a path-segregated pipeline |
| 254 | + |
| 255 | +Finally, we will attach our pipeline to the application, using path segregation: |
| 256 | + |
| 257 | +```php |
| 258 | +// in config/pipeline.php: |
| 259 | +$app->pipe('/api', \Api\Pipeline::class); |
| 260 | +``` |
| 261 | + |
| 262 | +This statement tells the application to pipe the pipeline returned by our |
| 263 | +`PipelineFactory` under the path `/api`; that path will be stripped from |
| 264 | +requests when passed to the underlying middleware. |
| 265 | + |
| 266 | +At this point, we now have a re-usable module, complete with its own routing, |
| 267 | +with URI generation that will include the base path under which we have |
| 268 | +segregated the pipeline! |
0 commit comments