Middleware e Zend Expressive

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

expressive1

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

4 comentários sobre “Middleware e Zend Expressive

  1. Sérgio disse:

    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…

Deixe uma resposta

This site uses Akismet to reduce spam. Learn how your comment data is processed.