Generators PHP
Os generators são uma forma mais simples de implementar um iterador, sem a complexidade ou sobrecarga de implementar uma classe com a interface Iterator
. Eles permitem escrever código que utiliza um laço foreach para iterar sobre um conjunto de dados sem a necessidade de carregá-los todos para a memória de uma vez, o que pode causar problemas de limite de memória e processamento em alguns casos. No entanto, os generators possuem uma desvantagem, não podem ser “rebobinados”, ou seja, só são capazes de avançar na iteração e não permitem voltar para um elemento anterior.
Um generator funciona de maneira similar a uma função, porém, em vez de ter um retorno, utiliza a instrução yield
para “produzir” quantos retornos forem necessários. Quando uma função geradora é chamada, ela retorna um objeto Generator
que implementa a interface Iterator
. Dessa forma, podemos iterar sobre ela usando um laço foreach, por exemplo.
Durante a iteração de um generator, ele irá executar até encontrar a instrução yield
, então ele irá salvar o seu estado e devolver o valor para o código que está solicitando-o. Quando o código que está utilizando o gerador solicita o próximo valor, o gerador recarrega o seu estado e continua a execução do ponto onde havia parado até encontrar outra instrução yield
, momento em que ele salva novamente o estado novamente e passa o valor para o código que está utilizando o gerador. Esse ciclo se repete até que a função generator seja finalizada.
function gerador(): Generator
{
yield "Valor 1";
yield "Valor 2";
yield "Valor 3";
}
foreach(gerador() as $value){
echo $value . PHP_EOL;
}
No exemplo criamos uma função generator com o criativo nome de gerador. Ela irá produzir três valores, os quais nós iremos iterar através de um laço foreach. No laço foreach invocamos nossa função gerador, o que irá retornar um objeto Generator e executar até a primeira instrução yield
(na linha 3), o que irá salvar o estado do gerador e devolver o valor ‘Valor 1″ para o laço foreach que irá atribuí-lo a variável $value
e executará seu bloco exibindo o valor de $value
na saída. Então o laço voltará para o generator e solicitará o próximo valor o que fará o generator recarregar seu estado e executar a partir do ponto em que tinha parado até o próximo yield
(linha 4) onde salvará o estado novamente e devolverá o valor “Valor 2” para o laço que o atribuirá para $value
e executará o bloco mostrando na saida o valor. E mais uma vez o laço irá solicitar mais um valor para o generator que recarregará o seu estado e continuará a executar de onde tinha parado(linha 4) até a próxima yield
(linha 5) devolvendo mais um valor para o laço que mais uma vez atribuirá para $value
e exibirá o valor na saída. Então tentará solicitar mais um valor para o generator que mais uma vez irá recarregar seu estado o voltar a executar a partir da linha que tinha parado, mas agora ele não tem mais nenhuma instrução yield chegando ao final do bloco de função devolvendo o controle para o laço foreach
que não recebe um valor fazendo ele ser encerrado.
O exemplo acima foi um exemplo simples para facilitar o entendimento, normalmente utilizamos algum tipo de laço para gerar os valores, seja lendo de um arquivo, banco de dados, etc.
function gerador(int $n = 3): Generator
{
for($i = 1; $i <= $n; $i++){
yield "Valor {$i}";
}
}
foreach(gerador(5) as $value){
echo $value . PHP_EOL;
}
Somente para lembrar uma função generator cria um objeto Generator que implementa a interface Iterator, sendo assim é possível chamar os métodos desta interface como current
(para pegar o valor atual), next
(para avançar para o próximo valor) e valid
(para verificar se o valor atual é válido) e como um generator não pode ser “rebobinado” se tentarmos chamar o método rewind
ele irá lançar uma exceção. Embora seja possível utilizar generators desta maneira precisaria de um caso bem específico para ser vantajoso.
function gerador($n = 3): Generator
{
for($i = 1; $i <= $n; $i++){
yield "Valor {$i}";
}
}
$gen = gerador(5);
while($gen->valid()){
echo $gen->current() . PHP_EOL;
$gen->next();
}
Podemos também produzir um valor no formato chave/valor com a instrução yield
para isso basta ao invés de retornar um valor a utilizamos uma sintaxe semelhante a que utilizamos para definir uma array associativo $key => $value
. Veja um exemplo:
$input = <<<'EOF'
A;Rodrigo
B;Ana
C;Carlos
EOF;
function process(string $input): Generator
{
foreach( explode("\n", $input) as $line){
$data = explode(';', $line);
yield $data[0] => $data[1];
}
}
foreach(process($input) as $key => $value){
echo "{$key} => {$value}\n";
}
Podemos delegar a produção de valores de um generator para outro generator, um objeto Traversable ou um array. Para isso utilizamos a instrução yield from [origem]
.
function xrange($a, $b): Generator
{
for($i = $a; $i <= $b; $i++){
yield $i;
}
}
function myGenerator(): Generator
{
yield 1;
yield 2;
yield from [3, 4, 5];
yield from xrange(6, 10);
}
foreach(myGenerator() as $value){
echo $value . PHP_EOL;
}
Um uso mais prático de Generators
Um uso comum dos generators é na leitura de arquivos, por exemplo, um cvs muito grande. Normalmente, para fins de organização, iriamos criar uma função para ler o arquivo e jogar os dados em um array para passá-los para outra função fazer o processamento. Utilizando um generator podemos ler cada linha do arquivo, processá-la como desejarmos(no caso criamos um objeto) e a retornamos sem a necessidade de armazenar todos ao mesmo tempo na memória.
class FileDataReaderGenerator
{
private mixed $file;
public function __construct(string $filename)
{
$this->file = fopen($filename, 'r');
if (!$this->file) throw new \Exception('Não foi possível abrir arquivo');
}
public function data(): \Generator
{
while($line = fgetcsv($this->file)){
yield new Pessoa(
nome: $line[0],
sobrenome: $line[1],
email: $line[2],
idade: $line[3],
);
}
}
public function __destruct()
{
fclose($this->file);
}
}
E agora utilizar esta classe
$fileDataReader = new FileDataReaderGenerator('data.csv');
foreach($fileDataReader->data() as $pessoa){
echo "{$pessoa->nome} {$pessoa->sobrenome}, {$pessoa->idade}. Contato: {$pessoa->email} \n";
}
Bom era isso, uma introdução ao Generators do PHP. T++