Nesta aula, você vai aprender a dominar a iteração de coleções em PHP usando as interfaces Iterator e IteratorAggregate, além de explorar algumas classes da Standard PHP Library (SPL) que facilitam o trabalho com estruturas de dados. Entender esses conceitos é essencial para escrever código mais expressivo, eficiente e orientado a objetos, especialmente quando você precisa percorrer coleções de dados de forma personalizada.

Vamos começar pela interface Iterator, que permite que você defina exatamente como a iteração deve funcionar para uma determinada classe. Em seguida, veremos uma alternativa mais concisa com IteratorAggregate, que delega a lógica de iteração para um iterador externo. Depois, exploraremos algumas classes SPL como ArrayObject e SplStack, que já vêm prontas para uso e implementam essas interfaces. Por fim, discutiremos quando cada abordagem é mais adequada, para que você possa tomar decisões conscientes em seus projetos.

Interface Iterator

A interface Iterator é uma das interfaces centrais da SPL. Ela define cinco métodos que qualquer classe deve implementar para se tornar iterável: current(), key(), next(), rewind() e valid(). Esses métodos são chamados pelo construtor de foreach em PHP, permitindo que você controle exatamente como os elementos da sua coleção são percorridos.

Vamos entender cada método:

  • current(): retorna o elemento atual da iteração.
  • key(): retorna a chave (índice) do elemento atual.
  • next(): avança o ponteiro interno para o próximo elemento.
  • rewind(): reinicia o ponteiro para o primeiro elemento.
  • valid(): retorna true se a posição atual é válida (ou seja, existe um elemento na posição atual).

Para ilustrar, vamos criar uma classe que representa uma lista de números e implementamos Iterator para percorrer apenas os números pares. Isso mostra como podemos filtrar elementos durante a iteração, algo que um foreach simples não faria sem uma estrutura condicional.

<?php

class EvenNumbers implements Iterator {
    private array $numbers;
    private int $position = 0;

    public function __construct(array $numbers) {
        $this->numbers = array_values($numbers); // reindexa
    }

    public function current(): int {
        return $this->numbers[$this->position];
    }

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

    public function next(): void {
        $this->position++;
        // Avança até o próximo número par
        while ($this->position < count($this->numbers) && $this->numbers[$this->position] % 2 !== 0) {
            $this->position++;
        }
    }

    public function rewind(): void {
        $this->position = 0;
        // Se o primeiro não for par, avança
        if ($this->valid() && $this->numbers[$this->position] % 2 !== 0) {
            $this->next();
        }
    }

    public function valid(): bool {
        return isset($this->numbers[$this->position]);
    }
}

$numbers = [1, 2, 3, 4, 5, 6, 7, 8];
$even = new EvenNumbers($numbers);

foreach ($even as $key => $value) {
    echo "$key => $value\n";
}
// Saída:
// 1 => 2
// 3 => 4
// 5 => 6
// 7 => 8

Observe que a implementação é um pouco complexa porque precisamos controlar a posição manualmente. Felizmente, o PHP oferece uma alternativa mais simples com IteratorAggregate.

IteratorAggregate

A interface IteratorAggregate permite que você implemente apenas um método: getIterator(), que deve retornar um objeto que implemente Iterator ou Traversable. Isso simplifica muito a criação de classes iteráveis, pois você pode usar um ArrayIterator ou um Generator para delegar a lógica de iteração.

Vamos reescrever o exemplo anterior usando IteratorAggregate e um Generator. Os generators são uma forma elegante de criar iteradores com sintaxe simples usando yield.

<?php

class EvenNumbers implements IteratorAggregate {
    private array $numbers;

    public function __construct(array $numbers) {
        $this->numbers = array_values($numbers);
    }

    public function getIterator(): Traversable {
        foreach ($this->numbers as $number) {
            if ($number % 2 === 0) {
                yield $number;
            }
        }
    }
}

$numbers = [1, 2, 3, 4, 5, 6, 7, 8];
$even = new EvenNumbers($numbers);

foreach ($even as $value) {
    echo "$value\n";
}
// Saída:
// 2
// 4
// 6
// 8

Muito mais limpo! O generator cuida de toda a lógica de iteração, e você só precisa se preocupar com a lógica de negócio. Além disso, o método getIterator() pode retornar qualquer iterador, como um ArrayIterator se você quiser percorrer todos os elementos.

SPL (ArrayObject, SplStack)

A SPL (Standard PHP Library) oferece várias classes prontas que implementam IteratorAggregate ou Iterator. Duas delas são muito úteis no dia a dia: ArrayObject e SplStack.

ArrayObject

O ArrayObject permite que você trabalhe com arrays como objetos, mantendo a sintaxe de array (acesso via colchetes) e adicionando métodos úteis. Ele implementa IteratorAggregate, então você pode usá-lo diretamente em foreach. Além disso, ele oferece métodos como count(), append(), asort(), entre outros.

<?php

$obj = new ArrayObject(['apple', 'banana', 'cherry']);

// Acessa como array
$obj[] = 'date';
echo $obj[1]; // banana

// Itera normalmente
foreach ($obj as $key => $value) {
    echo "$key: $value\n";
}

// Usa métodos de array
echo count($obj); // 4
$obj->asort();
print_r($obj->getArrayCopy());

O ArrayObject é especialmente útil quando você precisa passar um array por referência ou aplicar comportamentos de objeto a dados que normalmente seriam arrays.

