Middlewares no PHP – PHP sem framework – parte 5

Middlewares no PHP
Tempo de leitura 6 minutos

Middleware é um nome complicado e que assusta quem não sabe o quão simples e útil é, mas eu prometo que ao fim deste artigo você vai dominar esse carinha.

O foco do middleware no contexto deste artigo (desenvolver aplicações web com PHP) é fornecer camadas que vão lidar com a entrada e saída das requisições HTTP, ou seja, request e response. Middlewares apenas interceptam e executam ações ANTES ou DEPOIS da ação principal (a action de um controller, por exemplo).

O middleware facilita aos desenvolvedores de software implementarem comunicação de entrada/saída, de forma que eles possam focar no propósito específico de sua aplicação. Ele ganhou popularidade nos anos 80 como uma solução para o problema de como ligar aplicações mais novas a sistemas legados, apesar do termo ser usado desde 1968.

https://pt.wikipedia.org/wiki/Middleware

Não se esqueça de assinar a newsletter para saber quando novos artigos serão lançados.

Na prática, imagine um array de functions/closures que retornam true ou false, se retornar true ele passa pro próximo, se der false ele para por ali mesmo (mas claro que existem outras formas de se aplicar middlewares, eu não sou o mestre universal do mundo …. por enquanto).

Exemplo deuma pilha de execução de város middlewares

E é isso, bem simples, middlewares simplemente executam ações antes e/ou depois da ação principal, isso no contexto que estamos trabalhando. Note que na imagem anterior, a ação principal é muito parecida com um middleware, na verdade uma action de um controller não passa de um middleware com “super poderes”.

Eu digo que um middleware precisa ter sua razão de existência diretamente ligado a requisição/resposta, autenticação é um exemplo claro disso.

É bem comum falarmos sobre empilhamento de middlewares, isso quer dizer que ao ser executado, podemos dizer se um middleware vai passar para o próximo ou parar tudo ali mesmo.

Um exemplo legal é o processo de autenticação ou de aceitar os termos de uso de um aplicativo/site, se não der certo (na autenticação ou o usuáro não aceitar os termos), simplesmente bloqueamos o acesso ou redirecionamos para outro lugar, então também podemos ditar se vamos continuar ou não com a execução da pilha de middlewares, o que PODE inclui o controller e action, obviamente.

Middlewares com PHP na prática

Antes de continuar a leitura, este artigo é parte da série de artigos PHP sem Framework, então já existe um projeto de exemplo em andamento que aborda diferentes camadas de um projeto web, se você não quiser ler os demais artigos baixe os arquivos a seguir para você dar sequência no tutorial com mais facilidade.

https://github.com/erikfig/php-do-zero/tree/c1a6ca23952e0459746bc8dc12c2df5c1a9c3097

Como toda esta série, eu não quero ditar como fazer um “motor de middlewares” de forma definitiva, o propósito é entendermos como as coisas são e faciltar o entendimento em outros frameworks.

Com isso em mente, pra mim foi bem claro como eu ia implementar middlewares e eu vou listar alguns pontos que gostaria de abordar

  • Armazenar a lista de ações a serem executadas antes e depois da ação principal
  • Dar ao middleware o poder de decidir se ele quer que paremos nele ou se vamos continuar para o próximo.
  • Executar os middlewares antes e depois da ação principal (caso existam middlewares registrados).
  • Executar os middlewares em ordem controlada

A parte de executar em ordem controlada é bem simples, um array listando os middlewares e um loop já resolvem, já que fazem exatamente isso (executam o array na ordem que registramos os itens nele, bem óbvio).

O que eu fiz foi instanciar uma entidade para cada rota. Uma entidade nada mais é que uma classe que armazena informações em atributos, veremos um exemplo logo a seguir.

Dentro do diretório src na raiz do projeto, criei um arquivo chamado RouteEntity.php com o seguinte conteúdo:

<?php

namespace ErikFig\Framework;

class RouteEntity
{
    private $action;
    private $afterMiddleware = [];
    private $beforeMiddleware = [];

