PSR-15

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 web 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 primary 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 site, 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 Interop\Http as the top-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 Psr\Http\Server;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

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

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

namespace Psr\Http\Server;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

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:
$app->pipe(CheckOriginMiddleware::class);

// Use it within a route-specific pipeline:
$app->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 time 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 tracking 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 Psr\Http\Message\StreamInterface 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.