PSR 7 support - markstory/cakephp GitHub Wiki

With PSR-7 solidified, I think CakePHP can benefit from adopting/implementing the middleware aspects of the specification. I see supporting this new standard as providing a few benefits to the CakePHP:

  • Access to middleware from other communities.
  • Ability to share CakePHP middleware with other communities.
  • Ability to reduce/remove the amount of HTTP server code we maintain in the future. This wouldn't be completely achievable until 4.0, as we'd need to maintain the existing request/response API's until then.

PSR-7 Middleware naturally fits into the existing dispatcher, and is somewhat similar to the existing events we provide. There are two competing approaches to PSR-7 middleware. The 'nodejs' approach - as implemented by [diactros](https://github.com/zendframework/zend-diactoros. The 'WSGI' approach - as implemented by relayphp. I feel that the 'WSGI' approach provides a better functional model for building applications. I have always found WSGI in python to be a very flexible and powerful approach.

PSR-7 Middleware and Dispatcher Filters

There are obvious overlapping responsibilities with the existing DispatcherFilters and PSR-7 middleware. Two options I see are:

  1. Deprecate Dispatch Filters and recommend PSR-7 middleware going forward.
  2. Retrofit Dispatch Filters into PSR-7 middleware that also support the existing events.

There are likely other alternatives, and I feel we should discuss alternatives if they are compelling.

Option 1: Adapting Dispatch Filters into Middleware

Adapting dispatch filters into middleware is the least attractive solution to me. While we can maintain existing functionality/compatibility, and leverage the new standards, it will likely complicate the dispatch process. The current dispatch process looks like:

  1. Build the Request and Response
  2. Trigger the Dispatcher.beforeDispatch event. All attached filters are triggered in priority order.
  3. If a response was returned, abort the request.
  4. If the event had a controller attached, dispatch the requested action onto that controller.
  5. Trigger the Dispatcher.afterDispatch event.
  6. Send the response.

Middleware fits nicely in to step 4. Instead of the existing controller invocation code, the following flow could be used:

  1. Build the Request and Response
  2. Trigger the Dispatcher.beforeDispatch event. All attached filters are triggered in priority order.
  3. If a response was returned, abort the request.
  4. Convert the CakePHP request & response into their PSR7 equivalents.
  5. Invoke each attached Dispatch Filter in the order they were attached. Controller invocation would become just another piece of middleware.
  6. The controller invoker would convert the PSR7 request back into a CakePHP request/response. This allows controllers, components and views to continue using the same interface they enjoy today.
  7. After the middleware stack has been resolved, trigger the Dispatcher.afterDispatch event using a CakePHP request/response.
  8. Send the response that was part of the Dispatcher.afterDispatch process.

This approach does involve a few translations into and out of PSR7 requests which will be a source of overhead. With that in mind, it would allow us to maximize compatibility and still offer PSR7 support.

Using this new hybrid mode would likely see new methods added to DispatchFilters allowing them to opt into the PSR-7 mode.

Option 2: Deprecating Dispatch Filters

In this approach we would deprecate dispatch filters and the existing Dispatcher entirely. In their place we advocate that people begin using the new PSR-7 dispatcher. This dispatcher would take a PSR7 request/response, and use the following process:

  1. CakePHP would provide Development Assets, Routing, ControllerFactory, and ControllerInvoker middleware objects.
  2. User land middleware could be attached to the dispatcher.
  3. Apply each piece of middleware in sequence collecting a response.
  4. The ControllerInvoker middleware would convert the PSR7 request/response into the current CakePHP objects, and invoke the controller. This allows controllers to continue enjoying the same objects they currently do.
  5. The result of the ControllerInvoker would be converted back into a PSR-7 response and pushed back out the middleware stack.

This approach is much cleaner and requires fewer translations between PSR7 objects and our existing classes. It also lets people opt into the behavior as it makes sense for them.

In this approach we'd get a few new classes to implement the PSR7 features that we can't get from zendframework/zend-diactoros:

  • HttpFactory - Accepts the various middleware components and acts as a factory for dispatchers. Also provides factory methods for requests and response as diactoros is somewhat verbose.
  • Middleware - Base class that provides sugar like InstanceConfigTrait. Optional.
  • HttpDispatcher - A concrete dispatcher with the various middleware callables set.

Application Startup Example

// Add closures
HttpFactory::addMiddleware(function ($request, $response) {
});

// Add objects implementing __invoke
HttpFactory::addMiddleware(new InvokableObject);

// Add classes by name.
HttpFactory::addMiddleware('SomeClassName');

$request = HttpFactory::requestFromGlobals();
$response = HttpFactory::response();
HttpFactory::dispatcher()->dispatch($request, $response);

Option 3 - Make the CakePHP application a PSR7 compliant function

Instead of trying to augment dispatch filters and controllers, we could instead make the CakePHP 'application' a PSR7-compliant component. This new 'application' class would contain the following:

  1. Converting/decorating the PSR7 request/response into CakePHP equivalents.
  2. Loading any application middleware.
  3. Invoking the dispatch filters. Dispatcher filters would become deprecated.
  4. Applying routing.
  5. Invoking the controller.
  6. Extracting the PSR7 response out of the CakePHP response.

The CakePHP application could be decorated with any number of PSR7 'middleware' layers. This approach has the lowest risk of breaking an existing application as all the application internals remain the same.

Application Startup Example

With an 'application class' we could have a more object-orientated approach to bootstrapping. The Application object could be used to initialze the application's configuration and global state configuration:

require '../vendor/autoload.php';

use Cake\Http\Server;
use Cake\Http\MiddlewarePipe;
use App\Application;

$middleware = [
    // Add closures middleware
    function ($request, $response) {
    },
    // Add objects implementing __invoke
    new InvokableObject,
];

// Create a new server that can is a functional PSR7 stack.
// $middleware can also be an instance of MiddlewarePipe.
$server = new Server($middleware);

// Add the CakePHP application.
// This loads the various bootstrap.php files we load today.
// The application can use the middleware hook method to add more middleware.
$server->app(new Application());

// Request & response are optional. If not supplied, a request will be built
// from globals and a new response will be created.
$server->run($req, $res);

The new Application class also gives us an entry point that could be extended to create a more standalone console environment. But that will need to be tackled separately.

Adding Additional Middleware from the Application

Because plugins and other libraries might want to append PSR7 middleware, and that middleware might depend on application state. Application could provide a hook method to setup additional middleware:

class Application
{
    public function middleware(Cake\Http\MiddlewarePipe $middleware)
    {
        // Add to the head of the stack
        $middleware->prepend($cb);

        // Add to an arbitrary point in the stack
        $middleware->insertAt($n, $cb);

        // Insert before/after a class based middleware.
        $middleware->insertBefore('Vendor\Middleware', $cb);
        $middleware->insertAfter('Vendor\Middleware', $cb);

        // Add to the end of the stack
        $middleware->push($cb);
        return $middleware;
    }
}

This interface allows middleware stack manipulation similar to that offered by Rails config.middleware. The middleware hook would be invoked after application setup, but before the request was processed.

This approach would allow new middleware objects to receive PSR7 request/response objects. For backwards compatibility reasons, controllers would continue to receive the mutable Request/Response objects. The mutable objects would have their API's expanded to cover the PSR7 interface methods so that in a future release we can deprecate/remove the problematic/redundant features on the present day request/response objects.

Related Issues

There have been a few related issues come up recently. I think before we try and solve these, we should make a decision on if/how we want to approach PSR-7 middleware, as that could impact how these problems are approached.

  • #6925 - Separate Auth from controller logic.
  • #6769 - Separate Cookies from controller component.

Open Issues

  • While I've suggested using zend-diactoros, we could either re-implement the interfaces, or use a different implementation. I'm open to anything at this point.
  • Are there other high-level approaches we should consider?