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
- Crie o projeto na IDE com nome do módulo
Nota - Substitua cada arquivo pelo código acima
- Clique em "Analisar código" — deve aparecer "Módulo aprovado"
- Clique em "Ativar" — instala deps, roda migrations e ativa as rotas
- Pressione
Ctrl + Te 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}