Arquitetura Limpa com Slim Framework: camadas, limites e dependências

Capítulo 12

Tempo estimado de leitura: 9 minutos

+ Exercício

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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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 Command ou Query com os dados necessários.
  • Crie a interface (porta) do repositório/gateway que o caso de uso precisa.
  • Implemente o Handler chamando 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

CamadaPode depender deNão deve depender de
Domain(nada externo)HTTP, Slim, DB, SDKs, Application
ApplicationDomainHTTP, Slim, implementações concretas de DB/SDK
InfrastructureApplication (interfaces), DomainPresentation/HTTP
Presentation/HTTPApplicationInfrastructure 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.php

Namespaces 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.

Agora responda o exercício sobre o conteúdo:

Em um projeto com Arquitetura Limpa usando Slim, qual alternativa descreve corretamente o papel da camada Presentation/HTTP no fluxo de uma requisição?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Na Presentation/HTTP, o Slim fica na borda: extrai dados do Request, cria um Command/Query, chama o Handler (Application) e converte o Result em resposta HTTP (JSON/status/headers), sem conter regra de negócio nem acessar Infrastructure diretamente.

Próximo capitúlo

Casos de uso e DTOs em Slim Framework: comandos, queries e mapeamento

Arrow Right Icon
Capa do Ebook gratuito Back-end com Slim Framework (PHP): Rotas, Middlewares e Arquitetura Limpa
75%

Back-end com Slim Framework (PHP): Rotas, Middlewares e Arquitetura Limpa

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.