Objetivo do projeto e escopo
Neste projeto final, você vai implementar uma API completa de exemplo para gerenciamento de usuários e autenticação, aplicando em conjunto: rotas organizadas por domínio, handlers enxutos, validação/sanitização, middlewares (CORS, auth, logging), DI, configuração por ambiente, tratamento padronizado de erros e persistência via repositórios. A proposta é entregar um esqueleto realista, pronto para crescer com novas funcionalidades sem comprometer a arquitetura.
Funcionalidades do MVP
- Auth: login e refresh (ou re-login) retornando JWT.
- Users: criar usuário, listar usuários (paginado), obter usuário por id, atualizar e desativar.
- Infra: CORS, logging, autenticação via middleware, erros padronizados, repositórios com transação quando necessário.
Arquitetura e estrutura de pastas (referência do projeto)
Use uma estrutura que deixe explícitas as fronteiras entre HTTP (Slim), aplicação (casos de uso) e infraestrutura (DB, JWT, logger). Um exemplo:
project/ ├─ public/ │ └─ index.php ├─ config/ │ ├─ settings.php │ └─ routes.php ├─ src/ │ ├─ Http/ │ │ ├─ Actions/ │ │ ├─ Middlewares/ │ │ └─ Response/ │ ├─ Application/ │ │ ├─ UseCase/ │ │ ├─ DTO/ │ │ └─ Service/ │ ├─ Domain/ │ │ ├─ Entity/ │ │ └─ Repository/ │ └─ Infrastructure/ │ ├─ Persistence/ │ ├─ Security/ │ └─ Logging/ ├─ tests/ └─ composer.jsonRegra prática: o Slim (rotas/middlewares/actions) conhece a camada de aplicação, mas a camada de aplicação não conhece Slim. Repositórios são interfaces no domínio e implementações na infraestrutura.
Contrato de resposta e convenções da API
Defina um formato consistente para respostas de sucesso e erro. Isso reduz acoplamento no cliente e simplifica testes.
Resposta de sucesso
{ "data": { ... }, "meta": { ... }}Resposta de erro (padronizada)
{ "error": { "code": "validation_error", "message": "Invalid input", "details": [ { "field": "email", "issue": "invalid" } ], "traceId": "..." }}O traceId pode vir do logger/observabilidade e ajuda a correlacionar logs e erros.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Modelagem mínima: entidades e repositórios
Entidade User (domínio)
Mantenha a entidade com regras essenciais (ex.: e-mail obrigatório, status ativo/inativo). Evite colocar detalhes de HTTP/DB aqui.
final class User { public function __construct( public readonly string $id, public string $name, public string $email, public string $passwordHash, public bool $active, public readonly DateTimeImmutable $createdAt ) {} }Interfaces de repositório (domínio)
interface UserRepository { public function findById(string $id): ?User; public function findByEmail(string $email): ?User; /** @return array{items: User[], total: int} */ public function list(int $page, int $perPage): array; public function create(User $user): void; public function update(User $user): void; }Retornar array{items,total} facilita paginação e testes, sem expor SQL.
Passo a passo: implementando os casos de uso
O fluxo recomendado é: DTO de entrada → UseCase → Repository/Services → DTO de saída. As Actions HTTP apenas adaptam Request/Response.
1) DTOs de entrada e saída
Crie DTOs simples para transportar dados já validados/sanitizados.
final class CreateUserInput { public function __construct( public readonly string $name, public readonly string $email, public readonly string $password ) {} }final class UserOutput { public function __construct( public readonly string $id, public readonly string $name, public readonly string $email, public readonly bool $active ) {} }2) UseCase: CreateUser
O caso de uso orquestra: verificar duplicidade, gerar hash, persistir e retornar saída. Ele não deve conhecer Slim nem detalhes de PDO.
final class CreateUser { public function __construct( private UserRepository $users, private PasswordHasher $hasher, private IdGenerator $ids, private Clock $clock ) {} public function execute(CreateUserInput $in): UserOutput { $existing = $this->users->findByEmail($in->email); if ($existing) { throw new DomainException('email_already_in_use'); } $user = new User( id: $this->ids->newId(), name: $in->name, email: $in->email, passwordHash: $this->hasher->hash($in->password), active: true, createdAt: $this->clock->now() ); $this->users->create($user); return new UserOutput($user->id, $user->name, $user->email, $user->active); } }Ponto de qualidade: PasswordHasher, IdGenerator e Clock são abstrações para facilitar testes determinísticos.
3) UseCase: Login
O login valida credenciais e emite tokens via um serviço de segurança (infra). O caso de uso retorna um DTO com token e expiração.
final class LoginInput { public function __construct( public readonly string $email, public readonly string $password ) {} }final class TokenOutput { public function __construct( public readonly string $accessToken, public readonly int $expiresIn ) {} }final class Login { public function __construct( private UserRepository $users, private PasswordHasher $hasher, private TokenIssuer $tokens ) {} public function execute(LoginInput $in): TokenOutput { $user = $this->users->findByEmail($in->email); if (!$user || !$user->active) { throw new DomainException('invalid_credentials'); } if (!$this->hasher->verify($in->password, $user->passwordHash)) { throw new DomainException('invalid_credentials'); } return $this->tokens->issueForUser($user->id); } }HTTP: Actions enxutas + validação/sanitização
As Actions devem: ler o corpo/params, validar/sanitizar, montar DTO, chamar use case e serializar a resposta.
Action: POST /v1/users
final class CreateUserAction { public function __construct( private CreateUser $useCase, private InputValidator $validator, private JsonResponder $responder ) {} public function __invoke($request, $response): Psr\Http\Message\ResponseInterface { $payload = (array)($request->getParsedBody() ?? []); $data = $this->validator->validate($payload, [ 'name' => ['required', 'string', 'min:2'], 'email' => ['required', 'email'], 'password' => ['required', 'string', 'min:8'] ]); $out = $this->useCase->execute(new CreateUserInput( name: $data['name'], email: $data['email'], password: $data['password'] )); return $this->responder->created($response, ['data' => $out]); } }Observação: InputValidator representa o componente de validação/sanitização já adotado no projeto. A Action não deve conter regras de negócio.
Action: POST /v1/auth/login
final class LoginAction { public function __construct( private Login $useCase, private InputValidator $validator, private JsonResponder $responder ) {} public function __invoke($request, $response): Psr\Http\Message\ResponseInterface { $payload = (array)($request->getParsedBody() ?? []); $data = $this->validator->validate($payload, [ 'email' => ['required', 'email'], 'password' => ['required', 'string'] ]); $out = $this->useCase->execute(new LoginInput($data['email'], $data['password'])); return $this->responder->ok($response, ['data' => $out]); } }Rotas organizadas e versionadas
Centralize o registro em config/routes.php, agrupando por versão e domínio. Exemplo de desenho:
$app->group('/v1', function ($group) { $group->group('/auth', function ($auth) { $auth->post('/login', LoginAction::class); }); $group->group('/users', function ($users) { $users->post('', CreateUserAction::class); $users->get('', ListUsersAction::class); $users->get('/{id}', GetUserAction::class); $users->put('/{id}', UpdateUserAction::class); $users->delete('/{id}', DisableUserAction::class); });});Mantenha as rotas “finas”: sem closures com lógica, apenas apontando para Actions.
Middlewares aplicados no projeto (ordem e responsabilidade)
O comportamento final depende da ordem do pipeline. Um arranjo típico:
| Ordem | Middleware | Objetivo |
|---|---|---|
| 1 | CORS | Responder preflight e anexar headers. |
| 2 | RequestId/Trace | Gerar/propagar traceId. |
| 3 | Logging | Logar request/response com contexto. |
| 4 | ErrorHandler | Converter exceções em JSON padronizado. |
| 5 | Auth | Proteger rotas específicas e injetar usuário no request. |
Aplicando auth apenas onde precisa
Evite autenticar endpoints públicos (ex.: login). Aplique o middleware no grupo protegido:
$app->group('/v1', function ($group) { $group->group('/auth', function ($auth) { $auth->post('/login', LoginAction::class); }); $group->group('/users', function ($users) { $users->get('', ListUsersAction::class); $users->get('/{id}', GetUserAction::class); $users->put('/{id}', UpdateUserAction::class); $users->delete('/{id}', DisableUserAction::class); })->add(AuthMiddleware::class);});O endpoint de criação de usuário pode ser público (cadastro) ou protegido (admin). Decida no requisito e aplique o middleware conforme necessário.
DI e composição: registrando dependências essenciais
Garanta que Actions recebam dependências por construtor. No container, registre interfaces para implementações concretas.
Exemplo de bindings (pseudo)
$container->set(UserRepository::class, function($c) { return new PdoUserRepository($c->get( PDO::class));});$container->set(TokenIssuer::class, function($c) { return new JwtTokenIssuer($c->get('settings')['jwt']);});$container->set(CreateUser::class, function($c) { return new CreateUser( $c->get(UserRepository::class), $c->get(PasswordHasher::class), $c->get(IdGenerator::class), $c->get(Clock::class) );});Critério: Actions não devem criar repositórios/serviços com new. Isso preserva testabilidade e reduz acoplamento.
Persistência: implementação do repositório (infra)
Implemente o repositório com PDO (ou camada já definida) respeitando a interface do domínio. Exemplo reduzido:
final class PdoUserRepository implements UserRepository { public function __construct(private PDO $pdo) {} public function findByEmail(string $email): ?User { $stmt = $this->pdo->prepare('SELECT * FROM users WHERE email = :email LIMIT 1'); $stmt->execute(['email' => $email]); $row = $stmt->fetch( PDO::FETCH_ASSOC); return $row ? $this->mapRowToUser($row) : null; } public function create(User $user): void { $stmt = $this->pdo->prepare('INSERT INTO users (id,name,email,password_hash,active,created_at) VALUES (:id,:name,:email,:ph,:active,:ca)'); $stmt->execute([ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'ph' => $user->passwordHash, 'active' => $user->active ? 1 : 0, 'ca' => $user->createdAt->format('Y-m-d H:i:s') ]); } private function mapRowToUser(array $row): User { return new User( id: $row['id'], name: $row['name'], email: $row['email'], passwordHash: $row['password_hash'], active: (bool)$row['active'], createdAt: new DateTimeImmutable($row['created_at']) ); } }Se um caso de uso exigir múltiplas operações atômicas, encapsule em transação na infraestrutura (ou via um TransactionManager injetado).
Tratamento de erros: mapeamento para HTTP
Padronize o mapeamento de exceções de domínio/aplicação para status HTTP e códigos de erro. Mantenha uma tabela simples e previsível.
| Exceção/código | HTTP | code |
|---|---|---|
| validation_error | 422 | validation_error |
| email_already_in_use | 409 | email_already_in_use |
| invalid_credentials | 401 | invalid_credentials |
| not_found | 404 | not_found |
| unexpected | 500 | internal_error |
O middleware de erro deve capturar exceções, gerar traceId (ou reutilizar), logar e responder no formato padrão.
Checklist de qualidade (critérios de aceitação)
Arquitetura e separação de camadas
- Actions não contêm regra de negócio; apenas adaptam HTTP → DTO → UseCase → resposta.
- UseCases não dependem de Slim, Request/Response, nem de classes de infraestrutura.
- Repositórios são interfaces no domínio e implementações na infraestrutura.
- Serviços transversais (JWT, hashing, clock, logger) são abstraídos por interfaces quando impactam testes.
Consistência de API
- Todas as respostas seguem o contrato
{data, meta}ou{error}. - Erros de validação retornam 422 com detalhes por campo.
- Erros de autenticação retornam 401; autorização (quando aplicável) 403.
- Paginação retorna
metacompage,perPage,total.
Testabilidade
- UseCases testáveis com doubles de
UserRepository,TokenIssuer,PasswordHasher. - Actions testáveis via testes HTTP (integração) sem acessar DB real (quando desejado) ou com DB de teste.
- Configuração por ambiente permite apontar para banco/segredos diferentes em dev/test/prod.
Observabilidade e segurança
- Logging não inclui senha, token ou dados sensíveis.
- JWT secret e configs não ficam hardcoded no código.
- CORS restritivo por ambiente (origens permitidas).
- TraceId presente em logs e respostas de erro.
Passos para executar e testar o projeto
1) Variáveis de ambiente (exemplo)
Crie um arquivo .env (ou mecanismo equivalente) com:
APP_ENV=devDB_DSN=mysql:host=localhost;dbname=appDB_USER=rootDB_PASS=secretJWT_SECRET=change-meJWT_TTL=3600CORS_ALLOWED_ORIGINS=http://localhost:30002) Subir a aplicação
- Instalar dependências:
composer install - Rodar migrações/DDL do banco (conforme seu setup).
- Iniciar servidor:
php -S localhost:8080 -t public
3) Testar endpoints com curl
Criar usuário
curl -X POST http://localhost:8080/v1/users -H 'Content-Type: application/json' -d '{"name":"Ana","email":"ana@example.com","password":"secret123"}'Login
curl -X POST http://localhost:8080/v1/auth/login -H 'Content-Type: application/json' -d '{"email":"ana@example.com","password":"secret123"}'Acessar rota protegida
curl http://localhost:8080/v1/users -H 'Authorization: Bearer <TOKEN>'4) Testes automatizados (sugestão de organização)
- Unitários: UseCases (CreateUser, Login) com repositório fake/mocked.
- Integração: PdoUserRepository com banco de teste.
- HTTP: rotas + middlewares + error handler garantindo contrato de resposta.
Como evoluir o projeto: adicionando novas funcionalidades sem quebrar a arquitetura
Exemplo A: endpoint de troca de senha
- Rota:
POST /v1/users/{id}/change-password(protegida). - DTO:
ChangePasswordInputcomcurrentPasswordenewPassword. - UseCase: valida senha atual, aplica hash, atualiza via repositório.
- Action: valida payload, cria DTO, chama use case, retorna 204 ou
{data}.
Exemplo B: autorização por papéis (admin)
- Adicionar claim
roleno token. - Criar middleware
RequireRoleMiddleware('admin')aplicável a grupos específicos. - Manter a decisão de autorização fora das Actions, preferindo middleware/políticas.
Exemplo C: filtros e ordenação em listagem
- Expandir
ListUsersInputcomsearch,sort,direction. - Atualizar repositório para aceitar critérios (sem expor SQL para a camada de aplicação).
- Garantir que a validação de parâmetros de query aconteça na Action.
Roteiro de verificação rápida (antes de considerar “pronto”)
- Endpoints respondem com JSON válido e headers corretos (incluindo CORS quando aplicável).
- Erros de validação retornam 422 com
detailspor campo. - Rotas protegidas retornam 401 sem token e 200/204 com token válido.
- Logs possuem
traceIde não vazam segredos. - UseCases não importam namespaces de HTTP/Slim.
- Repositórios são substituíveis (interface no domínio).
- Configuração muda por ambiente sem alterar código.