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:
- composer require "markstory/cakephp-spekkoek:*"
Once installed the plugin you’ll need to update your webroot/index.php
to look like the following:
- <?php
- // for built-in server
- $file = __DIR__ . $url['path'];
- ) {
- return false;
- }
- }
- use Spekkoek\Server;
- use App\Application;
- // Bind your application to the server.
- // Run the request/response through the application
- // and emit the response.
- $server->emit($server->run());
There are a few notable differences between this entry-point script and the one CakePHP comes with:
- We aren’t loading our
config/bootstrap.php
file. Instead we’re using anApplication
class. We’ll cover this more in a bit. - We aren’t using the CakePHP Request, Response or Dispatcher classes. Instead we’re using
Server
class from Spekkoek. TheServer
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:
- It ‘bootstraps’ the application code by loading files like
config/bootstrap.php
and any other setup code your application needs. - 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:
- <?php
- // in src/Application.php
- namespace App;
- use Spekkoek\BaseApplication;
- use Spekkoek\Middleware\AssetMiddleware;
- use Spekkoek\Middleware\ErrorHandlerMiddleware;
- use Spekkoek\Middleware\RoutingMiddleware;
- class Application extends BaseApplication
- {
- public function middleware($middleware)
- {
- // Catch any exceptions in the lower layers,
- // and make an error page/response
- $middleware->push(new ErrorHandlerMiddleware());
- // Handle plugin/theme assets like CakePHP normally does.
- $middleware->push(new AssetMiddleware());
- // Apply routing
- $middleware->push(new RoutingMiddleware());
- return $middleware;
- }
- }
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:
- It is callable. Either because it is a
Closure
or it implements__invoke()
. - It returns a
Response
or raises an exception. - 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:
- <?php
- class CorsMiddleware
- {
- public function __invoke($request, $response, $next)
- {
- // Invoke the next layer in the middleware stack
- $response = $next($request, $response);
- // Add the Origin header.
- return $response->withHeader('Origin', '*.example.com');
- }
- }
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:
- // This does not work. The modified object is lost.
- $response->withHeader('Origin', '*.example.com');
- // This does
- $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.
Looking forward to see this as a default in CakePHP. A big step in the right direction. Thanks
Tarique Sani on 4/8/16