PHP Recursão

Uma função recursiva é uma função que chama a si mesma. Parece um truque de mágica (ou um jeito de travar o computador), mas é a forma mais natural de resolver problemas que têm "coisas dentro de coisas": pastas dentro de pastas, menus com submenus, comentários com respostas. Nesta aula você vai entender como isso funciona por dentro, quando usar e o que acontece quando dá errado.

Você vai aprender
  • O que é recursão e as duas partes obrigatórias: o caso base e o passo recursivo.
  • A contagem regressiva e o fatorial, passo a passo.
  • O que é a pilha de chamadas e por que o código depois da chamada roda "na volta".
  • Como percorrer uma árvore: um menu com submenus e as pastas da Ana (arrays dentro de arrays).
  • O que acontece quando a recursão não para (Allowed memory size … exhausted) e os outros erros comuns.
  • Quando é melhor usar um laço no lugar.

Antes, você precisa saber criar uma função com parâmetros e return (aula Funções) e percorrer um array com foreach. Os exemplos rodam na máquina da Ana, dentro da pasta ~/site.


A ideia antes do código: a fila do cinema

A Ana está numa fila enorme de cinema e quer saber em que posição está. Ela não consegue ver o começo da fila. Então ela pergunta para a pessoa da frente: "Em que posição você está?". Essa pessoa também não sabe, e faz a mesma pergunta para quem está na frente dela. E assim por diante…

…até chegar na primeira pessoa da fila. Ela não precisa perguntar a ninguém: responde "estou na posição 1". A resposta volta: quem estava atrás dela responde "então eu sou a 2", o próximo diz "3"… até a resposta chegar na Ana.

Repare nas duas partes dessa história. Toda recursão tem exatamente essas duas:

ParteNa fila do cinemaNo código
Caso base (onde para)A primeira pessoa sabe a resposta sem perguntar.Um if que devolve a resposta direto, sem chamar a função de novo.
Passo recursivo (a pergunta para a frente)"Pergunto para quem está na minha frente e somo 1."A função chama a si mesma com um problema menor (mais perto do caso base).

Se faltar o caso base, a pergunta nunca para. Se o problema não diminuir, nunca se chega ao caso base. Os dois erros terminam do mesmo jeito, como você vai ver no fim da aula.


Primeira recursão: contagem regressiva

Vamos começar pelo exemplo mais simples possível: contar de 3 até 1 e dizer "Fogo!".

<?php
function contagem(int $n): void
{
    if ($n === 0) {
        echo "Fogo!\n";
        return;
    }
    echo $n, "\n";
    contagem($n - 1);
}

contagem(3);
LinhaO que faz
function contagem(int $n): voidCria a função. Ela recebe um número inteiro e não devolve nada (void, "vazio"; veja Tipos e Retorno).
if ($n === 0) { … return; }Caso base: quando chegar em zero, escreve "Fogo!" e sai da função. Aqui a recursão para.
echo $n, "\n";Escreve o número atual.
contagem($n - 1);Passo recursivo: a função chama a si mesma com um número menor. Cada chamada chega mais perto do zero.
contagem(3);A primeira chamada, fora da função, que começa tudo.
$ php contagem.php
3
2
1
Fogo!

O que aconteceu, chamada por chamada:

  1. contagem(3): 3 não é 0, escreve 3 e chama contagem(2).
  2. contagem(2): escreve 2 e chama contagem(1).
  3. contagem(1): escreve 1 e chama contagem(0).
  4. contagem(0): é o caso base. Escreve Fogo! e volta. Como não há mais nada depois das chamadas, todas terminam.
Cada chamada tem o seu próprio $n

Quando contagem(3) chama contagem(2), o $n valendo 3 não é apagado. Cada chamada da função ganha uma cópia nova e separada das suas variáveis (você vai ver isso em detalhe em Escopo). Em certo momento existem quatro $n ao mesmo tempo: 3, 2, 1 e 0, cada um na sua chamada.

A ida e a volta: a pilha de chamadas

Esse "cada chamada espera a próxima terminar" tem nome: pilha de chamadas (call stack). Pense numa pilha de pratos: cada chamada nova é um prato colocado em cima; quando uma chamada termina, o prato de cima sai, e quem estava embaixo continua de onde parou.

