O que é um middleware no Slim (PSR-15)
No Slim, um middleware é uma camada que envolve o processamento da requisição HTTP. Ele pode ler/modificar o ServerRequestInterface, interromper o fluxo retornando uma resposta imediatamente, ou delegar para o próximo componente do pipeline e então pós-processar a resposta antes de devolvê-la ao cliente.
O Slim segue o padrão PSR-15, que define dois papéis principais:
- Middleware: implementa
MiddlewareInterfacee possuiprocess(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface. - Request Handler: implementa
RequestHandlerInterfacee possuihandle(ServerRequestInterface $request): ResponseInterface. No final do pipeline, o handler normalmente é a rota (seu action/controller).
Uma forma útil de pensar: middlewares são como filtros em série; cada um decide se chama o próximo e pode ajustar request/response.
Pipeline e ordem de execução
Como o Slim executa os middlewares
O Slim compõe um pipeline com middlewares aplicados em diferentes níveis:
- Globais (aplicados na aplicação inteira)
- De grupo (aplicados a um conjunto de rotas)
- De rota (aplicados a uma rota específica)
Em termos práticos, a execução tem um comportamento de “camadas”:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
- Antes (pré-processamento): executa do mais externo para o mais interno (tipicamente: globais → grupo → rota → handler).
- Depois (pós-processamento): ao retornar a resposta, a ordem se inverte (handler → rota → grupo → globais).
Isso permite que um middleware faça validações antes e, após chamar o próximo, normalize headers, logue o resultado, etc.
Exemplo mental de fluxo
Global A (before) -> Global B (before) -> Group C (before) -> Route D (before) -> Handler (rota) -> Route D (after) -> Group C (after) -> Global B (after) -> Global A (after)Observação: a forma exata de empilhamento pode variar conforme você adiciona middlewares e como organiza grupos, mas a regra de ouro é: quem envolve por fora executa antes e finaliza por último.
Criando um middleware PSR-15 (passo a passo)
1) Estrutura mínima
Crie uma classe que implemente Psr\Http\Server\MiddlewareInterface:
<?php declare(strict_types=1); namespace App\Middleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; final class ExampleMiddleware implements MiddlewareInterface { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { // 1) antes do próximo handler // ... // 2) delega para o próximo $response = $handler->handle($request); // 3) depois do próximo handler // ... return $response; } }2) Evite estado mutável
Middlewares devem ser preferencialmente stateless (sem armazenar dados por requisição em propriedades). Para passar dados adiante, use:
$request = $request->withAttribute('chave', $valor)- Headers (quando fizer sentido)
3) Como interromper o pipeline
Se uma condição falhar (ex.: autenticação), você pode retornar uma resposta imediatamente, sem chamar $handler->handle().
Aplicando middlewares: global, grupo e rota
Global (toda a aplicação)
Use $app->add() para adicionar um middleware global. A ordem de adição importa para o empilhamento.
// Exemplo: $app->add(new SomeMiddleware());Por grupo
Ao agrupar rotas, aplique middlewares ao grupo para compartilhar responsabilidades (ex.: autenticação em /api, CORS em /api, etc.).
// Exemplo conceitual: $group = $app->group('/api', function(...) { ... }); // $group->add(new AuthMiddleware(...));Por rota
Para casos específicos (ex.: rate limit apenas em uma rota sensível), aplique diretamente na rota.
// Exemplo conceitual: $app->get('/login', LoginAction::class)->add(new RateLimitMiddleware(...));Middlewares comuns (implementações práticas)
1) Logging (request/response)
Objetivo: registrar método, path, status e tempo. Use um logger (ex.: PSR-3) injetado no middleware.
<?php declare(strict_types=1); namespace App\Middleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\Log\LoggerInterface; final class LoggingMiddleware implements MiddlewareInterface { public function __construct(private LoggerInterface $logger) {} public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $start = microtime(true); $method = $request->getMethod(); $path = $request->getUri()->getPath(); $response = $handler->handle($request); $ms = (microtime(true) - $start) * 1000; $status = $response->getStatusCode(); $this->logger->info('http_request', [ 'method' => $method, 'path' => $path, 'status' => $status, 'duration_ms' => (int) $ms, ]); return $response; } }Dica: se você também implementar correlação (request-id), inclua o ID no contexto do log.
2) CORS (Cross-Origin Resource Sharing)
Objetivo: permitir que navegadores chamem sua API a partir de outros domínios. Um middleware de CORS normalmente:
- Responde
OPTIONS(preflight) imediatamente com headers adequados - Adiciona headers CORS na resposta de requisições normais
<?php declare(strict_types=1); namespace App\Middleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Slim\Psr7\Response; final class CorsMiddleware implements MiddlewareInterface { public function __construct( private string $allowOrigin = '*', private string $allowMethods = 'GET,POST,PUT,PATCH,DELETE,OPTIONS', private string $allowHeaders = 'Content-Type, Authorization, X-Request-Id', private string $exposeHeaders = 'X-Request-Id' ) {} public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if (strtoupper($request->getMethod()) === 'OPTIONS') { $response = new Response(204); return $this->withCorsHeaders($response); } $response = $handler->handle($request); return $this->withCorsHeaders($response); } private function withCorsHeaders(ResponseInterface $response): ResponseInterface { return $response ->withHeader('Access-Control-Allow-Origin', $this->allowOrigin) ->withHeader('Access-Control-Allow-Methods', $this->allowMethods) ->withHeader('Access-Control-Allow-Headers', $this->allowHeaders) ->withHeader('Access-Control-Expose-Headers', $this->exposeHeaders); } }Boas práticas: em produção, evite * se você usa cookies/credenciais; prefira lista de origens confiáveis.
3) Autenticação (Bearer token simples)
Objetivo: validar credenciais e anexar o “usuário autenticado” na requisição via attributes. O middleware deve:
- Ler
Authorization: Bearer <token> - Validar o token (via serviço injetado)
- Se inválido, retornar 401
- Se válido, adicionar
user(ouauth) em$request->withAttribute()
<?php declare(strict_types=1); namespace App\Middleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Slim\Psr7\Response; interface TokenValidatorInterface { /** @return array{id:string,roles:string[]} */ public function validate(string $token): array; } final class AuthMiddleware implements MiddlewareInterface { public function __construct(private TokenValidatorInterface $validator) {} public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $auth = $request->getHeaderLine('Authorization'); if (!preg_match('/^Bearer\s+(.*)$/i', $auth, $m)) { return $this->unauthorized('missing_token'); } $token = trim($m[1]); try { $user = $this->validator->validate($token); } catch (\Throwable $e) { return $this->unauthorized('invalid_token'); } $request = $request->withAttribute('user', $user); return $handler->handle($request); } private function unauthorized(string $code): ResponseInterface { $response = new Response(401); $response->getBody()->write(json_encode([ 'error' => 'unauthorized', 'code' => $code ], JSON_UNESCAPED_SLASHES)); return $response->withHeader('Content-Type', 'application/json'); } }Aplicação típica: adicionar esse middleware em um grupo /api ou em rotas privadas, mantendo rotas públicas fora do grupo.
4) Rate limiting simples (por IP + rota)
Objetivo: limitar chamadas por janela de tempo. Uma versão simples pode usar armazenamento em memória (útil para desenvolvimento) ou cache compartilhado (Redis) em produção. Abaixo um exemplo didático com um storage abstrato.
<?php declare(strict_types=1); namespace App\Middleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Slim\Psr7\Response; interface RateLimitStoreInterface { public function increment(string $key, int $ttlSeconds): int; } final class RateLimitMiddleware implements MiddlewareInterface { public function __construct( private RateLimitStoreInterface $store, private int $limit = 60, private int $windowSeconds = 60 ) {} public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $ip = $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown'; $routeKey = $request->getMethod() . ':' . $request->getUri()->getPath(); $key = 'rl:' . sha1($ip . '|' . $routeKey); $count = $this->store->increment($key, $this->windowSeconds); if ($count > $this->limit) { $response = new Response(429); $response->getBody()->write(json_encode([ 'error' => 'too_many_requests' ], JSON_UNESCAPED_SLASHES)); return $response ->withHeader('Content-Type', 'application/json') ->withHeader('Retry-After', (string) $this->windowSeconds); } $response = $handler->handle($request); return $response ->withHeader('X-RateLimit-Limit', (string) $this->limit) ->withHeader('X-RateLimit-Window', (string) $this->windowSeconds); } }Passo a passo para usar com segurança:
- Comece aplicando em rotas críticas (login, recuperação de senha).
- Depois mova para um grupo (ex.:
/api) com limites diferentes por subgrupo. - Em produção, implemente
RateLimitStoreInterfacecom Redis/Memcached para consistência entre instâncias.
5) Normalização de resposta (JSON padrão + headers)
Objetivo: padronizar respostas, adicionando headers e garantindo Content-Type consistente. Um middleware pode:
- Adicionar headers comuns (ex.:
X-Content-Type-Options) - Garantir que respostas JSON tenham
Content-Type: application/json - Opcionalmente, envelopar payloads (ex.:
{"data": ...}) quando aplicável
Como o Slim trabalha com handlers que podem escrever no body, uma normalização agressiva (re-serializar body) pode ser intrusiva. Um caminho seguro é focar em headers e em um formato de erro consistente.
<?php declare(strict_types=1); namespace App\Middleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; final class ResponseNormalizationMiddleware implements MiddlewareInterface { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $response = $handler->handle($request); $response = $response ->withHeader('X-Content-Type-Options', 'nosniff') ->withHeader('X-Frame-Options', 'DENY'); // Se já houver Content-Type, respeite. Caso contrário, defina JSON por padrão. if (!$response->hasHeader('Content-Type')) { $response = $response->withHeader('Content-Type', 'application/json'); } return $response; } }Dica: para padronizar erros (400/401/403/404/500), combine este middleware com um error handler central (sem duplicar lógica em cada action).
6) Correlação de request (Request-Id)
Objetivo: gerar/propagar um identificador único por requisição para rastrear logs e chamadas entre serviços. O middleware deve:
- Ler
X-Request-Idse o cliente já enviou - Se não houver, gerar um ID
- Adicionar o ID em
$request->withAttribute()para uso interno - Adicionar o header
X-Request-Idna resposta
<?php declare(strict_types=1); namespace App\Middleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; final class RequestIdMiddleware implements MiddlewareInterface { public function __construct(private string $headerName = 'X-Request-Id') {} public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $incoming = trim($request->getHeaderLine($this->headerName)); $requestId = $incoming !== '' ? $incoming : $this->generateRequestId(); $request = $request->withAttribute('request_id', $requestId); $response = $handler->handle($request); return $response->withHeader($this->headerName, $requestId); } private function generateRequestId(): string { // 16 bytes => 32 hex chars return bin2hex(random_bytes(16)); } }Integração com logging: no LoggingMiddleware, leia $request->getAttribute('request_id') e inclua no contexto do log.
Compondo middlewares com responsabilidades bem definidas
Separação por responsabilidade
Evite criar um “middleware monolítico” que faz CORS + auth + log + rate limit. Prefira middlewares pequenos e composáveis:
- RequestIdMiddleware: apenas correlação
- LoggingMiddleware: apenas log (mas pode usar request-id se existir)
- CorsMiddleware: apenas CORS
- AuthMiddleware: apenas autenticação
- RateLimitMiddleware: apenas limitação
- ResponseNormalizationMiddleware: apenas headers/padrões
Ordem sugerida (exemplo prático)
Uma ordem comum para maximizar rastreabilidade e consistência:
| Nível | Middleware | Por quê |
|---|---|---|
| Global (primeiros) | RequestId | Garante ID para todo o resto (logs, erros) |
| Global | Logging | Registra tudo, incluindo falhas e tempos |
| Global ou /api | CORS | Responde preflight cedo e padroniza headers |
| Grupo privado | Auth | Bloqueia acesso antes de executar actions |
| Rota sensível | RateLimit | Limita pontos críticos sem penalizar tudo |
| Global (últimos) | ResponseNormalization | Aplica headers finais em qualquer resposta |
Observação: “primeiros” e “últimos” aqui se referem ao empilhamento desejado. Como a pilha é em camadas, teste e ajuste para garantir que headers finais sejam aplicados mesmo em respostas curtas (ex.: 401/429).
Evitando dependências circulares ao reutilizar middlewares
Problema comum
Dependência circular aparece quando:
- Middleware A depende de Serviço B
- Serviço B depende de algo que, direta ou indiretamente, depende do Middleware A
Isso costuma acontecer quando middlewares passam a “conhecer” detalhes de controllers, rotas ou do container de DI, ou quando serviços de domínio tentam acessar o pipeline HTTP.
Estratégias práticas para evitar
- Dependa de interfaces pequenas: por exemplo,
TokenValidatorInterface,RateLimitStoreInterface,LoggerInterface. O middleware não deve depender de controllers nem de serviços que dependam do middleware. - Não injete o App/Router no middleware: se precisar de dados de rota, use atributos do request (quando disponíveis) ou aplique o middleware no local correto (grupo/rota).
- Use request attributes como contrato: ex.:
request_id,user. Outros componentes leem esses atributos sem precisar conhecer o middleware. - Evite “middleware chamando middleware”: composição deve ser feita pelo pipeline (global/grupo/rota), não por instanciar um middleware dentro de outro.
- Factories para construção: se a criação do middleware exige várias dependências, use uma factory dedicada (ou configuração do container) para montar o middleware sem acoplar camadas.
Padrão de composição recomendado
Em vez de um middleware “AuthAndRateLimitMiddleware”, componha assim:
// Grupo /api privado: AuthMiddleware // Rota /login: RateLimitMiddleware // Global: RequestIdMiddleware, LoggingMiddleware, ResponseNormalizationMiddlewareAssim, cada peça é reutilizável e você evita que um middleware precise conhecer regras de outro.
Checklist rápido de implementação
- Todo middleware PSR-15 implementa
process()e decide se chama$handler->handle(). - Use
withAttribute()para dados internos (user, request-id). - Use headers para comunicação com cliente (CORS, X-Request-Id, rate limit).
- Prefira interfaces para dependências (logger, validator, store).
- Componha por nível (global/grupo/rota) em vez de misturar responsabilidades.