Middleware
Algumas tarefas valem para muitas rotas: iniciar a sessão, conferir o token CSRF, exigir login, medir o tempo de resposta, acrescentar headers. Repetir isso em cada action não escala. No Yii3, essas tarefas são middlewares: classes que ficam entre a requisição e a action e podem agir antes, depois ou no lugar dela. Este capítulo mostra como eles funcionam, onde ficam configurados e como escrever o seu.
PSR-15 na prática
Os middlewares do Yii3 seguem a PSR-15. São duas interfaces:
interface MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface;
}
interface RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface;
}O middleware recebe a requisição e um $handler, que representa todo o resto da cadeia. Ele pode:
- Fazer algo antes, chamar
$handler->handle($request)e devolver a resposta. - Chamar o
$handlere alterar a resposta que voltou. - Não chamar o
$handlere devolver uma resposta própria, interrompendo a cadeia.
Um middleware que mede quanto tempo o resto da cadeia levou, em src/Web/Estrutura/Middleware/TempoDeRespostaMiddleware.php:
<?php
declare(strict_types=1);
namespace App\Web\Estrutura\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class TempoDeRespostaMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$inicio = microtime(true);
$response = $handler->handle($request);
$milissegundos = (microtime(true) - $inicio) * 1000;
return $response->withHeader('X-Tempo-Resposta', sprintf('%.2f ms', $milissegundos));
}
}Tudo antes do $handler->handle() roda na ida; tudo depois, na volta. Como a resposta PSR-7 é imutável, o withHeader() devolve uma resposta nova, e é ela que precisa ser retornada.
Esse middleware mede só o que vem depois dele. Para medir desde o começo do script, use o atributo applicationStartTime, que o runner coloca na requisição (veja Script de entrada).
A pilha global e a pilha por rota
Existem dois lugares para registrar middlewares.
A pilha global
Ela roda em toda requisição, inclusive nas que terminam em 404. Fica na definição da Application, em config/web/di/application.php:
'withMiddlewares()' => [
[
ErrorCatcher::class,
SessionMiddleware::class,
CsrfTokenMiddleware::class,
RequestCatcherMiddleware::class,
Router::class,
],
],A ordem é a de execução na ida:
ErrorCatchervem primeiro para capturar exceções de qualquer middleware ou action e transformá-las numa página de erro. Veja Tratamento de erros.SessionMiddlewareabre a sessão e grava o cookie na volta. Veja Sessões e cookies.CsrfTokenMiddlewarerecusa com 422 qualquer requisição sem token válido, exceto GET, HEAD e OPTIONS. Ele depende da sessão, por isso vem depois dela.RequestCatcherMiddlewareguarda a requisição atual noRequestProviderInterface, para serviços que precisam dela fora da action, como os resolvers de#[Query]e#[Body].Routerencontra a rota e executa a pilha dela. Se nada casar, passa para o fallback handler.
A ordem tem efeitos visíveis. Como o CSRF vem antes do roteamento, um DELETE /estrutura/contato recebe 422 do CsrfTokenMiddleware, e não 405 do Router: a requisição nem chega a ser roteada.
O app de exemplo deste guia muda uma só coisa nessa pilha: no lugar do CsrfTokenMiddleware entra um middleware que aplica o mesmo CSRF a todo o site, menos às rotas /api, que são autenticadas por token. O motivo está em APIs REST. No seu projeto, para um middleware que vale para tudo (headers de segurança, por exemplo), é aqui que ele entra.
A pilha da rota
Uma rota pode ter middlewares próprios, que rodam depois do Router e antes da action. A action é, na verdade, o último middleware da rota: por isso o método se chama action() e precisa vir depois de middleware().
Um grupo aplica middlewares a todas as suas rotas. O exemplo abaixo cria um painel em /estrutura/painel, com o middleware de tempo e um middleware que exige uma chave (visto na próxima seção):
Group::create('/estrutura/painel')
->middleware(TempoDeRespostaMiddleware::class, ExigeChaveMiddleware::class)
->routes(
Route::get('')
->action(Estrutura\Painel\Action::class)
->name('estrutura/painel'),
Route::get('/status')
->action(Estrutura\Painel\StatusAction::class)
->disableMiddleware(ExigeChaveMiddleware::class)
->name('estrutura/painel/status'),
),Os métodos disponíveis em Route e em Group:
middleware(...)acrescenta middlewares ao fim da lista. NumaRoute, precisa vir antes deaction(); numGroup, antes deroutes().prependMiddleware(...)coloca middlewares no começo da lista.disableMiddleware(...)tira um middleware herdado do grupo. Aqui, a rota/statusfica pública.
As duas actions do painel só escrevem um texto. A principal, em src/Web/Estrutura/Painel/Action.php:
<?php
declare(strict_types=1);
namespace App\Web\Estrutura\Painel;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
final readonly class Action
{
public function __construct(
private ResponseFactoryInterface $responseFactory,
) {}
public function __invoke(): ResponseInterface
{
$response = $this->responseFactory->createResponse();
$response->getBody()->write("Bem-vindo ao painel.\n");
return $response;
}
}A StatusAction é igual, mas escreve ok.
Escrevendo o seu middleware
O middleware que protege o painel confere o header X-Chave. Em src/Web/Estrutura/Middleware/ExigeChaveMiddleware.php:
<?php
declare(strict_types=1);
namespace App\Web\Estrutura\Middleware;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Yiisoft\Http\Status;
final readonly class ExigeChaveMiddleware implements MiddlewareInterface
{
public function __construct(
private ResponseFactoryInterface $responseFactory,
private string $chave,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (!hash_equals($this->chave, $request->getHeaderLine('X-Chave'))) {
$response = $this->responseFactory->createResponse(Status::FORBIDDEN);
$response->getBody()->write("Chave ausente ou inválida.\n");
return $response;
}
return $handler->handle($request);
}
}Quando a chave não confere, ele devolve 403 sem chamar o $handler, e a action nunca roda. O hash_equals() compara as strings em tempo constante, o que é o certo para segredos.
O construtor pede uma string, que o autowiring não tem como adivinhar. O valor vem dos parâmetros (chaveDoPainel, definido no capítulo Aplicação) por uma definição no container. Como middleware só existe na web, ela fica em config/web/di/estrutura.php:
<?php
declare(strict_types=1);
use App\Web\Estrutura\Middleware\ExigeChaveMiddleware;
/** @var array $params */
return [
ExigeChaveMiddleware::class => [
'__construct()' => [
'chave' => $params['estrutura']['chaveDoPainel'],
],
],
];O ResponseFactoryInterface continua vindo por autowiring; a definição só informa o argumento chave.
Testando
Sem a chave:
curl -i http://127.0.0.1:8080/estrutura/painelHTTP/1.1 403 Forbidden
Host: 127.0.0.1:8080
Date: Thu, 08 Oct 2026 05:08:23 GMT
Connection: close
X-Tempo-Resposta: 0.30 ms
Content-Length: 28
Content-type: text/html; charset=UTF-8
Chave ausente ou inválida.O header X-Tempo-Resposta aparece mesmo com o 403. O TempoDeRespostaMiddleware vem antes na lista, então ele envolve o ExigeChaveMiddleware e recebe a resposta dele na volta.
Com a chave:
curl -i -H "X-Chave: chave-de-exemplo" http://127.0.0.1:8080/estrutura/painelHTTP/1.1 200 OK
Host: 127.0.0.1:8080
Date: Thu, 08 Oct 2026 05:08:23 GMT
Connection: close
X-Tempo-Resposta: 0.47 ms
Content-Length: 21
Content-type: text/html; charset=UTF-8
Bem-vindo ao painel.As actions do painel não definem Content-Type, então vale o padrão do PHP, text/html.
E /estrutura/painel/status, sem chave nenhuma, responde 200 com ok, porque a rota desligou o ExigeChaveMiddleware. O header de tempo continua lá. Os valores de tempo mudam a cada requisição.
Outras formas de declarar um middleware
O MiddlewareFactory aceita mais do que o nome de uma classe que implementa MiddlewareInterface:
- Uma classe com
__invoke()ou um par[classe, método], como as actions. Se o retorno for umResponseInterface, ele é usado como resposta. - Uma closure, que recebe a requisição e o handler como argumentos.
- Uma definição em array, como
['class' => MeuMiddleware::class, '__construct()' => ['chave' => 'abc']], para configurar o middleware direto na rota.
Para passar dados de um middleware para a action, use atributos da requisição: o middleware chama $handler->handle($request->withAttribute('usuario', $usuario)), e a action lê com $request->getAttribute('usuario') ou com o atributo #[Request('usuario')].
TIP
Antes de escrever um middleware, procure nos pacotes. O yiisoft/yii-middleware traz middlewares prontos, como ForceSecureConnection (redireciona para HTTPS), Locale, IpFilter e HttpCache. Para autenticação, o yiisoft/auth tem o middleware Authentication, e o yiisoft/user tem o LoginMiddleware. Veja Autenticação.
Comparando com o Yii2
- No Yii2, o que acontecia antes ou depois de uma action ficava em
behaviors()e em filtros (AccessControl,VerbFilter,ContentNegotiator). No Yii3, tudo isso é middleware. - Os filtros do Yii2 eram ligados a um controller. Os middlewares do Yii3 são ligados a rotas e grupos, ou à aplicação inteira.
- O
beforeAction()que devolviafalsepara interromper a action equivale a um middleware que devolve uma resposta sem chamar o$handler. - Os middlewares seguem uma PSR, então middlewares PSR-15 de outros projetos também funcionam.