Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Commit 3e0ee65

Browse files
committed
Documents new factory features
Documents the new factory constructor arguments, and adds a cookbook recipe demonstrating path-segregated middleware with custom routers.
1 parent a2249d2 commit 3e0ee65

File tree

3 files changed

+302
-2
lines changed

3 files changed

+302
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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.

docs/book/factories.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,15 @@ configured instances for your use.
2525
- Registered as service: `Zend\Expressive\Hal\LinkGenerator`
2626
- Generates instance of: `Zend\Expressive\Hal\LinkGenerator`
2727
- Depends on:
28-
- `Zend\Expressive\Hal\LinkGenerator\UrlGenerator` service
28+
- `Zend\Expressive\Hal\LinkGenerator\UrlGeneratorInterface` service
29+
30+
Since version 1.1.0, this factory allows an optional constructor argument,
31+
`$urlGeneratorServiceName`. It defaults to
32+
`Zend\Expressive\Hal\LinkGenerator\UrlGeneratorInterface`,
33+
but you may specify an alternate service if desired. This may be useful, for
34+
instance, when using an alternate router in a path-segregated middleware
35+
pipeline, which would necessitate a different `UrlHelper` instance, and an
36+
alternate URL generator that consumes it.
2937

3038
## Zend\Expressive\Hal\LinkGenerator\ExpressiveUrlGeneratorFactory
3139

@@ -37,6 +45,12 @@ configured instances for your use.
3745
- `Zend\Expressive\Helper\ServerUrlHelper` service (optional; if not provided,
3846
URIs will be generated without authority information)
3947

48+
Since version 1.1.0, this factory allows an optional constructor argument, `$urlHelperServiceName`.
49+
It defaults to `Zend\Expressive\Helper\UrlHelper`, but you may specify an
50+
alternate service if desired. This may be useful, for instance, when using an
51+
alternate router in a path-segregated middleware pipeline, which would
52+
necessitate a different `UrlHelper` instance.
53+
4054
## Zend\Expressive\Hal\LinkGenerator\UrlGeneratorInterface
4155

4256
- Registered as service: `Zend\Expressive\Hal\LinkGenerator\UrlGeneratorInterface`
@@ -142,3 +156,10 @@ the namespace.
142156
If you wish to use a container implementation other than the
143157
`Zend\Hydrator\HydratorPluginManager`, either register it under that service
144158
name, or create an alternate factory.
159+
160+
Since version 1.1.0, this factory allows an optional constructor argument, `$linkGeneratorServiceName`.
161+
It defaults to `Zend\Expressive\Hal\LinkGenerator`, but you may specify an
162+
alternate service if desired. This may be useful, for instance, when using an
163+
alternate router in a path-segregated middleware pipeline, which would
164+
necessitate a different `UrlHelper` instance, an alternate URL generator that
165+
consumes it, and an alternate `LinkGenerator` consuming the URL generator.

mkdocs.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ pages:
1212
- "Factories": factories.md
1313
- Cookbook:
1414
- "Generating Custom Links In Middleware and Request Handlers": cookbook/generating-custom-links-in-middleware.md
15+
- "Using the ResourceGenerator in path-segregated middleware": cookbook/path-segregated-uri-generation.md
1516
site_name: Hypertext Application Language
1617
site_description: 'Hypertext Application Language for PSR-7 Applications'
1718
repo_url: 'https://github.com/zendframework/zend-expressive-hal'
18-
copyright: 'Copyright (c) 2017 <a href="http://www.zend.com/">Zend Technologies USA Inc.</a>'
19+
copyright: 'Copyright (c) 2017-2018 <a href="http://www.zend.com/">Zend Technologies USA Inc.</a>'

0 commit comments

Comments
 (0)