Estruturas de Repetição

Estruturas de Repetição

Repetir é onde o PHP oferece mais caminhos para o mesmo destino: for quando o índice importa, foreach para percorrer coleções, while quando o fim depende de uma condição. Junto vêm as armadilhas conhecidas — a referência que sobrevive ao laço e o array alterado durante a própria travessia.
PHP

17 min de leitura

Se as estruturas de controle ensinam o programa a tomar decisões, as estruturas de repetição ensinam o programa a ter persistência. Repetir uma ação dez, cem ou um milhão de vezes sem escrever o código uma única vez extra — isso é o poder dos loops.

O PHP oferece quatro estruturas de repetição: for, foreach, while e do while. Cada uma tem um contexto onde brilha. Entender qual usar em cada situação é uma das marcas de um desenvolvedor experiente.

for

O for é ideal quando você sabe de antemão quantas vezes quer repetir algo. Sua estrutura tem três partes: inicialização, condição de continuação e incremento.

<?php

// Estrutura: for (inicialização; condição; incremento)
// 1. Inicialização: executada UMA vez, no início
// 2. Condição: verificada ANTES de cada iteração — se false, para
// 3. Incremento: executado APÓS cada iteração

for ($i = 0; $i < 5; $i++) {
    echo "Iteração $i\n";
}
// Saída: Iteração 0, Iteração 1, Iteração 2, Iteração 3, Iteração 4

// Contando de trás para frente
for ($i = 5; $i > 0; $i--) {
    echo "$i ";
}
// Saída: 5 4 3 2 1

// Incremento de dois em dois
for ($i = 0; $i <= 10; $i += 2) {
    echo "$i "; // 0 2 4 6 8 10
}

// Iterando sobre um array com índice numérico
$frutas = ["maçã", "banana", "laranja", "uva"];

for ($i = 0; $i < count($frutas); $i++) {
    // count() retorna o número de elementos do array
    echo "$i: {$frutas[$i]}\n";
}
// 0: maçã
// 1: banana
// 2: laranja
// 3: uva

foreach

O foreach é a estrutura de repetição mais usada no PHP cotidiano. Ele foi feito especificamente para iterar sobre arrays e objetos, sem a necessidade de controlar um índice manualmente.

<?php

$frutas = ["maçã", "banana", "laranja", "uva"];

// Forma simples — acessa apenas o valor
foreach ($frutas as $fruta) {
    echo $fruta . "\n";
}

// Forma com chave e valor — acessa índice e valor
foreach ($frutas as $indice => $fruta) {
    echo "$indice: $fruta\n";
}
// 0: maçã
// 1: banana
// 2: laranja
// 3: uva

// foreach com array associativo (chave => valor)
$pessoa = [
    "nome"  => "Ana",
    "idade" => 28,
    "cidade" => "São Paulo",
];

foreach ($pessoa as $chave => $valor) {
    echo "$chave: $valor\n";
}
// nome: Ana
// idade: 28
// cidade: São Paulo

// foreach com array de arrays — muito comum em dados de banco
$usuarios = [
    ["nome" => "Ana",    "idade" => 28],
    ["nome" => "Carlos", "idade" => 35],
    ["nome" => "Beatriz","idade" => 22],
];

foreach ($usuarios as $usuario) {
    // Cada $usuario é um array associativo
    echo "{$usuario['nome']} tem {$usuario['idade']} anos.\n";
}

Modificando valores com foreach por referência

Por padrão, o foreach trabalha com uma cópia do valor. Se você precisar modificar os elementos originais do array durante a iteração, use o operador & para passar por referência:

<?php

$precos = [10.0, 25.5, 8.0, 42.0];

// Sem referência — $preco é uma cópia, o array original não muda
foreach ($precos as $preco) {
    $preco *= 1.1; // aplica 10% de aumento — mas não salva!
}
var_dump($precos[0]); // float(10) — não mudou

