On a recent project, I needed to implement CORS support for my Expressive API. The easiest way to do this is to use Mike Tuupola‘s PSR-7 CORS Middleware.
As this is a standard Slim-Style PSR-7 middleware implementation, we need to wrap it for Expressive, so we make a factory:
App/Factory/CorsMiddleware.php:
<?php declare(strict_types=1); namespace App\Factory; use Tuupola\Middleware\Cors; use Zend\Diactoros\Response; use Zend\Stratigility\Middleware\CallableMiddlewareWrapper; class CorsMiddlewareFactory { public function __invoke($container) { return new CallableMiddlewareWrapper( new Cors([ "origin" => ["*"], "methods" => ["GET", "POST", "PUT", "PATCH", "DELETE"], "headers.allow" => ["Content-Type", "Accept"], "headers.expose" => [], "credentials" => false, "cache" => 0, ]), new Response() ); } }
We then register this in our App\ConfigProvider::getDependencies() by adding to the factories key so that it looks something like this:
'factories' => [ Action\HomePageAction::class => Action\HomePageFactory::class, \Tuupola\Middleware\Cors::class => Factory\CorsMiddlewareFactory::class, ],
If you don’t want to fully qualify, add use Tuupola\Middleware\Cors; to the top of the file so that you can just use Cors::class here.
Lastly, we register the middleware in config/pipeline.php:
$app->pipe(\Tuupola\Middleware\Cors::class);
Place it somewhere near the top of the list; personally, I place it just after piping the ServerUrlMiddleware::class.
We now have working CORS:
$ curl -X OPTIONS -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: Accept, Content-Type" \ -H "Origin: http://localhost" -H "Accept: application/json" http://localhost:8890/ HTTP/1.1 200 OK Host: localhost:8890 Connection: close Access-Control-Allow-Origin: http://localhost Vary: Origin Access-Control-Allow-Headers: content-type, accept Content-type: text/html; charset=UTF-8
Failure looks like this:
$ curl -X OPTIONS -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: Accept, Content-Type X-Clacks-Overhead" \ -H "Origin: http://localhost" -H "Accept: application/json" http://localhost:8890/ HTTP/1.1 401 Unauthorized Host: localhost:8890 Connection: close Content-type: text/html; charset=UTF-8
By default, it doesn’t tell you what went wrong, which isn’t too helpful.
Providing JSON error responses
To provide a JSON error response, you need to set the error option to a callable and you can then return a JsonResponse:
App/Factory/CorsMiddleware.php:
<?php declare(strict_types=1); namespace App\Factory; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Tuupola\Middleware\Cors; use Zend\Diactoros\Response; use Zend\Diactoros\Response\JsonResponse; use Zend\Stratigility\Middleware\CallableMiddlewareWrapper; class CorsMiddlewareFactory { public function __invoke($container) { return new CallableMiddlewareWrapper( new Cors([ "origin" => ["*"], "methods" => ["GET", "POST", "PUT", "PATCH", "DELETE"], "headers.allow" => ["Content-Type", "Accept"], "headers.expose" => [], "credentials" => false, "cache" => 0, "error" => [$this, 'error'], ]), new Response() ); } public static function error( RequestInterface $request, ResponseInterface $response, $arguments) { return new JsonResponse($arguments); } }
As you can see, we’ve created a new error method that return a JsonResponse. We simply encode the $arguments as that contains the information about what the problem is:
$ curl -X OPTIONS -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: Accept, Content-Type X-Clacks-Overhead" \ -H "Origin: http://localhost" -H "Accept: application/json" http://localhost:8890/ HTTP/1.1 401 Unauthorized Host: localhost:8890 Date: Sat, 21 Oct 2017 10:41:18 +0000 Connection: close X-Powered-By: PHP/7.1.8 Content-Type: application/json {"message":"CORS requested header is not allowed."}
Responding with ProblemDetails
I’m a huge fan of RFC 7807, so prefer my error response to conform to this. The Problem Details component handles the creation of the correct Responses, so to use it with Cors, we change our error() method to this:
public static function error( RequestInterface $request, ResponseInterface $response, $arguments) { return self::$problemDetailsResponseFactory->createResponse( $request, 401, '', $arguments['message'], '', [] ); }
As error() is called statically, we need to define a static property $problemDetailsResponseFactory in the class to hold our ProblemDetailsResponseFactory when we grab it from the container in the __invoke() method:
self::$problemDetailsResponseFactory = $container->get(ProblemDetailsResponseFactory::class);
You’ll need a use Zend\ProblemDetails\ProblemDetailsResponseFactory; at the top too. The entire class can be seen in this gist.
Fin
It turns out that adding CORS support to an Expressive app is easy enough, mainly because Mike Tuupola has done most of the work for us!
from Rob Allen’s DevNotes http://ift.tt/2ALRGvi
via IFTTT