zendframework/zend-expressive-helpers

Helper/Utility classes for Expressive


README

Build Status Coverage Status

Helper classes for Expressive.

Installation

Install this library using composer:

$ composer require zendframework/zend-expressive-helpers

Helpers Provided

UrlHelper

Zend\Expressive\Helper\UrlHelper provides the ability to generate a URI path based on a given route defined in the Zend\Expressive\Router\RouterInterface. The provided Zend\Expressive\Helper\UrlHelperMiddleware can look for a Zend\Expressive\Router\RouteResult request attribute, and, if present, inject the UrlHelper with it; when this occurs, if the route being used to generate a URI was also the one matched during routing, you can provide a subset of routing parameters, and any not provided will be pulled from those matched.

To register the UrlHelperMiddleware:

use Zend\Expressive\Helper\UrlHelperMiddleware;
use Zend\Expressive\Router\Middleware\DispatchMiddleware;
use Zend\Expressive\Router\Middleware\RouteMiddleware;

$app->pipe(RouteMiddleware::class);
$app->pipe(UrlHelperMiddleware::class);
$app->pipe(DispatchMiddleware::class);

// Or use configuration:
// [
//     'middleware_pipeline' => [
//         'routing' => [
//             'middleware' => [
//                 RouteMiddleware::class,
//                 UrlHelperMiddleware::class,
//                 DispatchMiddleware::class,
//             ],
//             'priority' => 1,
//         ],
//     ],
// ]

Compose the helper in your handler (or elsewhere), and then use it to generate URI paths:

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response;
use Zend\Expressive\Helper\UrlHelper;

class FooHandler implements RequestHandlerInterface
{
    private $helper;

    public function __construct(UrlHelper $helper)
    {
        $this->helper = $helper;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        $response = new Response();

        return $response->withHeader(
            'Link',
            $this->helper->generate('resource', ['id' => 'sha1'])
        );
    }
}

You can use the methods generate() and __invoke() interchangeably (i.e., you can use the helper as a function if desired). The signature is:

function (
    string $routeName = null,
    array $routeParams = [],
    array $queryParams = [],
    string $fragmentIdentifier = null,
    array $options = []
) : string

Where:

  • $routeName is the name of a route defined in the composed router. You may omit this argument if you want to generate the path for the currently matched request.
  • $routeParams is an array of substitutions to use for the provided route, with the following behavior:
    • If a RouteResult is composed in the helper, and the $routeName matches it, the provided $params will be merged with any matched parameters, with those provided taking precedence.
    • If a RouteResult is not composed, or if the composed result does not match the provided $routeName, then only the $params provided will be used for substitutions.
    • If no $params are provided, and the $routeName matches the currently matched route, then any matched parameters found will be used. parameters found will be used.
    • If no $params are provided, and the $routeName does not match the currently matched route, or if no route result is present, then no substitutions will be made.

Each method will raise an exception if:

  • No $routeName is provided, and no RouteResult is composed.
  • No $routeName is provided, a RouteResult is composed, but that result represents a matching failure.
  • The given $routeName is not defined in the router.

Base Path support

If your application is running under a subdirectory, or if you are running pipeline middleware that is intercepting on a subpath, the paths generated by the router may not reflect the base path, and thus be invalid. To accommodate this, the UrlHelper supports injection of the base path; when present, it will be prepended to the path generated by the router.

As an example, perhaps you have middleware running to intercept a language prefix in the URL; this middleware could then inject the UrlHelper with the detected language, before stripping it off the request URI instance to pass on to the router:

use Locale;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Expressive\Helper\UrlHelper;

class LocaleMiddleware implements MiddlewareInterface
{
    private $helper;

    public function __construct(UrlHelper $helper)
    {
        $this->helper = $helper;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        $uri = $request->getUri();
        $path = $uri->getPath();
        if (! preg_match('#^/(?P<lang>[a-z]{2})/#', $path, $matches)) {
            return $handler->handle($request);
        }

        $lang = $matches['lang'];
        Locale::setDefault($lang);
        $this->helper->setBasePath($lang);

        return $handler->handle(
            $request->withUri(
                $uri->withPath(substr($path, 3))
            )
        );
    }
}

(Note: if the base path injected is not prefixed with /, the helper will add the slash.)

Paths generated by the UriHelper from this point forward will have the detected language prefix.

ServerUrlHelper

Zend\Expressive\Helper\ServerUrlHelper provides the ability to generate a full URI by passing only the path to the helper; it will then use that path with the current Psr\Http\Message\UriInterface instance provided to it in order to generate a fully qualified URI.

In order to use the helper, you will need to inject it with the current UriInterface from the request instance. To automate this, we provide Zend\Expressive\Helper\ServerUrlMiddleware, which composes a ServerUrl instance, and, when invoked, injects it with the URI instance.

As such, you will need to:

  • Register the ServerUrlHelper as a service in your container.
  • Register the ServerUrlMiddleware as a service in your container.
  • Register the ServerUrlMiddleware early in your middleware pipeline.

To register the ServerUrlMiddleware in your middleware pipeline:

use Zend\Expressive\Helper\ServerUrlMiddleware;
use Zend\Expressive\Router\Middleware\DispatchMiddleware;
use Zend\Expressive\Router\Middleware\RouteMiddleware;

// Do this early, before piping other middleware or routes:
$app->pipe(ServerUrlMiddleware::class);

/* ... */
$app->pipe(RouteMiddleware::class);
$app->pipe(DispatchMiddleware::class);

