Middleware

Middleware é geralmente usado para interceptar solicitações ou respostas. Por exemplo, realizar uma verificação de identidade do usuário de forma unificada antes da execução do controlador, como redirecionar ao login se o usuário não estiver logado, ou adicionar um cabeçalho específico na resposta. Também pode ser utilizado para estatísticas sobre a proporção de solicitações a um determinado URI, etc.

Modelo de Cebola do Middleware


            ┌──────────────────────────────────────────────────────┐
            │                     middleware1                      │ 
            │     ┌──────────────────────────────────────────┐     │
            │     │               middleware2                │     │
            │     │     ┌──────────────────────────────┐     │     │
            │     │     │         middleware3          │     │     │        
            │     │     │     ┌──────────────────┐     │     │     │
            │     │     │     │                  │     │     │     │
   ── Request ───────────────────> Controller ── Response ───────────────────────────> Client
            │     │     │     │                  │     │     │     │
            │     │     │     └──────────────────┘     │     │     │
            │     │     │                              │     │     │
            │     │     └──────────────────────────────┘     │     │
            │     │                                          │     │
            │     └──────────────────────────────────────────┘     │
            │                                                      │
            └──────────────────────────────────────────────────────┘

Middleware e controladores formam um clássico modelo de cebola, onde o middleware é como camadas de casca de cebola e o controlador é o núcleo da cebola. Como mostrado na figura, as solicitações atravessam o middleware 1, 2 e 3 como uma seta até chegarem ao controlador, que retorna uma resposta que, por sua vez, atravessa os middlewares 3, 2 e 1 em sequência antes de retornar ao cliente. Ou seja, em cada middleware, podemos tanto acessar a solicitação quanto a resposta.

Interceptação de Solicitações

Às vezes, não queremos que uma determinada solicitação chegue ao nível do controlador. Por exemplo, se em middleware2 descobrirmos que o usuário atual não está logado, podemos interceptar a solicitação diretamente e retornar uma resposta de login. Portanto, esse fluxo é semelhante ao seguinte.


            ┌────────────────────────────────────────────────────────────┐
            │                         middleware1                        │ 
            │     ┌────────────────────────────────────────────────┐     │
            │     │                   middleware2                  │     │
            │     │          ┌──────────────────────────────┐      │     │
            │     │          │        middleware3           │      │     │       
            │     │          │    ┌──────────────────┐      │      │     │
            │     │          │    │                  │      │      │     │
   ── Request ─────────┐     │    │    Controller    │      │      │     │
            │     │ Response │    │                  │      │      │     │
   <───────────────────┘     │    └──────────────────┘      │      │     │
            │     │          │                              │      │     │
            │     │          └──────────────────────────────┘      │     │
            │     │                                                │     │
            │     └────────────────────────────────────────────────┘     │
            │                                                            │
            └────────────────────────────────────────────────────────────┘

Como mostrado na figura, ao chegar ao middleware2, uma resposta de login é gerada, e a resposta atravessa de volta pelo middleware2 até o middleware1 e, finalmente, retorna ao cliente.

Interface do Middleware

O middleware deve implementar a interface Webman\MiddlewareInterface.

interface MiddlewareInterface
{
    /**
     * Process an incoming server request.
     *
     * Processes an incoming server request in order to produce a response.
     * If unable to produce the response itself, it may delegate to the provided
     * request handler to do so.
     */
    public function process(Request $request, callable $handler): Response;
}

O que significa que deve implementar o método process, que deve retornar um objeto support\Response. Por padrão, este objeto é criado por $handler($request) (a solicitação continuará a passar pelo núcleo da cebola), ou pode ser uma resposta gerada por funções auxiliares como response(), json(), xml(), redirect(), etc. (a solicitação para de atravessar o núcleo da cebola).

Obtendo Solicitações e Respostas Dentro do Middleware

