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

Capítulo 13

Tempo estimado de leitura: 9 minutos

+ Exercício

Casos de uso orientados a operações (Use Cases)

Em uma arquitetura limpa, um caso de uso representa uma operação de negócio executável e testável, como CreateUser, ListUsers e UpdateUser. Ele recebe uma entrada explícita (um DTO de input), executa regras de negócio e retorna uma saída explícita (um DTO de output). Assim, a camada HTTP (Slim) fica responsável apenas por: ler o request, mapear para DTOs, chamar o caso de uso e mapear a saída para response.

O objetivo é evitar que regras de domínio “vazem” para a camada de transporte (rotas/actions), mantendo o domínio independente de JSON, headers, status codes e detalhes do Slim.

Estrutura típica

  • Action/Controller (HTTP): mapeia payload HTTP → DTO de input; chama o caso de uso; mapeia DTO de output → JSON/HTTP.
  • Use Case: orquestra regras, validações de negócio, conflitos e persistência via portas (interfaces).
  • Domínio: entidades/VOs e regras invariantes.
  • Infra: repositórios concretos, gateways, etc.

Comandos vs Consultas (Commands vs Queries)

Separar operações em comandos e consultas ajuda a manter responsabilidades claras:

  • Command: altera estado (cria/atualiza/remove). Ex.: CreateUser, UpdateUser. Normalmente envolve validações de negócio e pode falhar por conflito.
  • Query: apenas lê dados, sem efeitos colaterais. Ex.: ListUsers. Pode ter paginação/filtros e tende a ser mais simples.

Na prática, ambos podem ser implementados como casos de uso, mas com contratos e retornos adequados. O importante é: não misturar escrita e leitura na mesma operação e não depender de detalhes HTTP.

DTOs de entrada e saída: contratos explícitos

DTOs (Data Transfer Objects) são objetos simples que carregam dados entre camadas. Aqui, eles servem para:

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

  • Definir claramente o que o caso de uso precisa para executar.
  • Evitar acoplamento do caso de uso ao formato do request (JSON, form-data etc.).
  • Padronizar o que sai do caso de uso, sem “vazar” entidades internas.

Exemplo de DTOs (CreateUser)

final class CreateUserInputDTO { public function __construct( public readonly string $name, public readonly string $email, public readonly string $passwordPlain ) {} } final class CreateUserOutputDTO { public function __construct( public readonly string $id, public readonly string $name, public readonly string $email, public readonly string $createdAt ) {} }

Note que o output não expõe passwordHash nem detalhes internos. O caso de uso decide o que é seguro e útil retornar.

Mapeadores: payload HTTP ↔ DTOs ↔ modelos internos

Para manter a camada HTTP fina, crie mapeadores (ou assemblers) responsáveis por converter:

  • Request JSONInputDTO
  • OutputDTOResponse JSON

Isso evita que Actions fiquem cheias de $body['campo'] e regras de transformação.

Mapper de input (CreateUser)

final class CreateUserHttpMapper { /** @param array<string,mixed> $payload */ public function toInputDTO(array $payload): CreateUserInputDTO { return new CreateUserInputDTO( name: (string)($payload['name'] ?? ''), email: (string)($payload['email'] ?? ''), passwordPlain: (string)($payload['password'] ?? '') ); } }

Repare que o mapper não valida regra de negócio; ele apenas converte. Validações de formato/sanitização podem ocorrer antes (camada HTTP), mas regras de domínio ficam no caso de uso.

Mapper de output (CreateUser)

final class UserOutputHttpMapper { /** @return array<string,mixed> */ public function toJson(CreateUserOutputDTO $dto): array { return [ 'id' => $dto->id, 'name' => $dto->name, 'email' => $dto->email, 'createdAt' => $dto->createdAt, ]; } }

Passo a passo prático: CreateUser (Command)

1) Defina portas (interfaces) necessárias ao caso de uso

O caso de uso não deve conhecer PDO/ORM. Ele depende de interfaces.

interface UserRepository { public function existsByEmail(string $email): bool; public function save(User $user): void; } interface PasswordHasher { public function hash(string $plain): string; }

2) Modele o domínio mínimo (entidade/VO) sem HTTP

