Um programa que executa sempre as mesmas instruções na mesma ordem é pouco útil. O que torna o software poderoso é a capacidade de tomar decisões — executar um bloco de código em determinada situação e outro bloco em situação diferente. Isso é feito com estruturas de controle.
Neste artigo vamos estudar as principais estruturas de decisão do PHP: if, else, elseif, switch e o match — introduzido no PHP 8 e que representa uma forma muito mais segura e expressiva de lidar com múltiplas condições.
if, else e elseif
A estrutura if é a mais fundamental de todas. Ela avalia uma condição e executa um bloco de código se essa condição for verdadeira.
<?php
$idade = 20;
// if básico — executa o bloco se a condição for true
if ($idade >= 18) {
echo "Maior de idade.";
}
// if + else — executa um bloco ou outro
if ($idade >= 18) {
echo "Maior de idade.";
} else {
echo "Menor de idade.";
}
// if + elseif + else — múltiplas condições em sequência
// O PHP avalia cada condição de cima para baixo
// e executa apenas o primeiro bloco verdadeiro
$nota = 75;
if ($nota >= 90) {
echo "Conceito A";
} elseif ($nota >= 75) {
echo "Conceito B"; // este será executado
} elseif ($nota >= 60) {
echo "Conceito C";
} else {
echo "Reprovado";
}
Uma regra importante: quando o if tem apenas uma instrução, as chaves são opcionais. Mas omiti-las é uma prática que gera bugs difíceis de encontrar — sempre use chaves, sem exceção.
<?php
$logado = true;
// Sem chaves — funciona, mas é perigoso
if ($logado)
echo "Bem-vindo!";
// Com chaves — sempre preferível
// Se você adicionar uma linha depois, ela será executada independentemente da condição
if ($logado) {
echo "Bem-vindo!";
echo "Seu painel está pronto.";
}
Condições compostas
Na prática, raramente verificamos apenas uma condição. Usamos os operadores lógicos && (E) e || (OU) para combinar condições:
<?php
$idade = 25;
$documento = true;
$pagamento = true;
// && (E) — todas as condições precisam ser verdadeiras
if ($idade >= 18 && $documento && $pagamento) {
echo "Acesso liberado.";
}
// || (OU) — basta uma condição ser verdadeira
$ehAdmin = false;
$ehModerador = true;
if ($ehAdmin || $ehModerador) {
echo "Pode moderar conteúdo.";
}
// Combinando && e || — use parênteses para deixar a precedência clara
// Sem parênteses a leitura fica ambígua e propensa a erros
$planoAtivo = true;
$dentroDoLimite = true;
$ehPremium = false;
// Parênteses explicitam a intenção: (plano ativo E dentro do limite) OU é premium
if (($planoAtivo && $dentroDoLimite) || $ehPremium) {
echo "Requisição permitida.";
}
O operador ternário em condições simples
Para atribuições condicionais simples, o operador ternário é uma alternativa compacta ao if/else:
<?php
$pontos = 450;
// if/else convencional
if ($pontos >= 500) {
$nivel = "Ouro";
} else {
$nivel = "Prata";
}
// Ternário — equivalente, mais conciso para casos simples
$nivel = ($pontos >= 500) ? "Ouro" : "Prata";
echo $nivel; // Prata
// Evite ternários aninhados — tornam o código ilegível
// Ruim:
$categoria = ($pontos >= 1000) ? "Platina" : (($pontos >= 500) ? "Ouro" : "Prata");
// Melhor: use if/elseif para múltiplos casos
if ($pontos >= 1000) {
$categoria = "Platina";
} elseif ($pontos >= 500) {
$categoria = "Ouro";
} else {
$categoria = "Prata";
}
switch
O switch compara uma expressão com múltiplos valores possíveis. É uma alternativa ao if/elseif quando você precisa comparar a mesma variável com vários valores diferentes:
<?php
$diaDaSemana = 3;
switch ($diaDaSemana) {
case 1:
echo "Segunda-feira";
break; // break interrompe o switch — sem ele, a execução "cai" para o próximo case
case 2:
echo "Terça-feira";
break;
case 3:
echo "Quarta-feira"; // este será executado
break;
case 4:
echo "Quinta-feira";
break;
case 5:
echo "Sexta-feira";
break;
case 6:
case 7:
// Múltiplos cases sem break — executa o mesmo bloco para ambos
echo "Final de semana";
break;
default:
// default é executado se nenhum case corresponder
echo "Dia inválido";
break;
}
O switch usa comparação frouxa (==) internamente — mais um motivo para preferi-lo apenas em situações adequadas e migrar para match quando possível.
match — a evolução do switch
Introduzido no PHP 8.0, o match resolve os principais problemas do switch:
- Usa comparação estrita (
===) — sem surpresas de coerção de tipos - Não precisa de
break— cada braço retorna um valor e encerra automaticamente - É uma expressão — pode ser atribuído a uma variável diretamente
- Lança
UnhandledMatchErrorse nenhum braço corresponder — falha explicitamente em vez de silenciosamente
<?php
$status = 2;
// match como expressão — retorna um valor
$descricao = match($status) {
1 => "Pendente",
2 => "Aprovado", // este será retornado
3 => "Reprovado",
4, 5 => "Em análise", // múltiplos valores por braço
default => "Desconhecido", // default é obrigatório se não cobrir todos os casos
};
echo $descricao; // Aprovado
Veja a diferença de comportamento entre switch e match com tipos:
<?php
$valor = "1"; // string "1"
// switch usa == — coerção de tipos
switch ($valor) {
case 1:
echo "switch: encontrou!"; // EXECUTA — "1" == 1 após coerção
break;
}
// match usa === — sem coerção
$resultado = match($valor) {
1 => "match: encontrou inteiro",
"1" => "match: encontrou string", // RETORNA ESTE
default => "match: não encontrou",
};
echo $resultado; // match: encontrou string
match com condições complexas
O match também aceita expressões booleanas, tornando-o uma alternativa elegante ao if/elseif em muitos casos:
<?php
$temperatura = 32;
// match com true como sujeito — avalia expressões booleanas
$clima = match(true) {
$temperatura >= 35 => "Muito quente",
$temperatura >= 28 => "Quente", // este será retornado
$temperatura >= 20 => "Agradável",
$temperatura >= 10 => "Frio",
default => "Muito frio",
};
echo $clima; // Quente
<?php
// Exemplo prático: classificação HTTP de status codes
$httpStatus = 404;
$categoria = match(true) {
$httpStatus >= 500 => "Erro do servidor",
$httpStatus >= 400 => "Erro do cliente", // este será retornado
$httpStatus >= 300 => "Redirecionamento",
$httpStatus >= 200 => "Sucesso",
default => "Informativo",
};
echo $categoria; // Erro do cliente
Sintaxe alternativa para templates
Quando você mistura PHP com HTML em arquivos de template, o PHP oferece uma sintaxe alternativa para estruturas de controle que é muito mais limpa do que abrir e fechar blocos com chaves:
<?php $usuarioLogado = true; $nome = "Ana"; ?>
<!-- Sintaxe alternativa: if: ... endif; -->
<?php if ($usuarioLogado): ?>
<header>
<p>Bem-vinda, <?= $nome ?>!</p>
</header>
<?php else: ?>
<header>
<a href="/login">Fazer login</a>
</header>
<?php endif; ?>
Essa sintaxe com : e endif é padrão em frameworks como Laravel (nos arquivos Blade) e em qualquer template PHP tradicional. Muito mais legível do que:
<?php if ($usuarioLogado) { ?>
<p>Bem-vinda!</p>
<?php } else { ?>
<a href="/login">Login</a>
<?php } ?>
Boas práticas em estruturas de controle
Sempre use chaves, mesmo para blocos de uma linha. Omitir chaves é uma das causas mais comuns de bugs introduzidos durante manutenção de código.
Prefira match ao switch em PHP 8. Comparação estrita, sem break, e falha explícita são vantagens que eliminam toda uma categoria de bugs.
Condições negativas são mais difíceis de ler. Sempre que possível, prefira a afirmativa:
<?php
// Difícil de ler — dupla negação
if (!$usuario->naoEstaAtivo()) { ... }
// Claro e direto
if ($usuario->estaAtivo()) { ... }
Extraia condições complexas para variáveis com nome. Isso transforma lógica em documentação:
<?php
// Difícil de entender à primeira leitura
if ($usuario->idade >= 18 && $usuario->documentoVerificado && $plano->estaAtivo() && !$conta->estaBloqueada()) {
// ...
}
// Com variável nomeada — lê-se como prosa
$podeAcessar = $usuario->idade >= 18
&& $usuario->documentoVerificado
&& $plano->estaAtivo()
&& !$conta->estaBloqueada();
if ($podeAcessar) {
// ...
}
Entre as cinco formas de decidir que este artigo mostrou, duas merecem preferência por motivos diferentes: if com chaves, sempre, porque a chave que falta hoje é o bug de amanhã; e match no lugar do switch, porque comparar com === e falhar alto quando nada corresponde elimina duas classes inteiras de defeito silencioso. O switch continua no seu caminho — há código demais escrito com ele para ignorá-lo —, e agora você sabe o que procurar quando um case casar onde não devia.
Fontes e leituras recomendadas
-
Estruturas de controle — Manual oficial do PHP https://www.php.net/manual/pt_BR/language.control-structures.php Documentação completa de todas as estruturas de controle, incluindo detalhes de comportamento e exemplos.
-
match — Manual oficial do PHP https://www.php.net/manual/pt_BR/control-structures.match.php Documentação específica do
match, com comparação detalhada comswitche todos os comportamentos do operador. -
RFC: Match expression v2 — PHP 8 https://wiki.php.net/rfc/match_expression_v2 A proposta formal que introduziu o
matchno PHP 8. Explica detalhadamente os problemas doswitchque omatchresolve. -
Replace Conditional with Polymorphism — Martin Fowler https://refactoring.guru/replace-conditional-with-polymorphism Padrão de refatoração que elimina
if/elseextensos substituindo-os por polimorfismo. Relevante já neste módulo para cultivar o pensamento correto. -
MARTIN, Robert C. Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall, 2008. https://www.oreilly.com/library/view/clean-code-a/9780136083238/ Capítulos 3 e 4 abordam funções pequenas e comentários — diretamente relacionados à legibilidade de estruturas condicionais.
-
PHP: The Right Way — Boas práticas gerais https://phptherightway.com Guia colaborativo com práticas modernas para toda a série.
Exercícios
Exercício 1
Este código funcionava em PHP 7.3 e hoje nem executa. Qual é o erro, e por que a linguagem passou a recusá-lo?
<?php
$papel = $admin ? "administrador" : $editor ? "editor" : "leitor";
Ver resposta
✓ Resposta: O PHP 8 devolve Fatal error: Unparenthesized `a ? b : c ? d : e` is not supported. O motivo é histórico e vale conhecer: em quase todas as linguagens o ternário é associativo à direita, ou seja, a ? b : c ? d : e significa a ? b : (c ? d : e). No PHP era o contrário — associava à esquerda, virando (a ? b : c) ? d : e, o que produzia resultados que ninguém esperava. No exemplo, quem lê entende "se admin, administrador; senão, se editor, editor; senão, leitor", mas o PHP avaliava primeiro $admin ? "administrador" : $editor e usava esse resultado — uma string, sempre verdadeira — como condição do segundo ternário, devolvendo "editor" tanto para administrador quanto para editor. Em vez de trocar o comportamento em silêncio, e quebrar código existente de um jeito invisível, a linguagem escolheu o caminho honesto: marcou como obsoleto no 7.4 e transformou em erro fatal no 8.0, obrigando quem escreveu a dizer o que queria. A correção é parentizar — $admin ? "administrador" : ($editor ? "editor" : "leitor") — mas ternário encadeado é difícil de ler de qualquer forma: aqui, match(true) expressa melhor a intenção.
Exercício 2
A variável $id vem de $_GET, portanto é string. O que este switch imprime, e o que aconteceria se fosse match?
<?php
$id = "1";
switch ($id) {
case 1:
echo "inteiro";
break;
case "1":
echo "string";
break;
}
Ver resposta
✓ Resposta: Imprime "inteiro". O switch compara com ==, comparação frouxa: a string "1" é numérica, então é convertida e casa com o case 1, que vem primeiro — o case "1" nunca é alcançado. Trocando por match, o resultado é "string", porque match usa === e compara valor e tipo. Essa diferença é a razão principal para preferir match, e ela aparece justamente onde dói: dados vindos de $_GET, $_POST, de JSON ou do banco chegam como string com muito mais frequência do que se imagina, e um switch "funciona" por acidente até o dia em que a coerção decide diferente. Vale registrar que o PHP 8 também mudou a comparação frouxa entre número e string não numérica: switch (0) com case "abc" casava no PHP 7 e não casa mais — mais uma fonte silenciosa de mudança de comportamento em migração. E há uma armadilha adicional no switch que não existe no match: esquecer o break faz a execução escorrer para o caso seguinte, executando dois blocos. É um defeito que não gera erro nenhum e só aparece no resultado errado.
Exercício 3
Uma coluna status passou a aceitar o valor 'arquivado', que ninguém previu no código. O que acontece em cada uma destas versões?
<?php
// versão A
switch ($status) {
case 'rascunho': $label = 'Rascunho'; break;
case 'publicado': $label = 'Publicado'; break;
}
// versão B
$label = match($status) {
'rascunho' => 'Rascunho',
'publicado' => 'Publicado',
};
Ver resposta
✓ Resposta: Na versão A, nada acontece — e esse é o problema. Nenhum caso corresponde, $label nunca é atribuída, e o erro só se manifesta adiante, como Warning: Undefined variable $label ou como um espaço em branco na tela, longe da causa. Na versão B, o PHP lança UnhandledMatchError: Unhandled match case 'arquivado' na hora, com o valor ofensor na mensagem. Parece pior e é melhor: o erro aparece onde foi causado, com a informação necessária para corrigir. É a diferença entre falhar alto e falhar em silêncio, e em código de produção a primeira quase sempre custa menos. Isso não quer dizer que default seja opcional por descuido — a escolha é deliberada. Quando os valores possíveis são conhecidos e fechados (status de um pedido, tipo de usuário), omitir o default é uma checagem de exaustividade de graça: no dia em que alguém acrescentar um status, o sistema avisa em vez de renderizar em branco. Quando o valor vem de fora e pode ser qualquer coisa, o default é obrigatório e deve tratar o inesperado explicitamente. E se os estados são realmente fixos, o passo seguinte é transformá-los num enum, que resolve isso no nível do tipo.
Exercício 4
Em um arquivo de template que mistura HTML e PHP, quando vale a pena usar a sintaxe alternativa (if: … endif;) em vez das chaves?
Ver resposta
✓ Resposta: Vale exatamente quando o bloco é interrompido por HTML — que é o caso em template. Com chaves, o fecha-chaves fica perdido no meio da marcação, a dezenas de linhas do abre-chaves, e não há como saber a que estrutura ele pertence sem contar. A sintaxe alternativa nomeia o fechamento: <?php endif; ?>, <?php endforeach; ?>, <?php endwhile; ?>. Num arquivo com três estruturas aninhadas, isso é a diferença entre ler e adivinhar. A regra prática que a comunidade adota é: sintaxe alternativa em arquivo de apresentação, chaves em arquivo de lógica — e nunca as duas no mesmo arquivo, porque a mistura é pior que qualquer uma das duas. Dois cuidados acompanham: dentro do template, use a forma curta de eco, <?= $titulo ?>, que é sempre habilitada desde o PHP 5.4 e não depende do ajuste short_open_tag; e nunca imprima valor sem escapar, <?= htmlspecialchars($titulo, ENT_QUOTES, 'UTF-8') ?>, porque template é justamente onde o XSS entra. Em projeto que usa Blade ou Twig a questão desaparece, mas ler PHP puro em template continua sendo necessário — é o que se encontra em qualquer sistema mais antigo.
Exercício 5
Desafio: reescreva esta cadeia com match(true). Depois explique por que a ordem dos braços não pode ser alterada.
<?php
if ($pontos >= 10000) {
$nivel = 'Diamante';
} elseif ($pontos >= 5000) {
$nivel = 'Ouro';
} elseif ($pontos >= 1000) {
$nivel = 'Prata';
} else {
$nivel = 'Bronze';
}
Ver resposta
✓ Resposta: A tradução direta é esta:
<?php
$nivel = match(true) {
$pontos >= 10000 => 'Diamante',
$pontos >= 5000 => 'Ouro',
$pontos >= 1000 => 'Prata',
default => 'Bronze',
};
O ganho não é economia de linhas: é que a atribuição passou a ser uma expressão só. No original, $nivel é atribuída em quatro lugares diferentes, e basta um ramo esquecido para a variável não existir; aqui existe um único ponto de atribuição, e o default garante que ele sempre produz valor. Isso também permite declarar $nivel como readonly ou passá-la direto a uma função, sem a variável intermediária.
Sobre a ordem: match(true) avalia os braços de cima para baixo e para no primeiro que der verdadeiro — exatamente como o elseif. Como as faixas se sobrepõem (quem tem 12 mil pontos satisfaz as três condições), inverter a ordem quebra a lógica: com >= 1000 no topo, todo mundo acima de mil pontos vira Prata, e Ouro e Diamante se tornam inalcançáveis. Não há erro nem aviso — o código roda e classifica errado. É a diferença entre o match por valor, em que a ordem dos braços é irrelevante porque as comparações são exclusivas, e o match(true), em que a ordem é a lógica. Quando as faixas não podem se sobrepor por descuido, vale explicitar os dois limites em cada braço ($pontos >= 5000 && $pontos < 10000): fica mais verboso e deixa de depender da ordem.