Introdução
Neste post vou falar um pouco sobre Middleware, Zend Stratigility, Zend Expressive e dois exemplos reais de middleware que fiz: LosRateLimit e LosLog.
Middleware
Muito tem se falado ultimamente sobre Middleware, mas o que é?
Middleware, no mundo do PHP, nada mais é que um código que recebe uma requisição HTTP, uma resposta HTTP, um callable e faz “alguma coisa” com eles. Os middlewares PHP ganharam força com a aprovação do PSR-7, onde temos requisições e respostas http padronizadas e cada vez mais frameworks estão se adequando.
Como sou uma pessoa que entende melhor as coisas através de exemplos, vamos ver um middleware bem simples que apenas adiciona um cabeçalho à requisição:
class CabecalhoAction { public function __invoke($request, $response, callable $next) { $response = $response->withHeader('X-Meu-Header', 'teste'); if ($next) { return $next($request, $response); } return $response; } }
Primeiro pegamos a resposta e adicionamos um novo header a ela. Em seguida, chamamos o próximo middleware, se existir, ou retornamos a resposta.
Uma grande vantagem dos middlewares, é que eles tendem a ser “framework-agnostic”, ou seja, independem de framework. A medida que as frameworks seguem mais os padrões, como o PSR-7, mais fácil fica para usarmos componentes middleware em qualquer uma. O exemplo acima poderia ser usado em qualquer framework php que aceite PSR-7.
Mas o que fazer com isso? Onde isso se encaixa na minha aplicação?
Um middleware deve seguir o princípio “Faça apenas uma coisa e faça bem”. Então na verdade uma aplicação seria um conjunto de middlewares trabalhando juntos. Por exemplo:
$app = new MiddlewareRunner(); $app->add('/contact', new ContactFormMiddleware()); $app->add('/forum', new ForumMiddleware()); $app->add('/blog', new BlogMiddleware()); $app->add('/store', new EcommerceMiddleware()); $app->run($request, $response);
Este seria um exemplo de um site usando middlewares, onde cada middleware fica responsável por uma “rota”. Poderíamos acrescentar um middleware de autorização que roda em primeiro lugar:
$app = new MiddlewareRunner(); $app->add(new AuthorizationMiddleware()); $app->add('/contact', new ContactFormMiddleware()); $app->add('/forum', new ForumMiddleware()); $app->add('/blog', new BlogMiddleware()); $app->add('/store', new EcommerceMiddleware()); $app->run($request, $response);
Mas para montarmos uma aplicação completa, precisamos de mais algumas coisas, e aí entram o Zend Stratigility e Zend Expressive!
Zend Stratigility
O Zend Stratigility é uma biblioteca que permite criar aplicações usando middlewares.
Vamos ver uma aplicação bem simples usando o zend-stratigility:
use Zend\Stratigility\MiddlewarePipe; use Zend\Diactoros\Server; require __DIR__ . '/../vendor/autoload.php'; $app = new MiddlewarePipe(); $server = Server::createServer($app, $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); // Landing page $app->pipe('/', function ($req, $res, $next) { if (! in_array($req->getUri()->getPath(), ['/', ''], true)) { return $next($req, $res); } return $res->end('Hello world!'); }); // Another page $app->pipe('/foo', function ($req, $res, $next) { return $res->end('FOO!'); }); $server->listen();
Neste exemplo, temos 2 middlewares.
O primeiro, escuta o caminho raiz. Se a url for “/” ou vazia, ele escreve “Hello world!” na tela e sai. Se a url for diferente, ele chama o próximo middleware da fila.
O segundo escuta a rota “/foo” e escreve “FOO!” na tela.
Bem simples não é? Vamos reescrever o exemplo das rotas mostrado anteriormente no zend-stratigility:
use Zend\Stratigility\MiddlewarePipe; use Zend\Diactoros\Server; require __DIR__ . '/../vendor/autoload.php'; $app = new MiddlewarePipe(); $server = Server::createServer($app, $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); $app->pipe('/contact', new ContactFormMiddleware()); $app->pipe('/forum', new ForumMiddleware()); $app->pipe('/blog', new BlogMiddleware()); $app->pipe('/store', new EcommerceMiddleware()); $server->listen();
Normalmente uma aplicação “real” não é tão simples, temos rotas com parâmetros (alguns opcionais), banco de dados, templates, etc. O Zend Expressive entra para nos ajudar nesta junção.
Zend Expressive
O Zend Expressive é uma microframework PHP baseada no Zend Statigility. Ela te dá liberdade de escolher os componentes que você quiser usar, mas já trás algumas opções.
O Zend Framework 3, deve usar o Zend Expressive internamente para tratar os middlewares.
Vamos iniciar criando um novo projeto:
composer create-project -s rc zendframework/zend-expressive-skeleton tutorial-expressive Installing zendframework/zend-expressive-skeleton (1.0.0rc2) - Installing zendframework/zend-expressive-skeleton (1.0.0rc2) Loading from cache Created project in tutorial-expressive > App\Composer\OptionalPackages::install Setup data and cache dir Setting up optional packages Which router you want to use? [1] Aura.Router [2] FastRoute [3] Zend Router Make your selection or type a composer package name and version (FastRoute): - Adding package zendframework/zend-expressive-fastroute (^0.2) - Copying /config/autoload/routes.global.php Which container you want to use for dependency injection? [1] Aura.Di [2] Pimple-interop [3] Zend ServiceManager Make your selection or type a composer package name and version (Zend ServiceManager): - Adding package zendframework/zend-servicemanager (^2.5) - Adding package ocramius/proxy-manager (^1.0) - Copying /config/container.php Which template engine you want to use? [1] Plates [2] Twig [3] Zend View installs Zend ServiceManager [n] None of the above Make your selection or type a composer package name and version (n): 3 - Adding package zendframework/zend-expressive-zendviewrenderer (^0.2) - Copying /config/autoload/templates.global.php - Copying /templates/error/404.phtml - Copying /templates/error/error.phtml - Copying /templates/layout/default.phtml - Copying /templates/app/home-page.phtml Which error handler do you want to use during development? [1] Whoops [n] None of the above Make your selection or type a composer package name and version (Whoops): - Adding package filp/whoops (^1.1) - Copying /config/autoload/errorhandler.local.php Remove installer Removing Expressive installer classes and configuration Loading composer repositories with package information Installing dependencies (including require-dev) ... Writing lock file Generating autoload files
Em seguida, vamos iniciar um servidor php pelo console na nossa aplicação:
cd tutorial-expressive php -S 0.0.0.0:8080 -t public/
E abrir no nosso navegador o endereço: http://localhost:8080
Vamos clicar no link “Ping Test” na barra superior e ver o retorno:
{ "ack": 1446412873 }
Vamos analisar como isso funciona internamente?
Olhando o arquivo config/autoload/routes.global.php:
<?php return [ 'dependencies' => [ 'invokables' => [ Zend\Expressive\Router\RouterInterface::class => Zend\Expressive\Router\FastRouteRouter::class, ], ], 'routes' => [ [ 'name' => 'home', 'path' => '/', 'middleware' => App\Action\HomePageAction::class, 'allowed_methods' => ['GET'], ], [ 'name' => 'api.ping', 'path' => '/api/ping', 'middleware' => App\Action\PingAction::class, 'allowed_methods' => ['GET'], ], ], ];
Nos diz que para a rota /api/ping será executado o middleware PingAction:
<?php namespace App\Action; use Zend\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; class PingAction { public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null) { return new JsonResponse(['ack' => time()]); } }
Que retorna apenas um json com o ack. Muito fácil, rápido e prático.
Como gera a Home? Primeiro vamos ver o src/Action/HomePageFactory.php:
<?php namespace App\Action; use Interop\Container\ContainerInterface; use Zend\Expressive\Router\RouterInterface; use Zend\Expressive\Template\TemplateRendererInterface; class HomePageFactory { public function __invoke(ContainerInterface $container) { $router = $container->get(RouterInterface::class); $template = ($container->has(TemplateRendererInterface::class)) ? $container->get(TemplateRendererInterface::class) : null; return new HomePageAction($router, $template); } }
Como por padrão podemos escolher qual roteador e templates usar, ele busca as configurações e cria o Middleware HomePageAction em si ( que mostro aqui de forma simplificada:
<?php namespace App\Action; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Zend\Diactoros\Response\HtmlResponse; use Zend\Diactoros\Response\JsonResponse; use Zend\Expressive\Router; use Zend\Expressive\Template; class HomePageAction { private $router; private $template; public function __construct(Router\RouterInterface $router, Template\TemplateRendererInterface $template = null) { $this->router = $router; $this->template = $template; } public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null) { $data = []; $data['routerName'] = 'FastRoute'; $data['routerDocs'] = 'https://github.com/nikic/FastRoute'; $data['templateName'] = 'Zend View'; $data['templateDocs'] = 'http://framework.zend.com/manual/current/en/modules/zend.view.quick-start.html'; return new HtmlResponse($this->template->render('app::home-page', $data)); } }
Se você for fazer um site ou sistema pequenos ou uma api simples, o Zend Expressive é uma ótima pedida.
LosRateLimit
Como exemplo real de um middleware, eu precisei escrever um limitador de acesso para uma API e, como ela foi escrita usando o Zend Expressive, criei o limitador como middleware e também o uso num projeto em ZF2:
https://github.com/Lansoweb/LosRateLimit
Ele é chamado no começo da requisição e controla quantos acessos foram feitos pelo usuário num espaço de tempo, se exceder um limite configurado, o acesso é negado retornando um erro 429.
O código principal do middleware está em aqui e o middleware mantém a filosofia que citei acima: Ele é responsável apenas por controlar a quantidade de acessos por tempo e bloquear se necessário, não importando o que veio antes nem o que será executado depois.
LosLog
Também atualizei o LosLog para a versão 2.0, adicionando um middleware e várias mudanças internas.
Pode conferir o módulo no github aqui.
Até a próxima!
Leandro Silva
Mais um ótimo artigo! Valew Leandro!
Obrigado!
Olá Leandro, tudo bem?
Tenho uma dúvida…
No caso…
$app = new MiddlewareRunner();
$app->add(new AuthorizationMiddleware());
$app->add(‘/contact’, new ContactFormMiddleware());
$app->add(‘/forum’, new ForumMiddleware());
$app->add(‘/blog’, new BlogMiddleware());
$app->add(‘/store’, new EcommerceMiddleware());
$app->run($request, $response);
o blog poderia ser um wordpress,
O store ser um magento
e por ai vai…
Olá Sergio! Isso mesmo, mas teria que criar um middleware para fazer o meio de campo com cada um deles.