PHP Generators

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++