// Com referência (&) — modifica o array original
foreach ($precos as &$preco) {
    $preco *= 1.1; // agora salva no array original
}
unset($preco); // IMPORTANTE: sempre destrua a referência após o loop
               // Sem isso, a variável $preco ainda aponta para o último elemento
               // e pode causar bugs sutis se o array for reutilizado

var_dump($precos[0]); // float(11) — mudou corretamente

O unset($preco) após o loop é uma das armadilhas mais clássicas do PHP. Nunca se esqueça dele ao usar referências em foreach.

while

O while repete enquanto uma condição for verdadeira. É ideal quando você não sabe de antemão quantas iterações serão necessárias — o loop termina quando a condição se torna falsa.

<?php

// Estrutura básica
$contador = 0;

while ($contador < 5) {
    echo "Contador: $contador\n";
    $contador++; // sem isso, loop infinito!
}

// Exemplo prático: lendo um arquivo linha por linha
// fgets() retorna false quando chega ao fim do arquivo
$arquivo = fopen("dados.txt", "r");

if ($arquivo) {
    while (($linha = fgets($arquivo)) !== false) {
        // processa cada linha
        echo trim($linha) . "\n";
    }
    fclose($arquivo);
}

// Simulando paginação — avança páginas enquanto houver resultados
$pagina  = 1;
$porPagina = 10;
$totalRegistros = 47;

while (($pagina - 1) * $porPagina < $totalRegistros) {
    $inicio = ($pagina - 1) * $porPagina + 1;
    $fim    = min($pagina * $porPagina, $totalRegistros);
    echo "Processando registros $inicio a $fim\n";
    $pagina++;
}
// Processando registros 1 a 10
// Processando registros 11 a 20
// Processando registros 21 a 30
// Processando registros 31 a 40
// Processando registros 41 a 47

do while

O do while é semelhante ao while, mas com uma diferença fundamental: o bloco de código é executado pelo menos uma vez antes de verificar a condição. Isso é útil quando você precisa que o código rode ao menos uma iteração independentemente da condição.

<?php

// do while garante ao menos uma execução
$tentativas = 0;

do {
    echo "Tentativa $tentativas\n";
    $tentativas++;
} while ($tentativas < 3);
// Tentativa 0
// Tentativa 1
// Tentativa 2

// Caso clássico: validação de entrada do usuário
// (em ambiente CLI — linha de comando)
do {
    $entrada = readline("Digite um número entre 1 e 10: ");
    $numero  = (int) $entrada;
} while ($numero < 1 || $numero > 10);

echo "Você digitou: $numero";

// Processamento com retry — tenta operação até ter sucesso ou esgotar tentativas
$maxTentativas = 3;
$tentativa     = 0;
$sucesso       = false;

do {
    $tentativa++;
    // simula uma operação que pode falhar
    $sucesso = rand(0, 1) === 1;
    echo "Tentativa $tentativa: " . ($sucesso ? "sucesso" : "falhou") . "\n";
} while (!$sucesso && $tentativa < $maxTentativas);

break e continue

Dois comandos que controlam o fluxo dentro de loops:

  • break — interrompe o loop completamente
  • continue — pula para a próxima iteração, ignorando o restante do bloco atual
<?php

$numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// break — para o loop quando encontra o primeiro número maior que 5
foreach ($numeros as $n) {
    if ($n > 5) {
        break; // sai do foreach completamente
    }
    echo "$n "; // 1 2 3 4 5
}

// continue — pula os números pares
foreach ($numeros as $n) {
    if ($n % 2 === 0) {
        continue; // vai para a próxima iteração
    }
    echo "$n "; // 1 3 5 7 9
}

// break com nível numérico — quebra loops aninhados
// break 2 sai de dois níveis de loop de uma vez
for ($i = 0; $i < 3; $i++) {
    for ($j = 0; $j < 3; $j++) {
        if ($i === 1 && $j === 1) {
            break 2; // sai dos dois loops
        }
        echo "($i,$j) ";
    }
}
// (0,0) (0,1) (0,2) (1,0)

Evitando loops infinitos

