v1.0
Acessar api.vupi.us

Exemplo Completo — Módulo com Todas as Camadas

Módulo Nota com Controller, Service, Repository, Entity, Migration, Seeder e Routes — cada camada se comunicando com a próxima.

Módulo sem Controller também funciona

Você pode usar closures diretamente nas rotas, sem criar um Controller. O sistema aceita qualquer callable como handler:

$router->get('/api/ping', function (Request $request): Response {
    return Response::json(['pong' => true]);
});

Estrutura do módulo Nota

Nota/
├── Controllers/NotaController.php
├── Services/NotaService.php
├── Repositories/NotaRepository.php
├── Entities/Nota.php
├── Database/
│   ├── connection.php
│   ├── Migrations/2026_01_01_000001_create_notas_table.php
│   └── Seeders/NotaSeeder.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_notas_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 notas (
                id          UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
                titulo      VARCHAR(255) NOT NULL,
                conteudo    TEXT         NOT NULL DEFAULT '',
                user_id     VARCHAR(255) NOT NULL,
                criado_em   TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
                atualizado_em TIMESTAMPTZ NULL
            )");
        } else {
            $pdo->exec("CREATE TABLE IF NOT EXISTS notas (
                id            CHAR(36)     PRIMARY KEY,
                titulo        VARCHAR(255) NOT NULL,
                conteudo      TEXT         NOT NULL DEFAULT '',
                user_id       VARCHAR(255) NOT NULL,
                criado_em     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
                atualizado_em DATETIME     NULL ON UPDATE CURRENT_TIMESTAMP
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        }
    },
    'down' => function (PDO $pdo): void {
        $pdo->exec("DROP TABLE IF EXISTS notas");
    },
];

Database/Seeders/NotaSeeder.php

<?php

use PDO;

return function (PDO $pdo): void {
    // Seeder de exemplo — insere uma nota de demonstração
    // Em produção, remova ou adapte conforme necessário
    $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 notas (id, titulo, conteudo, user_id) VALUES (?, ?, ?, ?)"
    )->execute([$id, 'Nota de boas-vindas', 'Bem-vindo ao módulo Nota!', 'sistema']);
};

Entities/Nota.php

<?php

namespace Src\Modules\Nota\Entities;

final class Nota
{
    public function __construct(
        private readonly string $id,
        private string $titulo,
        private string $conteudo,
        private readonly string $userId,
        private readonly string $criadoEm,
    ) {}

    public function getId(): string      { return $this->id; }
    public function getTitulo(): string  { return $this->titulo; }
    public function getConteudo(): string { return $this->conteudo; }
    public function getUserId(): string  { return $this->userId; }
    public function getCriadoEm(): string { return $this->criadoEm; }

    public function atualizar(string $titulo, string $conteudo): void
    {
        $this->titulo   = $titulo;
        $this->conteudo = $conteudo;
    }

    public function toArray(): array
    {
        return [
            'id'        => $this->id,
            'titulo'    => $this->titulo,
            'conteudo'  => $this->conteudo,
            'user_id'   => $this->userId,
            'criado_em' => $this->criadoEm,
        ];
    }

    public static function fromArray(array $data): self
    {
        return new self(
            id:        $data['id'],
            titulo:    $data['titulo'],
            conteudo:  $data['conteudo'],
            userId:    $data['user_id'],
            criadoEm:  $data['criado_em'],
        );
    }
}

Repositories/NotaRepository.php

<?php

namespace Src\Modules\Nota\Repositories;

use PDO;
use Src\Modules\Nota\Entities\Nota;

final class NotaRepository
{
    private string $table = 'notas';

    public function __construct(private readonly PDO $pdo) {}

    /** @return Nota[] */
    public function findByUser(string $userId): array
    {
        $stmt = $this->pdo->prepare(
            "SELECT * FROM {$this->table} WHERE user_id = ? ORDER BY criado_em DESC"
        );
        $stmt->execute([$userId]);
        return array_map(
            fn(array $row) => Nota::fromArray($row),
            $stmt->fetchAll(PDO::FETCH_ASSOC)
        );
    }

