DEV Community

Denis Augusto
Denis Augusto

Posted on

Seu array associativo vai te trair (e vai ser em produção)

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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'),
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

E o controller vira uma linha honesta:

$user = $this->service->handle(CriarUsuarioData::fromRequest($request));
Enter fullscreen mode Exit fullscreen mode

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,
));
Enter fullscreen mode Exit fullscreen mode

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)