O que é Arquitetura Limpa em um projeto Slim
Arquitetura Limpa organiza o código em camadas com responsabilidades bem definidas e, principalmente, com uma regra de dependência: dependências sempre apontam para dentro. Isso significa que regras de negócio (Domain) não conhecem detalhes de framework, banco de dados, HTTP, filas, SDKs etc. Esses detalhes ficam nas bordas (Infrastructure/HTTP) e dependem de abstrações definidas no centro (Application/Domain).
Uma divisão prática e comum para projetos com Slim é:
- Presentation/HTTP: endpoints, adaptação de Request/Response, serialização, status codes, headers.
- Application (Use Cases): casos de uso (commands/queries), orquestração, transações, regras de aplicação.
- Domain: entidades, Value Objects, invariantes, regras de negócio puras.
- Infrastructure: implementações concretas de repositórios, gateways externos, ORM/DBAL, clientes HTTP, mensageria, cache.
Regra de dependência (setas para dentro)
Uma forma de validar a arquitetura é checar “quem importa quem”:
- Domain não importa nada de Application/Infrastructure/HTTP.
- Application pode importar Domain (para usar entidades/VOs e regras).
- Infrastructure implementa interfaces definidas em Application (ou Domain, dependendo do estilo) e pode importar Domain para mapear entidades.
- HTTP (Slim) chama Application e conhece apenas DTOs/contratos de entrada/saída do caso de uso; não acessa Infrastructure diretamente.
Na prática, o Slim fica restrito à camada HTTP: ele recebe uma requisição, transforma em um comando/consulta interno, chama o caso de uso e transforma o resultado em uma resposta HTTP.
Modelagem do Domain: Entidades e Value Objects
No Domain, foque em expressar regras e invariantes. Evite tipos “soltos” (string/int) para conceitos importantes e prefira Value Objects (VOs). Isso reduz bugs e centraliza validações.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Exemplo: Value Object de Email
<?php declare(strict_types=1); namespace App\Domain\User\ValueObject; final class Email { private string $value; public function __construct(string $value) { $value = trim(mb_strtolower($value)); if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException('Email inválido'); } $this->value = $value; } public function value(): string { return $this->value; } public function equals(self $other): bool { return $this->value === $other->value; } }Exemplo: Entidade User com invariantes
<?php declare(strict_types=1); namespace App\Domain\User\Entity; use App\Domain\User\ValueObject\Email; final class User { private ?int $id; private Email $email; private string $name; public function __construct(?int $id, Email $email, string $name) { $name = trim($name); if ($name === '') { throw new \InvalidArgumentException('Nome é obrigatório'); } $this->id = $id; $this->email = $email; $this->name = $name; } public function id(): ?int { return $this->id; } public function email(): Email { return $this->email; } public function name(): string { return $this->name; } public function withId(int $id): self { return new self($id, $this->email, $this->name); } }Note que o Domain não sabe nada sobre HTTP, Slim, banco, JSON, etc. Ele só expressa regras.
Application: Casos de uso (Commands/Queries) e DTOs
A camada Application orquestra o fluxo: recebe um comando/consulta, valida regras de aplicação (não confundir com validação de entrada HTTP), chama portas (interfaces) e retorna um resultado. Uma abordagem simples é separar:
- Command: intenção de mudança de estado (ex.: CreateUser).
- Query: intenção de leitura (ex.: GetUserById).
- Handler/UseCase: executa o comando/consulta.
- Result/DTO: retorno estruturado para a camada HTTP serializar.
Command: CreateUserCommand
<?php declare(strict_types=1); namespace App\Application\User\Create; final class CreateUserCommand { public function __construct( public readonly string $email, public readonly string $name ) {} }Porta (interface) de repositório
Defina a interface no centro (Application) e implemente na borda (Infrastructure). Assim, Application depende de abstração.
<?php declare(strict_types=1); namespace App\Application\User\Port; use App\Domain\User\Entity\User; use App\Domain\User\ValueObject\Email; interface UserRepository { public function existsByEmail(Email $email): bool; public function save(User $user): User; }Use case: CreateUserHandler
<?php declare(strict_types=1); namespace App\Application\User\Create; use App\Application\User\Port\UserRepository; use App\Domain\User\Entity\User; use App\Domain\User\ValueObject\Email; final class CreateUserHandler { public function __construct(private UserRepository $users) {} public function handle(CreateUserCommand $cmd): CreateUserResult { $email = new Email($cmd->email); if ($this->users->existsByEmail($email)) { throw new \DomainException('Email já cadastrado'); } $user = new User(null, $email, $cmd->name); $saved = $this->users->save($user); return new CreateUserResult($saved->id(), $saved->email()->value(), $saved->name()); } }DTO de saída: CreateUserResult
<?php declare(strict_types=1); namespace App\Application\User\Create; final class CreateUserResult { public function __construct( public readonly ?int $id, public readonly string $email, public readonly string $name ) {} public function toArray(): array { return ['id' => $this->id, 'email' => $this->email, 'name' => $this->name]; } }O caso de uso não retorna Response HTTP; ele retorna um resultado de aplicação. A camada HTTP decide status code e formato.
Portas para serviços externos (Gateways)
Para integrações (ex.: envio de e-mail, antifraude, storage), crie interfaces (portas) na camada Application e implemente na Infrastructure.
Exemplo: porta de envio de e-mail
<?php declare(strict_types=1); namespace App\Application\Notification\Port; interface Mailer { public function send(string $to, string $subject, string $body): void; }O use case injeta Mailer e não conhece SDKs. A Infrastructure fornece uma implementação concreta (SMTP, API, etc.).
Infrastructure: implementações concretas (adapters)
Infrastructure contém detalhes: SQL, clientes HTTP, cache, filas. Ela implementa as interfaces definidas em Application.
Exemplo: implementação de UserRepository (pseudo-DB)
<?php declare(strict_types=1); namespace App\Infrastructure\Persistence\User; use App\Application\User\Port\UserRepository; use App\Domain\User\Entity\User; use App\Domain\User\ValueObject\Email; final class PdoUserRepository implements UserRepository { public function __construct(private \PDO $pdo) {} public function existsByEmail(Email $email): bool { $stmt = $this->pdo->prepare('SELECT 1 FROM users WHERE email = :email'); $stmt->execute(['email' => $email->value()]); return (bool) $stmt->fetchColumn(); } public function save(User $user): User { $stmt = $this->pdo->prepare('INSERT INTO users (email, name) VALUES (:email, :name)'); $stmt->execute(['email' => $user->email()->value(), 'name' => $user->name()]); $id = (int) $this->pdo->lastInsertId(); return $user->withId($id); } }Repare: Infrastructure pode conhecer PDO, SQL e detalhes de persistência, mas o Domain e Application não.
HTTP (Slim) restrito à camada Presentation
Na camada HTTP, o Slim roteia para uma Action/Controller que:
- Extrai dados do Request (path params, query params, body).
- Cria um Command/Query (DTO interno).
- Chama o Handler (caso de uso).
- Converte o Result em JSON/Response com status adequado.
Adapter de Request para Command (exemplo de Action)
<?php declare(strict_types=1); namespace App\Presentation\Http\Action\User; use App\Application\User\Create\CreateUserCommand; use App\Application\User\Create\CreateUserHandler; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; final class CreateUserAction { public function __construct(private CreateUserHandler $handler) {} public function __invoke(Request $request, Response $response): Response { $data = (array) $request->getParsedBody(); $cmd = new CreateUserCommand( email: (string)($data['email'] ?? ''), name: (string)($data['name'] ?? '') ); $result = $this->handler->handle($cmd); $payload = json_encode($result->toArray(), JSON_UNESCAPED_UNICODE); $response->getBody()->write($payload); return $response->withHeader('Content-Type', 'application/json')->withStatus(201); } }O Slim aparece apenas aqui. Se amanhã você trocar Slim por outro framework, a troca se concentra na camada Presentation/HTTP.
Query params para Query interna
Para leitura, faça o mesmo: adapte query params para um objeto de consulta.
<?php declare(strict_types=1); namespace App\Application\User\Get; final class GetUserByIdQuery { public function __construct(public readonly int $id) {} }Na Action, converta $args['id'] em int e chame o handler. O handler retorna um DTO (ou null/exception), e a Action decide 200/404.
Passo a passo prático: montando o fluxo completo
1) Defina o caso de uso (Application)
- Crie o
CommandouQuerycom os dados necessários. - Crie a interface (porta) do repositório/gateway que o caso de uso precisa.
- Implemente o
Handlerchamando Domain e portas. - Crie um
Result(DTO) para retorno.
2) Modele o Domain (Domain)
- Crie Value Objects para conceitos com validação e semântica (Email, Money, Document, etc.).
- Crie Entidades com invariantes no construtor/métodos.
- Evite dependências externas; use apenas PHP e regras do domínio.
3) Implemente as portas (Infrastructure)
- Crie classes concretas que implementam as interfaces da Application.
- Concentre SQL/HTTP clients/SDKs aqui.
- Faça mapeamento entre dados persistidos e entidades/VOs (quando necessário).
4) Crie o adaptador HTTP (Presentation/HTTP)
- Crie uma Action invocável que receba
Request/Response. - Transforme entrada HTTP em
Command/Query. - Chame o handler e serialize o
Result. - Decida status code e headers na borda.
5) Garanta o sentido das dependências
| Camada | Pode depender de | Não deve depender de |
|---|---|---|
| Domain | (nada externo) | HTTP, Slim, DB, SDKs, Application |
| Application | Domain | HTTP, Slim, implementações concretas de DB/SDK |
| Infrastructure | Application (interfaces), Domain | Presentation/HTTP |
| Presentation/HTTP | Application | Infrastructure diretamente (idealmente) |
Organização de pastas e namespaces alinhados às camadas
Uma estrutura simples e escalável (ajuste conforme o tamanho do projeto):
src/ Domain/ User/ Entity/ User.php ValueObject/ Email.php Application/ User/ Port/ UserRepository.php Create/ CreateUserCommand.php CreateUserHandler.php CreateUserResult.php Get/ GetUserByIdQuery.php GetUserByIdHandler.php GetUserByIdResult.php Notification/ Port/ Mailer.php Infrastructure/ Persistence/ User/ PdoUserRepository.php Notification/ SmtpMailer.php Presentation/ Http/ Action/ User/ CreateUserAction.php GetUserAction.php Routes/ user_routes.phpNamespaces sugeridos
App\Domain\...App\Application\...App\Infrastructure\...App\Presentation\Http\...
Uma regra prática: se uma classe precisa importar Psr\Http\Message\ServerRequestInterface ou qualquer coisa do Slim, ela pertence à camada Presentation\Http. Se precisa importar PDO/SDKs, pertence à Infrastructure. Se é regra de negócio, pertence ao Domain. Se coordena o fluxo e depende de portas, pertence à Application.
Erros e limites: onde cada tipo de regra deve viver
Regras do Domain
Invariantes que sempre devem ser verdade, independentemente do canal de entrada (HTTP, CLI, fila). Ex.: “nome não pode ser vazio”, “email deve ser válido”, “saldo não pode ficar negativo”. Essas regras devem lançar exceções de domínio ou impedir a criação de objetos inválidos.
Regras da Application
Políticas de aplicação e orquestração. Ex.: “não permitir cadastro com email já existente” (depende de consulta ao repositório), “enviar e-mail após criar usuário” (depende de gateway), “abrir transação e persistir múltiplas coisas”.
Regras da Presentation/HTTP
Decisões específicas do protocolo: status code, headers, serialização, leitura de body/query params. A camada HTTP não deve conter regra de negócio; ela apenas adapta e delega.