Um loop infinito trava o processo PHP e pode derrubar o servidor. As causas mais comuns são:

<?php

// ✗ LOOP INFINITO — condição nunca se torna false
$i = 0;
while ($i < 10) {
    echo $i;
    // esquecemos de incrementar $i!
}

// ✗ LOOP INFINITO — incremento errado
for ($i = 10; $i > 0; $i++) { // $i começa em 10 e só aumenta
    echo $i;
}

// ✓ Sempre verifique:
// 1. A variável de controle está sendo modificada dentro do loop?
// 2. A condição de parada é realmente atingível?
// 3. Há algum caso extremo (array vazio, valor zero) que precisa ser tratado?

// Proteção com contador de segurança — útil em loops complexos
$maxIteracoes = 1000;
$iteracao     = 0;

while (condicaoComplexaDificilDeVerificar()) {
    if ($iteracao++ > $maxIteracoes) {
        throw new RuntimeException("Loop excedeu o limite de segurança.");
    }
    // processamento...
}

Qual loop usar em cada situação

<?php

$nomes = ["Ana", "Bruno", "Carla"];

// foreach — iterando arrays ou coleções (uso mais comum)
foreach ($nomes as $nome) {
    echo $nome . "\n";
}

// for — quando o índice numérico importa ou você sabe o número de iterações
for ($i = 0; $i < count($nomes); $i++) {
    echo "$i: {$nomes[$i]}\n";
}

// while — condição desconhecida, leitura de streams, retries
$posicao = 0;
while ($posicao < count($nomes)) {
    echo $nomes[$posicao] . "\n";
    $posicao++;
}

// do while — quando precisa de ao menos uma execução garantida
$tentativa = 0;
do {
    echo "Tentativa $tentativa\n";
    $tentativa++;
} while ($tentativa < 3);

Boas práticas em loops

Prefira foreach para arrays. É mais legível, menos propenso a erros de índice e expressa melhor a intenção de "percorrer cada item".

Evite modificar o array que está sendo iterado. Se precisar filtrar ou transformar, prefira funções como array_filter() e array_map() — veremos no próximo módulo.

Não chame funções pesadas na condição do for. O código a seguir chama count() a cada iteração:

<?php

// ✗ count() é chamado em cada iteração
for ($i = 0; $i < count($array); $i++) { ... }

// ✓ Calcule uma vez só, antes do loop
$total = count($array);
for ($i = 0; $i < $total; $i++) { ... }

Sempre destrua referências após foreach com &. Como vimos, unset($variavel) após o loop é obrigatório.

Nomeie a variável de iteração com clareza. $usuario é melhor que $u. $produto é melhor que $p. $i é aceitável apenas para contadores numéricos simples.

A escolha do laço importa menos do que parece; o que separa código confiável de código que falha em dia de movimento é o cuidado com o que sobra depois dele. A referência que continua apontando para o último elemento, o array que muda de tamanho enquanto é percorrido, a memória que cresce junto com o arquivo de entrada — são três defeitos que não dão erro, só resultado errado. Quando o laço começar a ficar difícil de acompanhar, vale lembrar que array_map, array_filter e array_reduce resolvem boa parte dos casos sem laço nenhum.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Os preços deveriam terminar em 11, 22 e 33. O último item sai errado. Onde está o defeito?

<?php
$precos = [10.0, 20.0, 30.0];

foreach ($precos as &$preco) {
    $preco *= 1.1;
}

foreach ($precos as $preco) {
    echo $preco, " ";   // 11 22 22
}
Ver resposta

