Projeto completo na IDE: API de catálogo
Este tutorial começa em um projeto vazio e termina com REST, GraphQL, webhooks, banco individual, camadas de aplicação, segurança por rota e testes. Todo o trabalho é feito na IDE.
1. Criar o projeto
- Crie
Catálogo Exemplocom móduloCatalogoExemplo. - Aguarde o banco individual ser criado.
- Abra a IDE. A plataforma adiciona
/catalogoexemploàs rotas automaticamente.
$router, $autenticada e $administrativa são fornecidos em Routes/web.php. Sem autenticação a rota é pública; com $autenticada exige credencial válida do projeto.
2. Estrutura
CatalogoExemplo/
├── Controllers/ProdutoController.php
├── DTOs/CriarProdutoDTO.php
├── Entities/Produto.php
├── Exceptions/RegraDeNegocioException.php
├── GraphQL/CatalogoSchema.php
├── Middlewares/ExigirCabecalhoCliente.php
├── Repositories/ProdutoRepository.php
├── Routes/web.php
├── Services/ProdutoService.php
├── Validators/ProdutoValidator.php
├── Database/Migrations/001_create_produtos.sql
├── Database/Seeders/001_seed_produtos.sql
├── Storage/
├── tests/ProdutoValidatorTest.php
├── .env.example
└── composer.json
| Camada | Responsabilidade |
|---|---|
| Controller | Traduz HTTP; não concentra SQL ou regras. |
| DTO/Validator | Normalizam e validam entrada. |
| Entity | Representa o domínio. |
| Repository | Concentra persistência. |
| Service | Orquestra regras e transações. |
| Middleware | Aplica políticas antes do controller. |
3. Dependências
{
"name": "exemplo/catalogo",
"type": "project",
"require": { "php": ">=8.3", "webonyx/graphql-php": "^15.0" },
"autoload": { "psr-4": { "CatalogoExemplo\\": "" } }
}
Salve e clique em Sincronizar pacotes. O lock e o vendor pertencem ao projeto.
4. Migration e seeder
Database/Migrations/001_create_produtos.sql:
CREATE TABLE IF NOT EXISTS produtos (
id UUID PRIMARY KEY,
nome VARCHAR(120) NOT NULL,
preco_centavos INTEGER NOT NULL CHECK (preco_centavos >= 0),
ativo BOOLEAN NOT NULL DEFAULT TRUE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_produtos_ativos ON produtos (ativo, criado_em DESC);
Database/Seeders/001_seed_produtos.sql:
INSERT INTO produtos (id,nome,preco_centavos)
VALUES ('00000000-0000-4000-8000-000000000001','Produto de demonstração',1990)
ON CONFLICT (id) DO NOTHING;
Use Validar migrations, Executar migrations e Executar seeders. Não altere uma migration já aplicada; crie outra. Seeders devem ser idempotentes.
5. Entity, DTO e Validator
// Entities/Produto.php
namespace CatalogoExemplo\Entities;
final readonly class Produto {
public function __construct(public string $id,public string $nome,public int $precoCentavos,public bool $ativo) {}
}
// DTOs/CriarProdutoDTO.php
namespace CatalogoExemplo\DTOs;
final readonly class CriarProdutoDTO {
public function __construct(public string $nome,public int $precoCentavos) {}
public static function fromArray(array $d): self { return new self(trim((string)($d['nome']??'')),(int)($d['preco_centavos']??-1)); }
}
// Validators/ProdutoValidator.php
namespace CatalogoExemplo\Validators;
final class ProdutoValidator {
public static function validar(\CatalogoExemplo\DTOs\CriarProdutoDTO $dto): array {
$e=[];
if ($dto->nome==='' || mb_strlen($dto->nome)>120) $e['nome']='Informe um nome com até 120 caracteres.';
if ($dto->precoCentavos<0) $e['preco_centavos']='Informe um preço válido.';
return $e;
}
}
6. Repository, Service e Controller
// Repository: somente persistência e prepared statements
$stmt=$pdo->prepare('INSERT INTO produtos (id,nome,preco_centavos) VALUES (:id,:nome,:preco)');
$stmt->execute(['id'=>$id,'nome'=>$dto->nome,'preco'=>$dto->precoCentavos]);
// Service: validação e regra de negócio
$erros=ProdutoValidator::validar($dto);
if ($erros!==[]) return ['ok'=>false,'erros'=>$erros];
return $repository->criar($dto);
// Controller: traduz a requisição
public function criar($request): array {
return $this->service->criar(CriarProdutoDTO::fromArray($request->body));
}
Use parâmetros no SQL. O controller pode receber a requisição sem importar namespace interno. Nunca exponha stack trace, SQL, caminhos ou segredos.
7. Middleware próprio
namespace CatalogoExemplo\Middlewares;
final class ExigirCabecalhoCliente {
public function handle($request,callable $next) {
if (trim((string)$request->header('X-Cliente-Versao'))==='') return ['erro'=>'Informe X-Cliente-Versao.'];
return $next($request);
}
}
8. Rotas públicas, privadas, administrativas e rate limit
use CatalogoExemplo\Controllers\ProdutoController;
use CatalogoExemplo\Middlewares\ExigirCabecalhoCliente;
use Src\Kernel\Middlewares\RateLimitMiddleware;
$leitura=[[RateLimitMiddleware::class,['limit'=>120,'window'=>60,'key'=>'catalogo.leitura']]];
$escrita=[[RateLimitMiddleware::class,['limit'=>20,'window'=>60,'key'=>'catalogo.escrita']]];
$router->get('/rest/produtos',[ProdutoController::class,'listar'],$leitura);
$router->post('/rest/produtos',[ProdutoController::class,'criar'],array_merge($escrita,$autenticada,[ExigirCabecalhoCliente::class]));
$router->delete('/rest/produtos/{id}',[ProdutoController::class,'excluir'],$administrativa);
Cada rota pode ter limite, janela e chave próprios. CORS não é autenticação.
9. GraphQL e webhooks
$router->graphql('/graphql/publico',[GraphqlController::class,'executar'],$leitura);
$router->graphql('/graphql/privado',[GraphqlController::class,'executar'],array_merge($escrita,$autenticada));
$router->post('/webhooks/publico',[WebhookController::class,'receberPublico'],$escrita);
$router->webhook('/webhooks/assinado',[WebhookController::class,'receberAssinado'],'CATALOGO_WEBHOOK_SECRET',$escrita);
GraphQL deve limitar corpo, profundidade e complexidade, validar variables e autorizar campos nos resolvers. Em .env.example, documente CATALOGO_WEBHOOK_SECRET=; o segredo real fica no ambiente privado. O webhook usa HMAC-SHA256 de timestamp.corpo_bruto, timestamp recente e bloqueio de replay; o controller ainda valida evento, schema e idempotência.
10. Testes e ativação
- Sincronize pacotes, valide/execute migrations e execute seeders.
- Use Analisar código e corrija os erros.
- No API Route Tester, teste: público 200; privado sem chave 401/403; privado com chave 200; validação 422; limite 429; inexistente 404.
- Teste GraphQL válido, campo desconhecido, query profunda e privado sem chave.
- Teste webhook com assinatura correta, alterada, expirada e repetida.
- Configure origens CORS exatas, gere a credencial em Segurança do projeto e clique em Ativar.
- Valide a URL externa e acompanhe logs de aplicação, runtime e segurança.
Checklist final
- Namespaces e caminhos correspondem; autoload sincronizado.
- Entrada validada; SQL parametrizado; banco individual.
- Rotas privadas recusam ausência de credencial e autorizam o recurso.
- Rate limit definido conforme o custo de cada rota.
- GraphQL limita custo; webhooks impedem falsificação e replay.
- Segredos não aparecem no repositório nem nos logs.
- Testes cobrem sucesso, validação, autenticação, autorização, conflito e limite.