final class User { public function __construct( public readonly string $id, public string $name, public string $email, public string $passwordHash, public readonly 
DateTimeImmutable $createdAt ) {} }

3) Crie exceções de conflito/regra de negócio

Conflitos como “e-mail já cadastrado” são regras de negócio e devem ser sinalizados pelo caso de uso (não pela Action).

final class EmailAlreadyRegistered extends 
DomainException { public function __construct(string $email) { parent::__construct("E-mail j1 cadastrado: {$email}"); } }

4) Implemente o caso de uso CreateUser

final class CreateUser { public function __construct( private UserRepository $users, private PasswordHasher $hasher, private IdGenerator $ids, private Clock $clock ) {} public function execute(CreateUserInputDTO $input): CreateUserOutputDTO { // Regra de negcio: e-mail deve ser anico if ($this->users->existsByEmail($input->email)) { throw new EmailAlreadyRegistered($input->email); } // Regra de negcio: nome n3o pode ser vazio (exemplo) if (trim($input->name) === '') { throw new 
DomainException('Nome 9 obrigat3rio.'); } $id = $this->ids->generate(); $now = $this->clock->now(); $hash = $this->hasher->hash($input->passwordPlain); $user = new User( id: $id, name: $input->name, email: $input->email, passwordHash: $hash, createdAt: $now ); $this->users->save($user); return new CreateUserOutputDTO( id: $user->id, name: $user->name, email: $user->email, createdAt: $user->createdAt->format(DATE_ATOM) ); } }

Observe que o caso de uso não retorna User (entidade). Ele retorna um DTO de saída apropriado para consumo externo.

5) Conecte na Action do Slim usando mapeadores

A Action fica responsável por: ler JSON, mapear para DTO, executar e mapear saída. O tratamento de exceções e padronização de erros deve estar centralizado (por exemplo, via middleware/handler global), então aqui mostramos apenas o fluxo.

final class CreateUserAction { public function __construct( private CreateUser $useCase, private CreateUserHttpMapper $inputMapper, private UserOutputHttpMapper $outputMapper ) {} public function __invoke(
Psr\Http\Message\ServerRequestInterface $request, 
Psr\Http\Message\ResponseInterface $response): 
Psr\Http\Message\ResponseInterface { /** @var array<string,mixed> $payload */ $payload = (array)($request->getParsedBody() ?? []); $input = $this->inputMapper->toInputDTO($payload); $output = $this->useCase->execute($input); $json = $this->outputMapper->toJson($output); $response->getBody()->write(json_encode($json)); return $response->withHeader('Content-Type', 'application/json')->withStatus(201); } }

Passo a passo prático: ListUsers (Query)

Consultas podem usar um repositório específico de leitura (porta de query) e retornar DTOs prontos para serialização, sem carregar entidades completas.

1) DTOs de query

final class ListUsersInputDTO { public function __construct( public readonly int $page, public readonly int $perPage, public readonly ?string $search ) {} } final class UserListItemDTO { public function __construct( public readonly string $id, public readonly string $name, public readonly string $email ) {} } final class ListUsersOutputDTO { /** @param list<UserListItemDTO> $items */ public function __construct( public readonly array $items, public readonly int $page, public readonly int $perPage, public readonly int $total ) {} }

2) Porta de leitura

interface UserQuery { public function list(int $page, int $perPage, ?string $search): ListUsersOutputDTO; }

3) Caso de uso (query) como orquestrador simples

final class ListUsers { public function __construct(private UserQuery $query) {} public function execute(ListUsersInputDTO $input): ListUsersOutputDTO { // Regras de negcio de consulta (ex.: limites) $perPage = max(1, min(100, $input->perPage)); $page = max(1, $input->page); return $this->query->list($page, $perPage, $input->search); } }

4) Mapper de querystring → InputDTO

final class ListUsersHttpMapper { /** @param array<string,string> $queryParams */ public function toInputDTO(array $queryParams): ListUsersInputDTO { return new ListUsersInputDTO( page: isset($queryParams['page']) ? (int)$queryParams['page'] : 1, perPage: isset($queryParams['perPage']) ? (int)$queryParams['perPage'] : 20, search: isset($queryParams['search']) ? (string)$queryParams['search'] : null ); } }

Passo a passo prático: UpdateUser (Command) com conflitos e regras

Atualizações costumam exigir: carregar o agregado, aplicar mudanças, validar invariantes e persistir. Conflitos comuns: e-mail já usado por outro usuário.

1) DTOs de Update