Fica fácil de ver se colocarmos um echo depois da chamada recursiva:

<?php
function idaEVolta(int $n): void
{
    if ($n === 0) {
        echo "chegou ao fundo\n";
        return;
    }
    echo "descendo: $n\n";
    idaEVolta($n - 1);
    echo "subindo: $n\n";
}

idaEVolta(3);
$ php ida-e-volta.php
descendo: 3
descendo: 2
descendo: 1
chegou ao fundo
subindo: 1          « a chamada com $n = 1 continua depois que a de baixo terminou
subindo: 2
subindo: 3          « a primeira chamada é a última a terminar

A linha echo "subindo: $n\n" de cada chamada só roda quando a chamada de baixo termina. Por isso os números voltam ao contrário: 1, 2, 3. E repare que cada chamada se lembra do próprio $n, como dissemos.

Experimente: rode, depois troque idaEVolta(3) por idaEVolta(5). Em seguida, apague a linha do echo "descendo…" e veja que a contagem passa a sair só "na volta", de 1 até 5.

<?php
function idaEVolta(int $n): void
{
    if ($n === 0) {
        echo "chegou ao fundo\n";
        return;
    }
    echo "descendo: $n\n";
    idaEVolta($n - 1);
    echo "subindo: $n\n";
}

idaEVolta(3);

Uma recursão que devolve valor: o fatorial

O fatorial de um número (escrito com !) é a multiplicação dele por todos os inteiros menores até 1. Ele aparece em matemática para contar combinações: de quantas formas a Ana pode arrumar 5 livros na estante? 5! = 5 × 4 × 3 × 2 × 1 = 120.

Repare num detalhe: 5 × 4 × 3 × 2 × 1 é o mesmo que 5 × (4 × 3 × 2 × 1), ou seja, 5 × 4!. O fatorial de 5 é 5 vezes o fatorial de 4. Isso é um problema definido por uma versão menor dele mesmo, o lugar perfeito para recursão:

RegraParte
1! = 1 (e, por definição, 0! = 1)Caso base
n! = n × (n − 1)!Passo recursivo
<?php
function fatorial(int $n): int
{
    if ($n <= 1) {
        return 1;
    }
    return $n * fatorial($n - 1);
}

echo fatorial(5), "\n";
for ($i = 0; $i <= 6; $i++) {
    echo "$i! = ", fatorial($i), "\n";
}
echo fatorial(20), "\n";
function fatorial(int $n): int
Recebe um inteiro e devolve um inteiro.
if ($n <= 1) { return 1; }
Caso base. Usamos <= 1 (e não === 1) para que fatorial(0) também pare aqui.
return $n * fatorial($n - 1);
Passo recursivo: para calcular a multiplicação, o PHP precisa primeiro do resultado de fatorial($n - 1). Ele chama a função, espera a resposta, multiplica por $n e devolve.
$ php fatorial.php
120
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
2432902008176640000          « 20!, já um número enorme

Seguindo o fatorial(4) de perto

Na ida, cada chamada fica esperando. Na volta, cada uma recebe a resposta de baixo, faz a sua multiplicação e passa o resultado para cima:

ChamadaNa ida (esperando)Na volta (devolve)
fatorial(4)4 * fatorial(3)4 * 6 = 24
fatorial(3)3 * fatorial(2)3 * 2 = 6
fatorial(2)2 * fatorial(1)2 * 1 = 2
fatorial(1)caso base, não chama ninguém1

Leia a coluna "na volta" de baixo para cima: 1, 2, 6, 24. É a mesma fila do cinema, com multiplicação no lugar da soma.

Missão: o caso base devolve o valor errado, e tudo dá zero. Rode, veja o problema e corrija o return de dentro do if para que 4! = 24 e 5! = 120 apareçam.

<?php
function fatorial(int $n): int
{
    if ($n === 0) {
        return 0;
    }
    return $n * fatorial($n - 1);
}

echo "4! = ", fatorial(4), "\n";
echo "5! = ", fatorial(5), "\n";
<?php
function fatorial(int $n): int
{
    if ($n === 0) {
        return 1;
    }
    return $n * fatorial($n - 1);
}

