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

Herança, Interfaces e Traits
Herança, Interfaces e Traits

Herdar, assinar um contrato ou compor: três mecanismos que resolvem problemas…

Arrays em Profundidade
Arrays em Profundidade

O array do PHP é uma estrutura só que faz o papel de lista, mapa e registro —…

Design Patterns: Singleton, Factory e Builder
Design Patterns: Singleton, Factory e Builder

Três padrões de criação e o que cada um custa: o Singleton, que garante…