garotosopa

Voltas e mais voltas com SPL

Publicado em OOP, PHP por garotosopa em Abril 18, 2007

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:

  1. rewind() – recomeça a iteração, reiniciando um possível índice interno;
  2. valid() – este método deve retornar true para continuar, ou false quando não houver mais iteração;
  3. current() – retorna o valor atual;
  4. key() – retorna o índice atual e somente é executado caso a chave seja pedida, como em foreach($it as $key => $value);
  5. next() – deve mover o índice interno para a próxima iteração;
  6. 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:

  1. 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;
  2. A cada iteração em Proverbios_Aborla_Letras, o método hasChildren() é consultado pelo RecursiveIteratorIterator. Como o retorno é sempre true, o iterador filho é requisitado;
  3. 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;
  4. Ao ser instanciada, Proverbios_Aborla_Proverbios obtém os dados do site e os utiliza na ArrayIterator;
  5. O iterador externo consulta o método hasChildren() na classe Proverbios_Aborla_Proverbios, que vai sempre retornar false;
  6. Então o método Proverbios_Aborla_Proverbios::current(), estendido da ArrayIterator, é executado e retorna um provérbio da página lida do site;
  7. 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;
  8. 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

Etiquetado como:, ,

2 Respostas

Subscreva aos comentários comRSS.

  1. Felipe Nascimento said, on Abril 18, 2007 at 11:31 pm

    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

  2. Fábio T. da Costa said, on Junho 12, 2009 at 4:28 pm

    Parabéns por estes artigo, ficou muito didático e ao mesmo tempo com funcionalidades realmente práticas!


Deixe uma resposta