DEV Community

Igor Araujo
Igor Araujo

Posted on

DTOs no PHP: o alicerce de um backend claro, seguro e escalável

Quando a gente fala de arquitetura limpa no backend, tem um ingrediente que separa o improviso do profissionalismo: os DTOs. Eles tornam o fluxo de dados previsível, seguro e fácil de evoluir — especialmente em times que querem escalar com excelência e responsabilidade. Abaixo, um guia direto ao ponto, no meu estilo, para você adotar DTOs no seu PHP (com exemplo plug-and-play).

1) O que são DTOs (Data Transfer Objects)

DTO é um objeto simples, somente com dados, feito para transportar informações entre camadas (Controller → Use Case/Service → Repository, filas, etc.).
Sem regra de negócio, sem acesso a banco, sem side effects. Apenas um contrato explícito do que entra e do que sai.

2) Por que usar DTOs (e por que isso importa)

  • Clareza e contrato: o DTO define o formato dos dados. Diminui “adivinhação” e quebra de compatibilidade entre camadas.
  • Imutabilidade: com readonly, você evita mutações silenciosas e bugs difíceis. O que o caso de uso recebeu é o que será processado.
  • Segurança e governança: ao fechar o formato dos dados, você reduz risco de fields “perdidos” ou inputs inesperados.
  • Testabilidade: é simples criar DTOs em testes e simular cenários sem precisar de Request/Model.
  • Evolução controlada: novas versões de endpoints/fluxos podem ter novos DTOs (v2, v3…) sem quebrar o legado.

3) Como usar DTO no PHP “apenas definindo uma classe”

Abaixo está o seu exemplo de DTO — já no padrão moderno do PHP 8.x, com constructor property promotion e propriedades readonly:

<?php

namespace App\Domain\Entidade\DTO;

class NovaEntidadeDTO
{
    public function __construct(
        public readonly string $nome,
        public readonly string $cnpj,
        public readonly array $json = [],
        public readonly string $cep,
        public readonly string $rua,
        public readonly string $numero,
        public readonly string $complemento,
        public readonly string $bairro,
        public readonly string $cidade,
        public readonly string $estado,
        public readonly string $cnpj_correios
    ) {}

    public static function fromRequest($request): self
    {
        return new self(
            nome: $request->nome,
            cnpj: $request->cnpj,
            json: $request->json ?? [],
            cep: $request->cep,
            rua: $request->rua,
            numero: $request->numero,
            complemento: $request->complemento,
            bairro: $request->bairro,
            cidade: $request->cidade,
            estado: $request->estado,
            cnpj_correios: $request->cnpj_correios
        );
    }

    public function toArray(): array
    {
        return [
            'nome' => $this->nome,
            'cnpj' => $this->cnpj,
            'json' => $this->json,
            'cep' => $this->cep,
            'rua' => $this->rua,
            'numero' => $this->numero,
            'complemento' => $this->complemento,
            'bairro' => $this->bairro,
            'cidade' => $this->cidade,
            'estado' => $this->estado,
            'cnpj_correios' => $this->cnpj_correios
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Como integrar no fluxo (exemplo Laravel — mas serve para qualquer stack)

Controller (valida, cria o DTO e chama o caso de uso/serviço):

public function store(Request $request, CriaEntidade $service)
{
    // 1) Validação (exemplo)
    $request->validate([
        'nome' => ['required','string'],
        'cnpj' => ['required','string'],
        'json' => ['nullable','array'],
        'cep' => ['required','string'],
        'rua' => ['required','string'],
        'numero' => ['required','string'],
        'complemento' => ['nullable','string'],
        'bairro' => ['required','string'],
        'cidade' => ['required','string'],
        'estado' => ['required','string'],
        'cnpj_correios' => ['required','string'],
    ]);

    // 2) Transformação segura
    $dto = \App\Domain\Entidade\DTO\NovaEntidadeDTO::fromRequest($request);

    // 3) Orquestração de negócio
    $entidade = $service->handle($dto);

    return response()->json($entidade, 201);
}
Enter fullscreen mode Exit fullscreen mode

Service/Use Case (consome o DTO e persiste via Model/Repository):

final class CriaEntidade
{
    public function handle(\App\Domain\Entidade\DTO\NovaEntidadeDTO $dto)
    {
        // Persistência simples usando o array do DTO
        return \App\Models\Entidade::create($dto->toArray());
    }
}
Enter fullscreen mode Exit fullscreen mode

Observação prática (Laravel): se você tiver um campo do formulário chamado json, ele pode colidir conceitualmente com $request->json() (que é um método). No seu código acima funciona porque você usa $request->json como “propriedade”. Se quiser blindar isso, uma alternativa é criar também um fromArray(array $data): self e passar $request->validated().


Dicas para levar ao próximo nível (quando fizer sentido)

  • Value Objects: transformar CNPJ, CEP e UF em tipos próprios (com validação) aumenta a qualidade e reduz bugs.
  • Factories nomeadas: NovaEntidadeDTO::fromWebhook(...), ::fromCli(...) — ótimas quando as fontes de dados variam.
  • Versionamento: NovaEntidadeV1DTO, NovaEntidadeV2DTO ajudam a evoluir contratos sem quebrar integrações.
  • Anotações de static analysis: adicione docblocks para o shape de arrays (@var array<string,mixed>), melhorando insights de Psalm/PHPStan.

Conclusão: DTOs elevam a engenharia — clareza, segurança e escala. Em ambientes corporativos, onde governança e padronização importam, eles criam um vocabulário comum entre produto, dev e negócio. Seguimos construindo com ciência, colaboração e propósito.

Top comments (0)