PHP while e do-while

Até agora, cada linha do seu programa rodava uma vez, de cima para baixo. Com um laço (ou "loop", ou "repetição"), um pedaço de código roda várias vezes. O mais simples de todos é o while ("enquanto"): repita isto enquanto uma condição for verdadeira. Nesta aula você vai conhecê-lo, junto com o irmão dele, o do-while, e aprender a reconhecer (e parar) um laço que nunca termina.

Você vai aprender
  • Como o while funciona: testar, repetir, testar de novo.
  • As três peças de um laço com contador: início, condição e mudança.
  • Laços que não sabem quantas voltas vão dar: juntar dinheiro até a meta, atender uma fila.
  • O do-while, que roda pelo menos uma vez, e quando usá-lo.
  • O que é um laço infinito, como pará-lo (Ctrl+C) e o limite de tempo do PHP.
  • A sintaxe alternativa while (…): … endwhile; dentro do HTML.

Você vai usar comparações (<, <=, !==…) e o if, que são a base das condições. Os exemplos rodam na pasta ~/site da Ana.


A ideia antes do código: lavar a louça

Depois do almoço, a Ana segue uma regra simples: enquanto houver louça na pia, ela pega um prato e lava. Ela não sabe de antemão quantos pratos são; só olha a pia antes de cada um. Quando olha e a pia está vazia, para.

Repare na ordem: primeiro ela olha (a condição), depois lava (o corpo do laço), depois olha de novo. Se a pia já estiver vazia logo no começo, ela não lava nada. É exatamente assim que o while funciona.


O while mais simples: contar até 5

<?php
$i = 1;
while ($i <= 5) {
    echo "Volta $i\n";
    $i++;
}
echo "Fim. \$i vale $i\n";
$i = 1;
Início: cria o contador antes do laço. ($i é um nome tradicional para contadores, de "índice".)
while
"Enquanto". Começa o laço.
($i <= 5)
A condição, entre parênteses: "$i é menor ou igual a 5?". Testada antes de cada volta. Se for verdadeira, o corpo roda; se for falsa, o laço termina.
{ … }
O corpo: o código repetido.
$i++;
A mudança: soma 1 ao contador (veja Operadores de Atribuição). Sem ela, $i ficaria em 1 para sempre.
$ php contador.php
Volta 1
Volta 2
Volta 3
Volta 4
Volta 5
Fim. $i vale 6

Seguindo o PHP passo a passo:

  1. $i vale 1. 1 <= 5? Sim: mostra "Volta 1", $i vira 2.
  2. 2 <= 5? Sim: "Volta 2", $i vira 3. E assim por diante…
  3. 5 <= 5? Sim (é "menor ou igual"): "Volta 5", $i vira 6.
  4. 6 <= 5? Não. O laço termina e o programa segue para a linha depois da }.

Por isso $i termina valendo 6: foi o valor que fez a condição falhar. Cada repetição do corpo se chama uma volta (ou iteração).

Missão: usando while, mostre 10 8 6 4 2 0 (de 10 até 0, descendo de 2 em 2, separados por espaço). Dica: a condição é $n >= 0 e a mudança é $n -= 2. Se a tela começar a encher sem parar, você criou um laço infinito: confira a mudança.

<?php
$n = 10;

// escreva o while aqui

echo "\n";
<?php
$n = 10;
while ($n >= 0) {
    echo $n, " ";
    $n -= 2;
}
echo "\n";

Quando você não sabe quantas voltas serão

Para contar de 1 a 5, existe um laço mais prático, o for (próxima aula). O while brilha quando o número de voltas depende do que acontece dentro do laço. A Ana quer juntar R$ 1.000: deposita R$ 150 por mês, e a poupança rende 1% ao mês. Em quantos meses ela chega lá?

<?php
$saldo = 0;
$meta = 1000;
$meses = 0;

while ($saldo < $meta) {
    $saldo = $saldo + 150;      // depósito do mês
    $saldo = $saldo * 1.01;     // rendimento de 1%
    $meses++;
    echo "Mês $meses: R$ ", number_format($saldo, 2, ",", "."), "\n";
}
echo "A Ana chegou à meta em $meses meses.\n";
$ php poupanca.php
Mês 1: R$ 151,50
Mês 2: R$ 304,52
Mês 3: R$ 459,06
Mês 4: R$ 615,15
Mês 5: R$ 772,80
Mês 6: R$ 932,03
Mês 7: R$ 1.092,85
A Ana chegou à meta em 7 meses.

