by Matthew Weier O'Phinney
Principal Engineer
Rogue Wave Software, Inc.
ZendCon, Las Vegas, 24 Oct 2017
public function handle(
ServerRequestInterface $request
) : ResponseInterface
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $delegate
) : ResponseInterface
// RequestHandlerInterface
$response = $delegate->handle($request);
// DelegateInterface:
$response = $delegate->process($request);
public function process(ServerRequest $request, RequestHandler $delegate)
{
$item = $this->repository->fetchById(
$request->getAttribute('id')
);
return new JsonResponse($item);
}
$ composer require middlewares/access-log
use Middlewares\AccessLog;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
function (ContainerInterface $container) : AccessLog
{
$middleware = new AccessLog(
$container->get(LoggerInterface::class)
);
return $middleware;
}
$ composer require middlewares/https
use Middlewares\Https;
use Psr\Container\ContainerInterface;
function (ContainerInterface $container) : Https
{
$middleware = new Https();
$middleware->checkHttpsForward(true);
return $middleware;
}
$ composer require middlewares/trailing-slash
use Middlewares\TrailingSlash;
use Psr\Container\ContainerInterface;
function (ContainerInterface $container) : TrailingSlash
{
$middleware = new TrailingSlash();
$middleware->redirect(true);
return $middleware;
}
$ composer require middlewares/encoder
use Middlewares\GzipEncoder;
use Psr\Container\ContainerInterface;
function (ContainerInterface $container) : GzipEncoder
{
$middleware = new GzipEncoder();
return $middleware;
}
$ composer require middlewares/csp
{
"img-src": {
"self": true,
"data": true
},
"script-src": {
"allow": [ "https://www.google-analytics.com" ],
"self": true,
"unsafe-inline": false,
"unsafe-eval": false
}
}
use Middlewares\Csp;
use ParagonIE\CSPBuilder\CSPBuilder;
use Psr\Container\ContainerInterface;
function (ContainerInterface $container) : Csp
{
$config = $container->get('config');
$csp = CSPBuilder::fromFile($config['csp-file']);
$middleware = new Csp($csp);
return $middleware;
}
$ composer require bairwell/middleware-cors
// In a config file somewhere...
return [
'cors' => [
'origin' => ['*.example.com', 'example.com'],
'allowCredentials' => true,
'allowHeaders' => ['Accept', /* ... */],
],
];
$allowedMethods = function (ServerRequestInterface $request) {
$routeResult = $request->getAttribute(RouteResult::class);
$route = $routeResult->getMatchedRoute();
return $route->getAllowedMethods();
};
use Bairwell\MiddlewareCors;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Container\ContainerInterface;
function (ContainerInterface $container) : MiddlewareCors use ($allowedMethods)
{
$config = $container->get('config')['cors'] ?? [];
$config['allowedMethods'] = $allowedMethods;
$middleware = new MiddlewareCors($config);
return $middleware;
}
public function process(ServerRequest $request, RequestHandler $delegate)
{
$id = $request->getAttribute('id', false);
if (false === $id) {
throw new MissingBookIdentifierException();
}
$user = $request->getAttribute('user');
$book = $this->repository->fetch($id, $user);
$resource = get_object_vars($user);
$resource['_links']['self']['href'] = (string) $request->getUri();
return new JsonResponse($resource);
}
$ composer require zendframework/zend-problem-details
use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait;
class MissingBookIdentifierException
extends \DomainException
implements ProblemDetailsExceptionInterfaces
{
use CommonProblemDetailsExceptionTrait;
public function __construct()
{
parent::__construct('Missing book identifier');
$this->status = 400;
$this->detail = 'Your request was missing a book identifier value';
$this->type = '/api/error/missing-book-identifier';
$this->title = 'Missing Book Identifier';
}
}
$ composer require prezto/rate-limit
// In a config file:
return [
'rate-limit' => [
'server' => '10.0.242.42',
'port' => 6379,
'password' => getenv('REDIS_PASSWORD')
],
];
use Prezto\RateLimit\RateLimitMiddleware;
use Psr\Container\ContainerInterface;
function (ContainerInterface $container) : RateLimitMiddleware
{
$config = $container->get('config')['rate-limit'] ?? [];
$middleware = new RateLimitMiddleware(
$config['server'] ?? 'localhost',
$config['port'] ?? 6379,
$config['password'] ?? ''
);
return $middleware;
}
$ composer require slim/http-cache
use Psr\Container\ContainerInterface;
use Slim\HttpCache\Cache;
function (ContainerInterface $container) : Cache
{
$middleware = new Cache(
'public',
$ttl = 86400,
$revalidate = false
);
return $middleware;
}
public function process(ServerRequest $request, RequestHandler $delegate)
{
$id = $request->getAttribute('id');
$values = $request->getParsedBody();
$book = new Book($id, $values);
$user = $request->getAttribute('user');
$book = $this->repository->update($book, $user);
$resource = get_object_vars($user);
$resource['_links']['self']['href'] = (string) $request->getUri();
return new JsonResponse($resource);
}
$ composer require middlewares/method-override
use Middlewares\MethodOverride;
use Psr\Container\ContainerInterface;
function (ContainerInterface $container) : MethodOverride
{
$middleware = new MethodOverride();
$middleware->get(['HEAD', 'OPTIONS']);
$middleware->post(['PATCH', 'PUT', 'DELETE']);
return $middleware;
}
$ composer require middlewares/http-authentication
// In a config file... maybe!
return [
'authentication' => [
'users' => [
'samvimes' => 'thewatch',
'ridcully' => 'unseenuniversity',
],
],
];
use Middlewares\BasicAuthentication;
use Psr\Container\ContainerInterface;
function (ContainerInterface $container) : BasicAuthentication
{
$config = $container->get('config')['authentication']['users'] ?? [];
$middleware = new BasicAuthentication($config);
// Request attribute in which to store user:
$middleware->attribute('user');
return $middleware;
}
$ composer require zendframework/zend-expressive-helpers
use Psr\Container\ContainerInterface;
use Zend\Expressive\Helpers\BodyParams\BodyParamsMiddleware;
function (ContainerInterface $container) : BodyParamsMiddleware
{
return new BodyParamsMiddleware();
}
$ composer require respect/validation
use Psr\Container\ContainerInterface;
use Respect\Validation\Validator as v;
function (ContainerInterface $container) : v[]
{
$title = v::stringType()->notEmpty();
$author = v::stringType()->notEmpty();
return [
'title' => $title,
'author' => $author,
];
}
use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait;
class InvalidDataException
extends \DomainException
implements ProblemDetailsExceptionInterfaces {
use CommonProblemDetailsExceptionTrait;
public static function fromAssertion($prev) {
$e = new self($prev->getMessage(), $prev->getCode(), $prev);
$e->status = 422;
$e->detail = 'One or more data elements submitted were invalid';
$e->type = '/api/error/invalid-data';
$e->title = 'Invalid Data';
$e->additional = ['messages' => $prev->getMessage()];
return $e;
}
}
class ValidationMiddleware implements Middleware {
private $validators;
public function __construct(array $validators) {
$this->validators = $validators;
}
public function process(Request $request, Delegator $delegator) {
$data = $request->getParsedBody();
foreach ($this->v as $key => $validator) {
try {
$validator->assert($data[$key] ?? null);
} catch (\Throwable $e) {
throw InvalidDataException::fromAssertion($e);
}
}
return $delegator->process($request);
}
}
use Psr\Container\ContainerInterface;
function (ContainerInterface $container)
{
$validators = $container->get('BookValidators');
$middleware = new ValidationMiddleware($validators);
return $middleware;
}
// config/pipeline.php
$app->pipe(\Middlewares\AccessLog::class);
$app->pipe(\Middlewares\Https::class);
$app->pipe(\Middlewares\TrailingSlash::class);
$app->pipe(\Middlewares\GzipEncoder::class);
$app->pipe(\Middlewares\Csp::class);
// For our updatable resource:
$app->pipe(\Middlewares\MethodOverride::class);
// CORS is dependent on routing...
$app->pipeRoutingMiddleware();
$app->pipe(\Bairwell\MiddlewareCors::class);
// config/routes.php
$app->get('/api/books/{id:\d+}', [
\Zend\ProblemDetails\ProblemDetailsMiddleware::class,
\Prezto\RateLimit\RateLimitMiddleware::class,
\Slim\HttpCache\Cache::class,
FetchBookHandler::class,
], 'book');
// config/routes.php
$app->patch('/api/books/{id:\d+}', [
\Zend\ProblemDetails\ProblemDetailsMiddleware::class,
\Prezto\RateLimit\RateLimitMiddleware::class,
\Middlewares\BasicAuthentication::class,
\Zend\Expressive\Helper\BodyParams\BodyParamsMiddleware::class,
ValidationMiddleware::class,
UpdateBookHandler::class,
]);
Or find them all at http://linkbun.ch/05dz0
Contact me: matthew [at] zend.com
Follow me: @mwop