✓ Resposta: O defeito não está no primeiro laço, e sim no que sobrou dele. Depois de foreach ($precos as &$preco), a variável $preco continua sendo uma referência ao último elemento do array — ela não deixa de existir quando o laço termina. No segundo foreach, cada valor lido é atribuído a $preco, e como $preco ainda aponta para $precos[2], cada atribuição sobrescreve o último elemento. Na primeira volta, $precos[2] vira 11; na segunda, 22; na terceira, o PHP lê $precos[2], que agora vale 22, e atribui 22 nele mesmo. Daí a saída "11 22 22": o terceiro valor foi destruído pelo segundo laço, não pelo primeiro. A correção é uma linha: unset($preco); logo depois do laço por referência — sempre, sem exceção, porque o efeito só aparece quando alguém reutiliza o nome adiante, às vezes cem linhas abaixo. Vale saber que existe a alternativa de não usar referência nenhuma: foreach ($precos as $i => $preco) { $precos[$i] = $preco * 1.1; } escreve pelo índice e não deixa rastro, e array_map resolve o mesmo caso sem laço. Como regra geral, referência em PHP é ferramenta de último recurso: quase todo uso dela pode ser substituído por algo que não deixa armadilha atrás.

Exercício 2

Este laço deveria remover os itens cancelados. Ele remove só parte deles. Explique.

<?php
$pedidos = ['ok', 'cancelado', 'cancelado', 'ok'];

for ($i = 0; $i < count($pedidos); $i++) {
    if ($pedidos[$i] === 'cancelado') {
        array_splice($pedidos, $i, 1);
    }
}
print_r($pedidos);  // sobra um 'cancelado'
Ver resposta

✓ Resposta: O problema é modificar o array enquanto se caminha por índice sobre ele. Quando o item da posição 1 é removido, todos os seguintes andam uma casa para trás: o segundo 'cancelado', que estava em 2, passa a ocupar a posição 1. Mas o laço já incrementou $i para 2, e a posição 1 nunca mais é visitada — o item escapa. Com dois cancelados seguidos, sempre sobra um; com três, sobram dois. O sintoma é caracteristicamente confuso porque depende da disposição dos dados: a mesma rotina "funciona" quando os cancelados não estão adjacentes. Há três saídas, em ordem de preferência. A melhor é não remover em laço: $pedidos = array_values(array_filter($pedidos, fn($p) => $p !== 'cancelado')); constrói um array novo e não tem o problema (o array_values reindexa, já que array_filter preserva as chaves originais). A segunda é percorrer de trás para frente, for ($i = count($pedidos) - 1; $i >= 0; $i--), porque remover no fim não desloca o que ainda falta visitar. A terceira é coletar os índices a remover e apagá-los depois do laço. Repare, de passagem, no count() dentro da condição: ele é reavaliado a cada volta — aqui isso é até necessário, mas em laço que não altera o array vale guardar o total numa variável antes.

Exercício 3

O aviso "continue" targeting switch is equivalent to "break" aparece no log. O que o PHP está tentando dizer?

<?php
foreach ($linhas as $linha) {
    switch ($linha['tipo']) {
        case 'cabecalho':
            continue;     // pular esta linha
    }
    processar($linha);
}
Ver resposta

✓ Resposta: Está dizendo que o continue não faz o que o autor quis. Em PHP, o switch conta como uma estrutura de laço para efeito de break e continue — herança da sintaxe do C. Então continue dentro de um switch age sobre o switch, não sobre o foreach que o envolve: ele apenas sai do switch, exatamente como um break faria, e a linha processar($linha) é executada assim mesmo — inclusive para o cabeçalho, que era o caso a pular. O comportamento sempre foi esse; o que mudou é que, desde o PHP 7.3, a linguagem emite o aviso, porque a construção enganava praticamente todo mundo. A correção é dizer quantos níveis pular: continue 2 atravessa o switch e continua o foreach. Melhor ainda é evitar a ambiguidade: nesse caso específico, um if ($linha['tipo'] === 'cabecalho') { continue; } antes do switch expressa a intenção sem depender de contagem de níveis. E a lição vale além do caso: aviso no log é informação, não ruído — este aqui aponta um defeito real de lógica, e some da tela em produção justamente onde as consequências aparecem.

Exercício 4

Um relatório precisa parar de processar quando encontra o primeiro registro inválido, e outro precisa apenas pular os inválidos e seguir. Qual construção usar em cada caso, e qual é o risco de escolher errado?

