Exemplo com Middleware Própria
Módulo Comentario com todas as camadas + middleware própria que valida um header customizado antes de processar a requisição.
Como criar uma middleware própria do módulo que verifica um header X-App-Version obrigatório. A middleware é usada nas rotas junto com AuthHybridMiddleware, mostrando como combinar middlewares do kernel com middlewares próprias.
Estrutura do módulo Comentario
Comentario/
├── Controllers/ComentarioController.php
├── Services/ComentarioService.php
├── Repositories/ComentarioRepository.php
├── Entities/Comentario.php
├── Middlewares/AppVersionMiddleware.php ← middleware própria
├── Database/
│ ├── connection.php
│ ├── Migrations/2026_01_01_000001_create_comentarios_table.php
│ └── Seeders/ComentarioSeeder.php
└── Routes/web.php
Database/connection.php
<?php
// 'auto' → prioriza conexão personalizada da IDE (se configurada)
return 'auto';
Database/Migrations/2026_01_01_000001_create_comentarios_table.php
<?php
use PDO;
return [
'up' => function (PDO $pdo): void {
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
if ($driver === 'pgsql') {
$pdo->exec("CREATE TABLE IF NOT EXISTS comentarios (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
texto TEXT NOT NULL,
autor_id VARCHAR(255) NOT NULL,
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
)");
} else {
$pdo->exec("CREATE TABLE IF NOT EXISTS comentarios (
id CHAR(36) PRIMARY KEY,
texto TEXT NOT NULL,
autor_id VARCHAR(255) NOT NULL,
criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
}
},
'down' => function (PDO $pdo): void {
$pdo->exec("DROP TABLE IF EXISTS comentarios");
},
];
Database/Seeders/ComentarioSeeder.php
<?php
use PDO;
return function (PDO $pdo): void {
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
$id = $driver === 'pgsql'
? $pdo->query('SELECT gen_random_uuid()')->fetchColumn()
: bin2hex(random_bytes(16));
$pdo->prepare(
"INSERT INTO comentarios (id, texto, autor_id) VALUES (?, ?, ?)"
)->execute([$id, 'Primeiro comentario de exemplo!', 'sistema']);
};
Entities/Comentario.php
<?php
namespace Src\Modules\Comentario\Entities;
final class Comentario
{
public function __construct(
private readonly string $id,
private readonly string $texto,
private readonly string $autorId,
private readonly string $criadoEm,
) {}
public function getId(): string { return $this->id; }
public function getTexto(): string { return $this->texto; }
public function getAutorId(): string { return $this->autorId; }
public function getCriadoEm(): string { return $this->criadoEm; }
public function toArray(): array
{
return [
'id' => $this->id,
'texto' => $this->texto,
'autor_id' => $this->autorId,
'criado_em' => $this->criadoEm,
];
}
public static function fromArray(array $data): self
{
return new self(
id: $data['id'],
texto: $data['texto'],
autorId: $data['autor_id'],
criadoEm: $data['criado_em'],
);
}
}
Repositories/ComentarioRepository.php
<?php
namespace Src\Modules\Comentario\Repositories;
use PDO;
use Src\Modules\Comentario\Entities\Comentario;
final class ComentarioRepository
{
private string $table = 'comentarios';
public function __construct(private readonly PDO $pdo) {}
/** @return Comentario[] */
public function findAll(): array
{
$stmt = $this->pdo->query(
"SELECT * FROM {$this->table} ORDER BY criado_em DESC"
);
return array_map(
fn(array $row) => Comentario::fromArray($row),
$stmt->fetchAll(PDO::FETCH_ASSOC)
);
}
public function findById(string $id): ?Comentario
{
$stmt = $this->pdo->prepare(
"SELECT * FROM {$this->table} WHERE id = ?"
);
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row !== false ? Comentario::fromArray($row) : null;
}
public function create(string $texto, string $autorId): Comentario
{
$driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
$id = $driver === 'pgsql'
? $this->pdo->query('SELECT gen_random_uuid()')->fetchColumn()
: bin2hex(random_bytes(16));
$this->pdo->prepare(
"INSERT INTO {$this->table} (id, texto, autor_id) VALUES (?, ?, ?)"
)->execute([$id, $texto, $autorId]);
return $this->findById($id) ?? Comentario::fromArray([
'id' => $id,
'texto' => $texto,
'autor_id' => $autorId,
'criado_em' => date('c'),
]);
}
public function delete(string $id): void
{
$this->pdo->prepare(
"DELETE FROM {$this->table} WHERE id = ?"
)->execute([$id]);
}
}
Services/ComentarioService.php
<?php
namespace Src\Modules\Comentario\Services;
use Src\Modules\Comentario\Entities\Comentario;
use Src\Modules\Comentario\Repositories\ComentarioRepository;
final class ComentarioService
{
public function __construct(
private readonly ComentarioRepository $repository
) {}
/** @return array[] */
public function listar(): array
{
return array_map(
fn(Comentario $c) => $c->toArray(),
$this->repository->findAll()
);
}
public function criar(string $texto, string $autorId): array
{
$texto = trim($texto);
if ($texto === '') {
throw new \InvalidArgumentException('O texto do comentario e obrigatorio.');
}
if (mb_strlen($texto) > 1000) {
throw new \InvalidArgumentException('O comentario deve ter no maximo 1000 caracteres.');
}
return $this->repository->create($texto, $autorId)->toArray();
}
public function buscar(string $id): ?array
{
return $this->repository->findById($id)?->toArray();
}
public function deletar(string $id): void
{
if ($this->repository->findById($id) === null) {
throw new \RuntimeException('Comentario nao encontrado.', 404);
}
$this->repository->delete($id);
}
}
Middlewares/AppVersionMiddleware.php
Esta middleware verifica se o cliente enviou o header X-App-Version. Se não enviou, bloqueia a requisição com erro 400. Isso simula um cenário real onde você quer garantir que apenas versões específicas do seu app possam usar a API.
<?php
namespace Src\Modules\Comentario\Middlewares;
use Src\Kernel\Contracts\MiddlewareInterface;
use Src\Kernel\Http\Request\Request;
use Src\Kernel\Http\Response\Response;
/**
* Verifica se o cliente enviou o header X-App-Version.
* Bloqueia requisicoes sem versao declarada.
*/
final class AppVersionMiddleware implements MiddlewareInterface
{
private const VERSOES_SUPORTADAS = ['1.0', '1.1', '2.0'];
public function handle(Request $request, callable $next): Response
{
$versao = $request->header('X-App-Version');
// Header ausente
if ($versao === null || trim($versao) === '') {
return Response::json([
'error' => 'Header X-App-Version e obrigatorio.',
'hint' => 'Envie o header: X-App-Version: 2.0',
], 400);
}
// Versao nao suportada
if (!in_array(trim($versao), self::VERSOES_SUPORTADAS, true)) {
return Response::json([
'error' => "Versao '{$versao}' nao suportada.",
'suportadas' => self::VERSOES_SUPORTADAS,
], 400);
}
// Versao valida — passa para o proximo middleware/controller
return $next($request);
}
}
Controllers/ComentarioController.php
<?php
namespace Src\Modules\Comentario\Controllers;
use Src\Kernel\Http\Request\Request;
use Src\Kernel\Http\Response\Response;
use Src\Modules\Comentario\Services\ComentarioService;
final class ComentarioController
{
public function __construct(
private readonly ComentarioService $service
) {}
public function listar(Request $request): Response
{
return Response::json(['comentarios' => $this->service->listar()]);
}
public function criar(Request $request): Response
{
$autorId = $request->attribute('auth_user')->getUuid()->toString();
try {
$comentario = $this->service->criar(
$request->body['texto'] ?? '',
$autorId
);
return Response::json(['comentario' => $comentario], 201);
} catch (\InvalidArgumentException $e) {
return Response::json(['error' => $e->getMessage()], 422);
}
}
public function buscar(Request $request): Response
{
$comentario = $this->service->buscar($request->params['id']);
if ($comentario === null) {
return Response::json(['error' => 'Comentario nao encontrado.'], 404);
}
return Response::json(['comentario' => $comentario]);
}
public function deletar(Request $request): Response
{
try {
$this->service->deletar($request->params['id']);
return Response::json(['deleted' => true]);
} catch (\RuntimeException $e) {
return Response::json(['error' => $e->getMessage()], (int) $e->getCode() ?: 400);
}
}
}
Routes/web.php
Aqui combinamos três middlewares: AppVersionMiddleware (própria), AuthHybridMiddleware (kernel) e AdminOnlyMiddleware (kernel). A ordem importa — são executadas da esquerda para a direita.
<?php
use Src\Modules\Comentario\Controllers\ComentarioController;
use Src\Modules\Comentario\Middlewares\AppVersionMiddleware;
use Src\Kernel\Middlewares\AuthHybridMiddleware;
use Src\Kernel\Middlewares\AdminOnlyMiddleware;
/** @var \Src\Kernel\Contracts\RouterInterface $router */
// Middleware própria do módulo + autenticação do kernel
$protegido = [AppVersionMiddleware::class, AuthHybridMiddleware::class];
// Apenas admin pode deletar
$admin = [AppVersionMiddleware::class, AuthHybridMiddleware::class, AdminOnlyMiddleware::class];
// Leitura: exige versão do app, mas não precisa de login
$publico = [AppVersionMiddleware::class];
$router->get('/api/comentario', [ComentarioController::class, 'listar'], $publico);
$router->post('/api/comentario', [ComentarioController::class, 'criar'], $protegido);
$router->get('/api/comentario/{id}', [ComentarioController::class, 'buscar'], $publico);
$router->delete('/api/comentario/{id}', [ComentarioController::class, 'deletar'], $admin);
Fluxo com middleware própria
Request HTTP
↓
AppVersionMiddleware ← verifica X-App-Version (middleware própria do módulo)
↓
AuthHybridMiddleware ← verifica token JWT (middleware do kernel)
↓
ComentarioController ← processa a requisição
↓
ComentarioService ← valida e aplica regras de negócio
↓
ComentarioRepository ← executa SQL, retorna Entities
↓
Response::json() ← retorna JSON
Publicar e testar
- Crie o projeto na IDE com nome do módulo
Comentario - Crie os arquivos com o código acima
- Clique em "Analisar código" → "Publicar" → "Ativar"
- Pressione
Ctrl + Te teste:
// Sem o header — deve retornar erro 400
GET /api/comentario
// Resposta: {"error": "Header X-App-Version e obrigatorio.", "hint": "..."}
// Com versao invalida — deve retornar erro 400
GET /api/comentario
X-App-Version: 3.0
// Resposta: {"error": "Versao '3.0' nao suportada.", "suportadas": [...]}
// Correto — deve listar comentarios
GET /api/comentario
X-App-Version: 2.0
// Resposta: {"comentarios": [...]}
// Criar comentario (precisa de login + versao)
POST /api/comentario
X-App-Version: 2.0
Authorization: Bearer {token}
{"texto": "Meu primeiro comentario!"}
// Resposta: {"comentario": {"id": "...", "texto": "...", ...}}
A AppVersionMiddleware é executada antes de qualquer outra lógica. Se o header estiver errado, a requisição é bloqueada imediatamente — o controller nem chega a ser chamado. Você pode criar quantas middlewares próprias quiser e combiná-las livremente com as do kernel.