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:

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:

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'):

$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:

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:

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:

use Zend\Expressive\Helper\BodyParams\BodyParamsMiddleware;

Next, add the BodyParamsMiddleware to the pipeline after ServerUrlMiddleware:

$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:

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:

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:

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:

$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.

# No version specified
curl -X POST -H 'Content-Type: application/json' "http://localhost:8000/reverse" -d '{"firstname": "Michael"}'
# => {"firstname":"leahciM"}

# Version 1 specified
curl -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 specified
curl -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"}

Michael is a polyglot software engineer, committed to reducing complexity in systems and making them more predictable. Working with a variety of languages and tools, he shares his technical expertise to audiences all around the world at user groups and conferences. You can follow @mheap on Twitter

Thoughts on this post

Leave a comment?

Leave a Reply