Dentro do middleware, podemos obter tanto a solicitação quanto a resposta após a execução do controlador. Portanto, o middleware é dividido em três partes:

  1. Fase de atravessamento da solicitação, que é a fase anterior ao processamento da solicitação
  2. Fase de processamento da solicitação pelo controlador, que é a fase de processamento da solicitação
  3. Fase de saída da resposta, que é a fase após o processamento da solicitação

A manifestação dessas três fases dentro do middleware é a seguinte.

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Test implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        echo 'Aqui é a fase de atravessamento da solicitação, ou seja, antes do processamento da solicitação';

        $response = $handler($request); // Continua atravessando o núcleo da cebola até executar o controlador e obter a resposta

        echo 'Aqui é a fase de saída da resposta, ou seja, após o processamento da solicitação';

        return $response;
    }
}

Exemplo: Middleware de Verificação de Identidade

Crie o arquivo app/middleware/AuthCheckTest.php (se o diretório não existir, crie por conta própria) da seguinte forma:

<?php
namespace app\middleware;

use ReflectionClass;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class AuthCheckTest implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        if (session('user')) {
            // Já está logado, a solicitação continua atravessando o núcleo da cebola
            return $handler($request);
        }

        // Usando reflexão para obter quais métodos do controlador não precisam de login
        $controller = new ReflectionClass($request->controller);
        $noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];

        // O método acessado requer login
        if (!in_array($request->action, $noNeedLogin)) {
            // Interrompe a solicitação e retorna uma resposta de redirecionamento, a solicitação para de atravessar o núcleo da cebola
            return redirect('/user/login');
        }

        // Não precisa de login, a solicitação continua atravessando o núcleo da cebola
        return $handler($request);
    }
}

Crie o controlador app/controller/UserController.php.

<?php
namespace app\controller;
use support\Request;

class UserController
{
    /**
     * Métodos que não requerem login
     */
    protected $noNeedLogin = ['login'];

    public function login(Request $request)
    {
        $request->session()->set('user', ['id' => 10, 'name' => 'webman']);
        return json(['code' => 0, 'msg' => 'login ok']);
    }

    public function info()
    {
        return json(['code' => 0, 'msg' => 'ok', 'data' => session('user')]);
    }
}

Atenção
$noNeedLogin registra os métodos do controlador que podem ser acessados sem login.

No arquivo config/middleware.php, adicione o middleware global da seguinte forma:

return [
    // Middleware global
    '' => [
        // ... outros middlewares omitidos aqui
        app\middleware\AuthCheckTest::class,
    ]
];

Com o middleware de verificação de identidade, agora podemos nos concentrar em escrever o código do negócio no nível do controlador, sem se preocupar se o usuário está logado.

Exemplo: Middleware de Controle de Acesso

Crie o arquivo app/middleware/AccessControlTest.php (se o diretório não existir, crie por conta própria) da seguinte forma:

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class AccessControlTest implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        // Se for uma solicitação OPTIONS, retorne uma resposta vazia, caso contrário, continue atravessando o núcleo da cebola e obtenha uma resposta
        $response = $request->method() == 'OPTIONS' ? response('') : $handler($request);

        // Adicione cabeçalhos HTTP relacionados ao controle de acesso à resposta
        $response->withHeaders([
            'Access-Control-Allow-Credentials' => 'true',
            'Access-Control-Allow-Origin' => $request->header('origin', '*'),
            'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
            'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
        ]);

        return $response;
    }
}

Dica
Solicitações de origem cruzada podem gerar solicitações OPTIONS, e não queremos que as solicitações OPTIONS cheguem ao controlador, então respondemos diretamente com uma resposta vazia (response('')) para implementar a interceptação da solicitação.
Se sua interface precisar definir rotas, use Route::any(..) ou Route::add(['POST', 'OPTIONS'], ..) para configurar.

No arquivo config/middleware.php, adicione o middleware global como mostrado:

return [
    // Middleware global
    '' => [
        // ... outros middlewares omitidos aqui
        app\middleware\AccessControlTest::class,
    ]
];

Atenção
Se uma solicitação AJAX definir cabeçalhos personalizados, você precisa incluir esses cabeçalhos personalizados no campo Access-Control-Allow-Headers do middleware, caso contrário, receberá a mensagem Request header field XXXX is not allowed by Access-Control-Allow-Headers in preflight response.