echo "4! = ", fatorial(4), "\n";
echo "5! = ", fatorial(5), "\n";

Por que dava zero? O caso base devolvia 0, e qualquer número multiplicado por zero é zero. O valor do caso base "contamina" todas as voltas: escolher o valor certo nele é tão importante quanto ter um.


Onde a recursão brilha: árvores

Contagem e fatorial são bons para aprender, mas, na prática, um for resolve os dois (você vai ver isso daqui a pouco). A recursão fica insubstituível quando os dados têm uma forma de árvore: um item que tem itens dentro, que têm itens dentro… sem você saber quantos níveis existem.

Um menu com submenus

A Ana quer um menu para o site de receitas. Algumas seções têm subseções, que podem ter subseções também. Em PHP, isso é um array associativo em que cada valor é outro array (os filhos), vazio quando não há filhos:

<?php
$menu = [
    "Início"   => [],
    "Receitas" => [
        "Doces"    => [
            "Bolos"  => [],
            "Tortas" => [],
        ],
        "Salgados" => [],
    ],
    "Contato"  => [],
];

function mostrarMenu(array $itens, int $nivel = 0): void
{
    foreach ($itens as $nome => $filhos) {
        echo str_repeat("    ", $nivel), "- ", $nome, "\n";
        mostrarMenu($filhos, $nivel + 1);
    }
}

mostrarMenu($menu);
array $itens, int $nivel = 0
A função recebe um pedaço do menu e o nível de profundidade, que começa em 0 se ninguém disser nada (valor padrão; veja Parâmetros).
foreach ($itens as $nome => $filhos)
Para cada item deste nível: $nome é o texto ("Receitas") e $filhos é o array de dentro.
str_repeat(" ", $nivel)
Repete 4 espaços $nivel vezes: é o recuo que mostra a profundidade.
mostrarMenu($filhos, $nivel + 1)
Passo recursivo: "mostre os filhos deste item, um nível mais para dentro". O problema diminui porque $filhos é um pedaço menor da árvore.
$ php menu.php
- Início
- Receitas
    - Doces
        - Bolos
        - Tortas
    - Salgados
- Contato

Onde está o caso base? Ele está escondido: quando um item não tem filhos, $filhos é [] e o foreach de um array vazio não faz nenhuma volta. A função termina sem chamar mais ninguém. Todo array vazio é uma "folha" da árvore.

O mais bonito: se a Ana acrescentar "Brigadeiros" => [] dentro de "Bolos", a função continua funcionando sem mudar uma vírgula. Ela não sabe quantos níveis existem, nem precisa.

O mesmo menu numa página web

No site, o menu vira listas HTML (<ul>) dentro de listas. A função agora devolve o HTML como texto em vez de escrevê-lo. Repare no htmlspecialchars: o nome de cada item vai para o HTML protegido, como deve ser com todo texto que vira página (veja Escapar a Saída).

Missão: rode e veja o menu na aba Resultado. Depois acrescente o item "Pudins" dentro de "Doces", ao lado de Bolos e Tortas. A função não precisa mudar.

<?php
$menu = [
    "Início"   => [],
    "Receitas" => [
        "Doces"    => [
            "Bolos"  => [],
            "Tortas" => [],
        ],
        "Salgados" => [],
    ],
    "Contato"  => [],
];

function menuHtml(array $itens): string
{
    if ($itens === []) {
        return "";
    }
    $html = "<ul>\n";
    foreach ($itens as $nome => $filhos) {
        $html .= "<li>" . htmlspecialchars($nome) . menuHtml($filhos) . "</li>\n";
    }
    return $html . "</ul>\n";
}
?>
<!doctype html>
<html lang="pt-BR">
<head><meta charset="utf-8"><title>Receitas da Ana</title></head>
<body>
<h1>Receitas da Ana</h1>
<?= menuHtml($menu) ?>
</body>
</html>
<?php
$menu = [
    "Início"   => [],
    "Receitas" => [
        "Doces"    => [
            "Bolos"  => [],
            "Tortas" => [],
            "Pudins" => [],
        ],
        "Salgados" => [],
    ],
    "Contato"  => [],
];

