Middlewares
Os middlewares são geralmente usados para interceptar solicitações ou respostas. Por exemplo, realizar a verificação unificada da identidade do usuário antes de executar o controlador, redirecionar para a página de login se o usuário não estiver logado, ou adicionar um cabeçalho específico à resposta. Outro exemplo seria o acompanhamento da proporção de solicitações para uma URI específica, e assim por diante.
Modelo de Cebola dos Middlewares
┌──────────────────────────────────────────────────────┐
│ middleware1 │
│ ┌──────────────────────────────────────────┐ │
│ │ middleware2 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ middleware3 │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ │ │ │ │
── Request ───────────────────────> Controller ─ Response ───────────────────────────> Cliente
│ │ │ │ │ │ │ │
│ │ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘
Os middlewares e os controladores compõem um modelo clássico de cebola, onde os middlewares são como as camadas externas da cebola e o controlador é o núcleo da cebola. Como mostrado no gráfico, a solicitação atravessa os middlewares 1, 2 e 3 para chegar ao controlador. O controlador retorna uma resposta, que então atravessa os middlewares na ordem 3, 2, 1 antes de ser enviada para o cliente. Ou seja, em cada middleware podemos obter tanto a solicitação quanto a resposta.
Interceptação de Solicitações
Às vezes, não queremos que uma solicitação atinja a camada do controlador. Por exemplo, se descobrirmos, no middleware 2, que o usuário atual não está logado, podemos interceptar a solicitação e retornar uma resposta de login. Nesse caso, o fluxo se parece com o exemplo abaixo:
┌────────────────────────────────────────────────────────────┐
│ middleware1 │
│ ┌────────────────────────────────────────────────┐ │
│ │ middleware2 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ middleware3 │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ │ │ │ │
── Request ─────────┐ │ │ Controller │ │ │ │
│ │ Response │ │ │ │ │ │
<───────────────────┘ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘
Como mostrado no gráfico, a solicitação atinge o middleware 2 e gera uma resposta de login, que atravessa o middleware 1 e retorna para o cliente.
Interface do Middleware
Os middlewares devem implementar a interface Webman\MiddlewareInterface.
interface MiddlewareInterface
{
/**
* Processa uma solicitação recebida pelo servidor.
*
* Processa uma solicitação recebida pelo servidor para produzir uma resposta.
* Se não puder produzir a resposta por si só, poderá delegar ao manipulador de solicitações fornecido para fazê-lo.
*/
public function process(Request $request, callable $handler): Response;
}
Isso significa que os middlewares devem implementar o método process, que deve retornar um objeto support\Response. Por padrão, este objeto é gerado por $handler($request) (a solicitação continua a cruzar as camadas da cebola). Também pode ser uma resposta gerada por funções auxiliares como response(), json(), xml() ou redirect() (a solicitação para de atravessar as camadas da cebola).
Obter solicitação e resposta no middleware
No middleware, podemos obter a solicitação e também a resposta após o controle ser executado, portanto, o middleware é dividido em três partes internas.
- Fase de passagem da solicitação, isto é, antes do processamento da solicitação
- Fase de processamento do controlador, isto é, durante o processamento da solicitação
- Fase de saída da resposta, isto é, após o processamento da solicitação
A representação dessas três fases no 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 'Esta é a fase de passagem da solicitação, ou seja, antes do processamento da solicitação';
$response = $handler($request); // Continua a passagem até o núcleo da cebola, até que seja executado o controlador e se obtenha a resposta
echo 'Esta é a fase de saída da resposta, ou seja, após o processamento da solicitação';
return $response;
}
}
Exemplo: Middleware de autenticação
Crie o arquivo app/middleware/AuthCheckTest.php (se o diretório não existir, crie-o) 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á autenticado, a solicitação continua a passagem
return $handler($request);
}
// Obter os métodos do controlador que não exigem autenticação via reflexão
$controller = new ReflectionClass($request->controller);
$noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];
// O método acessado requer autenticação
if (!in_array($request->action, $noNeedLogin)) {
// Interceptar a solicitação e retornar uma resposta de redirecionamento, parando a passagem da solicitação
return redirect('/user/login');
}
// Não requer autenticação, a solicitação continua a passagem
return $handler($request);
}
}
Crie o controlador app/controller/UserController.php da seguinte forma:
<?php
namespace app\controller;
use support\Request;
class UserController
{
/**
* Métodos que não exigem autenticação
*/
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')]);
}
}
Observação
$noNeedLoginregistra os métodos no controlador que podem ser acessados sem autenticação
Adicione o middleware de autenticação global no arquivo config/middleware.php da seguinte forma:
return [
// Middleware global
'' => [
// ... outros middlewares
app\middleware\AuthCheckTest::class,
]
];
Com o middleware de autenticação, podemos nos concentrar em escrever o código de negócios na camada do controlador, sem se preocupar se o usuário está autenticado.
Exemplo: Middleware de solicitação de recursos
Crie o arquivo app/middleware/AccessControlTest.php (se o diretório não existir, crie-o) 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 de opções, retornar uma resposta vazia; caso contrário, continuar a passagem e obter uma resposta
$response = $request->method() == 'OPTIONS' ? response('') : $handler($request);
// Adicionar cabeçalhos http relacionados com o controle de recursos à 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
As solicitações cross-origin podem gerar solicitações OPTIONS de preflight. Não queremos que as solicitações OPTIONS cheguem ao controlador, portanto retornamos uma resposta vazia diretamente (response('')) para interceptar a solicitação.
Se a sua API precisar configurar rotas, useRoute::any(..)ouRoute::add(['POST', 'OPTIONS'], ..).
Adicione o middleware de solicitação de recursos global no arquivo config/middleware.php da seguinte forma:
return [
// Middleware global
'' => [
// ... outros middlewares
app\middleware\AccessControlTest::class,
]
];
Observação
Se a solicitação ajax personalizou cabeçalhos, é necessário adicionar este cabeçalho personalizado ao campoAccess-Control-Allow-Headersdo middleware. Caso contrário, ocorrerá o erroRequest header field XXXX is not allowed by Access-Control-Allow-Headers in preflight response.
Explicação
- O middleware é dividido em middleware global, middleware da aplicação (o middleware da aplicação só é válido no 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 - A configuração do middleware global está sob a chave
'' - A configuração do middleware da aplicação está sob o nome da aplicação específica, por exemplo:
return [
// Middleware global
'' => [
app\middleware\AuthCheckTest::class,
app\middleware\AccessControlTest::class,
],
// Middleware da aplicação "api" (o middleware da aplicação só é válido no modo de várias aplicações)
'api' => [
app\middleware\ApiOnly::class,
]
];
Middleware do controlador e middleware do método
Usando anotações, podemos atribuir middleware a um controlador ou a métodos específicos 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 configurar um ou um grupo de rotas com middleware específico.
Por exemplo, adicione a seguinte configuração no arquivo 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,
]);
Construtor de Middleware com Parâmetros
O arquivo de configuração suporta a instanciação direta de middlewares, facilitando a passagem de parâmetros ao middleware por meio do construtor.
Por exemplo, a configuração em config/middleware.php também pode ser feita da seguinte maneira:
return [
// Middleware global
'' => [
new app\middleware\AuthCheckTest($param1, $param2, ...),
new app\middleware\AccessControlTest($param1, $param2, ...)
],
// Middleware da aplicação API (somente válido no modo de múltiplas aplicações)
'api' => [
app\middleware\ApiOnly::class,
]
];
Da mesma forma, os middlewares de rotas também podem passar parâmetros por meio do construtor, como mostrado em config/route.php:
Route::any('/admin', [app\admin\controller\IndexController::class, 'index'])->middleware([
new app\middleware\MiddlewareA($param1, $param2, ...),
new app\middleware\MiddlewareB($param1, $param2, ...),
]);
Exemplo de uso de parâmetros no 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 do Middleware
- A ordem de execução do middleware é:
middleware global->middleware da aplicação->middleware do controlador->middleware de rota->middleware do método. - Quando há múltiplos middlewares no mesmo nível, eles são executados na ordem configurada.
- Requisições 404 não acionam nenhum middleware por padrão (ainda é possível adicionar middleware via
Route::fallback(function(){})->middleware()).
Passagem de Parâmetros para o Middleware (route->setParams)
Configuração de Rota config/route.php
<?php
use support\Request;
use Webman\Route;
Route::any('/test', [app\controller\IndexController::class, 'index'])->setParams(['some_key' =>'some value']);
Middleware (supondo ser 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
{
// Como o $request->route padrão é null, é necessário verificar se está vazio
if ($route = $request->route) {
$value = $route->param('some_key');
var_export($value);
}
return $handler($request);
}
}
Passagem de Parâmetros do Middleware para o Controlador
Às vezes, o controlador precisa usar dados gerados pelo middleware, nesse caso, podemos passar parâmetros para o controlador adicionando propriedades ao objeto $request. 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);
}
}
Middleware Obtendo Informações de Rota Atual da Requisição
Podemos usar $request->route para obter o objeto de rota, e chamando os métodos correspondentes, obter as informações desejadas.
Configuração de 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 requisição não corresponder a nenhuma rota (exceto a rota padrão), $request->route será null.
// Supondo que o navegador acesse o endereço /user/111, o seguinte será mostrado
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);
}
}
Middleware Obtendo Exceções
Durante o processamento de uma requisição, pode ocorrer uma exceção. No middleware, podemos usar $response->exception() para obtê-la.
Configuração de 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
Os middlewares globais do projeto principal afetam apenas o projeto principal e não têm impacto nos aplicativos de plugin. Às vezes, podemos querer adicionar um middleware que afete todos os plug-ins, incluindo o projeto principal. Nesse caso, podemos usar o middleware super global.
No arquivo config/middleware.php, a configuração seria a seguinte:
return [
'@' => [ // Adiciona um middleware global ao projeto principal e a todos os plug-ins
app\middleware\MiddlewareGlobl::class,
],
'' => [], // Adiciona um middleware global apenas ao projeto principal
];
Dica
O middleware super global@pode ser configurado não apenas no projeto principal, mas também em algum plug-in. Por exemplo, configurando o middleware super global@em um arquivoplugin/ai/config/middleware.php, ele afetaria o projeto principal e todos os plug-ins.
Adição de Middleware a um Plugin Específico
Às vezes, queremos adicionar um middleware a um aplicativo de plugin, sem modificar o código do próprio plug-in (uma vez que as alterações seriam substituídas durante uma atualização). Nesse caso, podemos configurar o middleware no projeto principal.
No arquivo config/middleware.php, a configuração para adicionar middleware a um plug-in específico seria a seguinte:
return [
'plugin.ai' => [], // Adiciona um middleware ao plug-in ai
'plugin.ai.admin' => [], // Adiciona um middleware ao módulo admin do plug-in ai (diretório plugin\ai\app\admin)
];
Dica
Da mesma forma, também é possível configurar um arquivo semelhante em um plug-in para afetar outros plug-ins. Por exemplo, adicionando essa configuração a um arquivoplugin/foo/config/middleware.php, ela afetaria o plug-in ai.