Norway


Yesterday, following a unanimous vote from its Core Committee,
PHP-FIG formally accepted the proposed
PSR-15, HTTP Server Handlers
standard.

This new standard defines interfaces for request handlers and middleware.
These have enormous potential impact on the PHP ecosystem, as they provide
standard mechanisms for writing HTTP-facing, server-side applications.
Essentially, they pave the way for developers to create re-usable components
that will work in any application that works with PSR-15 middleware or request
handlers!

Caveat

I acted as sponsor on PSR-15, and as final arbiter of changes during the review
period.

Background

PSR-15 was started by Woody Gilk, who has acted in the
role of Editor for its duration. The original intent was to ratify a middleware
standard, and it was initially thought that it would be a quick ratification of
a pattern that was already in wide use:

function (
    ServerRequestInterface $request,
    ResponseInterface $response,
    callable $next
) : ResponseInterface

where $next should implement the following signature:

function (
    ServerRequestInterface $request,
    ResponseInterface $response
) : ResponseInterface

“Double Pass”

The above pattern has been dubbed “double pass” middleware, for the fact that
it passes two instances to the collaborator to pass to the next layer.

However, a number of critiques of this existing practice started to arise almost
immediately, with one from Anthony Ferrara
holding particular weight. The problems noted were:

  • Passing the response from layer to layer can lead to issues where an outer
    layer makes a change to the response it passes to an inner layer, expecting it
    to propagate back out, but an inner layer returns a different response entirely.
    Essentially, the pattern promotes problematic practices. If middleware needs
    to operate on a response, it should operate on the response returned by
    another layer.

  • Typehinting $next as callable means there’s no way to ensure that the
    callable is actually capable of accepting the arguments passed to it. In other
    words, it’s not type safe.

After debate within the working group, the next iteration proposed the
following (some details differ, but basic interactions are the same):

interface DelegateInterface
{
    public function process(ServerRequestInterface $request) : ResponseInterface;
}

interface MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        DelegateInterface $delegate
    ) : ResponseInterface;
}

This largely solved the problems highlighted above. However, a few more details
came up as different teams developed implementations.

First, many noted that they felt defining the same method name prevented
polymorphism. A common use case was to define a “request handler” that could
be called and which would in turn process itself. So, we updated the
interfaces as follows:

interface RequestHandlerInterface
{
    public function handle(ServerRequestInterface $request) : ResponseInterface;
}

interface MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ) : ResponseInterface;
}

Second, once that change was done, a number of others noted that the request
handler could be useful in and of itself. For example, when creating a simple
, you could marshal a server request, pass it to a handler, and emit the
response returned; middleware might not be necessary in this case. Another use
case is for the final, internal end points of a middleware application: instead
of implementing these as middleware, one could implement them as request
handlers instead, as they do not operate on the results of the handler.

As a result, we made the change to ship the two interfaces as separate
packages
, with the package containing the MiddlewareInterface depending on
the package defining the RequestHandlerInterface.

Finally, over the close to two years that this specification was being
developed, PHP 7 gained in maturity, with 7.1 and 7.2 releases. We decided to
pin the specification to PHP 7 or greater, and formally adopted return type
hints within it.

While work on the specification was in progress, each iteration of the
interfaces was published within the github organization
http-interop, with packages matching whatever
the current specification detailed (http-middleware, then
http-server-middleware, and, eventually, adding http-server-handler). These
packages also used InteropHttp as the -level namespace. Members of
the working group, as well as other interested parties, would pin their
offerings to specific iterations.

The final packages are now owned by the PHP-FIG group, however, and use the
Psr top-level namespace.

The Interfaces

That brings us to the final standard:

psr/http-server-handler
provides the following interface:

namespace PsrHttpServer;

use PsrHttpMessageResponseInterface;
use PsrHttpMessageServerRequestInterface;

interface RequestHandlerInterface
{
    public function handle(ServerRequestInterface $request) : ResponseInterface;
}

psr/http-server-middleware
provides the following interface:

namespace PsrHttpServer;

use PsrHttpMessageResponseInterface;
use PsrHttpMessageServerRequestInterface;
use PsrHttpServerRequestHandlerInterface;

interface MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ) : ResponseInterface;
}

Both packages depend on PSR-7 as they
typehint against the HTTP message interfaces that package defines. The
http-server-middleware package depends on the http-server-handler package.

How to write re-usable middleware

Most middleware dispatchers available currently (and there are a LOT of them, as
it turns out!) allow you to compose middleware in such a way that none of it
needs to know how or what is composing it. This is A Good Thing™. It allows you
to write middleware that is de-coupled from the context in which it is used.

But how do you do that?

In the meta document for the specification, we suggest the
following
:

  • Test the request for required pre-conditions, if any. If it does not
    satisfy any, use a composed response prototype or response factory to
    generate and return a response.

  • If pre-conditions are met, delegate creation of the response to the provided
    handler, optionally providing a “new” request (PSR-7 requests are immutable,
    so this means calling one of its with*() methods, which return new
    instances).

  • Either pass the response back from the handler verbatim, or return a new
    response by manipulating the one returned (again, via one of the with*()
    methods).

The first point is probably the most important here: do not directly instantiate
a response in your middleware, but instead use a prototype or a factory that
is provided during instantiation. This allows you to de-couple your middleware
from the PSR-7 implementation used by the application.