function menuHtml(array $itens): string
{
    if ($itens === []) {
        return "";
    }
    $html = "<ul>\n";
    foreach ($itens as $nome => $filhos) {
        $html .= "<li>" . htmlspecialchars($nome) . menuHtml($filhos) . "</li>\n";
    }
    return $html . "</ul>\n";
}
?>
<!doctype html>
<html lang="pt-BR">
<head><meta charset="utf-8"><title>Receitas da Ana</title></head>
<body>
<h1>Receitas da Ana</h1>
<?= menuHtml($menu) ?>
</body>
</html>

Aqui o caso base é explícito: if ($itens === []) return "";. Sem ele, todo item sem filhos ganharia um <ul></ul> vazio no HTML. O código-fonte gerado pelo exemplo (antes da missão) é este; note como cada <ul> de dentro fica no meio de um <li>:

$ php menu-web.php
<!doctype html>
<html lang="pt-BR">
<head><meta charset="utf-8"><title>Receitas da Ana</title></head>
<body>
<h1>Receitas da Ana</h1>
<ul>
<li>Início</li>
<li>Receitas<ul>
<li>Doces<ul>
<li>Bolos</li>
<li>Tortas</li>
</ul>
</li>
<li>Salgados</li>
</ul>
</li>
<li>Contato</li>
</ul>
</body>
</html>

As pastas da Ana: somar o que está dentro

Agora uma árvore em que a função precisa juntar respostas. A Ana representou as pastas do computador assim: um arquivo é nome => tamanho em bytes; uma pasta é nome => [o que tem dentro]. Quanto espaço tudo ocupa?

<?php
// nome do arquivo => tamanho em bytes; nome da pasta => [o que tem dentro]
$casa = [
    "frutas.txt" => 47,
    "Documentos" => [
        "notas.txt"  => 69,
        "alunos.csv" => 180,
        "projetos"   => [
            "site" => [
                "index.php" => 340,
                "ola.php"   => 26,
            ],
        ],
    ],
    "Música" => [],
];

function tamanhoTotal(array $pasta): int
{
    $total = 0;
    foreach ($pasta as $nome => $conteudo) {
        if (is_array($conteudo)) {
            $total += tamanhoTotal($conteudo);   // é uma pasta: pergunte a ela
        } else {
            $total += $conteudo;                 // é um arquivo: some o tamanho
        }
    }
    return $total;
}

echo "Tudo junto: ", tamanhoTotal($casa), " bytes\n";
echo "Só Documentos: ", tamanhoTotal($casa["Documentos"]), " bytes\n";
TrechoO que faz
$total = 0;Cada chamada tem o seu próprio total, que começa em zero.
is_array($conteudo)"Isto é um array?" Se for, é uma pasta; se não, é o tamanho de um arquivo.
$total += tamanhoTotal($conteudo);Passo recursivo: pergunta à subpasta quanto ela ocupa e soma a resposta no total desta pasta.
$total += $conteudo;Caso base: um arquivo sabe o próprio tamanho, sem perguntar a ninguém.
return $total;Devolve a soma para quem perguntou (a pasta de cima, ou o echo no fim).
$ php pastas.php
Tudo junto: 662 bytes
Só Documentos: 615 bytes

Confira de cabeça: site = 340 + 26 = 366; projetos = 366; Documentos = 69 + 180 + 366 = 615; tudo = 47 + 615 + 0 (a pasta Música está vazia) = 662. Cada pasta responde só por si e pergunta às de dentro, exatamente como as pessoas da fila.

O PHP também tem funções prontas para árvores

array_walk_recursive() visita todas as "folhas" de um array aninhado, e count($array, COUNT_RECURSIVE) conta os elementos de todos os níveis (pastas inclusive). Elas resolvem casos simples; quando você precisa saber o nível, o caminho ou montar HTML, escrever a sua função recursiva é o caminho. Veja Funções de Array.


Recursão × laço

Tudo o que se faz com recursão dá para fazer com um laço, e vice-versa. O fatorial com um for fica assim:

<?php
function fatorial(int $n): int
{
    $resultado = 1;
    for ($i = 2; $i <= $n; $i++) {
        $resultado *= $i;
    }
    return $resultado;
}

echo fatorial(5), "\n";
$ php fatorial-laco.php
120

