DEV Community

Denis Augusto
Denis Augusto

Posted on

Pare de usar first(): existe um método do Eloquent feito pra 'só pode existir um'

Você pediu "o usuário desse email" e o banco te deu um. Mas e se tinha dois?

Deixa eu adivinhar como você busca um registro que deveria ser único:

$user = User::where('email', $email)->first();
Enter fullscreen mode Exit fullscreen mode

Funciona, né? Roda em produção, ninguém reclama. Até o dia em que aparece um segundo usuário com o mesmo email — bug de cadastro, migração mal feita, importação duplicada, sei lá — e o seu first() continua lá, feliz da vida, te entregando um dos dois.

Qual dos dois? O que o banco resolver devolver primeiro. Boa sorte descobrindo isso às 2h da manhã.

O problema: first() é otimista demais

O first() tem uma filosofia simples: "me dá o primeiro que aparecer, e se não aparecer nenhum, tá bom, toma um null".

Isso é ótimo pra listagem, pra "pega qualquer um", pra "o mais recente". Mas é péssimo quando o registro representa uma regra de negócio que diz só pode existir um.

// "email é único no meu sistema" — será mesmo?
$user = User::where('email', $email)->first();

// "esse cupom é exclusivo" — tem certeza?
$cupom = Cupom::where('codigo', $codigo)->first();
Enter fullscreen mode Exit fullscreen mode

O first() nunca vai te avisar que a sua premissa quebrou. Ele engole a duplicata em silêncio e segue a vida. E aí o bug não estoura na hora — ele vaza devagar, entregando o pedido pro cliente errado, aplicando o desconto na conta errada.

A solução: sole(), o método que exige exatamente um

O Eloquent tem um método feito exatamente pra esse contrato: "eu espero um, e só um registro aqui".

$user = User::where('email', $email)->sole();
Enter fullscreen mode Exit fullscreen mode

A diferença está no que acontece quando a realidade não bate com a expectativa:

  • Nenhum registro? Lança ModelNotFoundException (a mesma do findOrFail, então seu handler já trata como 404).
  • Mais de um registro? Lança MultipleRecordsFoundException.
  • Exatamente um? Retorna o model, limpinho.

Ou seja: o sole() transforma uma premissa silenciosa ("aqui só tem um") numa garantia explícita. Se ela for violada, você fica sabendo na hora, com uma exception clara, em vez de descobrir três semanas depois investigando um dado torto.

Como usar na prática

Busca por campo "único" que o banco não garante:

// Se algum dia aparecer email duplicado, você quer saber AGORA
$user = User::where('email', $email)->sole();
Enter fullscreen mode Exit fullscreen mode

Validando uma regra de negócio de unicidade:

// "só existe um pedido em aberto por mesa"
$pedido = Pedido::where('mesa_id', $mesaId)
    ->where('status', 'aberto')
    ->sole();
Enter fullscreen mode Exit fullscreen mode

Em collections também funciona:

$config = collect($configs)->sole(fn ($c) => $c['ativo']);
Enter fullscreen mode Exit fullscreen mode

Aqui o sole() pega o único item que passa no teste — e reclama se passar mais de um.

Pegadinha: sole() não substitui first() sempre

Calma, não sai trocando tudo. O sole() só faz sentido quando "mais de um" é um erro de verdade.

Pra "pega o mais recente", "pega qualquer um ativo", "o primeiro da fila"? Continua no first() — ali ter vários resultados é normal e esperado.

E cuidado com o custo: o sole() precisa saber se existe um segundo registro, então ele não faz um LIMIT 1 como o first(). Em tabela gigante sem índice na coluna que você filtra, isso pesa. Na prática, se é um campo que deveria ser único, você provavelmente já tem um índice ali — então relaxa.

Bônus: pensa no sole() como um assert de dados

O legal do sole() não é o método em si — é a mentalidade. Ele te obriga a declarar no código o que você acredita ser verdade sobre seus dados.

Toda vez que você escrever ->first() num registro que deveria ser único, para um segundo e pergunta: "e se vier dois?". Se a resposta for "aí é bug", troca por sole(). Você acabou de transformar um bug silencioso num erro barulhento — que é sempre mais fácil de caçar.

Antes de você fechar a aba

Quantos ->first() no seu projeto são, na verdade, ->sole() disfarçados esperando um dado duplicado pra estragar sua sexta?

Dá uma busca por ->first() no código hoje e me conta nos comentários quantos você encontrou que deveriam ser sole(). Aposto que tem pelo menos um. 👀


Top comments (0)