    public function findById(string $id, string $userId): ?Nota
    {
        $stmt = $this->pdo->prepare(
            "SELECT * FROM {$this->table} WHERE id = ? AND user_id = ?"
        );
        $stmt->execute([$id, $userId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row !== false ? Nota::fromArray($row) : null;
    }

    public function save(Nota $nota): void
    {
        $driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
        $now    = $driver === 'pgsql' ? 'NOW()' : 'CURRENT_TIMESTAMP';

        $exists = $this->pdo->prepare(
            "SELECT 1 FROM {$this->table} WHERE id = ?"
        );
        $exists->execute([$nota->getId()]);

        if ($exists->fetchColumn()) {
            $this->pdo->prepare(
                "UPDATE {$this->table}
                 SET titulo = ?, conteudo = ?, atualizado_em = {$now}
                 WHERE id = ? AND user_id = ?"
            )->execute([$nota->getTitulo(), $nota->getConteudo(), $nota->getId(), $nota->getUserId()]);
        } else {
            $this->pdo->prepare(
                "INSERT INTO {$this->table} (id, titulo, conteudo, user_id)
                 VALUES (?, ?, ?, ?)"
            )->execute([$nota->getId(), $nota->getTitulo(), $nota->getConteudo(), $nota->getUserId()]);
        }
    }

    public function delete(string $id, string $userId): void
    {
        $this->pdo->prepare(
            "DELETE FROM {$this->table} WHERE id = ? AND user_id = ?"
        )->execute([$id, $userId]);
    }

    private function generateId(): string
    {
        $driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
        return $driver === 'pgsql'
            ? $this->pdo->query('SELECT gen_random_uuid()')->fetchColumn()
            : bin2hex(random_bytes(16));
    }

    public function createNew(string $titulo, string $conteudo, string $userId): Nota
    {
        $nota = Nota::fromArray([
            'id'        => $this->generateId(),
            'titulo'    => $titulo,
            'conteudo'  => $conteudo,
            'user_id'   => $userId,
            'criado_em' => date('c'),
        ]);
        $this->save($nota);
        return $nota;
    }
}

Services/NotaService.php

<?php

namespace Src\Modules\Nota\Services;

use Src\Modules\Nota\Entities\Nota;
use Src\Modules\Nota\Repositories\NotaRepository;

final class NotaService
{
    public function __construct(
        private readonly NotaRepository $repository
    ) {}

    /** @return array[] */
    public function listar(string $userId): array
    {
        return array_map(
            fn(Nota $nota) => $nota->toArray(),
            $this->repository->findByUser($userId)
        );
    }

    public function criar(string $titulo, string $conteudo, string $userId): array
    {
        $titulo   = trim($titulo);
        $conteudo = trim($conteudo);

        if ($titulo === '') {
            throw new \InvalidArgumentException('O titulo e obrigatorio.');
        }

        return $this->repository->createNew($titulo, $conteudo, $userId)->toArray();
    }

    public function buscar(string $id, string $userId): ?array
    {
        $nota = $this->repository->findById($id, $userId);
        return $nota?->toArray();
    }

    public function atualizar(string $id, string $userId, string $titulo, string $conteudo): array
    {
        $nota = $this->repository->findById($id, $userId);
        if ($nota === null) {
            throw new \RuntimeException('Nota nao encontrada.', 404);
        }

        $titulo = trim($titulo);
        if ($titulo === '') {
            throw new \InvalidArgumentException('O titulo e obrigatorio.');
        }

        $nota->atualizar($titulo, trim($conteudo));
        $this->repository->save($nota);
        return $nota->toArray();
    }

    public function deletar(string $id, string $userId): void
    {
        if ($this->repository->findById($id, $userId) === null) {
            throw new \RuntimeException('Nota nao encontrada.', 404);
        }
        $this->repository->delete($id, $userId);
    }
}

Controllers/NotaController.php

<?php

namespace Src\Modules\Nota\Controllers;

use Src\Kernel\Http\Request\Request;
use Src\Kernel\Http\Response\Response;
use Src\Modules\Nota\Services\NotaService;

final class NotaController
{
    public function __construct(
        private readonly NotaService $service
    ) {}

    public function listar(Request $request): Response
    {
        $userId = $request->attribute('auth_user')->getUuid()->toString();
        return Response::json(['notas' => $this->service->listar($userId)]);
    }

    public function criar(Request $request): Response
    {
        $userId = $request->attribute('auth_user')->getUuid()->toString();
        try {
            $nota = $this->service->criar(
                $request->body['titulo']   ?? '',
                $request->body['conteudo'] ?? '',
                $userId
            );
            return Response::json(['nota' => $nota], 201);
        } catch (\InvalidArgumentException $e) {
            return Response::json(['error' => $e->getMessage()], 422);
        }
    }

    public function buscar(Request $request): Response
    {
        $userId = $request->attribute('auth_user')->getUuid()->toString();
        $nota   = $this->service->buscar($request->params['id'], $userId);
        if ($nota === null) {
            return Response::json(['error' => 'Nota nao encontrada.'], 404);
        }
        return Response::json(['nota' => $nota]);
    }

    public function atualizar(Request $request): Response
    {
        $userId = $request->attribute('auth_user')->getUuid()->toString();
        try {
            $nota = $this->service->atualizar(
                $request->params['id'],
                $userId,
                $request->body['titulo']   ?? '',
                $request->body['conteudo'] ?? ''
            );
            return Response::json(['nota' => $nota]);
        } catch (\InvalidArgumentException $e) {
            return Response::json(['error' => $e->getMessage()], 422);
        } catch (\RuntimeException $e) {
            return Response::json(['error' => $e->getMessage()], (int) $e->getCode() ?: 400);
        }
    }

    public function deletar(Request $request): Response
    {
        $userId = $request->attribute('auth_user')->getUuid()->toString();
        try {
            $this->service->deletar($request->params['id'], $userId);
            return Response::json(['deleted' => true]);
        } catch (\RuntimeException $e) {
            return Response::json(['error' => $e->getMessage()], (int) $e->getCode() ?: 400);
        }
    }
}

Routes/web.php

<?php

use Src\Modules\Nota\Controllers\NotaController;
use Src\Kernel\Middlewares\AuthHybridMiddleware;

/** @var \Src\Kernel\Contracts\RouterInterface $router */

$auth = [AuthHybridMiddleware::class];

$router->get('/api/nota',       [NotaController::class, 'listar'],    $auth);
$router->post('/api/nota',      [NotaController::class, 'criar'],     $auth);
$router->get('/api/nota/{id}',  [NotaController::class, 'buscar'],    $auth);
$router->put('/api/nota/{id}',  [NotaController::class, 'atualizar'], $auth);
$router->delete('/api/nota/{id}', [NotaController::class, 'deletar'], $auth);

Fluxo das camadas

Cada requisição percorre o seguinte caminho:

Request HTTP
    ↓
AuthHybridMiddleware  ← verifica token JWT, popula auth_user
    ↓
NotaController        ← recebe Request, extrai userId, chama Service
    ↓
NotaService           ← valida dados, aplica regras de negócio
    ↓
NotaRepository        ← executa SQL, converte rows em Entities
    ↓
Nota (Entity)         ← representa o dado com comportamento
    ↓
Response::json()      ← retorna JSON para o cliente

Publicar e testar

  1. Crie o projeto na IDE com nome do módulo Nota
  2. Substitua cada arquivo pelo código acima
  3. Clique em "Analisar código" — deve aparecer "Módulo aprovado"
  4. Clique em "Ativar" — instala deps, roda migrations e ativa as rotas
  5. Pressione Ctrl + T e teste:
// 1. Login
POST /api/login
{"login": "seu_usuario", "senha": "sua_senha"}

// 2. Criar nota
POST /api/nota
Authorization: Bearer {token}
{"titulo": "Minha nota", "conteudo": "Conteudo da nota"}

// 3. Listar notas
GET /api/nota
Authorization: Bearer {token}

// 4. Atualizar nota
PUT /api/nota/{id}
Authorization: Bearer {token}
{"titulo": "Titulo atualizado", "conteudo": "Novo conteudo"}

// 5. Deletar nota
DELETE /api/nota/{id}
Authorization: Bearer {token}