Ninguém escreveu "7" no programa: o laço descobriu sozinho. A condição $saldo < $meta é testada todo mês, e o que a faz ficar falsa é o próprio $saldo crescendo lá dentro. (O number_format mostra o dinheiro no formato brasileiro; veja Formatar Números.)

Experimente: mude a meta para 5000 e o depósito para 300. Quantos meses agora? Depois coloque uma meta já batida ($saldo = 2000; no começo) e veja que o corpo não roda nenhuma vez.

<?php
$saldo = 0;
$meta = 1000;
$meses = 0;

while ($saldo < $meta) {
    $saldo = $saldo + 150;      // depósito do mês
    $saldo = $saldo * 1.01;     // rendimento de 1%
    $meses++;
    echo "Mês $meses: R$ ", number_format($saldo, 2, ",", "."), "\n";
}
echo "A Ana chegou à meta em $meses meses.\n";

A fila de atendimento

Outro caso clássico: tratar itens de uma lista até ela acabar. array_shift tira o primeiro elemento de um array e o devolve, como chamar o primeiro da fila:

<?php
$fila = ["Ana", "Bruno", "Carla"];

while (count($fila) > 0) {
    $cliente = array_shift($fila);
    echo "Atendendo $cliente. Ainda na fila: ", count($fila), "\n";
}
echo "Fila vazia!\n";
$ php fila.php
Atendendo Ana. Ainda na fila: 2
Atendendo Bruno. Ainda na fila: 1
Atendendo Carla. Ainda na fila: 0
Fila vazia!

Aqui a "mudança" que leva ao fim do laço é o array_shift: cada volta tira uma pessoa, até count($fila) (o tamanho da fila) chegar a zero. O mesmo padrão aparece ao ler um arquivo linha por linha, até o fim (veja Ler e Escrever Arquivos).


do-while: primeiro faz, depois pergunta

O while testa antes de cada volta. Se a condição já for falsa no começo, o corpo nunca roda. O do-while ("faça… enquanto") inverte a ordem: roda o corpo primeiro e só depois testa. Por isso ele sempre roda pelo menos uma vez.

<?php
$saldo = 2000;
$meta = 1000;

while ($saldo < $meta) {
    echo "Guardando dinheiro...\n";
    $saldo += 150;
}
echo "Fim (saldo: $saldo)\n";

do {
    echo "Guardando dinheiro (do-while)...\n";
    $saldo += 150;
} while ($saldo < $meta);
echo "Fim (saldo: $saldo)\n";
do {
"Faça". Abre o corpo, sem condição nenhuma ainda.
} while ($saldo < $meta);
A condição vem no fim, depois da chave. Atenção ao ponto e vírgula no final: aqui ele é obrigatório.
$ php nunca.php
Fim (saldo: 2000)                       « o while não rodou nenhuma vez
Guardando dinheiro (do-while)...        « o do-while rodou uma vez antes de testar
Fim (saldo: 2150)

Com o saldo já acima da meta, o while olhou a condição, viu que era falsa e pulou o corpo. O do-while rodou o corpo, só então olhou a condição, e parou.

O uso típico: perguntar até a resposta servir

O do-while é perfeito quando você precisa fazer algo antes de ter o que testar. Para conferir uma senha, primeiro é preciso pedir a senha. A Ana fez um programa de terminal que dá três chances (ler o teclado com fgets(STDIN) é assunto de PHP na Linha de Comando):

<?php
$tentativas = 0;

do {
    echo "Senha: ";
    $senha = trim(fgets(STDIN));
    $tentativas++;
} while ($senha !== "1234" && $tentativas < 3);

if ($senha === "1234") {
    echo "Acesso liberado após $tentativas tentativa(s).\n";
} else {
    echo "Bloqueado: 3 tentativas erradas.\n";
}
TrechoO que faz
fgets(STDIN)Lê uma linha digitada no teclado.
trim(…)Tira o Enter (a quebra de linha) do fim do que foi digitado.
$senha !== "1234" && $tentativas < 3Repete enquanto a senha estiver errada e ainda houver chances (veja Operadores Lógicos). Duas formas de sair: acertar, ou gastar as 3 chances.

Para testar sem digitar, a Ana mandou as respostas pelo printf, uma por linha. As perguntas "Senha:" saem coladas porque as respostas não aparecem na tela, só são lidas:

$ printf "abc\n1234\n" | php senha.php
Senha: Senha: Acesso liberado após 2 tentativa(s).
$ printf "a\nb\nc\n" | php senha.php
Senha: Senha: Senha: Bloqueado: 3 tentativas erradas.