    // a ação principal
    public function __construct($action)
    {
        $this->action = $action;
    }

    // adicionar middlewares antes da ação principal
    public function before($middleware)
    {
        $this->beforeMiddleware[] = $middleware;
        return $this;
    }

    // adicionar middlewares depois da ação principal
    public function after($middleware)
    {
        $this->afterMiddleware[] = $middleware;
        return $this;
    }

    // retorna todas as ações a serem executadas
    public function getData()
    {
        return [
            'action' => $this->action,
            'after' => $this->afterMiddleware,
            'before' => $this->beforeMiddleware,
        ];
    }
}

O grande truque aqui são as linhas com return $this, elas vão retornar a própria instância da classe atual e assim poderemos rodar o after e before nos próprios métodos, seria exatamente isto:

$route = ErikFig\Framework\RouteEntity($action);
$route->before($middleware1)
    ->before($middleware2)
    ->after($middleware3);

Isso de chamar um método depois do outro é o que chamamos de interface fluente.

Também vou precisar alterar o Router.php, que agora terá um return no add(), get() e post(). Também vou criar uma instância de RouteEntity e passar o parâmetro $action.

<?php

namespace ErikFig\Framework;

class Router
{
    private $routes = [];
    private $method;
    private $path;
    private $params;

    public function __construct($method, $path)
    {
        $this->method = $method;
        $this->path = $path;
    }

    public function get(string $route, $action)
    {
        return $this->add('GET', $route, $action); // adicionei o return
    }

    public function post(string $route, $action)
    {
        return $this->add('POST', $route, $action); // adicionei o return
    }

    public function add(string $method, string $route, $action)
    {
        $this->routes[$method][$route] = new RouteEntity($action); // usei nossa nova classe
        return $this->routes[$method][$route]; // adicionei o return
    }

    public function getParams()
    {
        return $this->params;
    }

    public function handler()
    {
        if (empty($this->routes[$this->method])) {
            return false;
        }

        if (isset($this->routes[$this->method][$this->path])) {
            return $this->routes[$this->method][$this->path];
        }

        foreach ($this->routes[$this->method] as $route => $action) {
            $result = $this->checkUrl($route, $this->path);
            if ($result >= 1) {
                return $action;
            }
        }

        return false;
    }

    private function checkUrl(string $route, $path)
    {
        preg_match_all('/\{([^\}]*)\}/', $route, $variables);

        $regex = str_replace('/', '\/', $route);

        foreach ($variables[0] as $k => $variable) {
            $replacement = '([a-zA-Z0-9\-\_\ ]+)';
            $regex = str_replace($variable, $replacement, $regex);
        }

        $regex = preg_replace('/{([a-zA-Z]+)}/', '([a-zA-Z0-9+])', $regex);
        $result = preg_match('/^' . $regex . '$/', $path, $params);
        $this->params = $params;

        return $result;
    }
}

Quebrando o SOLID?

Eu fiquei meio preocupado com o RouteEntity diretamente no método, isso quebra o D (Dependency Inversion Principle) do SOLID. Eu poderia criar uma interface e injetar diretamente no construtor, mas uma coisa que eu aprendi nesses anos de programação é que até boas práticas podem ser ruins as vezes, então eu vou deixar para fazer essa melhoria quando ela realmente fizer sentido, ou seja, eu ter outra classe que vá ter a mesma assinatura da RouteEntity, mas com comportamento diferente.

Ainda vou escrever artigos sobre SOLID e quando quebrá-lo, mas agora não é a hora.

Erik Figueiredo

Continuando com o middleware

E por último, para ficar realmente funcional, vou alterar o bootstrap.php:

<?php

require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/database.php';
$twig = require(__DIR__ . '/renderer.php');

$method = $_SERVER['REQUEST_METHOD'];
$path = $_SERVER['PATH_INFO'] ?? '/';

$router = new ErikFig\Framework\Router($method, $path);

$router->get('/', function () {
    return 'Olá mundo';
});

ErikFig\Framework\Users\Register::handle($twig, $router);