SplStack

SplStack é uma implementação de pilha (stack) que segue a política LIFO (Last In, First Out). Ela implementa Iterator, permitindo iterar sobre os elementos na ordem da pilha (do topo para a base). Métodos principais: push(), pop(), top(), isEmpty().

<?php

$stack = new SplStack();
$stack->push('primeiro');
$stack->push('segundo');
$stack->push('terceiro');

echo $stack->top(); // terceiro

// Itera do topo para a base
foreach ($stack as $item) {
    echo $item . PHP_EOL;
}
// terceiro
// segundo
// primeiro

$stack->pop(); // remove 'terceiro'
echo $stack->top(); // segundo

Outras classes SPL de estruturas de dados incluem SplQueue (fila), SplHeap, SplFixedArray, entre outras. Cada uma tem suas particularidades e é importante conhecê-las para escolher a estrutura certa para o problema.

Quando usar

A escolha entre Iterator e IteratorAggregate depende do seu cenário. Se você precisa de um controle muito fino sobre a iteração, como pular elementos ou manter um estado interno complexo, a implementação direta de Iterator pode ser necessária. Porém, na maioria dos casos, IteratorAggregate é mais simples e menos propenso a erros, especialmente se você puder usar um Generator.

Quanto às classes SPL, use ArrayObject quando quiser tratar um array como objeto e aproveitar métodos utilitários. Use SplStack quando precisar de uma pilha LIFO, SplQueue para fila FIFO, e assim por diante. Essas estruturas são mais eficientes que arrays nativos em operações de inserção/remoção nas extremidades, pois evitam a reindexação.

Em geral, se você está criando uma classe que representa uma coleção de dados e deseja que ela seja percorrida com foreach, implementar IteratorAggregate é a abordagem recomendada. Essa interface também permite que você troque a lógica de iteração sem alterar a classe principal, promovendo a flexibilidade.

Boas práticas

  • Prefira IteratorAggregate a Iterator quando não precisar de controle manual do ponteiro.
  • Use yield em generators para criar iteradores de forma concisa e legível.
  • Aproveite as classes SPL prontas para evitar reinventar a roda.
  • Lembre-se de que foreach funciona com qualquer objeto que implemente Traversable.
  • Teste seus iteradores com vários cenários, incluindo coleções vazias.

Referências

Exercícios

  1. Crie uma classe RangeIterator que implementa Iterator e itera de um valor inicial até um valor final, incrementando de 1 em 1. Teste com um foreach de 1 a 5.

    ✓ Resposta:
    <?php
    
    class RangeIterator implements Iterator {
        private int $start;
        private int $end;
        private int $current;
    
        public function __construct(int $start, int $end) {
            $this->start = $start;
            $this->end = $end;
            $this->current = $start;
        }
    
        public function current(): int {
            return $this->current;
        }
    
        public function key(): int {
            return $this->current - $this->start;
        }
    
        public function next(): void {
            $this->current++;
        }
    
        public function rewind(): void {
            $this->current = $this->start;
        }
    
        public function valid(): bool {
            return $this->current <= $this->end;
        }
    }
    
    $range = new RangeIterator(1, 5);
    foreach ($range as $key => $value) {
        echo "$key => $value\n";
    }
    
  2. Implemente uma classe ProductCollection que implementa IteratorAggregate e itera sobre uma lista de produtos (use um array de strings). Use um Generator para retornar os produtos.

    ✓ Resposta:
    <?php
    
    class ProductCollection implements IteratorAggregate {
        private array $products;
    
        public function __construct(array $products) {
            $this->products = $products;
        }
    
        public function getIterator(): Traversable {
            foreach ($this->products as $product) {
                yield $product;
            }
        }
    }
    
    $collection = new ProductCollection(['Notebook', 'Mouse', 'Teclado']);
    foreach ($collection as $product) {
        echo $product . PHP_EOL;
    }
    
  3. Usando ArrayObject, crie um array de nomes de frutas e adicione uma nova fruta. Depois, itere e imprima cada fruta em maiúsculas.

    ✓ Resposta:
    <?php
    
    $fruits = new ArrayObject(['maçã', 'banana', 'laranja']);
    $fruits[] = 'uva';
    
    foreach ($fruits as $fruit) {
        echo strtoupper($fruit) . PHP_EOL;
    }
    // MAÇÃ
    // BANANA
    // LARANJA
    // UVA
    
  4. Crie uma pilha SplStack com os números 1, 2, 3. Empilhe o número 4 e depois desempilhe um elemento. Mostre o topo da pilha após cada operação.

    ✓ Resposta:
    <?php
    
    $stack = new SplStack();
    $stack->push(1);
    $stack->push(2);
    $stack->push(3);
    echo $stack->top() . PHP_EOL; // 3
    
    $stack->push(4);
    echo $stack->top() . PHP_EOL; // 4
    
    $stack->pop(); // remove 4
    echo $stack->top() . PHP_EOL; // 3
    
  5. Escreva uma função que recebe um array e retorna um ArrayIterator que percorre os elementos em ordem reversa. Dica: use array_reverse e ArrayIterator.

    ✓ Resposta:
    <?php
    
    function reverseIterator(array $array): ArrayIterator {
        return new ArrayIterator(array_reverse($array));
    }
    
    $it = reverseIterator([1, 2, 3]);
    foreach ($it as $value) {
        echo $value . PHP_EOL; // 3, 2, 1
    }