Explicação

  • Os middlewares são divididos em middleware global, middleware de aplicação (middleware de aplicação é eficaz apenas em modo de várias aplicações, consulte Múltiplas Aplicações) e middleware de rota.
  • O arquivo de configuração do middleware está localizado em config/middleware.php.
  • O middleware global é configurado sob a chave ''.
  • O middleware de aplicativo é configurado sob o nome específico da aplicação, por exemplo:
return [
    // Middleware global
    '' => [
        app\middleware\AuthCheckTest::class,
        app\middleware\AccessControlTest::class,
    ],
    // Middleware da aplicação API (middleware de aplicação é eficaz apenas em modo de várias aplicações)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

Middleware de Controlador e Middleware de Método

Usando anotações, podemos definir middlewares para um controlador específico ou para um método específico dentro do controlador.

<?php
namespace app\controller;
use app\middleware\Controller1Middleware;
use app\middleware\Controller2Middleware;
use app\middleware\Method1Middleware;
use app\middleware\Method2Middleware;
use support\annotation\Middleware;
use support\Request;

#[Middleware(Controller1Middleware::class, Controller2Middleware::class)]
class IndexController
{
    #[Middleware(Method1Middleware::class, Method2Middleware::class)]
    public function index(Request $request): string
    {
        return 'hello';
    }
}

Middleware de Rota

Podemos definir middlewares para uma ou um grupo de rotas.
Por exemplo, adicione a seguinte configuração no config/route.php:

<?php
use support\Request;
use Webman\Route;

Route::any('/admin', [app\admin\controller\IndexController::class, 'index'])->middleware([
    app\middleware\MiddlewareA::class,
    app\middleware\MiddlewareB::class,
]);

Route::group('/blog', function () {
   Route::any('/create', function () {return response('create');});
   Route::any('/edit', function () {return response('edit');});
   Route::any('/view/{id}', function ($r, $id) {response("view $id");});
})->middleware([
    app\middleware\MiddlewareA::class,
    app\middleware\MiddlewareB::class,
]);

Passando Parâmetros para o Construtor do Middleware

O arquivo de configuração suporta a criação direta de instâncias de middleware ou funções anônimas, permitindo passar parâmetros para o middleware através do construtor com facilidade.
Por exemplo, você também pode configurar no config/middleware.php assim:

return [
    // Middleware global
    '' => [
        new app\middleware\AuthCheckTest($param1, $param2, ...),
        function(){
            return new app\middleware\AccessControlTest($param1, $param2, ...);
        },
    ],
    // Middleware da aplicação API (middleware de aplicação é eficaz apenas em modo de várias aplicações)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

Da mesma forma, você pode passar parâmetros para o middleware por meio do construtor para middleware de rota, como por exemplo no config/route.php:

Route::any('/admin', [app\admin\controller\IndexController::class, 'index'])->middleware([
    new app\middleware\MiddlewareA($param1, $param2, ...),
    function(){
        return new app\middleware\MiddlewareB($param1, $param2, ...);
    },
]);

Exemplo de uso de parâmetros dentro do middleware:

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class MiddlewareA implements MiddlewareInterface
{
    protected $param1;

    protected $param2;

    public function __construct($param1, $param2)
    {
        $this->param1 = $param1;
        $this->param2 = $param2;
    }

    public function process(Request $request, callable $handler) : Response
    {
        var_dump($this->param1, $this->param2);
        return $handler($request);
    }
}

Ordem de Execução dos Middlewares

  • A ordem de execução dos middlewares é middleware global -> middleware de aplicação -> middleware de controlador -> middleware de rota -> middleware de método.
  • Quando houver vários middlewares no mesmo nível, eles são executados na ordem em que foram configurados.
  • Solicitações 404 não acionam nenhum middleware por padrão (mas ainda é possível adicionar middleware usando Route::fallback(function(){})->middleware()).

Passando Parâmetros para o Middleware a Partir da Rota

Configuração da rota em config/route.php

<?php
use support\Request;
use Webman\Route;

Route::any('/test', [app\controller\IndexController::class, 'index'])->setParams(['some_key' => 'some value']);

Middleware (assumindo que é um middleware global)

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        // Por padrão, $request->route é nulo, então precisamos verificar se $request->route não é vazio
        if ($route = $request->route) {
            $value = $route->param('some_key');
            var_export($value);
        }
        return $handler($request);
    }
}

