Estruturas de Controle

Estruturas de Controle

Decidir é o que separa um script de um programa. Como o PHP encadeia condições com if e elseif, por que o switch compara de um jeito que surpreende, e o que o match do PHP 8 resolve ao exigir comparação estrita e falhar alto quando nenhum caso corresponde.
PHP

• • 15 min de leitura

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 UnhandledMatchError se 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

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.

Comentários

Mais em PHP

Testes Automatizados com PHPUnit
Testes Automatizados com PHPUnit

Do teste unitário de uma entidade ao de integração com SQLite em memória…

Estruturas de Repetição
Estruturas de Repetição

Repetir é onde o PHP oferece mais caminhos para o mesmo destino: for quando o…

Deploy e Ambiente de Produção
Deploy e Ambiente de Produção

NGINX com PHP-FPM, script de deploy, pipeline no GitHub Actions, Docker e logs…