by Matthew Weier O'Phinney
Principal Engineer
Rogue Wave Software, Inc.
namespace Psr\Http\Server;
use Psr\Http\Message;
interface RequestHandlerInterface
{
public function handle(
Message\ServerRequestInterface $request
) : Message\ResponseInterface
}
namespace Psr\Http\Server;
use Psr\Http\Message;
interface MiddlewareInterface
{
public function process(
Message\ServerRequestInterface $request,
RequestHandlerInterface $handler
) : Message\ResponseInterface
}
// RequestHandlerInterface
$response = $handler->handle($request);
public function process(ServerRequest $request, RequestHandler $handler)
{
$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
{
return new GzipEncoder();
}
$ 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 tuupola/cors-middleware
// In a config file somewhere...
return [
'cors' => [
'origin' => ['*.example.com', 'example.com'],
'methods' => 'Api\introspectAllowedMethods'
'credentials' => true,
'headers.allow' => ['Accept', /* ... */],
],
];
namespace Api;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Expressive\Router\RouteResult;
function introspectAllowedMethods(ServerRequestInterface $request)
{
$routeResult = $request->getAttribute(RouteResult::class);
return $routeResult->getAllowedMethods();
}
use Tuupola\Middleware\CorsMiddleware;
use Psr\Container\ContainerInterface;
function (ContainerInterface $container) : CorsMiddleware
{
$config = $container->get('config')['cors'] ?? [];
$middleware = new CorsMiddleware($config);
return $middleware;
}
use Zend\Expressive\Authentication\UserInterface;
public function handle(ServerRequestInterface $request)
{
$id = $request->getAttribute('id', false);
if (false === $id) {
throw MissingBookIdentifierException::create();
}
$user = $request->getAttribute(UserInterface::class);
$book = $this->repository->fetch($id, $user->getIdentity());
$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 ProblemDetailsExceptionInterface {
use CommonProblemDetailsExceptionTrait;
public static function create() : self {
$e = new self('Missing book identifier');
$e->status = 400;
$e->detail = 'Your request was missing a book identifier value';
$e->type = '/api/error/missing-book-identifier';
$e->title = 'Missing Book Identifier';
return $e;
}
}
use Psr\Container\ContainerInterface;
use Zend\ProblemDetails\ProblemDetailsMiddleware;
use Zend\ProblemDetails\ProblemDetailsResponseFactory;
function (ContainerInterface $container) : ProblemDetailsMiddleware
{
$middleware = new ProblemDetailsMiddleware(
$container->get(ProblemDetailsResponseFactory::class)
);
return $middleware;
}
$ composer require zendframework/zend-expressive-authentication
$ composer require zendframework/zend-expressive-authentication-basic
https://docs.zendframework.com/zend-expressive-authentication
// In a config file:
return [
'authentication' => [
'htpasswd' => 'path/to/htpasswd',
],
];
# In data/htpasswd:
matthew:$2y$10$zUW9br6jbkMz.6vpn0sAQeH0Jx74jKjyVmb83eQrFZ2mz4GzgNjPu
use Psr\Container\ContainerInterface;
use Zend\Expressive\Authentication\AuthenticationMiddleware;
use Zend\Expressive\Authentication\Basic\BasicAccess;
use Zend\Expressive\Authentication\UserRepository\Htpasswd;
use Zend\ProblemDetails\ProblemDetailsResponseFactory;
function (ContainerInterface $container) : BasicAuthentication {
$config = $container->get('config')['authentication'] ?? [];
$repository = new Htpasswd($config['htpasswd'] ?? 'data/htpasswd');
$adapter = new BasicAccess(
$repository,
'api',
$container->get(ProblemDetailsResponseFactory::class)
);
$middleware = new AuthenticationMiddleware($adapter);
return $middleware;
}
$ composer require los/los-rate-limit
// In a config file:
return [
'los-rate-limit' => [
'max_requests' => '100',
'reset_time' => '3600',
'api_header' => 'X-Api-Key',
'headers' => [
'limit' => 'X-RateLimit-Limit',
'remaining' => 'X-RateLimit-Remaining',
'reset' => 'X-RateLimit-Reset',
],
],
];
use LosMiddleware\RateLimit\RateLimitMiddleware;
use LosMiddleware\RateLimit\Storage\FileStorage;
use Psr\Container\ContainerInterface;
use Zend\ProblemDetails\ProblemDetailsResponseFactory;
function (ContainerInterface $container) : RateLimitMiddleware
{
$config = $container->get('config')['los-rate-limit'] ?? [];
$storageFile = $config['storage_file'] ?? 'data/rate-limits.json';
$middleware = new RateLimitMiddleware(
new FileStorage($storageFile),
$container->get(ProblemDetailsResponseFactory()),
$config
);
return $middleware;
}
$ composer require slim/http-cache
use Psr\Container\ContainerInterface;
use Slim\HttpCache\Cache;
use Zend\Diactoros\Response;
use Zend\Stratigility\doublePassMiddleware;
function (ContainerInterface $container) : Cache
{
$middleware = new Cache(
'public',
$ttl = 86400,
$revalidate = false
);
return doublePassMiddleware($middleware, new Response());
}
use Zend\Expressive\Authentication\UserInterface;
public function handle(ServerRequestInterface $request)
{
$id = $request->getAttribute('id');
$values = $request->getParsedBody();
$book = new Book($id, $values);
$user = $request->getAttribute(UserInterface::class);
$book = $this->repository->update($book, $user->getIdentity());
$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 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 ProblemDetailsExceptionInterface {
use CommonProblemDetailsExceptionTrait;
public static function fromAssertion($prev) : self {
$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' => $e->getMessage()];
return $e;
}
}
class ValidationMiddleware implements Middleware {
private $validators;
public function __construct(array $validators) {
$this->validators = $validators;
}
public function process(Request $request, RequestHandler $handler) {
$data = $request->getParsedBody();
foreach ($this->validators as $key => $validator) {
try {
$validator->assert($data[$key] ?? null);
} catch (\Throwable $e) {
throw InvalidDataException::fromAssertion($e);
}
}
return $handler->handle($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);
$app->pipe(\Zend\Expressive\Router\Middleware\RouteMiddleware::class);
// CORS is dependent on routing...
$app->pipe(\Tuupola\Middleware\CorsMiddleware::class);
$app->pipe(\Zend\Expressive\Router\Middleware\DispatchMiddleware::class);
$app->pipe(\Zend\Expressive\Handler\NotFoundHandler::class);
// config/routes.php
$app->get('/api/books/{id:\d+}', [
\Zend\ProblemDetails\ProblemDetailsMiddleware::class,
\Zend\Expressive\Authentication\AuthenticationMiddleware::class,
\LosMiddleware\RateLimit\RateLimitMiddleware::class,
\Slim\HttpCache\Cache::class,
FetchBookHandler::class,
], 'book');
// config/routes.php
$app->patch('/api/books/{id:\d+}', [
\Zend\ProblemDetails\ProblemDetailsMiddleware::class,
\Zend\Expressive\Authentication\AuthenticationMiddleware::class,
\LosMiddleware\RateLimit\RateLimitMiddleware::class,
\Zend\Expressive\Helper\BodyParams\BodyParamsMiddleware::class,
ValidationMiddleware::class,
UpdateBookHandler::class,
]);
Or find them all at http://linkbun.ch/07afq
Contact me: matthew [at] zend.com
Follow me: @mwop