Mesmo resultado. Então, qual usar? Há uma diferença prática importante: cada chamada que fica esperando na pilha ocupa memória. Um laço de um milhão de voltas usa a mesma memória do começo ao fim; uma recursão de um milhão de níveis guarda um milhão de chamadas ao mesmo tempo. Veja a soma de 1 até n feita com recursão:

<?php
function somarAte(int $n): int
{
    if ($n === 0) {
        return 0;
    }
    return $n + somarAte($n - 1);
}

echo somarAte(100), "\n";
echo somarAte(100000), "\n";
echo somarAte(1000000), "\n";
$ php somar.php | head -4
5050
5000050000

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 262144 bytes) in /home/ana/site/somar.php on line 7

Cem mil níveis funcionaram; um milhão, não: a memória que o PHP pode usar (128 MB, o padrão do memory_limit) acabou antes. O | head -4 está ali porque, depois dessa linha, o PHP 8.5 mostra o caminho de todas as chamadas que estavam na pilha, mais de 700 mil linhas (mais sobre isso em Quando dá errado). Com um laço, o mesmo cálculo para um milhão não tem problema nenhum.

SituaçãoUsePor quê
Repetir algo N vezes, contar, somar uma listaLaço (for, foreach, while)Mais simples de ler e não gasta memória a cada volta.
Dados em árvore: menus, pastas, categorias, comentários com respostas, JSON aninhadoRecursãoVocê não sabe quantos níveis existem; um laço precisaria de uma "pilha" feita à mão.
Problema definido por uma versão menor dele mesmo (fatorial, potência)Os dois servemA recursão fica parecida com a definição; o laço aguenta números maiores.
Profundidade muito grande (dezenas de milhares de níveis ou mais)LaçoA recursão pode esgotar a memória.
Árvores reais quase nunca são fundas

Um menu tem 3 ou 4 níveis; uma pasta de computador, raramente mais de 20. Nesses casos a memória nunca é problema, e a recursão é a escolha certa. O limite só preocupa quando a profundidade depende de um número grande, como no somarAte(1000000).


Quando dá errado

Sem caso base: Allowed memory size of 134217728 bytes exhausted
<?php
function contagem(int $n): void
{
    echo $n, "\n";
    contagem($n - 1);
}

contagem(3);

Não há nenhum if que pare a função. Ela conta 3, 2, 1, 0, −1, −2… e não para nunca. Como a saída é gigantesca, a Ana mandou tudo para um arquivo e olhou só os pedaços:

$ php infinita.php > saida.txt
$ echo $?
255                              « 255 = o PHP terminou com erro fatal
$ grep -A4 "Fatal" saida.txt
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 262144 bytes) in /home/ana/site/infinita.php on line 5
Stack trace:
#0 /home/ana/site/infinita.php(5): contagem(-915005)
#1 /home/ana/site/infinita.php(5): contagem(-915004)
#2 /home/ana/site/infinita.php(5): contagem(-915003)
$ tail -3 saida.txt
#915007 /home/ana/site/infinita.php(5): contagem(2)
#915008 /home/ana/site/infinita.php(8): contagem(3)
#915009 {main}
$ wc -l saida.txt
1830022 saida.txt                « quase 2 milhões de linhas!

O que significa: "o tamanho de memória permitido, de 134217728 bytes (128 MB), se esgotou (ao tentar reservar mais 262144 bytes)". A função chamou a si mesma cerca de 915 mil vezes; cada chamada que fica esperando ocupa um pouco de memória, até o limite. O Stack trace ("rastro da pilha") lista as chamadas que estavam na pilha, da mais recente (#0, com $n = −915005) até a primeira (contagem(3), na linha 8). O PHP 8.5 passou a mostrar esse rastro também nos erros fatais como este PHP 8.5+; nas versões antigas aparecia só a linha do Fatal error.

Como consertar: toda função recursiva precisa de um caso base, um if que devolva a resposta sem chamar a função de novo. Quando vir Allowed memory size … exhausted e um stack trace em que a mesma função se repete, procure o caso base que falta ou que nunca é alcançado.

No editor do guia: o PHP do navegador tem outros limites de memória e de pilha, então o número de chamadas e a mensagem podem ser diferentes (e a página pode demorar). Prefira testar recursão infinita só na sua máquina, com Ctrl+C à mão.

O caso base existe, mas nunca é alcançado
<?php
function fatorial(int $n): int
{
    if ($n === 0) {
        return 1;
    }
    return $n * fatorial($n - 1);
}

echo fatorial(-1), "\n";
$ php negativo.php | head -3

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 262144 bytes) in /home/ana/site/negativo.php on line 7
Stack trace:

