Quantos jeitos de escrever "pendente" existem no seu banco?
Abre a tabela de pedidos aí do seu projeto e dá uma olhada na coluna status. Aposto que você acha pending, Pending, talvez um pendente perdido de quando o cliente pediu pra traduzir, e vai saber, um pnding que passou batido numa madrugada.
Isso acontece porque status virou uma string solta. Qualquer um pode escrever qualquer coisa ali. Não tem trava, não tem autocomplete, não tem ninguém pra dizer "opa, esse valor não existe".
Já perdeu meia hora caçando um bug que era só um paid escrito payed? Senta aí, porque o PHP 8.1 resolveu isso e o Laravel abraçou a solução com carinho.
O reino da string mágica
O cenário clássico. Status espalhado como texto puro pelo código inteiro:
// No controller
$pedido->status = 'pending';
// No outro controller
if ($pedido->status === 'paid') {
// ...
}
// Naquela blade
@if($pedido->status == 'canceled')
// E no service, com um pequeno "typo" que ninguém viu
$pedido->update(['status' => 'canceld']);
Cada um desses 'pending', 'paid', 'canceled' é o que a gente chama de string mágica: um valor cru, sem significado formal, que você reza pra ter digitado certo.
E o problema não é só o typo. É que nada te avisa dos valores possíveis. Chegou dev novo no time? Ele vai ter que caçar no grep quais status existem. Mudou paid pra payment_confirmed? Boa sorte achando os 23 lugares.
A meia-solução que muita gente usa
Aí você lembra das constantes e pensa "já resolvi isso":
class Pedido extends Model
{
const STATUS_PENDING = 'pending';
const STATUS_PAID = 'paid';
const STATUS_CANCELED = 'canceled';
}
$pedido->status = Pedido::STATUS_PAID;
Melhorou? Um pouco. Ganhou autocomplete e centralizou os valores. Mas ainda tem furo: nada impede alguém de gravar $pedido->status = 'qualquer_coisa' direto. A constante é uma sugestão, não uma regra. E você continua sem um lugar único pra pendurar comportamento (a cor do badge, o label bonito, se aquele status permite cancelamento...).
Dá pra fazer melhor. E de fábrica.
A solução: Enums de verdade
O PHP 8.1 trouxe os backed enums — enums com um valor por trás. É um tipo novo, de verdade, que só aceita os casos que você definiu:
<?php
namespace App\Enums;
enum StatusPedido: string
{
case Pending = 'pending';
case Paid = 'paid';
case Canceled = 'canceled';
}
Repara no : string ali no topo: é isso que faz cada caso ter um valor de string por trás, perfeito pra salvar no banco. Agora StatusPedido::Paid é um objeto tipado, não um texto solto. E aqui mora a mágica: no Laravel, você fala pro model tratar a coluna como esse enum, usando o cast:
class Pedido extends Model
{
protected function casts(): array
{
return [
'status' => StatusPedido::class,
];
}
}
Pronto. A partir daqui o Eloquent faz a ponte sozinho: grava a string no banco, devolve o enum quando você lê:
$pedido->status = StatusPedido::Paid; // grava 'paid' no banco
$pedido->status; // volta como StatusPedido::Paid (objeto)
if ($pedido->status === StatusPedido::Paid) {
// type-safe, com autocomplete, sem chance de typo
}
Tentou atribuir uma string inválida? Estoura na hora. O valor errado nem chega no banco.
Como usar na prática
Comparação sem medo. Acabou o === 'paid'. Agora é === StatusPedido::Paid, e o editor te entrega o autocomplete de todos os casos:
if ($pedido->status === StatusPedido::Canceled) {
return back()->with('erro', 'Esse pedido já foi cancelado.');
}
Comportamento junto do dado. O melhor dos enums: eles têm métodos. Aquela lógica de "qual a cor do badge" para de viver espalhada na blade e vira parte do próprio status:
enum StatusPedido: string
{
case Pending = 'pending';
case Paid = 'paid';
case Canceled = 'canceled';
public function label(): string
{
return match($this) {
self::Pending => 'Aguardando pagamento',
self::Paid => 'Pago',
self::Canceled => 'Cancelado',
};
}
public function cor(): string
{
return match($this) {
self::Pending => 'yellow',
self::Paid => 'green',
self::Canceled => 'red',
};
}
}
Na view, fica limpo assim:
<span class="badge badge-{{ $pedido->status->cor() }}">
{{ $pedido->status->label() }}
</span>
Preencher um select. O método cases() já te dá todos os valores possíveis, prontos pra iterar:
@foreach (StatusPedido::cases() as $status)
<option value="{{ $status->value }}">{{ $status->label() }}</option>
@endforeach
Pegadinha: value vs. name vs. o caso em si
Aqui rola uma confusão comum. Um backed enum tem três coisas fáceis de embolar:
-
StatusPedido::Paid— o caso em si, o objeto tipado. É o que você usa em comparação. -
StatusPedido::Paid->value— a string por trás ('paid'). É o que vai pro banco, provaluedo<option>, pra API. -
StatusPedido::Paid->name— o nome do caso no código ('Paid'). Raramente é o que você quer mostrar pro usuário.
Regra rápida: comparou lógica? Usa o caso. Precisou do valor cru (banco, HTML, JSON)? Usa ->value. E pra transformar uma string de volta em enum — tipo o que chega de um request — tem o from() e o tryFrom():
StatusPedido::from('paid'); // StatusPedido::Paid
StatusPedido::from('inexistente'); // estoura ValueError
StatusPedido::tryFrom('xpto'); // null, sem estourar (ótimo pra input do usuário)
Bônus: validação de request de brinde
Já que estamos falando de dados que entram, o Laravel valida enum direto na regra. Se alguém mandar um status que não existe no request, nem passa:
use Illuminate\Validation\Rule;
'status' => ['required', Rule::enum(StatusPedido::class)],
Junta isso com o Form Request que você já usa e o status inválido morre antes de chegar no controller. String mágica não passa nem na porta.
Antes de você fechar a aba
Faz um teste rápido: dá um grep por 'pending', 'paid' ou qualquer status do seu projeto. Quantos lugares apareceram? Cada um deles é uma string mágica esperando pra te dar dor de cabeça numa refatoração.
Me conta aí embaixo: qual é a string mágica mais cabeluda que ainda vive no seu código? Status de pedido, tipo de usuário, forma de pagamento... todo projeto tem a sua. 😄
E se esse post te deu vontade de matar umas strings soltas hoje, salva ele e manda pro colega que ainda escreve === 'paid' por aí.
Top comments (0)