final class UpdateUserInputDTO { public function __construct( public readonly string $id, public readonly ?string $name, public readonly ?string $email ) {} } final class UpdateUserOutputDTO { public function __construct( public readonly string $id, public readonly string $name, public readonly string $email, public readonly string $updatedAt ) {} }

2) Portas necessárias

interface UserRepository { public function getById(string $id): ?User; public function existsByEmail(string $email): bool; public function save(User $user): void; } final class UserNotFound extends 
DomainException { public function __construct(string $id) { parent::__construct("Usu1rio n3o encontrado: {$id}"); } }

3) Caso de uso UpdateUser

final class UpdateUser { public function __construct( private UserRepository $users, private Clock $clock ) {} public function execute(UpdateUserInputDTO $input): UpdateUserOutputDTO { $user = $this->users->getById($input->id); if (!$user) { throw new UserNotFound($input->id); } if ($input->name !== null) { if (trim($input->name) === '') { throw new 
DomainException('Nome n3o pode ser vazio.'); } $user->name = $input->name; } if ($input->email !== null) { // Conflito: e-mail j1 cadastrado (idealmente: existsByEmailExcludingId) if ($input->email !== $user->email && $this->users->existsByEmail($input->email)) { throw new EmailAlreadyRegistered($input->email); } $user->email = $input->email; } $this->users->save($user); $updatedAt = $this->clock->now(); return new UpdateUserOutputDTO( id: $user->id, name: $user->name, email: $user->email, updatedAt: $updatedAt->format(DATE_ATOM) ); } }

Se seu repositório suportar, prefira uma checagem mais precisa:

interface UserRepository { public function existsByEmailExcludingId(string $email, string $excludeId): bool; }

Onde colocar validações: transporte vs negcio

Tipo de valida73oExemploOnde deve ficar
Formato/sintaxeemail com formato inv1lido, campo ausente, tipo erradoCamada de transporte (HTTP) ou mapeadores/validador de entrada
Regras de negcioe-mail anico, limite de perPage, nome obrigat3rio para criarCasos de uso e/ou domdnio
Invariantes do domdnioentidade n3o pode existir em estado inv1lidoDomdnio (entidade/VO) e refor7adas pelo caso de uso

Mesmo que a camada HTTP valide campos obrigat3rios, o caso de uso deve proteger as regras essenciais (defesa em profundidade), pois ele pode ser chamado por outros meios (fila, CLI, testes, integra75es).

Tratamento de conflitos (ex.: e-mail j1 cadastrado) sem acoplar ao HTTP

O caso de uso sinaliza conflitos com exce75es de domdnio (ex.: EmailAlreadyRegistered). A camada HTTP n3o decide regra; ela apenas traduz o erro para uma resposta adequada (por exemplo, 409 Conflict) por meio do mecanismo centralizado de erros.

Uma pr1tica atil e9 manter um mapeamento entre exce75es de domdnio e c3digos HTTP em um ponto anico (handler/middleware), para que Actions n3o precisem conhecer detalhes de status code.

Checklist de implementa73o (para manter Actions enxutas)

  • Crie um caso de uso por opera73o: CreateUser, ListUsers, UpdateUser.
  • Defina InputDTO e OutputDTO para cada opera73o.
  • Use mapeadores para converter HTTP → DTO e DTO → JSON.
  • Coloque conflitos e regras de negcio no caso de uso (ex.: e-mail anico).
  • N3o retorne entidades diretamente para fora do caso de uso; retorne DTOs.
  • Para queries, prefira portas de leitura que retornem DTOs de listagem (mais leves).

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

Qual abordagem melhor mantém a camada HTTP (Slim) enxuta e evita que regras de negócio vazem para rotas/actions?

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

Você errou! Tente novamente.

A camada HTTP deve apenas ler o request, mapear para DTOs, chamar o caso de uso (onde ficam regras e conflitos) e mapear a saída. A tradução de exceções de domínio para status HTTP deve ficar centralizada para não acoplar o domínio ao transporte.

Próximo capitúlo

Testes para Back-end com Slim Framework: unitários, integração e HTTP

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

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.