Ver resposta

✓ Resposta: Parar é break: ele abandona o laço inteiro e a execução segue depois dele. Pular é continue: ele encerra apenas a volta atual e vai para a próxima. Trocar um pelo outro produz dois erros de natureza bem diferente. Usar break onde cabia continue faz o relatório parar no primeiro problema e entregar resultado parcial — mas ao menos o resultado é obviamente curto, o que costuma ser percebido. Usar continue onde cabia break é pior: o processamento segue depois da condição que deveria interrompê-lo, e o dano fica invisível. Imagine uma importação que deve abortar quando o arquivo está corrompido: com continue, ela grava metade dos registros e considera o trabalho feito. Dois detalhes práticos. Os dois aceitam número — break 2 e continue 2 atravessam laços aninhados —, e é um recurso que economiza a variável de controle ($achou = true) que muita gente usa no lugar; mas passar de dois níveis é sinal de que o trecho deveria ser uma função com return. E, quando o laço termina sem encontrar nada, o estado posterior precisa ser definido antes: inicialize a variável de resultado antes do laço, ou você vai depender de uma variável que pode nunca ter sido atribuída.

Exercício 5

Desafio: um CSV de 2 GB precisa ser somado por coluna. Este código estoura a memória. Reescreva e explique por que a sua versão não estoura.

<?php
$linhas = file('vendas.csv');       // Fatal error: Allowed memory size exhausted
$total = 0;
foreach ($linhas as $linha) {
    $total += (float) explode(';', $linha)[3];
}
Ver resposta

✓ Resposta: O problema é file(), que lê o arquivo inteiro e devolve um array com todas as linhas na memória de uma vez — 2 GB de arquivo viram bem mais que 2 GB de array, porque cada string PHP carrega sua própria estrutura. O memory_limit estoura antes da primeira soma. A correção é ler uma linha por vez, mantendo na memória apenas a linha atual:

<?php

function linhasDe(string $caminho): Generator
{
    $h = fopen($caminho, 'rb');
    if ($h === false) {
        throw new RuntimeException("Não consegui abrir {$caminho}");
    }

    try {
        // fgetcsv já trata aspas e separador dentro do campo —
        // explode(';') quebra no primeiro ponto e vírgula dentro de um texto
        while (($campos = fgetcsv($h, 0, ';')) !== false) {
            yield $campos;
        }
    } finally {
        fclose($h);   // fecha mesmo se quem consome interromper o laço
    }
}

$total = 0.0;
foreach (linhasDe('vendas.csv') as $i => $campos) {
    if ($i === 0) {
        continue;         // cabeçalho
    }
    $total += (float) str_replace(',', '.', $campos[3]);
}

A peça central é o gerador: yield devolve um valor e suspende a função, que retoma de onde parou na volta seguinte do foreach. O consumo de memória passa a ser o de uma linha, independentemente do tamanho do arquivo — 2 GB ou 200 GB rodam igual. Três detalhes importam. O fgetcsv substitui o explode porque respeita as aspas: um campo com ponto e vírgula dentro quebraria o explode e deslocaria todas as colunas seguintes. O finally garante o fclose mesmo que o consumidor use break, caso em que o gerador é descartado no meio. E o str_replace trata o decimal com vírgula, que é como sistemas brasileiros exportam. Para arquivo grande, vale ainda desligar o memory_limit apenas onde é necessário, mas a regra permanece: se o consumo de memória cresce com o tamanho da entrada, o desenho está errado.

Comentários

Mais em PHP

A História do PHP: de script pessoal a pilar da web
A História do PHP: de script pessoal a pilar da web

De um script para contar visitas a um currículo até a linguagem que move boa…

Configurando o Ambiente de Desenvolvimento
Configurando o Ambiente de Desenvolvimento

Instalar o PHP é a parte fácil; o que decide se o ambiente vai ajudar ou…

Arrays em Profundidade
Arrays em Profundidade

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