If, switch ou match no PHP: como escolher antes da condição virar escada
Regras diferentes pedem if com nome. Um valor com vários casos pede match ou um array. Entenda a comparação frouxa do switch e quando o enum resolve de vez.
O primeiro if calculava o desconto de estudante. Dois anos depois, a função tem onze elseif, cobre convênio, cupom, aniversariante e "cliente antigo", e ninguém sabe dizer se um estudante aniversariante ganha os dois descontos. A resposta curta: quando você compara a mesma variável com vários valores fixos, use match (PHP 8) ou um array; quando cada ramo testa uma regra diferente, use if, mas dê nome a cada regra com uma função; e evite switch em código novo, porque ele compara de forma frouxa e cai para o caso seguinte se você esquecer o break.
O exemplo é uma loja que mostra o status do pedido e calcula descontos. Se o seu problema é o if que cresce para o lado, aninhado em vários níveis, o caminho é outro e está em if e else que crescem para o lado pedem nome.
Por que a escada cresce
Cada caso novo chega como uma urgência, e o jeito mais rápido de atendê-la é acrescentar um degrau no fim. Ninguém reorganiza, porque a função "funciona". O custo aparece depois: a ordem dos ramos passa a importar sem que isso esteja escrito, dois ramos que deveriam valer juntos se excluem, e o else final vira um depósito de tudo que ninguém previu, inclusive valores inválidos.
O primeiro passo para sair disso é perguntar o que os ramos têm em comum. Se todos olham $status, é uma tabela de valores. Se cada um olha uma coisa diferente (data, perfil, total do carrinho), são regras de negócio, e cada uma merece nome.
Caso 1: um valor, vários resultados
A tela de pedidos mostra um rótulo para cada status. A versão em escada:
<?php
if ($status === 'pendente') {
$rotulo = 'Aguardando pagamento';
} elseif ($status === 'pago') {
$rotulo = 'Pagamento confirmado';
} elseif ($status === 'enviado') {
$rotulo = 'A caminho';
} elseif ($status === 'entregue') {
$rotulo = 'Entregue';
} else {
$rotulo = 'Desconhecido';
}
Com match, os casos ficam lado a lado e o resultado é uma expressão:
<?php
$rotulo = match ($status) {
'pendente' => 'Aguardando pagamento',
'pago' => 'Pagamento confirmado',
'enviado' => 'A caminho',
'entregue' => 'Entregue',
'cancelado', 'estornado' => 'Cancelado',
};
Três diferenças importam. O match compara com ===, sem conversão de tipo. Não existe queda para o próximo caso, então não há break para esquecer. E se nenhum braço casar e não houver default, ele lança UnhandledMatchError. Isso é bom: um status novo que ninguém tratou quebra no teste, em vez de virar "Desconhecido" em silêncio na tela do cliente. Se você quer um valor de reserva, acrescente default => 'Desconhecido' conscientemente.
Quando um array basta
Se cada valor só escolhe um dado, a tabela pode morar num array, e o código só busca:
<?php
const ROTULOS_STATUS = [
'pendente' => 'Aguardando pagamento',
'pago' => 'Pagamento confirmado',
'enviado' => 'A caminho',
'entregue' => 'Entregue',
];
$rotulo = ROTULOS_STATUS[$status] ?? 'Desconhecido';
Acrescentar um status vira uma linha. O mesmo array alimenta o filtro da tela e a validação do formulário (array_key_exists($status, ROTULOS_STATUS)), e a lista deixa de estar espalhada por três arquivos.
Quando o valor é um conceito do sistema: enum
No PHP 8.1 ou superior, um conjunto fechado de valores fica melhor como enum. O rótulo vira um método, e o match dentro dele é verificado caso a caso:
<?php
enum StatusPedido: string
{
case Pendente = 'pendente';
case Pago = 'pago';
case Enviado = 'enviado';
case Entregue = 'entregue';
public function rotulo(): string
{
return match ($this) {
self::Pendente => 'Aguardando pagamento',
self::Pago => 'Pagamento confirmado',
self::Enviado => 'A caminho',
self::Entregue => 'Entregue',
};
}
}
$status = StatusPedido::tryFrom($linha['status']);
echo $status?->rotulo() ?? 'Desconhecido';
O tryFrom devolve null para valores que não existem no enum, então um texto estranho vindo do banco não passa despercebido. Quando alguém acrescentar um case novo e esquecer o rótulo, o match lança erro na primeira chamada.
A armadilha do switch
O switch compara com ==, a comparação frouxa. Valores vindos de formulário chegam como texto, e textos numéricos são comparados como números:
<?php
$codigo = '1e1'; // veio do formulário
switch ($codigo) {
case '10':
echo 'Frete expresso';
break;
default:
echo 'Frete normal';
}
// imprime "Frete expresso": '1e1' == '10' é verdadeiro no PHP
echo match ($codigo) {
'10' => 'Frete expresso',
default => 'Frete normal',
};
// imprime "Frete normal"
O outro problema é a queda: um case sem break continua executando o próximo. Às vezes é intencional, quase sempre é esquecimento. Se o projeto ainda roda PHP 7, o switch continua sendo uma opção legítima; normalize o tipo antes e trate todo case sem break como suspeito na revisão.
Caso 2: regras diferentes, if com nome
O desconto mistura perfil, data e total. Nenhum match resolve isso de forma legível, porque não há um valor único sendo comparado. O que resolve é dar nome a cada regra e deixar explícito se elas se somam:
<?php
function ehEstudante(array $cliente): bool
{
return $cliente['perfil'] === 'estudante';
}
function fazAniversarioHoje(array $cliente, DateTimeImmutable $hoje): bool
{
return $cliente['nascimento'] !== null
&& substr($cliente['nascimento'], 5, 5) === $hoje->format('m-d');
}
function percentualDeDesconto(array $cliente, int $totalCentavos, DateTimeImmutable $hoje): int
{
$descontos = [];
if (ehEstudante($cliente)) {
$descontos[] = 10;
}
if (fazAniversarioHoje($cliente, $hoje)) {
$descontos[] = 15;
}
if ($totalCentavos >= 30000) {
$descontos[] = 5;
}
return $descontos ? max($descontos) : 0;
}
A decisão que estava escondida na ordem dos elseif agora está escrita: os descontos não se somam, vale o maior. Se a regra mudar para "somam até 20%", muda uma linha, e o teste correspondente diz isso. Note também que o total chega em centavos, como inteiro, pelo motivo explicado em dinheiro em centavos.
Erros comuns
- Acrescentar
elseifno fim para cada caso novo. A ordem passa a decidir o resultado sem que ninguém tenha escolhido essa ordem. - Usar
switchcom valores de formulário. A comparação frouxa casa textos que você considerava diferentes. - Esquecer o
break. O caso seguinte também executa, e o bug só aparece com aquele valor específico. - Colocar
defaultem todomatchpor reflexo. Você perde o erro que avisaria de um valor não tratado. - Forçar
switch (true)para regras diferentes. É a mesma escada deif, com uma sintaxe que menos gente lê com segurança. - Repetir a lista de valores em vários lugares. O filtro aceita um status que o rótulo não conhece.
Checklist
- Cada condição longa foi classificada: mesmo valor com vários casos, ou regras diferentes.
- Mapeamentos simples de valor para resultado estão num array ou num
match. - Conjuntos fechados de valores usados em vários lugares viraram enum (PHP 8.1+).
- Código novo não usa
switch, ou usa com tipo normalizado ebreakconferido. - Regras de negócio têm funções com nome e a combinação entre elas está explícita.
- Há teste para o caso em que duas regras valem ao mesmo tempo.
Abra a função com a maior cadeia de elseif do seu projeto e conte os ramos. Todos olham a mesma variável? Se sim, quantas linhas sobram depois de trocar por um match; se não, quais dois ramos poderiam ser verdade ao mesmo tempo, e o que o código faz nesse caso?
Referência: http://www.oficinadanet.com.br/post/9507-condicoes-em-php-if-else-if-else-switch