Skip to content

Instantly share code, notes, and snippets.

@weierophinney weierophinney/readme.md
Last active Oct 26, 2016

Embed
What would you like to do?
Proposal to implement PSR-15 in zend-stratigility

[zend-stratigility] RFC: Implement PSR-15

PSR-15 proposes a standard around PHP middleware that consumes PSR-7 HTTP message instances. One goal of the standard is to create interfaces that can co-exist with existing projects, which would allow adoption without necessarily leading to backwards compatibility breaks.

The most recent proposal has the following interfaces:

namespace Interop\Http\Middleware;

use PSR\Http\Message\RequestInterface;
use PSR\Http\Message\ResponseInterface;
use PSR\Http\Message\ServerRequestInterface;

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

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

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

DelegateInterface is roughly analogous to Zend\Stratigility\Next, while ServerMiddlewareInterface is analogous to Zend\Stratigility\MiddlewareInterface.

Proposed changes

We'd take a tiered approach, over a minor version and a following major version.

DelegateInterface adoption

First, we can update Next to implement DelegateInterface. At first, this would be adding a process() method that proxies to __invoke(), and making the ResponseInterface argument optional:

namespace Zend\Stratigility;

use Interop\Http\Middleware\DelegateInteface;
use PSR\Http\Message\RequestInterface;
use PSR\Http\Message\ResponseInterface;
use PSR\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response;

class Next implements DelegateInteface
{
    public function __invoke(
        ServerRequestInterface $request,
        ResponseInterface $response = null,
        $err = null
    ) {
        /* ... */
    }

    public function process(RequestInterface $request)
    {
        return $this($request);
    }
}

We'd also mark __invoke() as deprecated via annotation.

For the next major version, we'd modify __invoke() to proxy to process(). Since one suggested change for v2 includes removal of the $err argument already, this version would also drop the $response argument, so that it now reads:

namespace Zend\Stratigility;

use Interop\Http\Middleware\DelegateInteface;
use PSR\Http\Message\RequestInterface;
use PSR\Http\Message\ResponseInterface;
use PSR\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response;

class Next implements DelegateInteface
{
    public function __invoke(ServerRequestInterface $request)
    {
        return $this->next($request);
    }

    public function next(RequestInterface $request)
    {
        /* ... */
    }
}

At the same time, we'd also mark the __invoke() method as deprecated; we might potentially also raise an E_USER_DEPRECATED error within the __invoke() method to encourage usage of the process() method.

MiddlewareInterface

For a minor version:

  • We'd update MiddlewarePipe to also allow PSR-15 ServerMiddlewareInterface and MiddlewareInterface implementations.
  • We'd update Dispatch to check for ServerMiddlewareInterface and MiddlewareInterface instances; if found, it would invoke them with only the passed request.
  • We'd mark MiddlewareInterface as deprecated.
  • We'd add a class that can decorate callable middleware to be invoked as ServerMiddlewareInterface implementations. This would check the arity of the callable to ensure that it can be invoked per the PSR-15 specification. (See comment below for details on why this is now omitted.)

For a major version:

  • We'd remove MiddlewareInterface entirely.
  • Next would check if the handler is a ServerMiddlewareInterface implementation before dispatch (see the error handler proposal for details on why this goes in Next and not Dispatch).
  • We'd update MiddlewarePipe to only allow ServerMiddlewareInterface and MiddlewareInterface implementations.

Challenges

PSR-15 is not yet accepted, and could still undergo revision, making it a moving target.

@weierophinney

This comment has been minimized.

Copy link
Owner Author

weierophinney commented Oct 24, 2016

The one issue I'm running up against in current implementation of the next minor release is within Next::__invoke(): lack of a $response argument means:

  • Allowing the $response argument to be null
  • But, if not provided, we need to provide a response argument when calling the next layer or the $done argument

This latter presents a problem, as it may require:

  • an implementation be present in the dependencies
  • injecting Next with a prototype response to use in these situations
  • storing the first $response provided as the prototype
@weierophinney

This comment has been minimized.

Copy link
Owner Author

weierophinney commented Oct 26, 2016

we need to provide a response argument when calling the next layer or the $done argument

I've solved this locally by using the last two suggestions I made:

  • injecting Next with a prototype response to use in these situations
  • storing the first $response provided as the prototype

I have it work as follows:

  • Next now defines the method setResponsePrototype(), which accepts a ResponseInterface instance.
  • Next now raises an exception prior to calling the $done callback or the Dispatch instance if $response is null and no response prototype is present.
  • The first time Next is invoked with a Response instance, and no prototype is present, the instance is memoized for later use.

Additionally, I have updated MiddlewarePipe's invocation method to pass the provided Response instance into the new Next instance via the setResponsePrototype() method; this ensures that if http-interop middleware is provided as the outermost layer, it may still be invoked.

@weierophinney

This comment has been minimized.

Copy link
Owner Author

weierophinney commented Oct 26, 2016

We'd add a class that can decorate callable middleware to be invoked as ServerMiddlewareInterface implementations.

I've decided to move this functionality either into its own repository or include it as part of zend-expressive.

The reason is because this sort of wrapper will require a Response instance.

To illustrate:

use Interop\Http\Middleware\DelegateInterface;
use Interop\Http\Middleware\ServerMiddlewareInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class CallableMiddlewareDecorator implements ServerMiddlewareInterface
{
    private $middleware;

    public function __construct(callable $middleware)
    {
        $this->middleware = $middleware;
    }

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $middleware = $this->middleware;
        return $middleware($request, /* what goes here? */, function ($request) {
            return $delegate->process($request);
        });
    }
}

Note the comment inside the process() method: the second argument currently in Stratigility and Slim middleware is a response instance, but we do not have one!

There are two ways to make this work:

  • Instantiate a response when invoking the middleware. This is easiest, but poses a problem: we currently do not require a concrete PSR-7 library with Stratigility, and have pushed hard for that to be true for production installs. Users should be able to choose the PSR-7 implementation that fits their project.
  • Inject a response prototype or response factory via the constructor. This is relatively easy, but means that we cannot do auto-wrapping of callable middleware within MiddlewarePipe, as it would require the pipeline to have one of these artifacts, which breaks SRP and the Law of Demeter.

One further possibility that would make the second approach work is to create a "callable middleware decorator factory", which could be injected in a MiddlewarePipe in order to allow auto-wrapping of callable middleware:

final class CallableMiddlewareDecoratorFactory
{
    private $responsePrototype;

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

    public function createDecorator(callable $middleware)
    {
        return new CallableMiddlewareDecorator($middleware, $this->responsePrototype);
    }
}

Interestingly, zend-expressive already requires zend-diactoros, which would solve the problems presented in the first bullet point. Additionally, I'd argue any auto-negotiation of arguments should likely be relegated to something built on top of MiddlewarePipe, similar to how Expressive adds support for pulling named middleware from a container (instead of requiring callable middleware). This may take the form of a new method (e.g., pipeCallableMiddleware(callable $middleware)), or just be inlined in the pipe() method; either way, it is not the responsibility of Stratigility.

As such, I'm omitting that task from this RfC.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.