O que aconteceu: o caso base espera $n === 0, mas começando em −1 o $n só diminui: −2, −3, −4… nunca passa pelo zero. É o mesmo erro de não ter caso base.

Como consertar: use uma condição que cubra todos os casos "pequenos demais": if ($n <= 1). Melhor ainda, recuse entradas que não fazem sentido (fatorial de número negativo não existe); você vai aprender a fazer isso com exceções em Exceções.

Esqueceu o return na chamada recursiva: none returned
<?php
function fatorial(int $n): int
{
    if ($n <= 1) {
        return 1;
    }
    $n * fatorial($n - 1);
}

echo fatorial(5), "\n";
$ php sem-return.php

Fatal error: Uncaught TypeError: fatorial(): Return value must be of type int, none returned in /home/ana/site/sem-return.php:8
Stack trace:
#0 /home/ana/site/sem-return.php(7): fatorial(2)
#1 /home/ana/site/sem-return.php(7): fatorial(3)
#2 /home/ana/site/sem-return.php(7): fatorial(4)
#3 /home/ana/site/sem-return.php(10): fatorial(5)
#4 {main}
  thrown in /home/ana/site/sem-return.php on line 8

O que significa: "o valor devolvido deve ser do tipo int, nenhum foi devolvido". A linha 7 calcula $n * fatorial($n - 1), mas joga o resultado fora; a função chega ao } da linha 8 sem return. Quem quebrou foi fatorial(2), a primeira a voltar depois do caso base: o stack trace mostra que ela tinha sido chamada por fatorial(3), que foi chamada por fatorial(4), que foi chamada na linha 10 com 5.

Como consertar: return $n * fatorial($n - 1);. O tipo de retorno : int foi o que denunciou o erro; sem ele, a função devolveria null em silêncio e o echo mostraria uma linha vazia. Mais um motivo para declarar tipos (aula Tipos e Retorno).

Chamou a recursão, mas esqueceu de usar a resposta (resultado errado, sem erro)
<?php
$casa = [
    "frutas.txt" => 47,
    "Documentos" => [
        "notas.txt"  => 69,
        "alunos.csv" => 180,
    ],
];

function tamanhoTotal(array $pasta): int
{
    $total = 0;
    foreach ($pasta as $nome => $conteudo) {
        if (is_array($conteudo)) {
            tamanhoTotal($conteudo);
        } else {
            $total += $conteudo;
        }
    }
    return $total;
}

echo tamanhoTotal($casa), " bytes\n";
$ php esqueceu-somar.php
47 bytes                 « deveria ser 296: os arquivos de Documentos sumiram

O que aconteceu: a função perguntou à pasta Documentos quanto ela ocupa (a chamada calculou 249 direitinho), mas não guardou a resposta. Cada chamada tem o seu próprio $total; o da subpasta não é o mesmo da pasta de cima. Nenhum aviso aparece, porque o código é válido: ele só não faz o que você queria.

Como consertar: $total += tamanhoTotal($conteudo);. Regra prática: numa função recursiva que devolve valor, a chamada recursiva quase sempre aparece num return, numa conta ou numa atribuição, nunca sozinha na linha.

O resultado ficou grande demais: float returned
$ php fatorial.php       « o mesmo fatorial.php, com echo fatorial(21) no fim
…
2432902008176640000

Fatal error: Uncaught TypeError: fatorial(): Return value must be of type int, float returned in /home/ana/site/fatorial.php:7
Stack trace:
#0 /home/ana/site/fatorial.php(15): fatorial(21)
#1 {main}
  thrown in /home/ana/site/fatorial.php on line 7

O que aconteceu: não é um erro da recursão. 21! passa do maior inteiro que o PHP guarda (PHP_INT_MAX, 9223372036854775807); a conta vira um float e o tipo de retorno int recusa (veja int e float). O erro só aparece na última volta, em fatorial(21), que é quem faz 21 * 20!.

