PSR-15 middleware, Zend Expressive and versioning
As it turns out, I mis-remembered what Matthew said in his talk and yesterday's post with Slim isn't actually PSR-15 compatible. Slim currently uses function($request, $response, $next)
whilst PSR-15 uses function($request, $handler)
.
To build a real PSR-15 middleware, I chose to use Zend Expressive, which supports PSR-15 as of version 3.
Bootstrapping our Zend Expressive application
Start by creating an example Expressive application using composer:
bash
composer create-project "zendframework/zend-expressive-skeleton:3.0.x-dev" psr15-demo --no-interaction
Change in to the psr15-demo
directory, then run the built in PHP server:
bash
php -t public -S localhost:8000
We're going to add a new endpoint that reverses a string. Open up config/routes.php
and add the following underneath $app->get('/api/ping')
:
php
$app->post('/reverse', App\Handler\ReverseHandler::class, 'reverse');
As we've told Expressive to look for App\Handler\ReverseHandler
we need to implement that too. Run the following to bootstrap this new handler:
bash
composer expressive handler:create "App\Handler\ReverseHandler"
Open up src/App/Handler/ReverseHandler.php
, add use Zend\Diactoros\Response\JsonResponse;
to your list of imports and replace the handle
function with the following:
php
public function handle(ServerRequestInterface $request) : ResponseInterface{$body = $request->getParsedBody();if (!isset($body['firstname'])) {return new JsonResponse(['error' => 'Missing firstname']);}return new JsonResponse(['firstname' => strrev($body['firstname'])]);}
Notice how the code is almost exactly the same as the Slim version? That's thanks to the fact that both project use PSR-7 request and response objects (standardisation!).
We need to make one more change to our Expressive config. By default it doesn't parse the request body on all requests, so open up config/pipeline.php
and add the following use statement:
php
use Zend\Expressive\Helper\BodyParams\BodyParamsMiddleware;
Next, add the BodyParamsMiddleware
to the pipeline after ServerUrlMiddleware
:
php
$app->pipe(ServerUrlMiddleware::class);$app->pipe(BodyParamsMiddleware::class);
Now, you should be able to make a request to your application and have the name provided reversed:
bash
curl -X POST -H 'Content-Type: application/json' "http://localhost:8000/reverse" -d '{"firstname": "Michael"}'# => {"firstname":"leahciM"}
Create the middleware
We're now in a position to write some middleware!
Generate a new middleware using composer and the Expressive helper:
bash
composer expressive middleware:create "App\VersionMiddleware"
This will create src/App/VersionMiddleware.php
which is a minimal middleware definition (it doesn't do anything yet!). Let's update it to translate from first_name
to firstname
and then firstname
back to first_name
like our Slim middleware:
php
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface{$isV2 = in_array('application/vnd.test.v2+json', $request->getHeader('Accept'));if ($isV2) {$body = $request->getParsedBody();$body['firstname'] = $body['first_name'];unset($body['first_name']);$request = $request->withParsedBody($body);}$response = $handler->handle($request);if ($isV2) {$body = json_decode((string)$response->getBody(), true);$body['first_name'] = $body['firstname'];unset($body['firstname']);$response->getBody()->rewind();$response->getBody()->write(json_encode($body));}return $response;}
Register the middleware on our route
Finally, we need to register our middleware with Expressive. As routes are already middlewares, we can update our config/routes.php
to run App\VersionMiddleware
before it runs App\Handler\ReverseHandler
:
php
$app->post('/reverse', [App\VersionMiddleware::class, App\Handler\ReverseHandler::class], 'reverse');
At this point, we can make requests to both version 1 and version 2 of the API like we could with Slim.
bash
# No version specifiedcurl -X POST -H 'Content-Type: application/json' "http://localhost:8000/reverse" -d '{"firstname": "Michael"}'# => {"firstname":"leahciM"}# Version 1 specifiedcurl -X POST -H 'Content-Type: application/json' "http://localhost:8000/reverse" -d '{"firstname": "Michael"}' -H 'Accept: application/vnd.test.v1+json'# => {"firstname":"leahciM"}# Version 2 specifiedcurl -X POST -H 'Content-Type: application/json' "http://localhost:8000/reverse" -d '{"first_name": "Michael"}' -H 'Accept: application/vnd.test.v2+json'# => {"first_name":"leahciM"}