Com o while comum, a primeira pergunta teria de ficar repetida antes do laço (ou a variável $senha nem existiria no primeiro teste). A missão no fim da aula mostra esse problema.

while ou do-while?

whiledo-while
Quando testaAntes de cada voltaDepois de cada volta
Mínimo de voltas01
Ponto e vírgula no fimNãoSim: } while (…);
Use quando…Pode não ser preciso fazer nada (fila vazia, meta já batida). É o caso mais comum.Precisa fazer uma vez para ter o que testar (pedir uma resposta, mostrar um menu).

O laço infinito

Se a condição nunca ficar falsa, o laço nunca termina. É o erro mais comum com while, e quase sempre a causa é a mesma: esquecer a linha que muda a variável da condição.

<?php
$i = 1;
while ($i <= 5) {
    echo "Volta $i\n";
}
$ php esqueceu.php | head -4
Volta 1
Volta 1
Volta 1
Volta 1                 « o head mostrou 4 linhas e encerrou; sem ele, isto nunca para

Sem o $i++, $i vale 1 para sempre, e 1 <= 5 é sempre verdade. No terminal, sem o | head, a tela se encheria de "Volta 1" sem parar.

Como parar um programa preso: Ctrl+C

No terminal, segure Ctrl e aperte C: o programa é interrompido e o prompt volta. Isso vale para qualquer programa, não só para o PHP. Num laço infinito que não escreve nada, o terminal parece "travado", sem mostrar nada; o Ctrl+C resolve do mesmo jeito.

O limite de tempo do PHP

Num site, o PHP não deixa uma página rodar para sempre: depois de 30 segundos (o padrão da configuração max_execution_time, "tempo máximo de execução"), ele interrompe o script com um erro fatal. No terminal, esse limite vem desligado, mas dá para ligá-lo com -d. A Ana testou um laço infinito que não escreve nada, com limite de 1 segundo:

<?php
$i = 1;
$total = 0;
while ($i <= 5) {
    $total += $i;
}
echo "Total: $total\n";
$ php -d max_execution_time=1 silencioso.php

Fatal error: Maximum execution time of 1 second exceeded in /home/ana/site/silencioso.php on line 5
Stack trace:
#0 {main}

"Tempo máximo de execução de 1 segundo excedido". Num site, você veria a mesma mensagem com 30 seconds, depois de esperar meio minuto com a página carregando. Se aparecer, procure um laço cuja condição nunca fica falsa.

Laços infinitos de propósito

Às vezes você quer um laço sem fim na condição, com a saída no meio do corpo: while (true) { … if (…) break; … }. O break ("quebrar") sai do laço na hora. Esse padrão, junto com o continue, está em break e continue.


A sintaxe alternativa: while (…): … endwhile;

Dentro do HTML (veja PHP Dentro do HTML), as chaves somem no meio das tags. Por isso existe uma forma com dois-pontos e endwhile; ("fim do while"):

<?php $i = 1; ?>
<ul>
<?php while ($i <= 3): ?>
    <li>Item <?= $i ?></li>
<?php $i++; endwhile; ?>
</ul>
$ php alt.php
<ul>
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
</ul>

O : substitui o {, e o endwhile; substitui o }. O mesmo existe para for (endfor), foreach (endforeach) e if (endif). Na prática, para gerar listas no HTML, o foreach e o for são mais usados que o while.


Quando dá errado

A tela se enche da mesma linha: faltou mudar a variável
$ php esqueceu.php | head -4
Volta 1
Volta 1
Volta 1
Volta 1

O que aconteceu: a condição depende de $i, mas nada dentro do laço muda $i.

Como consertar: interrompa com Ctrl+C e acrescente a mudança ($i++;) dentro do corpo. Regra: toda variável da condição precisa mudar dentro do laço, na direção que leva a condição a ficar falsa.

Nada aparece e o programa não termina: ponto e vírgula depois do while
<?php
$i = 1;
while ($i <= 5); {
    echo "Volta $i\n";
    $i++;
}
$ timeout 2 php pv.php
$ echo $?
124                      « 124: o timeout teve de parar o PHP depois de 2 segundos

O que aconteceu: o ; logo depois de while (…) é um corpo vazio. O PHP fica repetindo "nada" enquanto $i <= 5, e o bloco { … } de baixo (que tem o $i++) nunca é alcançado. Não aparece erro, nem saída: o programa só fica preso. (A Ana usou o comando timeout 2 para o terminal desistir sozinho; sem ele, seria Ctrl+C.)

