PHP

Iterators no PHP

Iterator é um mecanismo que permite que um objeto seja iterado e ele próprio fica no controle granular dessa iteração.

há 4 anos 2 semanas

Formação Desenvolvedor PHP
Conheça a formação em detalhes

Esse é o primeiro artigo de dois sobre Iterators e Generators no PHP. Este será sobre Iterators, que é base para que possamos entender e usar Generators.

Todos os exemplos desse artigo e do artigo de Generators estão disponíveis nesse repositório: https://github.com/KennedyTedesco/php-iterators-generators

PHP - Fundamentos
Curso PHP - Fundamentos
Conhecer o curso

Iterators

Nós, como desenvolvedores PHP, estamos acostumados a usar arrays para trabalhar e iterar sobre listas. E se pudermos encapsular esse tipo de comportamento em um objeto? No contexto prático do PHP, Iterator é um mecanismo que permite que um objeto seja iterado e ele próprio fica no controle granular dessa iteração. Mas o seu uso não limita a essa “feature”.

Antes, entretanto, é importante destacar o conceito do que é “iterável” no PHP. Existe, inclusive, um pseudo-tipo chamado iterable que aceita arrays e objetos que implementem a interface Traversable, mas isso veremos mais pra frente.

Então, de forma prática, tudo o que você pode iterar num foreach é considerável iterável. E existe até uma curiosidade aqui, de algo não tão comum, é possível iterar até sobre os atributos públicos de um objeto arbitrário qualquer:

<?php

declare(strict_types=1);

function iterateAndPrint($list): void
{
    foreach ($list as $key => $value) {
        echo "{$key} -> {$value}\n";
    }
}

iterateAndPrint(['foo', 'bar', 'baz']);

iterateAndPrint(new class() {
    public int $foo = 1;
    public string $bar = 'bar';
    protected string $baz = 'baz';
});

O resultado:

0 -> foo
1 -> bar
2 -> baz
foo -> 1
bar -> bar

No caso desse exemplo iteramos tanto sobre um array quanto sobre uma instância de uma classe anônima. Então, podemos dizer que essas duas entidades são iteráveis.

A interface Iterator

Essa interface declara cinco métodos e, quando implementados, os objetos provenientes ganham a capacidade de serem iterados. Mais que isso, ganham o poder sobre o que e como iterar.

Iterator extends Traversable {
    abstract public current ( void ) : mixed
    abstract public key ( void ) : scalar
    abstract public next ( void ) : void
    abstract public rewind ( void ) : void
    abstract public valid ( void ) : bool
}
  • current() - Retorna elemento atual;
  • key() - Obtêm a chave atual;
  • next() - Avança o cursor para o próximo elemento;
  • rewind() - Retorna o cursor para o início (ponto zero);
  • valid() - Retorna true se o ponteiro (posição) encontra-se numa faixa acessível dos dados (se não passou da posição máxima disponível, por exemplo). Caso contrário, false é retornado.

Vamos então a um exemplo:

<?php

declare(strict_types=1);

namespace Iterators;

use Iterator;

final class BookStoreIterator implements Iterator
{
    /** @var string[] $books */
    private array $books;
    private int $index = 0;

    /**
     * @param string[] $books
     */
    public function __construct(array $books)
    {
        $this->books = $books;
    }

    public function current(): string
    {
        return $this->books[$this->index];
    }

    public function key(): int
    {
        return $this->index;
    }

    public function next(): void
    {
        $this->index++;
    }

    public function rewind(): void
    {
        $this->index = 0;
    }

    public function valid(): bool
    {
        return \array_key_exists($this->index, $this->books);
    }
}

Observe que os atributos são privados e que mantemos em $index o “cursor” atual da iteração.

Uma possível forma de iterar sobre esse objeto BookStoreIterator:

<?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use Iterators\BookStoreIterator;

$books = new BookStoreIterator([
    'Book 1',
    'Book 2',
    'Book 3',
    'Book 4',
    'Book 5',
    'Book 6',
    'Book 7',
    'Book 8',
    'Book 9',
    'Book 10',
]);

while ($books->valid()) {
    echo "{$books->key()} -> {$books->current()} \n";

    $books->next();
}

Resultado:

0 -> Book 1 
1 -> Book 2 
2 -> Book 3 
3 -> Book 4 
4 -> Book 5 
5 -> Book 6 
6 -> Book 7 
7 -> Book 8 
8 -> Book 9 
9 -> Book 10

