Voltas e mais voltas com SPL
Recentemente, precisei de um banco de dados de provérbios e fiz um singelo script para ler alguns sites, mas era chato ter que fazer um script inteiro ou aglomerar laços pra cada site que eu encontrava. Pra evitar repetição de código, mantive uma classe por site, que, em conjunto com as classes e interfaces da Standard PHP Library (SPL), resumiram o código final a um único foreach:
$proverbios = new Proverbios;
$proverbios->append(new Proverbios_LifesABirch);
$proverbios->append(new Proverbios_Aborla);
$proverbios->append(new Proverbios_JangadaBrasil);
foreach($proverbios as $proverbio){
echo $proverbio, “\n”;
}
Iterator
Para que isso fosse possível, cada classe implementou a interface Iterator disponível na extensão SPL, assumindo o comportamento de um iterador e podendo então ser utilizada normalmente em um loop.
O papel de uma interface é definir o que um objeto deverá ser capaz de fazer, e essa define o seguinte:
interface Iterator extends Traversable
{
function rewind();
function current();
function key();
function next();
function valid();
}
Então, todo objeto a ser utilizado como iterador precisa ter esses métodos implementados, que são executados na seguinte ordem durante um foreach:
- rewind() – recomeça a iteração, reiniciando um possível índice interno;
- valid() – este método deve retornar true para continuar, ou false quando não houver mais iteração;
- current() – retorna o valor atual;
- key() – retorna o índice atual e somente é executado caso a chave seja pedida, como em foreach($it as $key => $value);
- next() – deve mover o índice interno para a próxima iteração;
- Volta ao passo número 2.
ArrayIterator
No primeiro site, a busca foi apenas uma expressão regular que retornava os provérbios em uma array que seria lida depois em um loop.
Como a iteração em array é bastante óbvia, a SPL dispõe da classe ArrayIterator, que já implementa as interfaces ArrayAccess, Countable e SeekableIterator, que por sua vez estende a Iterator, sendo esta última a única que interessa no momento.
A classe recebe uma array ou objeto no constructor, e tem o mesmo resultado como se estes elementos fossem utilizados diretamente em um foreach. Se a ArrayIterator não existisse, seria necessário implementar a interface Iterator e manipular a array manual e repetidamente onde fosse necessário.
class Proverbios_LifesABirch extends ArrayIterator
{
public function __construct()
{
$url = 'http://www.lifesabirch.org/proverbios/index.php';
$contents = file_get_contents($url);
preg_match_all('/^(?:<\/?p>)?(\S.+) <br\/>$/mU',
$contents, $matches);
if(isset($matches[1]) && is_array($matches[1])){
parent::__construct($matches[1]);
}
}
}
Ao instanciar esta classe, o constructor é executado, a leitura do site é efetuada e a array retornada é passada para o constructor da ArrayIterator. A partir daí, o objeto passa a ser, de certa forma, a própria array retornada pela expressão regular, onde cada item representa um provérbio do site.
A leitura poderia ser feita da seguinte forma:
foreach(new Proverbios_LifesABirch as $proverbio){
echo $proverbio, "\n";
}
RecursiveIterator
O segundo site que encontrei estava dividido por letras, sendo necessário percorrê-las e retornar os provérbios de cada uma delas.
Em uma estrutura básica, existiria um loop para as letras e dentro dele um outro para os provérbios. Esta estrutura é fácil de ser percebida, mas difícil de ser integrada a um sistema maior sem que todos os provérbios sejam lidos antes e retornados depois em um único array.
Utilizando iteradores recursivos, é possível navegar por entre estas divisões sem nenhuma alteração no código final, que continua sendo um único foreach. A interface RecursiveIterator estende a Iterator e acrescenta dois métodos:
interface RecursiveIterator extends Iterator
{
function hasChildren();
function getChildren();
}
Durante a iteração, o método hasChildren() é chamado pra identificar se o item atual é ou não um iterador a ser percorrido. Caso seja, o iterador, que também deve implementar a interface RecursiveIterator, é recuperado pelo getChildren() e começa uma nova iteração a partir dele até que não haja mais itens. Quando isso acontece, o processo continua no próximo item do iterador anterior, ou é finalizado caso seja o principal. Se hasChildren() retornar false, então trata-se de um item normal e o current() é chamado para recuperar seu valor.
Na classe deste site foi usada uma array com as letras a serem percorridas, e pra cada uma delas, uma classe é retornada como filha.
Para percorrer arrays multi-dimensionais, existe a classe RecursiveArrayIterator, que verifica no método hasChildren() se o item atual é uma outra array e retorna no getChildren() uma nova instância de si mesma a partir dela, fazendo o efeito recursivo.
Nesse caso a RecursiveArrayIterator não poderia ser utilizada, porque a array de letras não é multi-dimensional. Ao invés dela, foi usada a própria ArrayIterator para percorrer a array, e implementada a interface RecursiveIterator manualmente para retornar o objeto dos provérbios de cada letra.
class Proverbios_Aborla_Letras
extends ArrayIterator implements RecursiveIterator
{
public function __construct()
{
parent::__construct(array(
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'l',
'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'z'));
}
public function hasChildren()
{
return true;
}
public function getChildren()
{
return new Proverbios_Aborla_Proverbios($this->current());
}
}
RecursiveIteratorIterator
Diferente de um iterador sem filhos, a classe acima não poderia ser utilizada diretamente em um foreach, mesmo com a interface RecursiveIterator, porque ele apenas recupera o valor atual pelo método current(), e retornaria somente as letras a cada iteração.
Uma vez implementados os métodos hasChildren() e getChildren(), é preciso que eles sejam utilizados de fato, e quem faz uso deles é o iterador externo RecursiveIteratorIterator, que recebe o iterador recursivo como parâmetro no constructor e coordena a recursividade. Portanto, a classe principal deste site fica assim:
class Proverbios_Aborla extends RecursiveIteratorIterator
{
public function __construct()
{
parent::__construct(new Proverbios_Aborla_Letras);
}
}
E já que a classe RecursiveIteratorIterator executa o método hasChildren() em todos os elementos que passam por ela, o retorno de getChildren() deve obrigatoriamente implementar a interface RecursiveIterator pra garantir ao iterador externo que os métodos existem, mesmo que não seja recursiva, como é o caso da classe de provérbios:
class Proverbios_Aborla_Proverbios
extends ArrayIterator implements RecursiveIterator
{
public function __construct($letra)
{
$url = "http://proverbios.aborla.net/p" . $letra . ".php";
$contents = file_get_contents($url);
$start = strpos($contents, '</h2><p>') + 8;
$length = strpos($contents, '<br></p>') - $start;
$array = explode('<br>', substr($contents, $start, $length));
parent::__construct($array);
}
public function hasChildren()
{
return false;
}
public function getChildren(){}
}
De uma forma bastante semelhante à primeira classe feita, o conteúdo do site é lido no constructor, e os provérbios passados como array para o constructor do parent, que é a ArrayIterator. Por esse iterador nunca conter um filho, o método getChildren() foi somente declarado, já que não será chamado pelo RecursiveIteratorIterator, dado que o hasChildren() retorna sempre false.
foreach(new Proverbios_Aborla as $proverbio){
echo $proverbio, "\n";
}
Acompanhando o código:
- O foreach ocorre em um objeto da classe Proverbios_Aborla, que estende RecursiveIteratorIterator. Esse iterador externo recebe o iterador recursivo Proverbios_Aborla_Letras como parâmetro no constructor e começa a iteração;
- A cada iteração em Proverbios_Aborla_Letras, o método hasChildren() é consultado pelo RecursiveIteratorIterator. Como o retorno é sempre true, o iterador filho é requisitado;
- Ao chamar Proverbios_Aborla_Letras::getChildren(), uma nova instância de Proverbios_Aborla_Proverbios é criada para a letra da iteração atual e repassada para o iterador externo;
- Ao ser instanciada, Proverbios_Aborla_Proverbios obtém os dados do site e os utiliza na ArrayIterator;
- O iterador externo consulta o método hasChildren() na classe Proverbios_Aborla_Proverbios, que vai sempre retornar false;
- Então o método Proverbios_Aborla_Proverbios::current(), estendido da ArrayIterator, é executado e retorna um provérbio da página lida do site;
- O iterador externo repassa o provérbio para o foreach, disponibilizando-o no código final. Os passos 5, 6 e 7 se repetem até acabarem os provérbios e o método Proverbios_Aborla_Proverbios::valid() retornar false;
- Quando isso acontece, o iterador externo avança com o método Proverbios_Aborta_Letras::next() que foi estendido da ArrayIterator, e recomeça o processo a partir do número 2 enquanto houver letras no primeiro iterador recursivo.
O último site que usei como fonte também exigiu iteradores recursivos, e precisou ainda de mais um nível de recursividade. Os provérbios estavam divididos em letras, e em cada uma delas havia paginação de resultados.
Os passos iniciais foram os mesmos, primeiro criando o iterador externo RecursiveIteratorIterator para percorrer as letras e seus filhos.
class Proverbios_JangadaBrasil extends RecursiveIteratorIterator
{
public function __construct()
{
parent::__construct(new Proverbios_JangadaBrasil_Letras);
}
}
A classe que representa as letras funciona da mesma forma que a do site anterior, passando a array de letras para o constructor da classe que estende, ArrayIterator.
class Proverbios_JangadaBrasil_Letras
extends ArrayIterator implements RecursiveIterator
{
public function __construct()
{
parent::__construct(range('a', 'z'));
}
public function hasChildren()
{
return true;
}
public function getChildren()
{
return new Proverbios_JangadaBrasil_Paginas($this->current());
}
}
Ao contrário das letras, a paginação não tem uma array fixa. A solução mais prática foi implementar um iterador contínuo que a cada iteração carrega os provérbios para a letra solicitada naquela página, e continua lendo a páginas seguinte enquanto provérbios forem encontrados.
class Proverbios_JangadaBrasil_Paginas implements RecursiveIterator
{
private $_letra;
private $_pagina;
private $_valid;
public function __construct($letra)
{
$this->_letra = $letra;
}
public function rewind()
{
$this->_pagina = 1;
$this->_valid = true;
}
public function valid()
{
return $this->_valid;
}
public function hasChildren()
{
return true;
}
public function getChildren()
{
$it = new Proverbios_JangadaBrasil_Proverbios(
$this->_letra, $this->_pagina);
$this->_valid = count($it) > 0;
return $it;
}
public function current()
{
return $this->_pagina;
}
public function key()
{
return $this->_pagina;
}
public function next()
{
$this->_pagina++;
}
}
Ao ser instanciada, a letra atual é recebida como argumento no constructor. No próximo passo, o método rewind() é chamado, a página definida como 1 e uma variável interna definida como true. Esta variável será usada pelo método valid() para definir se a iteração deve continuar na próxima página dessa letra. Como o hasChildren() retorna sempre true, o RecursiveIteratorIterator recupera o iterador filho pelo método getChildren(). A classe de provérbios é instanciada, e a variável interna usada pelo valid() é atualizada; se a quantidade de provérvios nesta página não for maior que zero, o valid() retornará false e a iteração neste nível pára e recomeça na letra seguinte.
Finalizando, a classe de provérbios segue a mesma lógica da classe do site anterior, sendo que essa recebe no constructor a letra e a página, que são utilizadas pra montar o link do site:
class Proverbios_JangadaBrasil_Proverbios
extends ArrayIterator implements RecursiveIterator
{
public function __construct($letra, $pagina)
{
$url = 'http://www.jangadabrasil.com.br/proverbios/'
. $letra . '.asp?PageIndex=' . $pagina;
$contents = file_get_contents($url);
preg_match_all(
'/<div class="textoproverbios">([^<]+)<\/div>/i',
$contents, $matches);
if(isset($matches[1]) && is_array($matches[1])){
parent::__construct($matches[1]);
}
}
public function hasChildren()
{
return false;
}
public function getChildren(){}
}
AppendIterator
Agora que as classes já estão funcionando individualmente, basta utilizar a AppendIterator para unir todas elas. Esta classe possui o método append(), que recebe um iterador como parâmetro. Ao ser utilizada em um foreach, ela percorre cada um dos iteradores em seqüência, resultando em todos os provérbios de todos os sites em um único loop.
class Proverbios extends AppendIterator
{
public function current()
{
$proverbio = parent::current();
$proverbio = trim($proverbio, " .\"'\t\n\x0B\r");
$proverbio = html_entity_decode($proverbio);
$proverbio = strip_tags($proverbio);
return $proverbio;
}
}
Aproveitando que todos os provérbios são retornados para o foreach pelo método current() desta classe, apliquei alguns filtros básicos pra garantir que os dados estão no formato desejado pro que eu precisava.
Foreach
Enfim, o script final simplificado de uma forma complexa:
$proverbios = new Proverbios;
$proverbios->append(new Proverbios_LifesABirch);
$proverbios->append(new Proverbios_Aborla);
$proverbios->append(new Proverbios_JangadaBrasil);
foreach($proverbios as $proverbio){
echo $proverbio, "\n";
}
Referências
http://www.php.net/~helly/php/ext/spl/
http://somabo.de/talks/200509_toronto_iterator_debug_session_2.pdf




A explicação está muito boa, de forma que até eu entendi!
E realmente facilitou bastante a utilização dos iterators, e detalhe, o script final ficou tão limpo!
Enfim, parabéns pelo conteúdo apresentado! E também pela façanha de postar códigos pelo editor do WordPress! hahaha
Parabéns por estes artigo, ficou muito didático e ao mesmo tempo com funcionalidades realmente práticas!