Entendendo os Generators do PHP e a Palavra-chave yield
Aprenda como os Generators do PHP funcionam e domine a palavra-chave yield com exemplos práticos. Reduza o consumo de memória, processe grandes volumes de dados com eficiência e descubra quando utilizar Generators.
Índice
- O que são os Generators no PHP?
- O problema dos Arrays
- Introduzindo o yield
- Como os Generators Funcionam Internamente
- Gerando Sequências Infinitas
- Lendo Arquivos Grandes de Forma Eficiente
- Utilizando yield com Chaves
- Delegando com yield from
- Generator vs Array
- Casos de Uso Mais Comuns
- Limitações
- Boas Práticas
- Perguntas Frequentes
- Conclusão
Ao trabalhar com coleções em PHP, a abordagem mais comum é construir um array e retorná-lo. Embora isso funcione muito bem para conjuntos de dados pequenos, essa estratégia pode rapidamente se tornar ineficiente ao processar milhares — ou até milhões — de registros.
Os Generators do PHP resolvem esse problema permitindo que os valores sejam produzidos um de cada vez, em vez de armazenar toda a coleção na memória. Eles oferecem uma forma simples e elegante de implementar avaliação preguiçosa (lazy evaluation), tornando as aplicações mais eficientes em termos de consumo de memória.
Neste artigo, você aprenderá como os Generators funcionam, o que a palavra-chave yield realmente faz, quando utilizá-los, quais são suas limitações e verá diversos exemplos práticos que poderá aplicar imediatamente.
O que são os Generators no PHP?
Um Generator é um tipo especial de função que permite ao PHP produzir valores um de cada vez, em vez de construir e retornar uma coleção inteira de uma só vez. Diferentemente de uma função comum, que retorna um valor apenas uma vez, um Generator pode pausar sua execução, produzir um valor e, posteriormente, continuar exatamente do ponto onde havia parado.
Os Generators são criados utilizando a palavra-chave yield. Assim que o PHP encontra um yield, a execução da função é temporariamente suspensa, preservando todas as variáveis locais e o estado atual da execução. Quando o próximo valor é solicitado, a função continua exatamente do ponto onde foi interrompida.
Esse comportamento é conhecido como lazy evaluation (ou lazy loading), pois os valores são gerados apenas quando realmente são necessários.
Uma função comum normalmente retorna um array contendo todos os valores:
function numbers(): array
{
return [1, 2, 3];
}
Um Generator produz cada valor individualmente:
function numbers(): Generator
{
yield 1;
yield 2;
yield 3;
}
Embora ambas as funções possam ser percorridas com foreach, elas se comportam de maneira muito diferente internamente. Um array precisa existir completamente na memória antes que a iteração possa começar, enquanto um Generator cria cada valor somente quando ele é solicitado.
Isso torna os Generators especialmente úteis ao trabalhar com:
- Grandes volumes de dados
- Registros de banco de dados
- Arquivos CSV
- Arquivos de log
- APIs paginadas
- Sequências infinitas
- Fluxos contínuos de dados (streams)
Em vez de alocar memória para cada elemento, um Generator mantém apenas o estado atual da execução em memória, tornando-se uma solução extremamente eficiente para o processamento sequencial de dados.
O problema dos Arrays
Os arrays são uma das estruturas de dados mais utilizadas no PHP. Eles são flexíveis, fáceis de usar e oferecem acesso rápido aos seus elementos. No entanto, possuem uma limitação importante: todos os elementos precisam existir na memória antes que o array possa ser retornado.
Considere a seguinte função:
function numbers(): array
{
$result = [];
for ($i = 1; $i <= 1_000_000; $i++) {
$result[] = $i;
}
return $result;
}
Seu uso é bastante simples:
foreach (numbers() as $number) {
// Processa cada número...
}
O problema é que o PHP não consegue iniciar o foreach até que todo o array tenha sido construído.
Internamente, o processo acontece mais ou menos assim:
- Aloca memória para um array vazio.
- Cria um milhão de inteiros.
- Armazena todos eles no array.
- Retorna o array completo.
- Só então inicia a iteração.
Mesmo que seu código precise apenas dos primeiros valores, o PHP já gastou tempo e memória gerando todos os elementos.
Para coleções pequenas, isso dificilmente representa um problema. No entanto, imagine processar:
- um arquivo CSV com 20 milhões de linhas;
- uma tabela de banco de dados contendo milhões de registros;
- uma API que retorna milhares de páginas de resultados;
- anos de arquivos de log de uma aplicação.
Carregar tudo em um array pode aumentar drasticamente o consumo de memória e até ultrapassar o limite definido por memory_limit.
Quanto maior o conjunto de dados, maior será o consumo de memória da aplicação. Eventualmente, isso pode resultar em uma execução mais lenta ou em erros fatais como:
Allowed memory size exhausted
Em situações como essas, criar toda a coleção antecipadamente é desnecessário. A maioria das aplicações processa os elementos de forma sequencial, um após o outro, tornando muito mais eficiente gerar cada valor apenas quando ele for necessário.
Foi exatamente para resolver esse problema que os Generators foram criados.
Introduzindo o yield
Os Generators são criados utilizando a palavra-chave yield. À primeira vista, ela pode parecer semelhante ao return, mas seu comportamento é completamente diferente.
Uma função comum encerra sua execução assim que encontra um return:
function numbers(): array
{
return [1, 2, 3];
}
Já um Generator pode produzir vários valores durante uma única execução:
function numbers(): Generator
{
yield 1;
yield 2;
yield 3;
}
Consumir um Generator é praticamente idêntico a percorrer um array:
foreach (numbers() as $number) {
echo $number . PHP_EOL;
}
Saída:
1
2
3
Apesar da sintaxe semelhante, essa função não retorna um array.
Em vez disso, ela retorna uma instância da classe nativa Generator do PHP:
$generator = numbers();
var_dump($generator);
Saída:
object(Generator)
Cada vez que o foreach solicita um novo valor, o Generator retoma sua execução até encontrar o próximo yield.
Você pode pensar no yield como se ele dissesse:
"Aqui está o próximo valor. Pause a função neste ponto e continue exatamente daqui quando o próximo valor for solicitado."
Esse comportamento permite que o PHP processe os dados de forma incremental, em vez de construir toda a coleção antecipadamente.
Um Generator também pode ser combinado com código PHP comum:
function countdown(int $from): Generator
{
while ($from > 0) {
echo "Gerando {$from}..." . PHP_EOL;
yield $from;
$from--;
}
}
Observe que a mensagem é exibida apenas quando o próximo valor é solicitado:
foreach (countdown(3) as $number) {
echo "Recebido {$number}" . PHP_EOL;
}
Saída:
Gerando 3...
Recebido 3
Gerando 2...
Recebido 2
Gerando 1...
Recebido 1
Isso demonstra uma das principais características dos Generators: a execução acontece de forma progressiva, e não toda de uma vez. Em vez de produzir uma coleção inteira antecipadamente, cada valor é gerado somente quando o consumidor o solicita, tornando os Generators uma excelente escolha para processar grandes volumes de dados ou até fluxos potencialmente infinitos.
Como os Generators Funcionam Internamente
Um dos aspectos mais interessantes dos Generators é que eles não executam toda a função de uma só vez. Em vez disso, o PHP pausa a execução sempre que encontra uma instrução yield e preserva todo o estado atual da função.
Isso significa que todas as variáveis locais, a posição atual da execução e todo o contexto da função permanecem armazenados. Quando o próximo valor é solicitado, a função continua exatamente do ponto onde havia sido interrompida.
Considere o exemplo abaixo:
function demo(): Generator
{
echo "Etapa A" . PHP_EOL;
yield 1;
echo "Etapa B" . PHP_EOL;
yield 2;
echo "Etapa C" . PHP_EOL;
}
Agora percorra o Generator:
foreach (demo() as $value) {
echo "Recebido: {$value}" . PHP_EOL;
}
Saída:
Etapa A
Recebido: 1
Etapa B
Recebido: 2
Etapa C
Observe o que acontece:
- A função começa a ser executada.
"Etapa A"é exibida.yield 1produz o primeiro valor.- A execução da função é pausada.
- O
foreachrecebe esse valor. - O PHP retoma a execução logo após o primeiro
yield. "Etapa B"é exibida.yield 2produz o segundo valor.- A função é pausada novamente.
- Após o último valor ser consumido, a execução continua até o final da função.
Esse comportamento é completamente diferente do de uma função comum. Uma função tradicional executa do início ao fim sem interrupções antes de devolver o controle ao código que a chamou.
Os Generators se comportam mais como máquinas de estado (state machines): eles pausam a execução, preservam todo o seu contexto interno e continuam posteriormente exatamente do ponto onde haviam parado.
Como apenas o estado atual da execução precisa ser armazenado, os Generators consomem muito pouca memória, independentemente da quantidade de valores que possam produzir.
Gerando Sequências Infinitas
Uma das maiores vantagens dos Generators é que eles podem representar sequências infinitas.
Um array jamais poderia conter uma quantidade infinita de elementos, pois seria necessário alocar memória para cada um deles. Já um Generator produz valores sob demanda, portanto não precisa saber antecipadamente onde a sequência termina.
Veja um contador simples:
function counter(): Generator
{
$i = 1;
while (true) {
yield $i++;
}
}
Embora esse Generator nunca termine por conta própria, consumi-lo de forma segura é bastante simples:
$count = 0;
foreach (counter() as $number) {
echo $number . PHP_EOL;
if (++$count === 10) {
break;
}
}
Saída:
1
2
3
4
5
6
7
8
9
10
O Generator poderia continuar produzindo números indefinidamente, mas quem decide quando parar é o consumidor.
Esse padrão é extremamente útil para aplicações que trabalham com fluxos contínuos de dados, como:
- Processamento de eventos
- Filas de mensagens
- Dados de sensores
- Pacotes de rede
- Monitoramento de logs em tempo real
- Geração de números aleatórios
- Paginação sem um número conhecido de páginas
Você também pode gerar sequências infinitas mais complexas.
Por exemplo, a sequência de Fibonacci:
function fibonacci(): Generator
{
$a = 0;
$b = 1;
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
Consumindo apenas os quinze primeiros valores:
$count = 0;
foreach (fibonacci() as $number) {
echo $number . PHP_EOL;
if (++$count === 15) {
break;
}
}
Embora essa sequência seja teoricamente infinita, o consumo de memória permanece praticamente constante, pois o Generator mantém apenas o estado atual da execução (as variáveis $a e $b), e não todos os números gerados anteriormente.
Lendo Arquivos Grandes de Forma Eficiente
Um dos usos mais comuns dos Generators em aplicações reais é a leitura de arquivos muito grandes.
Imagine um arquivo de log contendo vários gigabytes de dados. Uma abordagem ingênua seria carregar todo o arquivo para a memória:
$lines = file('application.log');
foreach ($lines as $line) {
// Processa cada linha...
}
Embora seja bastante conveniente, a função file() lê todas as linhas antes mesmo de o laço começar. Em arquivos muito grandes, isso pode consumir centenas de megabytes — ou até mesmo gigabytes — de memória.
Um Generator oferece uma solução muito mais escalável:
function readLines(string $filename): Generator
{
$handle = fopen($filename, 'r');
if ($handle === false) {
throw new RuntimeException("Não foi possível abrir o arquivo.");
}
try {
while (($line = fgets($handle)) !== false) {
yield rtrim($line, "\r\n");
}
} finally {
fclose($handle);
}
}
Seu uso continua igualmente simples:
foreach (readLines('application.log') as $line) {
echo $line . PHP_EOL;
}
A diferença mais importante é que apenas uma linha permanece na memória por vez.
O fluxo de execução acontece, aproximadamente, da seguinte maneira:
- Abre o arquivo.
- Lê a primeira linha.
- Produz essa linha com
yield. - Aguarda até que o consumidor solicite o próximo valor.
- Lê a linha seguinte.
- Repete esse processo até o final do arquivo.
- Fecha o arquivo.
Essa abordagem escala para arquivos de praticamente qualquer tamanho, pois o consumo de memória permanece praticamente constante.
Generators também são excelentes para processar:
- Importações de arquivos CSV
- Exportações de XML
- Arquivos JSON Lines (JSONL)
- Logs de acesso do Apache ou do Nginx
- Arquivos de backup
- Dumps de banco de dados
- Fluxos de dados provenientes de serviços externos
Sempre que os dados puderem ser processados sequencialmente, um Generator normalmente será uma escolha melhor do que carregar toda a coleção na memória antes de iniciar o processamento.
Utilizando yield com Chaves
Assim como os arrays, os Generators também podem produzir pares de chave e valor. Isso os torna totalmente compatíveis com o foreach, permitindo acessar tanto a chave quanto o valor durante a iteração.
Em vez de produzir apenas um valor:
yield 'Alice';
você pode definir explicitamente uma chave:
function getUser(): Generator
{
yield 'id' => 42;
yield 'name' => 'Alice';
yield 'email' => 'alice@example.com';
}
Percorrer esse Generator funciona exatamente como em um array associativo:
foreach (getUser() as $key => $value) {
echo "{$key}: {$value}" . PHP_EOL;
}
Saída:
id: 42
name: Alice
email: alice@example.com
Os Generators também podem produzir chaves numéricas.
Quando nenhuma chave é informada, o PHP atribui índices inteiros sequenciais automaticamente:
function colors(): Generator
{
yield 'Red';
yield 'Green';
yield 'Blue';
}
O código acima é equivalente a:
function colors(): Generator
{
yield 0 => 'Red';
yield 1 => 'Green';
yield 2 => 'Blue';
}
Você também pode definir suas próprias chaves numéricas:
function statusCodes(): Generator
{
yield 200 => 'OK';
yield 404 => 'Not Found';
yield 500 => 'Internal Server Error';
}
Isso pode ser bastante útil ao produzir dados estruturados nos quais a chave possui significado próprio, como:
- Códigos de status HTTP
- IDs de produtos
- Chaves primárias de bancos de dados
- Opções de configuração
- Códigos de idioma
Embora os Generators suportem chaves, lembre-se de que eles continuam sendo iteradores, e não arrays. Isso significa que não é possível acessar diretamente um valor pela chave (por exemplo, $generator[404] não é suportado). As chaves estão disponíveis apenas durante a iteração.
Delegando com yield from
À medida que uma aplicação cresce, é comum precisar combinar vários Generators menores para formar um fluxo maior de dados. Em vez de percorrer manualmente outro Generator e produzir cada valor individualmente, o PHP oferece a sintaxe yield from.
Imagine que você possua Generators separados para diferentes tipos de mensagens de log:
function infoLogs(): Generator
{
yield '[INFO] Application started';
yield '[INFO] User authenticated';
}
function errorLogs(): Generator
{
yield '[ERROR] Database connection failed';
yield '[ERROR] Retry limit exceeded';
}
Sem yield from, seria necessário escrever outro laço para combinar ambos:
function allLogs(): Generator
{
foreach (infoLogs() as $message) {
yield $message;
}
foreach (errorLogs() as $message) {
yield $message;
}
}
Esse código funciona perfeitamente, mas é mais verboso do que o necessário.
Com yield from, a implementação fica muito mais simples:
function allLogs(): Generator
{
yield from infoLogs();
yield from errorLogs();
}
O resultado é exatamente o mesmo:
foreach (allLogs() as $message) {
echo $message . PHP_EOL;
}
Saída:
[INFO] Application started
[INFO] User authenticated
[ERROR] Database connection failed
[ERROR] Retry limit exceeded
Na prática, o Generator delegado se comporta como se seus valores tivessem sido produzidos diretamente pelo Generator principal.
Além disso, yield from não funciona apenas com outros Generators. Ele pode delegar a qualquer objeto iterável, incluindo arrays.
function configuration(): Generator
{
yield 'Application Configuration';
yield from [
'Environment: Production',
'Cache: Enabled',
'Debug: Disabled',
];
yield 'Configuration Loaded';
}
De maneira geral, utilizar yield from resulta em um código:
- Menor
- Mais legível
- Mais fácil de manter
- Menos sujeito a erros
Sempre que o único objetivo do seu Generator for encaminhar os valores de outro iterável, yield from normalmente é a melhor escolha.
Generator vs Array
Generators e arrays são estruturas iteráveis, mas foram projetados para resolver problemas diferentes. A escolha entre um e outro depende da forma como seus dados serão utilizados.
A tabela abaixo resume as principais diferenças:
| Característica | Generator | Array |
|---|---|---|
| Consumo de memória | Muito baixo | Cresce conforme a quantidade de elementos |
| Lazy evaluation | ✅ Sim | ❌ Não |
Acesso aleatório ($items[5]) |
❌ Não | ✅ Sim |
| Pode ser reutilizado | ❌ Não (após ser consumido) | ✅ Sim |
Suporta count() diretamente |
❌ Não | ✅ Sim |
| Adequado para sequências infinitas | ✅ Sim | ❌ Não |
| Processamento sequencial | ✅ Excelente | ✅ Bom |
| Ideal para grandes volumes de dados | ✅ Sim | ⚠️ Depende da memória disponível |
Consumo de Memória
Arrays alocam memória para todos os elementos que contêm.
Já os Generators mantêm apenas o estado atual da execução, independentemente da quantidade de valores que possam produzir.
Por exemplo, processar um milhão de registros de um banco de dados utilizando um array exige que todos eles sejam carregados na memória antes do início da iteração. Com um Generator, cada registro pode ser processado, descartado e substituído pelo próximo, mantendo o consumo de memória praticamente constante.
Acesso Aleatório
Arrays são a melhor opção quando você precisa acessar elementos específicos diretamente.
$products = [
'Laptop',
'Keyboard',
'Mouse',
];
echo $products[1];
Generators não oferecem esse recurso.
Como os valores são produzidos sob demanda, eles simplesmente não existem até que o iterador alcance aquele ponto da sequência.
Reutilização
Um array pode ser percorrido quantas vezes forem necessárias:
foreach ($products as $product) {
// ...
}
foreach ($products as $product) {
// Percorre novamente
}
Já um Generator é consumido durante a iteração.
Depois que ele chega ao fim, não pode simplesmente ser reiniciado. Para percorrê-lo novamente, é necessário criar uma nova instância chamando novamente a função que o produz.
Desempenho
Existe um equívoco bastante comum de que Generators são sempre mais rápidos que arrays.
Na realidade, o principal benefício dos Generators é reduzir o consumo de memória, e não aumentar a velocidade de execução.
Como eles produzem um valor por vez, existe um pequeno custo adicional para retomar a execução da função a cada yield. Na maioria dos casos esse custo é insignificante e é amplamente compensado pela possibilidade de processar conjuntos de dados muito maiores sem esgotar a memória disponível.
Quando Utilizar Cada Um
Prefira um Generator quando precisar:
- Processar grandes volumes de dados
- Ler arquivos linha por linha
- Consumir dados provenientes de APIs
- Trabalhar com resultados paginados
- Construir pipelines de processamento
- Produzir valores sob demanda
- Gerar sequências potencialmente infinitas
Prefira um Array quando:
- O conjunto de dados for relativamente pequeno
- Você precisar de acesso aleatório aos elementos
- Precisar ordenar, filtrar ou modificar a coleção repetidamente
- Precisar utilizar funções como
count(),array_map()ouarray_filter() - A coleção precisar ser reutilizada diversas vezes
Nenhuma das duas estruturas é universalmente melhor. Arrays continuam sendo a escolha mais prática para grande parte das tarefas do dia a dia, enquanto os Generators se destacam quando eficiência de memória e processamento sequencial passam a ser os fatores mais importantes.
Casos de Uso Mais Comuns
Embora os Generators possam ser utilizados em qualquer situação onde um iterável seja esperado, eles se destacam quando os dados podem ser processados sequencialmente, sem que toda a coleção precise permanecer na memória.
Veja alguns dos casos de uso mais comuns em aplicações reais.
Processando Grandes Conjuntos de Resultados do Banco de Dados
Ao consultar uma tabela muito grande, geralmente não há necessidade de carregar todos os registros em um array antes de processá-los.
Em vez disso, cada registro pode ser produzido individualmente:
function users(PDO $pdo): Generator
{
$statement = $pdo->query('SELECT id, name FROM users');
while ($user = $statement->fetch(PDO::FETCH_ASSOC)) {
yield $user;
}
}
Dessa forma, sua aplicação pode processar milhões de registros mantendo um consumo de memória muito baixo.
Consumindo APIs Paginadas
Muitas APIs REST retornam seus resultados em várias páginas.
Um Generator pode encapsular toda a lógica de paginação, tornando seu uso transparente para quem consome os dados:
function fetchRepositories(ApiClient $client): Generator
{
$page = 1;
while ($repositories = $client->repositories($page++)) {
foreach ($repositories as $repository) {
yield $repository;
}
}
}
Quem utiliza essa função não precisa saber quantas páginas existem. Basta percorrer os resultados normalmente.
Importando Arquivos CSV
Em vez de carregar um arquivo CSV inteiro para a memória, cada linha pode ser processada assim que for lida.
function readCsv(string $filename): Generator
{
$handle = fopen($filename, 'r');
try {
while (($row = fgetcsv($handle)) !== false) {
yield $row;
}
} finally {
fclose($handle);
}
}
Essa abordagem continua eficiente mesmo para arquivos contendo milhões de registros.
Construindo Pipelines de Transformação de Dados
Generators são excelentes para criar pipelines de processamento, onde cada etapa transforma os dados antes de passá-los para a próxima.
function uppercase(iterable $names): Generator
{
foreach ($names as $name) {
yield strtoupper($name);
}
}
Como cada valor é transformado individualmente, não é necessário criar arrays intermediários durante o processamento.
Processando Fluxos Contínuos de Dados
Alguns serviços fornecem dados continuamente, em vez de retornar uma resposta completa de uma única vez.
Os Generators se encaixam perfeitamente nesses cenários, pois podem produzir cada valor assim que ele estiver disponível.
Alguns exemplos incluem:
- Filas de mensagens
- Mensagens via WebSocket
- Fluxos de eventos (event streams)
- Telemetria em tempo real
- Monitoramento de logs
Gerando Dados Sob Demanda
Em alguns casos, os dados sequer existem previamente: eles podem simplesmente ser calculados quando forem solicitados.
Alguns exemplos são:
- Geração de UUIDs
- Números aleatórios
- Intervalos de datas
- Candidatos a senhas
- Sequências matemáticas
Em vez de gerar milhares de valores antecipadamente, um Generator produz apenas o próximo valor necessário.
Limitações
Embora os Generators sejam extremamente poderosos, eles não substituem os arrays. Conhecer suas limitações é tão importante quanto entender suas vantagens.
Não Há Acesso Aleatório
Generators só podem avançar para frente.
Ao contrário dos arrays, não é possível acessar diretamente um elemento específico:
$generator[10];
Isso não é suportado porque os valores anteriores não ficam armazenados na memória.
Apenas Uma Iteração
Um Generator é consumido à medida que é percorrido.
Depois que sua execução termina, ele não pode simplesmente ser reutilizado:
$generator = getItems();
foreach ($generator as $item) {
// ...
}
foreach ($generator as $item) {
// Não funcionará como esperado
}
Se for necessário percorrer os dados novamente, será preciso criar uma nova instância chamando outra vez a função que produz o Generator.
Não Pode Ser Contado Diretamente
Arrays funcionam naturalmente com count():
count($items);
Generators não.
Como seus valores ainda não foram produzidos, o PHP não tem como saber quantos elementos existirão sem consumir completamente o iterador.
Apenas Processamento Sequencial
Generators foram projetados para iteração sequencial.
Se o seu algoritmo precisar frequentemente:
- acessar posições arbitrárias;
- ordenar elementos;
- embaralhar dados;
- realizar buscas repetidas;
- comparar elementos distantes;
um array — ou outra estrutura de dados — provavelmente será uma escolha mais adequada.
Os Valores Não São Armazenados
Cada valor produzido por um Generator existe apenas durante a iteração atual.
Se gerar cada valor for uma operação computacionalmente cara e você precisar desses mesmos dados novamente, recriar o Generator repetidas vezes pode ser menos eficiente do que armazenar os resultados em um array.
Em outras palavras, os Generators trocam consumo de memória pela capacidade de recalcular os valores sempre que necessário.
Boas Práticas
Generators oferecem o máximo de benefício quando utilizados de forma consciente. As recomendações abaixo ajudam a manter seu código eficiente, legível e fácil de manter.
Utilize Generators para Grandes Volumes de Dados
Generators são especialmente vantajosos ao trabalhar com:
- Arquivos muito grandes
- Cursores de banco de dados
- APIs paginadas
- Fluxos contínuos de dados
- Processamentos computacionalmente caros
Para coleções pequenas, contendo apenas algumas dezenas de elementos, um array normalmente é mais simples e perfeitamente adequado.
Prefira iterable em APIs Públicas
Se uma função precisa apenas percorrer os dados recebidos, utilize o tipo iterable.
function export(iterable $records): void
{
foreach ($records as $record) {
// ...
}
}
Assim, quem utilizar sua função poderá fornecer tanto um array quanto um Generator, sem necessidade de alterar a implementação.
Utilize yield from para Compor Generators
Sempre que seu Generator apenas encaminhar valores produzidos por outro iterável, prefira utilizar yield from em vez de escrever manualmente vários laços foreach.
Além de reduzir código repetitivo, isso deixa a intenção da implementação muito mais clara.
Mantenha Cada Generator com uma Única Responsabilidade
Idealmente, um Generator deve realizar apenas uma tarefa.
Por exemplo:
- Ler registros
- Transformar registros
- Filtrar registros
- Exportar registros
Em vez de criar um único Generator responsável por todo o processamento, prefira compor vários Generators menores. Essa abordagem facilita testes, reutilização de código e manutenção.
Libere Recursos Externos Corretamente
É muito comum que Generators trabalhem com arquivos, conexões de rede ou cursores de banco de dados.
Sempre que houver recursos externos envolvidos, garanta que eles sejam liberados corretamente, mesmo que a iteração seja interrompida antes do final.
Uma forma simples e segura de fazer isso é utilizando try/finally:
function lines(string $filename): Generator
{
$handle = fopen($filename, 'r');
try {
while (($line = fgets($handle)) !== false) {
yield $line;
}
} finally {
fclose($handle);
}
}
Assim, o arquivo será fechado independentemente da forma como a execução do Generator terminar.
Não Utilize Generators Prematuramente
Generators são uma excelente ferramenta de otimização, mas não devem ser utilizados apenas porque existem.
Se você precisa de:
- acesso aleatório aos elementos;
- múltiplas iterações;
- ordenação;
- filtragem utilizando funções de arrays;
- buscas frequentes;
um array provavelmente continuará sendo a solução mais simples e apropriada.
Como acontece com praticamente qualquer técnica de otimização, a melhor escolha depende do problema que você está tentando resolver, e não da tecnologia em si.
Perguntas Frequentes
Generators são mais rápidos do que arrays?
Nem sempre.
Existe um equívoco bastante comum de que Generators melhoram a velocidade de execução do código. Na realidade, sua principal vantagem é reduzir o consumo de memória, e não tornar a aplicação mais rápida.
Para conjuntos de dados pequenos, arrays costumam ser tão rápidos quanto — ou até ligeiramente mais rápidos que — Generators, já que todos os valores já estão disponíveis na memória.
Os Generators passam a se destacar quando o processamento envolve grandes volumes de dados que exigiriam uma quantidade significativa de memória para serem armazenados em um array.
Generators economizam memória?
Sim.
Em vez de armazenar todos os valores em um array, um Generator produz cada valor apenas quando ele é solicitado e mantém em memória somente o estado atual da execução.
Ao trabalhar com milhares ou milhões de elementos, isso pode reduzir drasticamente o consumo de memória da aplicação.
Qual é a diferença entre yield e return?
A instrução return encerra imediatamente a execução da função e retorna um único valor para quem a chamou.
function greeting(): string
{
return 'Hello';
}
Já a palavra-chave yield produz um valor sem encerrar a execução da função.
function letters(): Generator
{
yield 'A';
yield 'B';
yield 'C';
}
Após cada yield, a execução é pausada e será retomada quando o próximo valor for solicitado.
Um Generator pode retornar um valor?
Sim.
Embora Generators sejam utilizados principalmente para produzir valores através de yield, eles também podem finalizar sua execução utilizando uma instrução return.
function process(): Generator
{
yield 'Step 1';
yield 'Step 2';
return 'Completed';
}
Diferentemente dos valores produzidos por yield, o valor retornado por return não faz parte da iteração.
$generator = process();
foreach ($generator as $step) {
echo $step . PHP_EOL;
}
echo $generator->getReturn();
Saída:
Step 1
Step 2
Completed
O valor retornado pode ser recuperado através do método getReturn(), desde que o Generator já tenha terminado sua execução.
Caso você tente obtê-lo antes disso, o PHP lançará uma exceção:
$generator = process();
echo $generator->getReturn();
Exception: Cannot get return value of a generator that hasn't returned
Na prática, esse recurso é pouco utilizado em aplicações do dia a dia. Seu principal uso é permitir que um Generator informe um resultado final após produzir todos os seus valores, como a quantidade de registros processados, estatísticas da execução ou um código indicando que o processamento foi concluído com sucesso.
function parseRecords(array $records): Generator
{
$processed = 0;
foreach ($records as $record) {
yield strtoupper($record);
$processed++;
}
return $processed;
}
$generator = parseRecords(['php', 'generator', 'yield']);
foreach ($generator as $record) {
echo $record . PHP_EOL;
}
echo "Processed: {$generator->getReturn()} records";
Saída:
PHP
GENERATOR
YIELD
Processed: 3 records
Embora esse recurso faça parte da linguagem, a grande maioria dos Generators simplesmente produz valores com yield e encerra sua execução sem retornar nenhuma informação adicional.
O que faz o yield from?
A instrução yield from delega a iteração para outro iterável.
Em vez de percorrer manualmente outro Generator e produzir cada valor individualmente, o PHP encaminha automaticamente todos os valores para o Generator atual.
Isso resulta em um código mais limpo, mais legível e, na maioria dos casos, é a forma recomendada de compor Generators.
Um Generator pode ser reiniciado?
Em geral, não.
Depois que um Generator avança em sua execução, ele não pode simplesmente voltar ao início.
Se for necessário percorrer novamente a mesma sequência de dados, será preciso criar uma nova instância chamando novamente a função que produz o Generator.
Devo substituir todos os arrays por Generators?
Não.
Arrays e Generators foram criados para resolver problemas diferentes.
Arrays são ideais para coleções pequenas, acesso aleatório aos elementos, ordenação e situações em que os dados precisam ser reutilizados várias vezes.
Já os Generators oferecem maior benefício quando o objetivo é processar grandes volumes de dados ou fluxos contínuos de forma sequencial.
A escolha entre um e outro deve ser baseada nas necessidades da sua aplicação, e não na ideia de que um seja sempre mais performático que o outro.
Conclusão
Os Generators são um dos recursos mais elegantes do PHP para escrever código com baixo consumo de memória e avaliação preguiçosa (lazy evaluation). Por meio da palavra-chave yield, eles permitem que funções produzam valores gradualmente, em vez de alocar toda uma coleção na memória antes do processamento.
Ao longo deste artigo, você aprendeu como os Generators funcionam internamente, qual é a diferença entre yield e return, como utilizar yield from para compor Generators e em quais situações eles realmente fazem diferença. Também vimos que eles são especialmente úteis para processar grandes volumes de dados, ler arquivos extensos, consumir APIs paginadas, trabalhar com fluxos contínuos de dados e construir pipelines de processamento.
Da mesma forma, é importante entender o que os Generators não são. Eles não substituem os arrays em todas as situações, nem representam uma otimização mágica de desempenho. Seu principal objetivo é reduzir o consumo de memória enquanto permitem o processamento sequencial de dados que não precisam existir completamente na memória ao mesmo tempo.
Como regra geral:
- Utilize arrays quando precisar de acesso aleatório aos elementos, múltiplas iterações, ordenação ou manipulação frequente da coleção.
- Utilize Generators quando os valores puderem ser produzidos e consumidos um de cada vez.
Compreender essa diferença permitirá escrever aplicações PHP mais eficientes, escaláveis e fáceis de manter. Em muitos cenários, substituir um processamento baseado em arrays por um Generator bem projetado pode transformar uma tarefa limitada pela memória em uma solução capaz de lidar com volumes de dados praticamente ilimitados.