Como consertar: apague o ;. No while, nada vai entre o ) e a {. (O ponto e vírgula depois do while só existe no fim do do-while.)

O laço não rodou nenhuma vez (sem erro)
$ php nunca.php
Fim (saldo: 2000)
…

O que aconteceu: a condição já era falsa no primeiro teste. Pode ser o certo (não havia o que fazer) ou um erro no valor inicial ou na comparação (> no lugar de <).

Como consertar: confira os valores antes do laço com var_dump (veja var_dump e print_r). Se o corpo precisa rodar pelo menos uma vez, use do-while.

Warning: Undefined variable na condição
$ printf "abc\n1234\n" | php senha0.php

Warning: Undefined variable $senha in /home/ana/site/senha0.php on line 4
Senha: Senha: Acesso liberado após 2 tentativa(s).

O que aconteceu: um while ($senha !== "1234" …) testa $senha antes de ela ser criada (ela só nasce dentro do corpo). O programa até funcionou, porque a variável inexistente vale null, que é diferente de "1234", mas o aviso mostra que o código está torto.

Como consertar: crie a variável antes do laço ($senha = "";) ou, melhor, use do-while, que pergunta antes de testar.

Maximum execution time of 30 seconds exceeded

O que aconteceu: na web, um script rodou por mais de 30 segundos, quase sempre por causa de um laço infinito (ou de um laço com dados demais). Veja o limite de tempo do PHP, acima.

Como consertar: procure o laço cuja condição nunca fica falsa. Aumentar o limite (set_time_limit) só faz sentido para tarefas longas de verdade, nunca para esconder um laço infinito.


Referência

FormaO que faz
while (condição) { … }Testa; se verdadeira, roda o corpo e testa de novo. Pode rodar 0 vezes.
do { … } while (condição);Roda o corpo, depois testa. Roda pelo menos 1 vez. Ponto e vírgula no fim.
while (condição): … endwhile;Sintaxe alternativa, para templates HTML.
while (true) { … break; … }Laço com a saída no meio (veja break e continue).
Ctrl+CInterrompe um programa preso no terminal.
max_execution_timeTempo máximo de um script: 30 s na web, sem limite no terminal. php -d max_execution_time=N muda para N segundos.

Exercícios

1. A condição

Complete para repetir enquanto $i for menor que 10:

while () { echo $i; $i++; }

2. A peça que falta

Este laço nunca termina. Complete a linha que falta dentro dele:

$n = 1; while ($n <= 3) { echo $n; }

3. O fim do do-while

Complete a última linha de um do-while que repete enquanto $opcao for diferente de "sair":

do { … }

4. Quantas vezes "oi" aparece? $x = 10; while ($x < 5) { echo "oi"; $x++; }

5. E agora? $x = 10; do { echo "oi"; $x++; } while ($x < 5);

6. Seu programa de terminal ficou preso, sem mostrar nada. O que fazer primeiro?

Missão final

Missão: este programa de senha usa while e testa $senha antes de ela existir: rode e veja o Warning. Transforme o laço num do-while (o do { no começo, o } while (…); no fim) para o aviso sumir. As respostas digitadas já estão prontas na caixa: abc e depois 1234.

<?php
$tentativas = 0;

while ($senha !== "1234" && $tentativas < 3) {
    echo "Senha: ";
    $senha = trim(fgets(STDIN));
    $tentativas++;
}

if ($senha === "1234") {
    echo "Acesso liberado após $tentativas tentativa(s).\n";
} else {
    echo "Bloqueado: 3 tentativas erradas.\n";
}
<?php
$tentativas = 0;

do {
    echo "Senha: ";
    $senha = trim(fgets(STDIN));
    $tentativas++;
} while ($senha !== "1234" && $tentativas < 3);

if ($senha === "1234") {
    echo "Acesso liberado após $tentativas tentativa(s).\n";
} else {
    echo "Bloqueado: 3 tentativas erradas.\n";
}

Resumo

  • while (condição) { … } repete o corpo enquanto a condição for verdadeira, testando antes de cada volta. Pode não rodar nenhuma vez.
  • Com contador, são três peças: o início antes do laço, a condição no while e a mudança dentro do corpo.
  • O while é ideal quando você não sabe quantas voltas serão: até bater a meta, até a fila acabar.
  • do { … } while (condição); testa depois: roda pelo menos uma vez. Ideal para pedir algo e repetir enquanto não servir.
  • Laço infinito: a condição nunca fica falsa (quase sempre faltou a mudança). Pare com Ctrl+C; na web, o PHP para em 30 segundos com Maximum execution time … exceeded.
  • No HTML, existe while (…): … endwhile;.