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:
- Fase de atravessamento da solicitação, que é a fase anterior ao processamento da solicitação
- Fase de processamento da solicitação pelo controlador, que é a fase de processamento da solicitação
- 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, useRoute::any(..)
ouRoute::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 campoAccess-Control-Allow-Headers
do middleware, caso contrário, receberá a mensagemRequest 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@
noplugin/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 noplugin/foo/config/middleware.php
para impactar o plugin ai.