v1.0
Acessar api.vupi.us

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.

O que este exemplo demonstra

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

  1. Crie o projeto na IDE com nome do módulo Comentario
  2. Crie os arquivos com o código acima
  3. Clique em "Analisar código""Publicar""Ativar"
  4. Pressione Ctrl + T e 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": "...", ...}}
Middleware própria funcionando

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.