Cookbook
Using the ResourceGenerator in path-segregated middleware
- Since 1.1.0.
You may want to develop your API as a separate module that you can then drop in to an existing application; you may even want to path-segregate it.
In such cases, you will want to use a different router instance to isolate your routes, which has a huge number of ramifications:
- You'll need separate routing middleware.
- You'll need a separate UrlHelper instance, as well as its related middleware.
- You'll need a separate URL generator for HAL that consumes the separate
UrlHelper
instance. - You'll need a separate
LinkGenerator
for HAL that consumes the separate URL generator. - You'll need a separate
ResourceGenerator
for HAL that consumes the separateLinkGenerator
.
This can be accomplished by writing your own factories, but that means a lot of extra code, and the potential for it to go out-of-sync with the official factories for these services. What should you do?
Virtual services
Since version 1.1.0 of this package, and versions 3.1.0 of zend-expressive-router and 5.1.0 of zend-expressive-helpers, you can now pass additional constructor arguments to a number of factories to allow varying the service dependencies they look for.
In our example below, we will create an Api
module. This module will have its
own router, and be segregated in the path /api
; all routes we create will be
relative to that path, and not include it in their definitions. The handler we
create will return HAL-JSON, and thus need to generate links using the
configured router and base path.
To begin, we will alter the ConfigProvider
for our module to add the
definitions noted below:
// in src/Api/ConfigProvider.php:
namespace Api;
use Zend\Expressive\Hal\LinkGeneratorFactory;
use Zend\Expressive\Hal\LinkGenerator\ExpressiveUrlGeneratorFactory;
use Zend\Expressive\Hal\Metadata\MetadataMap;
use Zend\Expressive\Hal\ResourceGeneratorFactory;
use Zend\Expressive\Helper\UrlHelperFactory;
use Zend\Expressive\Helper\UrlHelperMiddlewareFactory;
use Zend\Expressive\Router\FastRouteRouter;
use Zend\Expressive\Router\Middleware\RouteMiddlewareFactory;
use Zend\Expressive\Router\FastRouteRouterFactory;
class ConfigProvider
{
public function __invoke() : array
{
return [
'dependencies' => $this->getDependencies(),
MetadataMap::class => $this->getMetadataMap(),
];
}
public function getDependencies() : array
{
return [
'factories' => [
// module-specific class name => factory
LinkGenerator::class => new LinkGeneratorFactory(UrlGenerator::class),
ResourceGenerator::class => new ResourceGeneratorFactory(LinkGenerator::class),
Router::class => FastRouteRouterFactory::class,
RouteMiddleware::class => new RouteMiddlewareFactory(Router::class),
UrlHelper::class => new UrlHelperFactory('/api', Router::class),
UrlHelperMiddleware::class => new UrlHelperMiddlewareFactory(UrlHelper::class),
UrlGenerator::class => new ExpressiveUrlGeneratorFactory(UrlHelper::class),
// Our handler:
CreateBookHandler::class => CreateBookHandlerFactory::class,
// And our pipeline:
Pipeline::class => PipelineFactory::class,
],
];
}
public function getMetadataMap() : array
{
return [
// ...
];
}
}
Note that the majority of these service names are virtual; they do not resolve
to actual classes. PHP allows usage of the ::class
pseudo-constant anywhere,
and will resolve the value based on the current namespace. This gives us virtual
services such as Api\Router
, Api\UrlHelper
, etc.
Also note that we are creating factory instances. Normally, we recommend not using closures or instances for factories due to potential problems with configuration caching. Fortunately, we have provided functionality in each of these factories that allows them to be safely cached, retaining the context-specific configuration required.
What about the hard-coded path?
You'll note that the above example hard-codes the base path for the
UrlHelper
. What if you want to use a different path?You can override the service in an application-specific configuration under
config/autoload/
, specifying a different path!\Api\UrlHelper::class => new UrlHelperFactory('/different/path', \Api\Router::class),
Using virtual services with a handler
Now let's turn to our CreateBookHandler
. We'll define it as follows:
// in src/Api/CreateBookHandler.php:
namespace Api;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Expressive\Hal\HalResponseFactory;
use Zend\Expressive\Hal\ResourceGenerator;
class CreateBookHandler implements RequestHandlerInterface
{
private $resourceGenerator;
private $responseFactory;
public function __construct(ResourceGenerator $resourceGenerator, HalResponseFactory $responseFactory)
{
$this->resourceGenerator = $resourceGenerator;
$this->responseFactory = $responseFactory;
}
public function handle(ServerRequestInterface $request) : ResponseInterface
{
// do some work ...
$resource = $this->resourceGenerator->fromObject($book, $request);
return $this->responseFactory->createResponse($request, $book);
}
}
This handler needs a HAL resource generator. More specifically, it needs the one specific to our module. As such, we'll define our factory as follows:
// in src/Api/CreateBookHandlerFactory.php:
namespace Api;
use Psr\Container\ContainerInterface;
use Zend\Expressive\Hal\HalResponseFactory;
class CreateBookHandlerFactory
{
public function __invoke(ContainerInterface $container) : CreateBookHandler
{
return new CreateBookHandler(
ResourceGenerator::class, // module-specific service name!
HalResponseFactory::class
);
}
}
You can create any number of such handlers for your module; the above demonstrates how and where injection of the alternate resource generator occurs.
Creating our pipeline and routes
Now we can create our pipeline and routes.
Generally when piping to an application instance, we can specify a class name of middleware to pipe, or an array of middleware:
// in config/pipeline.php:
$app->pipe('/api', [
\Zend\ProblemDetails\ProblemDetailsMiddleware::class,
\Api\RouteMiddleware::class, // module-specific routing middleware!
ImplicitHeadMiddleware::class,
ImplicitOptionsMiddleware::class,
MethodNotAllowedMiddleware::class,
\Api\UrlHelperMiddleware::class, // module-specific URL helper middleware!
DispatchMiddleware::class,
\Zend\ProblemDetails\ProblemDetailsNotFoundHandler::class,
]);
However, we have both the pipeline and routes, and we likely want to indicate the exact behavior of this pipeline. Additionally, we may want to re-use this pipeline in other applications; pushing this into the application configuration makes that more error-prone.
As such, we will create a factory that generates and returns a
Zend\Stratigility\MiddlewarePipe
instance that is fully configured for our
module. As part of this functionality, we will also add our module-specific
routing.
// In src/Api/PipelineFactory.php:
namespace Api;
use Psr\Container\ContainerInterface;
use Zend\Expressive\MiddlewareFactory;
use Zend\Expressive\Router\Middleware as RouterMiddleware;
use Zend\Expressive\Router\RouteCollector;
use Zend\ProblemDetails\ProblemDetailsMiddleware;
use Zend\ProblemDetails\ProblemDetailsNotFoundHandler;
use Zend\Stratigility\MiddlewarePipe;
class PipelineFactory
{
public function __invoke(ContainerInterface $container) : MiddlewarePipe
{
$factory = $container->get(MiddlewareFactory::class);
// First, create our middleware pipeline
$pipeline = new MiddlewarePipe();
$pipeline->pipe($factory->lazy(ProblemDetailsMiddleware::class));
$pipeline->pipe($factory->lazy(RouteMiddleware::class)); // module-specific!
$pipeline->pipe($factory->lazy(RouterMiddleware\ImplicitHeadMiddleware::class));
$pipeline->pipe($factory->lazy(RouterMiddleware\ImplicitOptionsMiddleware::class));
$pipeline->pipe($factory->lazy(RouterMiddleware\MethodNotAllowedMiddleware::class));
$pipeline->pipe($factory->lazy(UrlHelperMiddleware::class)); // module-specific!
$pipeline->pipe($factory->lazy(RouterMiddleware\DispatchMiddleware::class));
$pipeline->pipe($factory->lazy(ProblemDetailsNotFoundHandler::class));
// Second, we'll create our routes
$router = $container->get(Router::class); // Retrieve our module-specific router
$routes = new RouteCollector($router); // Create a route collector to simplify routing
// Start routing:
$routes->post('/books', $factory->lazy(CreateBookHandler::class));
// Return the pipeline now that we're done!
return $pipeline;
}
}
Note that the routing definitions do not include the prefix /api
; this is
because that prefix will be stripped when we path-segregate our API middleware
pipeline. All routing will be relative to that path.
Creating a path-segregated pipeline
Finally, we will attach our pipeline to the application, using path segregation:
// in config/pipeline.php:
$app->pipe('/api', \Api\Pipeline::class);
This statement tells the application to pipe the pipeline returned by our
PipelineFactory
under the path /api
; that path will be stripped from
requests when passed to the underlying middleware.
At this point, we now have a re-usable module, complete with its own routing, with URI generation that will include the base path under which we have segregated the pipeline!
Found a mistake or want to contribute to the documentation? Edit this page on GitHub!