In practice, that might look something like this:

class CheckOriginMiddleware implements MiddlewareInterface
{
    private $acceptedOrigins;
    private $responsePrototype;

    public function __construct(array $acceptedOrigins, ResponseInterface $responsePrototype)
    {
        $this->acceptedOrigins = $acceptedOrigins;
        $this->responsePrototype = $responsePrototype;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        $origin = $request->getHeaderLine('origin');
        if (! in_array($origin, $this->acceptedOrigins, true)) {
            return $this->responsePrototype
                ->withStatus(401)
                ->withHeader('X-Invalid-Origin', $origin);
        }

        $response = $handler->handle($request);

        return $response->withHeader('X-Origin', $origin);
    }
}

A few things to note about this middleware:

  • It accepts dependencies via its constructor. This practice allows us to easily
    test the middleware, and defines what it needs in order to do its work.

  • The response prototype ensures we are de-coupled from the PSR-7
    implementation. I can pass a Diactoros response, a Guzzle response, a Slim
    response, or any other implementation. As a result, consumers of this
    middleware will not need to potentially install another PSR-7 implementation.

  • The middleware has no idea where it will be used, or what stack it will be used
    in. It simply operates on the request and the handler provided when
    process() is called.

How might I consume such middleware?

In Expressive), I might do
any one of the following:

// Pipe it as a service to pull from the DI container:
$->pipe(CheckOriginMiddleware::class);

// Use it within a route-specific pipeline:
$->post('/api/foo', [
    CheckOriginMiddleware::class,
    FooMiddleware::class,
]);

In northwoods/broker (maintained by
Woody Gilk, the PSR-15 editor), it looks like this:

$broker->always([CheckOriginMiddleware::class]);

In middlewares/utils Dispatcher, you’d
do this:

$dispatcher = new Dispatcher([
    /* ... */
    new CheckOriginMiddleware($acceptedOrigins, $responsePrototype),
    /* ... */
]);

With any one of these solutions, if your middleware is executed, it will act
exactly the same; how it is composed doesn’t matter, as the way it operates is
only dependent on the request and handler passed to the middleware during
process().

What about request handlers?

Most of the libraries I’ve looked at at this define handlers in one of two
ways:

  • As a middleware dispatcher. In this particular case, each middleware is
    processed until one returns a response. If the last one processed calls on the
    handler again, then a canned response is returned, an exception is thrown, or
    the next scenario comes into play:

  • As a “final” handler to pass to the middleware dispatcher. In other words, if
    the last middleware processed also calls on its handler, this “fallback” or
    “final” handler will be what’s invoked. This will typically return a 404
    response or a 500 response, depending on the implementation.

One other possibility that’s been floated by several is for use with routing
middleware
. In this case, when routing middleware matches a request, it would
then call on a request handler mapped to that request.

Notes for implementors

PSR-15 is accepted; let’s make all the things PSR-15!

But have some patience! While a number of projects have been working with
various iterations of the http-interop packages, and may need some time to
update to the final PSR-15 specification.

For example, we’ve been various iterations of http-interop in both
Stratigility and Expressive, but updating to the PSR-15 specification requires
backwards-incompatible changes, necessitating a new 3.0 version — which
need a few more weeks to drop. Slim also has a patch submitted with PSR-15 support,
which would likely not drop until an upcoming 4.0 release.

As such, have patience with library and framework maintainers, and help test
releases for them.

Additionally, consider tracking and testing the proposed PSR-17
specification
.
This proposal will standardize PSR-7 factories, which will provide a standard
way for middleware to generate, in particular, responses to return. Instead of
composing a response prototype, you would compose a factory. Why is this easier?
Well, in cases where you may also want to address the response body, which is
a PsrHttpMessageStreamInterface instance, it allows you to create new
instances of those as well. Since streams cannot be immutable (due to language
limitations), any time you write to a stream, you could be appending existing
content, which means middleware that writes to the response prototype body
generally needs to also compose a stream prototype. What if you could compose
a single factory instead?

Closing thoughts

When I started work on PSR-7 originally, it was because I wanted a standard
middleware interface for PHP
. I’d been playing with Node, and, more
specifically, Sencha Connect and ExpressJS. The middleware ecosystem in Node was
and continues to be tremendous. The reason it exists is because of two factors:

  • Accepted, standard middleware signature. Even though JS doesn’t provide
    interfaces, and there is no userland standards body, a consensus signature
    emerged, and everyone used it. These were possible because of:

  • Built-in HTTP message abstractions in the Node core library.

If I wanted standard middleware in PHP, we first needed standard HTTP messages,
which PSR-7 accomplished. That could have been the end of it, as many libraries
started using the same middleware signatures; however, we soon had at least two,
and possibly as many as a half-dozen different approaches. Thankfully, Woody
stepped up to the task and proposed what became PSR-15; further, he, and the
other members of the working group, had the patience and stamina to see it to
acceptance (though I know there were several times he and others almost threw
the towel in!).

With PSR-15 accepted, we are a step closer to something I have long envisioned:
a possibility for PHP developers to no longer work within monolithic MVC
frameworks, but instead compose applications out of commodity, reusable
middleware.



Source link

LEAVE A REPLY

Please enter your comment!
Please enter your name here