Como consertar: para números tão grandes, devolva float (perdendo precisão) ou use a extensão bcmath, que faz contas com quantos dígitos você quiser.


Referência

Termo / recursoO que é
Função recursivaUma função que chama a si mesma, direta ou indiretamente (a() chama b(), que chama a()).
Caso baseA situação em que a função responde sem se chamar. Obrigatório.
Passo recursivoA chamada a si mesma com um problema menor, mais perto do caso base.
Pilha de chamadas (call stack)As chamadas que começaram e ainda esperam terminar. A última a entrar é a primeira a sair.
Stack traceA lista dessas chamadas que o PHP mostra num erro, da mais recente (#0) até o {main} (o programa principal).
is_array($x)"É um array?" Separa pasta de arquivo, galho de folha.
memory_limitMemória máxima de um script (padrão 128 MB). Esgotá-la gera Allowed memory size of … bytes exhausted.
array_walk_recursive()Função pronta que visita todas as folhas de um array aninhado.
count($a, COUNT_RECURSIVE)Conta os elementos de todos os níveis.

Exercícios

1. O caso base

Complete a condição para a contagem parar quando chegar em zero:

if ($n ) { echo "Fogo!"; return; }

2. O passo recursivo

Complete a linha do fatorial:

return $n * ;

3. Folha ou galho?

Complete o teste que separa uma pasta (um array) de um arquivo:

if () { $total += tamanhoTotal($conteudo); }

4. O que idaEVolta(2) mostra?

5. Uma função recursiva terminou com Fatal error: Allowed memory size of 134217728 bytes exhausted e o stack trace repete a mesma função milhares de vezes. O que verificar primeiro?

6. Em qual destes casos a recursão é a escolha mais natural?

Missão final

Missão: complete a função contarArquivos para contar quantos arquivos existem em todas as pastas (as pastas não contam). Se o conteúdo for um array, some o que a subpasta responder; se não for, conte mais um. A resposta certa é A Ana tem 6 arquivos.

<?php
$casa = [
    "frutas.txt" => 47,
    "Documentos" => [
        "notas.txt"  => 69,
        "alunos.csv" => 180,
        "projetos"   => [
            "site" => [
                "index.php" => 340,
                "ola.php"   => 26,
            ],
        ],
    ],
    "Música" => [
        "forro.mp3" => 4100000,
    ],
];

function contarArquivos(array $pasta): int
{
    $quantos = 0;
    foreach ($pasta as $conteudo) {
        // complete aqui
    }
    return $quantos;
}

echo "A Ana tem ", contarArquivos($casa), " arquivos.\n";
<?php
$casa = [
    "frutas.txt" => 47,
    "Documentos" => [
        "notas.txt"  => 69,
        "alunos.csv" => 180,
        "projetos"   => [
            "site" => [
                "index.php" => 340,
                "ola.php"   => 26,
            ],
        ],
    ],
    "Música" => [
        "forro.mp3" => 4100000,
    ],
];

function contarArquivos(array $pasta): int
{
    $quantos = 0;
    foreach ($pasta as $conteudo) {
        if (is_array($conteudo)) {
            $quantos += contarArquivos($conteudo);
        } else {
            $quantos++;
        }
    }
    return $quantos;
}

echo "A Ana tem ", contarArquivos($casa), " arquivos.\n";

Resumo

  • Uma função recursiva chama a si mesma. Ela precisa de um caso base (responde sem se chamar) e de um passo recursivo (se chama com um problema menor).
  • Cada chamada tem as suas próprias variáveis. As chamadas ficam empilhadas esperando; o código depois da chamada recursiva roda "na volta", em ordem contrária.
  • Numa recursão que devolve valor, use a resposta da chamada: return $n * fatorial($n - 1);, $total += tamanhoTotal($sub);.
  • A recursão brilha em árvores: menus, pastas, categorias, comentários com respostas. Para contar e repetir, prefira um laço.
  • Sem caso base (ou com um caso base inalcançável), o PHP esgota a memória: Fatal error: Allowed memory size of … bytes exhausted, seguido de um stack trace com a mesma função repetida.