Documentação

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

  1. Crie Catálogo Exemplo com módulo CatalogoExemplo.
  2. Aguarde o banco individual ser criado.
  3. Abra a IDE. A plataforma adiciona /catalogoexemplo às rotas automaticamente.
Contrato público das rotas

$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
CamadaResponsabilidade
ControllerTraduz HTTP; não concentra SQL ou regras.
DTO/ValidatorNormalizam e validam entrada.
EntityRepresenta o domínio.
RepositoryConcentra persistência.
ServiceOrquestra regras e transações.
MiddlewareAplica 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

  1. Sincronize pacotes, valide/execute migrations e execute seeders.
  2. Use Analisar código e corrija os erros.
  3. No API Route Tester, teste: público 200; privado sem chave 401/403; privado com chave 200; validação 422; limite 429; inexistente 404.
  4. Teste GraphQL válido, campo desconhecido, query profunda e privado sem chave.
  5. Teste webhook com assinatura correta, alterada, expirada e repetida.
  6. Configure origens CORS exatas, gere a credencial em Segurança do projeto e clique em Ativar.
  7. Valide a URL externa e acompanhe logs de aplicação, runtime e segurança.

Checklist final