// faço o router encontrar a rota que o usuário acessou
$result = $router->handler();

if (!$result) {
    http_response_code(404);
    echo 'Página não encontrada!';
    die();
}

// pego os dados da entidade
$data = $result->getData();

// rodo os middlewares before
foreach ($data['before'] as $before) {
    // rodo o middleware
    if (!$before($router->getParams())) {
        // se retornar false eu paro a execução do código
        die();
    }
}

// rodo a ação principal
if ($data['action'] instanceof Closure) {
    echo $data['action']($router->getParams());
} elseif (is_string($data['action'])) {
    $data['action'] = explode('::', $data['action']);

    $controller = new $data['action'][0]($twig);
    $action = $data['action'][1];

    echo $controller->$action($router->getParams());
}

// rodo os middlewares after
foreach ($data['after'] as $after) {
    // rodo o middleware
    if (!$after($router->getParams())) {
        // se retornar false eu paro a execução do código
        die();
    }
}

Eu comentei as linhas que fiz alteração, se ainda tiver dúvidas, use os comentários no fim deste artigo.

E finalmente vamos criar middlewares.

Abra o arquivo module/users/src/Register.php e simplesmente adicione os métodos before() e after() após a rota. Você deve retornar true para informar que NÃO é para parar nesse middleware e false para dizer que não quer que continue, eu criei exemplos:

<?php

namespace ErikFig\Framework\Users;

use Twig\Environment;
use ErikFig\Framework\Router;

class Register
{
    public static function handle(Environment $twig, Router $router)
    {
        $loader = $twig->getLoader();
        $loader->addPath(__DIR__ . '/../templates');

        $router->get('/users', 'ErikFig\Framework\Users\Controllers\UsersController::index')
            // um middleware de autenticação
            ->before(function () {
                // troque de true para false e vice versa para ver o resultado
                // estou simulando um processo de verificação de usuário autenticado
                $checkUserIsAuth = true;
                if (!$checkUserIsAuth) {
                    http_response_code(401);
                    echo '<h1 style="color: red">Você não está autenticado</h1>';
                }
                return $checkUserIsAuth;
            })
            ->before(function () {
                echo '<p>segundo middleware</p>';
                return true;
            })
            ->before(function () {
                echo '<p>terceiro middleware</p>';
                return true;
            })
            ->after(function () {
                echo '<p>finalização</p>';
                return true;
            });
    }
}

É isso, middlewares são simples de se trabalhar e muito úteis em diversos aspectos, você pode “plugar” e “desplugar” novas features somente incluindo novos middlewares.

Uma dica legal é que você pode “guardar” funções anônimas em variáveis e usar em vários lugares, embora um bom Dependency Injection Container como o Pimple, seja uma melhor alternativa.

$auth = function () {
    $checkUserIsAuth = true;
    if (!$checkUserIsAuth) {
        http_response_code(401);
        echo '<h1 style="color: red">Você não está autenticado</h1>';
    }
    return $checkUserIsAuth;
}

$router->get('/users', 'ErikFig\Framework\Users\Controllers\UsersController::index')->before($auth);
$router->get('/users/{id}', 'ErikFig\Framework\Users\Controllers\UsersController::index')->before($auth);
$router->get('/users/{id}/edit', 'ErikFig\Framework\Users\Controllers\UsersController::index')->before($auth);

Ou usar classes com inkove (ainda vou falar disso)

Bacana né, o céu é o limite!

Aqui os arquivos deste turorial:

https://github.com/erikfig/php-do-zero/commit/1cc950b710fe0d6470f8dd6eefb35e482f05e3c1

Não se esqueça de se cadastrar na newsletter do blog:

Mais artigos desta série

Artigo anterior

Próximo artigo

Todos os artigos

Autor: Erik Figueiredo

Músico, gamer amador, tutor de programação, desenvolvedor freelancer full cycle, com foco em PHP (Laravel e CakePHP), Javascript (Front e Node.js), Dart (Front e Flutter) e infra.

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *