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 completamentecontinue— 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
-
Estruturas de repetição — Manual oficial do PHP https://www.php.net/manual/pt_BR/language.control-structures.php Documentação completa de
for,foreach,whileedo whilecom todos os detalhes de comportamento. -
foreach — Manual oficial do PHP https://www.php.net/manual/pt_BR/control-structures.foreach.php Documentação específica do
foreach, incluindo o comportamento de referências com&e as notas de compatibilidade. -
array_map, array_filter, array_walk — php.net https://www.php.net/manual/pt_BR/ref.array.php Funções funcionais que substituem loops em muitos casos. Veremos em detalhes no Módulo 2, mas vale uma espiada agora.
-
PHP: The Right Way — Arrays https://phptherightway.com/#arrays Seção sobre arrays e boas práticas de iteração, complementando o conteúdo deste artigo.
-
HUNT, Andrew; THOMAS, David. The Pragmatic Programmer. 20th Anniversary Edition. Addison-Wesley, 2019. https://pragprog.com/titles/tpp20/the-pragmatic-programmer-20th-anniversary-edition/ Capítulo sobre loops e estruturas de dados — leitura que transcende qualquer linguagem específica.
-
Generators — Manual oficial do PHP https://www.php.net/manual/pt_BR/language.generators.php Tema avançado para explorar depois: generators permitem iterar sobre grandes conjuntos de dados sem carregá-los todos na memória. Uma evolução natural dos loops.
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.