Passando Parâmetros do Middleware para o Controlador

Às vezes, o controlador precisa usar dados gerados dentro do middleware. Nesse caso, podemos agregar propriedades ao objeto $request para passar parâmetros ao controlador. Por exemplo:

Middleware

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        $request->data = 'some value';
        return $handler($request);
    }
}

Controlador:

<?php
namespace app\controller;

use support\Request;

class FooController
{
    public function index(Request $request)
    {
        return response($request->data);
    }
}

Obtendo Informações da Rota da Solicitação Atual no Middleware

Podemos usar $request->route para acessar o objeto da rota e obter informações relevantes através de métodos correspondentes.

Configuração da Rota

<?php
use support\Request;
use Webman\Route;

Route::any('/user/{uid}', [app\controller\UserController::class, 'view']);

Middleware

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        $route = $request->route;
        // Se a solicitação não corresponder a nenhuma rota (exceto a rota padrão), então $request->route será nulo
        // Supondo que o navegador acesse a URL /user/111, as seguintes informações serão impressas
        if ($route) {
            var_export($route->getPath());       // /user/{uid}
            var_export($route->getMethods());    // ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD','OPTIONS']
            var_export($route->getName());       // user_view
            var_export($route->getMiddleware()); // []
            var_export($route->getCallback());   // ['app\\controller\\UserController', 'view']
            var_export($route->param());         // ['uid'=>111]
            var_export($route->param('uid'));    // 111 
        }
        return $handler($request);
    }
}

Atenção

Obtendo Exceções no Middleware

Durante o processamento, podem ocorrer exceções. Dentro do middleware, podemos usar $response->exception() para obter a exceção.

Configuração da Rota

<?php
use support\Request;
use Webman\Route;

Route::any('/user/{uid}', function (Request $request, $uid) {
    throw new \Exception('exception test');
});

Middleware:

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        $response = $handler($request);
        $exception = $response->exception();
        if ($exception) {
            echo $exception->getMessage();
        }
        return $response;
    }
}

Middleware Super Global

O middleware global do projeto principal só afeta o projeto principal e não terá impacto sobre plugins de aplicativo. Às vezes, queremos adicionar um middleware que impacte globalmente todos os plugins, então podemos usar o middleware super global.

No config/middleware.php, configure da seguinte forma:

return [
    '@' => [ // Adiciona middleware global ao projeto principal e todos os plugins
        app\middleware\MiddlewareGlobl::class,
    ], 
    '' => [], // Apenas adiciona middleware global ao projeto principal
];

Dica
O middleware super global @ pode ser configurado não apenas no projeto principal, mas também em qualquer plugin. Por exemplo, se você configurar o middleware super global @ no plugin/ai/config/middleware.php, isso também afetará o projeto principal e todos os plugins.

Adicionando Middleware a um Plugin Específico

Às vezes, queremos adicionar um middleware a um plugin de aplicativo específico, mas não queremos modificar o código do plugin (pois isso seria sobrescrito em atualizações). Nesse caso, podemos configurar um middleware no projeto principal.

No config/middleware.php, configure da seguinte forma:

return [
    'plugin.ai' => [], // Adiciona middleware ao plugin ai
    'plugin.ai.admin' => [], // Adiciona middleware ao módulo admin do plugin ai (diretório plugin\ai\app\admin)
];

Dica
Você também pode adicionar configurações semelhantes em qualquer plugin para afetar outros plugins, por exemplo, adicione as configurações acima no plugin/foo/config/middleware.php para impactar o plugin ai.