// Or use configuration:
// [
//     'middleware_pipeline' => [
//         [
//             'middleware' => ServerUrlMiddleware::class,
//             'priority' => PHP_INT_MAX,
//         ],
//     ],
// ]

Compose the helper in your hanlder (or elsewhere), and then use it to generate URI paths:

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response;
use Zend\Expressive\Helper\ServerUrlHelper;

class FooHandler implements RequestHandlerInterface
{
    private $helper;

    public function __construct(ServerUrlHelper $helper)
    {
        $this->helper = $helper;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        $response = new Response();

        return $response->withHeader(
            'Link',
            $this->helper->generate() . '; rel="self"'
        );
    }
}

You can use the methods generate() and __invoke() interchangeably (i.e., you can use the helper as a function if desired). The signature is:

function (string $path = null) : string

Where:

  • $path, when provided, can be a string path to use to generate a URI.

BodyParams middleware

One aspect of PSR-7 is that it allows you to parse the raw request body, and then create a new instance with the results of parsing that later processes can fetch via getParsedBody(). It does not provide any actual facilities for parsing, which means you must write middleware to do so.

This package provides such facilities via Zend\Expressive\Helper\BodyParams\BodyParamsMiddleware. By default, this middleware will detect the following content types:

  • application/x-www-form-urlencoded (standard web-based forms, without file uploads)
  • application/json, application/*+json (JSON payloads)

You can register it manually:

use Zend\Expressive\Helper\BodyParams\BodyParamsMiddleware;

$app->pipe(BodyParamsMiddleware::class);

or, if using Expressive, as pipeline middleware:

// config/autoload/middleware-pipeline.global.php
use Zend\Expressive\Helper;

return [
    'dependencies' => [
        'invokables' => [
            Helper\BodyParams\BodyParamsMiddleware::class => Helper\BodyParams\BodyParamsMiddleware::class,
            /* ... */
        ],
        'factories' => [
            /* ... */
        ],
    ],
    'middleware_pipeline' => [
        [
            'middleware' => Helper\BodyParams\BodyParamsMiddleware::class,
            'priority' => 1000,
        ],
        'routing' => [
            /* ... */
        ],
    ],
];

Strategies

If you want to intercept and parse other payload types, you can add strategies to the middleware. Strategies implement Zend\Expressive\Helper\BodyParams\StrategyInterface:

namespace Zend\Expressive\Helper\BodyParams;

use Psr\Http\Message\ServerRequestInterface;

interface StrategyInterface
{
    /**
     * Match the content type to the strategy criteria.
     *
     * @param string $contentType
     * @return bool Whether or not the strategy matches.
     */
    public function match($contentType);

    /**
     * Parse the body content and return a new response.
     *
     * @param ServerRequestInterface $request
     * @return ServerRequestInterface
     */
    public function parse(ServerRequestInterface $request);
}

You then register them with the middleware using the addStrategy() method:

$bodyParams->addStrategy(new MyCustomBodyParamsStrategy());

To automate the registration, we recommend writing a factory for the BodyParamsMiddleware, and replacing the invokables registration with a registration in the factories section of the middleware-pipeline.config.php file:

use Zend\Expressive\Helper\BodyParams\BodyParamsMiddleware;

class MyCustomBodyParamsStrategyFactory
{
    public function __invoke($container)
    {
        $bodyParams = new BodyParamsMiddleware();
        $bodyParams->addStrategy(new MyCustomBodyParamsStrategy());
        return $bodyParams;
    }
}

// In config/autoload/middleware-pipeline.config.php:
use Zend\Expressive\Helper;

return [
    'dependencies' => [
        'invokables' => [
            // Remove this line:
            Helper\BodyParams\BodyParamsMiddleware::class => Helper\BodyParams\BodyParamsMiddleware::class,
            /* ... */
        ],
        'factories' => [
            // Add this line:
            Helper\BodyParams\BodyParamsMiddleware::class => MyCustomBodyParamsStrategy::class,
            /* ... */
        ],
    ],
];

Removing the default strategies

If you do not want to use the default strategies (form data and JSON), you can clear them from the middleware using clearStrategies():

$bodyParamsMiddleware->clearStrategies();

Note: if you do this, all strategies will be removed! As such, we recommend doing this only immediately before registering any custom strategies you might be using.

Content-Length middleware

In some cases, you may want to include an explicit Content-Length response header, without having to inject it manually. To facilitate this, we provide Zend\Expressive\Helper\ContentLengthMiddleware.

This middleware delegates the request, and operates on the returned response. It will return a new response with the Content-Length header injected under the following conditions:

  • No Content-Length header is already present AND
  • the body size is non-null.

To register it in your application, you will need to do two things: register the middleware with the container, and register the middleware in either your application pipeline, or within routed middleware.

To add it to your container, add the following configuration:

// In a `config/autoload/*.global.php` file, or a `ConfigProvider` class:

use Zend\Expressive\Helper;

return [
    'dependencies' => [
        'invokables' => [
            Helper\ContentLengthMiddleware::class => Helper\ContentLengthMiddleware::class,
        ],
    ],
];

To register it as pipeline middleware to execute on any request:

// In `config/pipeline.php`:

use Zend\Expressive\Helper;

$app->pipe(Helper\ContentLengthMiddleware::class);

To register it within a routed middleware pipeline:

// In `config/routes.php`:

use Zend\Expressive\Helper;

$app->get('/download/tarball', [
    Helper\ContentLengthMiddleware::class,
    Download\Tarball::class,
], 'download-tar');

Documentation

See the zend-expressive documentation tree, or browse online at https://docs.zendframework.com/zend-expressive/features/helpers/intro/