Essa interface faz com que o objeto tenha a capacidade de decidir como e o que iterar. Destaco novamente esses termos em negrito pois isso é essência dos iterators.

Enquanto existir um índice válido a ser recuperado os valores serão impressos. Na última iteração, $books->next() incrementa em mais um o valor de $index chegando ao valor 10, no entanto, no array $books o maior índice existente é o 9, logo, o while é interrompido.

Outra forma de iterar esse objeto usando a estrutura for:

for ($books->rewind(); $books->valid(); $books->next()) {
    echo "{$books->key()} -> {$books->current()} \n";
}

Essas são as formas mais “primitivas” de iterar esse objeto iterável. Estamos manualmente cuidando de avançar o cursor usando $books->next();.

Podemos facilitar isso se usarmos a estrutura foreach, ela reconhece quando o objeto implementa a interface Traversable e trata de avançar o cursor e recuperar os valores automaticamente.

É bom lembrar que a interface Iterator estende a Traversable:

Iterator extends Traversable {
    // ...
}

A iteração poderia ser simplificada para:

foreach ($books as $key => $value) {
    echo "{$key} -> {$value} \n";
}

Esse é um exemplo simples, que usa arrays. Mas a grande sacada dos iterators é nos dar o poder sobre como e o que iterar, ou seja, ficamos no poder de definir o comportamento de toda a iteração.

PHP - Orientação a Objetos - Parte 1
Curso PHP - Orientação a Objetos - Parte 1
Conhecer o curso

Refatorando para ArrayIterator

O lado chato da abordagem de implementar diretamente a interface Iterator é a necessidade de implementar todos os seus métodos. É útil quando a lógica para se iterar ou avançar o cursor precisa ser específica. Agora, se ela é trivial, como é o caso do nosso exemplo anterior, podemos usar a classe ArrayIterator, que faz parte do conjunto de Iterators da Standard PHP Library (SPL) do PHP.

Ela recebe no seu construtor um array e itera sobre ele, abstraindo de nós a necessidade de implementar os métodos necessários da interface Iterator.

Podemos simplificar e alterar a nossa classe para implementar a interface IteratorAggregate que assina um único método, chamado getIterator(). E, nele, tudo o que temos que retornar é um Iterator, no caso, vamos retornar a instância da ArrayIterator:

<?php

declare(strict_types=1);

namespace Iterators;

use ArrayIterator;
use IteratorAggregate;

final class BookStoreArrayIterator implements IteratorAggregate
{
    /** @var string[] $books */
    private array $books;

    /**
     * @param string[] $books
     */
    public function __construct(array $books)
    {
        $this->books = $books;
    }

    public function getIterator(): ArrayIterator
    {
        return new ArrayIterator($this->books);
    }
}

Exemplo de uso:

<?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use Iterators\BookStoreArrayIterator;

$books = new BookStoreArrayIterator([
    'Book 1',
    'Book 2',
    'Book 3',
    'Book 4',
    'Book 5',
    'Book 6',
    'Book 7',
    'Book 8',
    'Book 9',
    'Book 10',
]);

foreach ($books as $key => $value) {
    echo "{$key} -> {$value} \n";
}

A classe ArrayObject

Nos exemplos anteriores vimos como encapsulamos arrays em objetos com iterators. Mas a forma mais simples e elementar de implementar um array como objeto é usando a classe ArrayObject:

<?php

declare(strict_types=1);

$books = new ArrayObject([
    'Book 1',
    'Book 2',
    'Book 3',
    'Book 4',
    'Book 5',
    'Book 6',
    'Book 7',
    'Book 8',
    'Book 9',
    'Book 10',
]);

foreach ($books as $key => $value) {
    echo "{$key} -> {$value} \n";
}

Essa classe implementa diversas interfaces e ela já abstrai tudo pra gente:

ArrayObject implements IteratorAggregate , Traversable , ArrayAccess , Countable {

Não é o assunto do nosso artigo, mas a interface ArrayAccess nos fornece os seguintes métodos:

ArrayAccess {
    abstract public offsetExists ( mixed $offset ) : bool
    abstract public offsetGet ( mixed $offset ) : mixed
    abstract public offsetSet ( mixed $offset , mixed $value ) : void
    abstract public offsetUnset ( mixed $offset ) : void
}

Por exemplo, na instância do ArrayObject podemos acessar:

$books->offsetExists(1) // true

A ArrayObject também encapsula várias funções úteis para manipulação do Iterator:

  • append()
  • asort()
  • count()
  • ksort()
  • natcasesort()
  • natsort()
  • serialize()
  • uasort()
  • uksort()
  • unserialize()

Exemplo:

<?php

declare(strict_types=1);

$list = new ArrayObject([
    5, 4, 3, 2, 1,
]);

for ($i = 5; $i <= 10; $i++) {
    $list->append($i);
}

// Ordena os valores
$list->natsort();

foreach ($list as $key => $value) {
    echo "{$key} -> {$value}\n";
}

Resultado:

4 -> 1
3 -> 2
2 -> 3
1 -> 4
0 -> 5
5 -> 5
6 -> 6
7 -> 7
8 -> 8
9 -> 9
10 -> 10

Podemos, ainda, no segundo argumento da ArrayObject passar a flag ArrayObject::ARRAY_AS_PROPS para permitir que acessemos as chaves do array na forma de objeto:

<?php

declare(strict_types=1);

$list = new ArrayObject(
    [
        'nome' => 'Pedro',
        'idade' => 20,
        'nascimento' => '1990-10-10',
    ],
    ArrayObject::ARRAY_AS_PROPS
);

echo "{$list->nome}, {$list->idade} ({$list->nascimento})";

Resultado:

Pedro, 20 (1990-10-10)

A função iterator_to_array()

Essa função faz uma cópia dos itens do Iterator para um array:

<?php

declare(strict_types=1);

$books = new ArrayObject([
    'Book 1',
    'Book 2',
    'Book 3',
    'Book 4',
    'Book 5',
    'Book 6',
    'Book 7',
    'Book 8',
    'Book 9',
    'Book 10',
]);

var_dump(iterator_to_array($books));

O pseudo-tipo iterable

O PHP implementou na versão 7.1 o pseudo-tipo iterable, que espera receber um array ou algum objeto que implementa a interface Traversable.

Exemplo:

<?php

declare(strict_types=1);

function iterateAndPrint(iterable $list): void
{
    foreach ($list as $key => $value) {
        echo "{$key} -> {$value}\n";
    }
}

iterateAndPrint(['foo', 'bar', 'baz']);

$books = new ArrayObject([
    'Book 1',
    'Book 2',
    'Book 3',
    'Book 4',
]);

iterateAndPrint($books);

Observe que a função iterateAndPrint() agora espera receber uma entidade do pseudo-tipo iterable.

O resultado:

0 -> foo
1 -> bar
2 -> baz
0 -> Book 1
1 -> Book 2
2 -> Book 3
3 -> Book 4

A interface SeekableIterator

Vimos anteriormente que a interface Iterator declara cinco métodos:

Iterator extends Traversable {
    abstract public current ( void ) : mixed
    abstract public key ( void ) : scalar
    abstract public next ( void ) : void
    abstract public rewind ( void ) : void
    abstract public valid ( void ) : bool
}

A interface SeekableIterator é uma extensão da Iterator que adiciona o método seek():

SeekableIterator extends Iterator {
    /* Novo método */
    abstract public seek (int $position) : void

    /* Métodos herdados */
    abstract public Iterator::current(void) : mixed
    abstract public Iterator::key(void) : scalar
    abstract public Iterator::next(void) : void
    abstract public Iterator::rewind(void) : void
    abstract public Iterator::valid(void) : bool
}

Esse método espera receber um valor para mudar a posição do ponteiro interno do iterator. Vamos supor que o iterator está na posição 0 e arbitrariamente queremos pular para a posição 4, esse método seek() pode ser usado para isso.

Vejamos um exemplo. A classe:

<?php

declare(strict_types=1);

namespace Iterators;

use OutOfBoundsException;
use SeekableIterator;

final class BookStoreSeekableIterator implements SeekableIterator
{
    /** @var string[] $books */
    private array $books;
    private int $index = 0;

    /**
     * @param string[] $books
     */
    public function __construct(array $books)
    {
        $this->books = $books;
    }

    public function seek($index): void
    {
        if (! isset($this->books[$index])) {
            throw new OutOfBoundsException("Invalid position ({$index}).");
        }

        $this->index = $index;
    }

    public function current(): string
    {
        return $this->books[$this->index];
    }

    public function key(): int
    {
        return $this->index;
    }

    public function next(): void
    {
        $this->index++;
    }

    public function rewind(): void
    {
        $this->index = 0;
    }

    public function valid(): bool
    {
        return \array_key_exists($this->index, $this->books);
    }
}

O uso da classe:

<?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use Iterators\BookStoreSeekableIterator;

$books = new BookStoreSeekableIterator([
    'Book 1',
    'Book 2',
    'Book 3',
    'Book 4',
    'Book 5',
    'Book 6',
    'Book 7',
    'Book 8',
    'Book 9',
    'Book 10',
]);

echo $books->current() . \PHP_EOL; // Book 1

$books->seek(4);

echo $books->current() . \PHP_EOL; // Book 5

O resultado:

Book 1
Book 5

O seek() permite que arbitrariamente pulemos a posição do ponteiro interno.

Esse é um exemplo puramente didático para termos acesso à essência de como o seek() pode ser usado. Mas no artigo sobre Generators (que muito recomendo a leitura), temos um exemplo onde o seek() tem um sentido de uso real bem mais amplo.

Iterators da SPL

Temos na Standard PHP Library (SPL) dezenas de iterators disponíveis para uso, cada qual com seu caso de uso. Você pode ver a relação deles clicando aqui.

Um exemplo de uso do FilesystemIterator:

<?php

declare(strict_types=1);

$iterator = new FilesystemIterator(__DIR__.'/../');

/** @var SplFileInfo $file */
foreach ($iterator as $file) {
    echo $file->getFilename() . ' -> ' . ($file->isFile() ? 'file' : 'dir') . PHP_EOL;
}

O resultado da iteração:

.php_cs.cache -> file
.php_cs -> file
.gitignore -> file
composer.json -> file
.git -> dir
src -> dir
psalm.xml -> file
examples -> dir
.idea -> dir
vendor -> dir
composer.lock -> file

Outro exemplo é o RegexIterator que aplica um filtro de regex em cima de outro Iterator, por exemplo:

<?php

declare(strict_types=1);

$iterator = new RegexIterator(
    new FilesystemIterator(__DIR__),
    '/^.+\.php$/'
);

/** @var SplFileInfo $file */
foreach ($iterator as $file) {
    echo $file->getFilename() . PHP_EOL;
}

Esse exemplo vai retornar o nome de todos os arquivos que terminam com a extensão .php.

Outra opção seria usar FilterIterator que permite que criemos filtros e os apliquemos em cima de Iterators. Para isso, vamos criar uma classe que estende a FilterIterator:

<?php

declare(strict_types=1);

namespace Iterators;

use FilterIterator;
use Iterator;
use SplFileInfo;

class FileExtensionFilter extends FilterIterator
{
    private string $extension;

    public function __construct(Iterator $iterator, string $extension)
    {
        parent::__construct($iterator);

        $this->extension = $extension;
    }

    public function accept(): bool
    {
        /** @var SplFileInfo $file */
        $file = $this->getInnerIterator()->current();

        return $this->extension === $file->getExtension();
    }
}

E então podemos usar:

<?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use Iterators\FileExtensionFilter;

$iterator = new FileExtensionFilter(new FilesystemIterator(__DIR__), 'php');

/** @var SplFileInfo $file */
foreach ($iterator as $file) {
    echo $file->getFilename() . \PHP_EOL;
}

O resultado desse exemplo é exatamente o mesmo do exemplo anterior.

A classe FileExtensionFilter estende a FilterIterator que é um OuterIterator. A ideia é que um OuterIterator possui um InnerIterator (um iterator interno). Por esse motivo dentro do método accept() da classe de filtro conseguimos acessar o InnerIterator que é o FilesystemIterator e então obter a extensão do arquivo do ponteiro atual.

Palavras finais

Um iterator pode iterar recursivamente sobre outros iterators. As possibilidades são muitas quando se trata de agregar iterators. Não é possível em um artigo colocar todos os exemplos possíveis, por isso eu recomendo navegar na documentação e usar na prática os iterators que mais lhe chamam atenção.

Não se esqueça de ler o artigo sobre Generators, lá você terá exemplos bem legais do mundo real e verá como eles são eficientes para lidar com a memória.

Desenvolvedor PHP
Formação Desenvolvedor PHP
Conhecer a formação

Autor(a) do artigo

Kennedy Tedesco
Kennedy Tedesco

Head de desenvolvimento. Vasta experiência em desenvolvimento Web com foco em PHP. Graduado em Sistemas de Informação. Pós-graduando em Arquitetura de Software Distribuído pela PUC Minas. Zend Certified Engineer (ZCE) e Coffee Addicted Person (CAP). @KennedyTedesco

Todos os artigos

Artigos relacionados Ver todos