PSR7 Bridge for CakePHP

I’m excited to announce the availability of a PSR7 Bridge plugin for CakePHP. This plugin lets you bridge PSR7 Middleware with CakePHP 3.3+ applications.

What is PSR7?

PSR7 is a recent recommendation from the PHP-FIG group (of which CakePHP is a member). It describes a set of interfaces for requests and responses. When implemented these interfaces allow the creation and consumption of framework agnostic request & response handling code. As an application developer PSR7 interests me because it enables me to more easily leverage tools built outside the CakePHP community. As a maintainer of CakePHP PSR7 interests me as I can maintain less code and users new to CakePHP have less to learn if they’ve used other PSR7 projects before. If you’ve ever used Rack in Ruby, or WSGI in Python, that is the general intent of PSR7 as well.

How the Plugin Works

Because PSR7 is a community standard, there are several existing implementations. I didn’t feel that re-implementing the PSR7 interfaces would provide any value so I opted to use an existing implementation. After evaluating a few different libraries, I chose to use zend-diactoros because the team at ZF produces well built, well tested libraries. The cakephp-spekkoek plugin is unlike most plugins you have used in the past. Because it needs to replace the low-level HTTP layers of CakePHP, it requires a slightly more invasive installation and setup. Installation is done with composer:

Show Plain Text
  1. composer require "markstory/cakephp-spekkoek:*"

Once installed the plugin you’ll need to update your webroot/index.php to look like the following:

Show Plain Text
  1. <?php
  2. // for built-in server
  3. if (php_sapi_name() === 'cli-server') {
  4.     $_SERVER['PHP_SELF'] = '/' . basename(__FILE__);
  5.  
  6.     $url = parse_url(urldecode($_SERVER['REQUEST_URI']));
  7.     $file = __DIR__ . $url['path'];
  8.     if (strpos($url['path'], '..') === false &&
  9.         strpos($url['path'], '.') !== false &&
  10.         is_file($file)
  11.     ) {
  12.         return false;
  13.     }
  14. }
  15. require dirname(__DIR__) . '/vendor/autoload.php';
  16.  
  17. use Spekkoek\Server;
  18. use App\Application;
  19.  
  20. // Bind your application to the server.
  21. $server = new Server(new Application(dirname(__DIR__) . '/config'));
  22.  
  23. // Run the request/response through the application
  24. // and emit the response.
  25. $server->emit($server->run());

There are a few notable differences between this entry-point script and the one CakePHP comes with:

  1. We aren’t loading our config/bootstrap.php file. Instead we’re using an Application class. We’ll cover this more in a bit.
  2. We aren’t using the CakePHP Request, Response or Dispatcher classes. Instead we’re using Server class from Spekkoek. The Server class knows how to dispatch PSR7 requests and emit PSR7 responses to the webserver.

As mentioned earlier, a critical part of the new dispatch process is the Application. The Application class defines the following:

  1. It ‘bootstraps’ the application code by loading files like config/bootstrap.php and any other setup code your application needs.
  2. It defines the middleware your application uses. Middleware replaces dispatch filters, and the Application class itself behaves as a middleware component.

A starter Application class would look like:

Show Plain Text
  1. <?php
  2. // in src/Application.php
  3. namespace App;
  4.  
  5. use Spekkoek\BaseApplication;
  6. use Spekkoek\Middleware\AssetMiddleware;
  7. use Spekkoek\Middleware\ErrorHandlerMiddleware;
  8. use Spekkoek\Middleware\RoutingMiddleware;
  9.  
  10. class Application extends BaseApplication
  11. {
  12.     public function middleware($middleware)
  13.     {
  14.         // Catch any exceptions in the lower layers,
  15.         // and make an error page/response
  16.         $middleware->push(new ErrorHandlerMiddleware());
  17.  
  18.         // Handle plugin/theme assets like CakePHP normally does.
  19.         $middleware->push(new AssetMiddleware());
  20.  
  21.         // Apply routing
  22.         $middleware->push(new RoutingMiddleware());
  23.         return $middleware;
  24.     }
  25. }

Because our new middleware replaces all the core provided dispatch filters, you should remember to remove all the relevant calls to DispatcherFactory::add() in config/bootstrap.php. The BaseApplication handles loading our bootstrap file, and we primarily need to define the middleware our application uses. I’ve used the term ‘middleware’ a few times so far but what is middleware? In a PSR7 application middleware has the following properties:

  1. It is callable. Either because it is a Closure or it implements __invoke().
  2. It returns a Response or raises an exception.
  3. It supports the following signature __invoke($request, $response, $next)

Each middleware object is arranged into a stack, and when a request is handled each layer in the stack has the opportunity to update the response or replace the response and return it, delgate to the next layer, or throw an exception. Lets go over a simple example that adds the Origin header to a response:

Show Plain Text
  1. <?php
  2. class CorsMiddleware
  3. {
  4.     public function __invoke($request, $response, $next)
  5.     {
  6.         // Invoke the next layer in the middleware stack
  7.         $response = $next($request, $response);
  8.         // Add the Origin header.
  9.         return $response->withHeader('Origin', '*.example.com');
  10.     }
  11. }

The $next object is an important cog in the dispatch process. It permits a middleware object to delegate control to the ‘next’ middleware object in the stack. When using the PSR7 requests/responses it’s really important to remember that these objects are immutable. This means when you modify them, you need to re-assign the variable. For example:

Show Plain Text
  1. // This does not work. The modified object is lost.
  2. $response->withHeader('Origin', '*.example.com');
  3.  
  4. // This does
  5. $response = $response->withHeader('Origin', '*.example.com');

What’s Next

Having a plugin is just the first step for PSR7 & CakePHP. I’ve published the plugin with the goal of getting feedback from you – the community. Over the next few months I plan on integrating the new PSR7 stack into CakePHP as the Cake\Http package. This package will contain the parts necessary to make a PSR7 Server, and PSR7 HTTP client.

The PSR7 stack will be an opt-in component as of 3.3, but it will be the default for new applications. In 3.4, the request object that controllers see, will implement the PSR7 interfaces but while still being mutable objects. In addition, redundant methods on both the request and response will be deprecated. The goal of the deprecations is to prepare for 4.0 where immutable PSR7 requests/responses will be standard across all of CakePHP. I also plan on adding PSR7 middleware for both DebugKit and AssetCompress to further prove out the implementation and provide examples of how to use the new interfaces.

Comments

Looking forward to see this as a default in CakePHP. A big step in the right direction. Thanks

Tarique Sani on 4/8/16

Have your say: