O array que ninguém sabe o que tem dentro
Você abre um Service depois de três meses sem mexer. A assinatura do método é essa aqui:
public function handle(array $data): User
E aí? O que vai dentro desse $data? Tem nome ou name? O enviar_boas_vindas é booleano ou a string "1" que veio do formulário? Tem telefone ou aquilo era opcional?
A resposta honesta é: ninguém sabe. Pra descobrir, você lê o corpo do método inteiro, abre o Form Request, e ainda dá um dd($data) só pra ter certeza. Toda vez.
E o pior nem é isso. O pior é que no dia em que alguém escrever $data['name'] em vez de $data['nome'], o PHP não vai gritar em lugar nenhum útil. Vai gritar em produção, num log que você só vê depois que o cliente reclama.
O problema na prática
Olha esse fluxo bem comum — Form Request valida, controller repassa, service usa:
// Controller
public function store(CriarUsuarioRequest $request)
{
$user = $this->service->handle($request->validated());
return redirect()->route('usuarios.show', $user);
}
// Service — boa sorte adivinhando o que tem aqui dentro
public function handle(array $data): User
{
$user = User::create([
'name' => $data['nome'],
'email' => $data['email'],
]);
// esse aqui vem como string "0" do form... ou não vem nunca. Depende.
if ($data['enviar_boas_vindas']) {
Mail::to($user)->send(new BoasVindas($user));
}
return $user;
}
Repara nos problemas empilhados:
O $data['enviar_boas_vindas'] estoura um Undefined array key se o campo não veio marcado no formulário — porque checkbox desmarcado simplesmente não é enviado. E se vier a string "0", o if considera falso, mas "false" seria verdadeiro. Divirta-se.
Sua IDE não te ajuda em nada. Zero autocomplete, zero aviso. O array é uma caixa preta que aceita qualquer coisa e só reclama quando o código já rodou.
O conceito: dar um nome e uma forma pros dados
DTO é Data Transfer Object. Nome pomposo pra uma ideia bem simples: em vez de carregar dados soltos num array, você cria uma classe que descreve exatamente quais campos existem e de que tipo cada um é.
Só isso. Não tem lógica de negócio, não fala com banco, não é model. É um envelope tipado que atravessa as camadas do seu sistema.
A vantagem é que o contrato para de morar na sua cabeça e passa a morar no código. E o PHP vira seu revisor de código de graça.
A solução: uma classe boba que resolve tudo
Com PHP 8 e readonly, um DTO cabe em poucas linhas:
final class CriarUsuarioData
{
public function __construct(
public readonly string $nome,
public readonly string $email,
public readonly bool $enviarBoasVindas = true,
public readonly ?string $telefone = null,
) {}
public static function fromRequest(CriarUsuarioRequest $request): self
{
return new self(
nome: $request->validated('nome'),
email: $request->validated('email'),
// boolean() resolve "1", "on", "true", ausente... tudo
enviarBoasVindas: $request->boolean('enviar_boas_vindas'),
telefone: $request->validated('telefone'),
);
}
}
Agora o service fica assim:
public function handle(CriarUsuarioData $data): User
{
$user = User::create([
'name' => $data->nome,
'email' => $data->email,
]);
if ($data->enviarBoasVindas) {
Mail::to($user)->send(new BoasVindas($user));
}
return $user;
}
E o controller vira uma linha honesta:
$user = $this->service->handle(CriarUsuarioData::fromRequest($request));
O que mudou de verdade: se você digitar $data->nomee, a IDE marca em vermelho antes de você salvar. Se esquecer de passar o e-mail, o PHP reclama na hora de construir o objeto, não três camadas depois. E o readonly garante que ninguém vai alterar o dado no meio do caminho e te deixar sem entender de onde veio aquele valor.
Como usar na prática
Na fronteira do sistema. O DTO nasce onde o dado externo entra — Form Request, comando de console, payload de webhook — e daí pra dentro tudo trafega tipado. Um fromRequest(), um fromArray(), um fromWebhook(), cada um sabendo traduzir seu formato de origem.
Em Jobs, que agradecem muito. Job recebe array e você descobre o problema quando o worker já engoliu a fila? Com DTO, o construtor tipado é uma barreira antes do dispatch:
ProcessarPedido::dispatch(new PedidoData(
pedidoId: $pedido->id,
valorTotal: $pedido->total,
));
Na saída também. Integração com API externa devolvendo um JSON gigante e imprevisível? Converte pra DTO logo na resposta. Se o fornecedor mudar um campo, você quebra num lugar só — no fromResponse() — em vez de em oito arquivos espalhados.
A pegadinha: DTO não é model, e nem sempre vale
Aqui vem o cuidado, porque tem quem descubra DTO e saia criando um pra cada método do projeto.
DTO não substitui o Eloquent Model. Model representa uma linha do banco com comportamento; DTO representa dados em trânsito. Se você criou um UserData que é uma cópia exata da tabela users, provavelmente você só reescreveu o model com mais trabalho.
DTO não guarda regra de negócio. Sem método salvar(), sem calcularDesconto() dentro dele. No momento em que ele começa a decidir coisas, virou service disfarçado.
E DTO não faz validação. Quem valida é o Form Request. O DTO assume que o dado já chegou limpo e só garante o formato daí pra frente.
A régua que uso: se o dado atravessa mais de uma camada, ou se você já se pegou perguntando "o que vem nesse array?", vale o DTO. Pra um $request->only('email') que morre no controller, não vale.
Bônus: e quando o DTO tem 15 campos?
Escrever fromRequest() na mão fica chato rápido quando o formulário é grande. É exatamente aí que entra o spatie/laravel-data, que faz o mapeamento automático, integra com validação, serializa pra JSON e ainda funciona com collections tipadas.
Mas comece na mão. Escreve dois ou três DTOs com readonly puro, sente o que muda no seu dia a dia, e só depois traga o pacote. Assim você usa a ferramenta entendendo o problema que ela resolve — em vez de instalar mais uma dependência por hype.
Antes de você fechar a aba
Faz um teste rápido: abre seu projeto e procura por array $data nas assinaturas de método. Quantos você achou? Agora escolhe o mais bagunçado e tenta lembrar, sem abrir o Form Request, o que exatamente vai dentro dele.
Se você travou, achou seu primeiro DTO. 🎯
E aí, você já usa DTO ou ainda tá no time do array associativo com fé? Conta nos comentários — e se conhece alguém que vive dando dd($data) pra descobrir o que chegou, marca a pessoa lá.
Top comments (0)