garotosopa

Fluent Interface no PHP

Publicado em OOP, PHP por garotosopa em Outubro 29, 2008
Uma implementação mais recente, já com interface para consulta pelo usuário, está disponível no post sobre Domain Specific Language externa.

Esses dias testei o uso de Fluent Interface para consulta de dados de forma semântica, tentando criar uma API legível e reutilizável que eventualmente servisse diretamente ao usuário.

Em uma API comum, uma classe de negócio tem diversos métodos que retornam uma lista de acordo com diferentes critérios. As consultas ficam próximas do seguinte:

$alunos = new Alunos();
 
$alunosDoRio = $alunos->getPorCidade('Rio de Janeiro');
/* … */
$lista = $alunos->getMatriculadosPorCursoCidade('Enfermagem', 'Rio de Janeiro');
/* … */
$emOrdem = $alunos->getMatriculadosPorCursoCidade('Enf…', 'Rio…', 'nome');

Em um sistema real são muitas as variações de critérios e a classe acaba crescendo em quantidade de código e dificuldade de uso e manutenção.

A proposta é eliminar a necessidade de um método para cada tipo de consulta. Os critérios apenas configuram a consulta que será realizada uma única vez quando necessário, possibilitando que esses critérios de busca funcionem independentemente.

As chamadas passam a ser realizadas assim:

$alunos = new Alunos();
 
$alunos->daCidade('Rio de Janeiro');
/* … */
$alunos->emOrdem()
       ->daCidade('Rio de Janeiro')
       ->matriculados()
       ->noCurso('Enfermagem');

Para conseguir esta API é preciso:

  1. encadear os métodos
  2. ter a possibilidade de configurar a query dinamicamente
  3. identificar quando a configuração termina para então realizar a consulta.

Ainda que existam outras técnicas para refatorar a primeira API, o uso de Fluent Interface é um avanço significativo até a implementação de Domain Specific Language para uso do usuário.

Uma implementação mais recente, já com interface para consulta pelo usuário, está disponível no post sobre Domain Specific Language externa.

Method Chaining

O primeiro passo é perceber a simplicidade do encadeamento de métodos. Geralmente as propriedades de uma classe são definidas por métodos setters que não retornam nada. O objetivo é retornar o próprio objeto a partir de cada um desses métodos.

Esta técnica por si só apenas reduz a quantidade de código uma vez que não precisamos repetir a instância do objeto para cada setter.

Fluent Interface

Feito o encadeamento, é necessária uma mudança conceitual. O ponto principal é que a nomenclatura dos métodos deixe que a interface flua naturalmente.

Renomeando os métodos e retornando o próprio objeto é possível fazer a chamada de forma amigável:

class Alunos {
    // anteriormente chamado de setFiltroCidade
    public function daCidade($cidade) {
        echo "Alunos serão filtrados pela cidade $cidade";
 
        return $this;
    }
 
    // anteriormente chamado de setOrderByNome
    public function emOrdem() {
        echo "Alunos serão ordenados pelo nome";
 
        return $this;
    }
}
 
$alunos = new Alunos();
$alunos->daCidade('Rio de Janeiro')
       ->emOrdem();

Acima de tudo é importante manter consistência para que fique claro quais são as classes que trabalham com Method Chaining, evitando que o programador espere o próprio objeto como retorno equivocadamente.

Nestes exemplos será utilizado o prefixo Catalogo como tentativa de identificar essas classes. O uso de namespaces seria extremamente bem-vindo se a linguagem permitisse.

Montando a query

Diferente de uma API tradicional, os métodos anteriores devem apenas configurar o critério da consulta.

Mesmo sendo possível montar a SQL manualmente, uma camada de persistência que permita queries orientadas a objeto diminui muito a complexidade do código com o qual você precisa se preocupar. Os exemplos utilizam a modesta Zend_Db, mas uma biblioteca mais madura pode ser necessária no futuro.

Sobre o Zend Framework, basta saber que cada tabela é representada por uma classe que estende Zend_Db_Table_Abstract. Nessa classe, o método select retorna uma representação de SELECT na tabela, que pode ser configurada até o momento de executar a SQL.

Cada catálogo utilizará uma classe abstrata como base e implementará um método para identificar a tabela à qual está associado:

abstract class Catalogo_Abstract {
    private $_table;
    private $_select;
 
    abstract protected function _factoryTable();
 
    public function table() {
        if (null === $this->_table) {
            $this->_table = $this->_factoryTable();
        }
 
        return $this;
    }
 
    public function select() {
        if (null === $this->_select) {
            $this->_select = $this->table()->select();
        }
 
        return $this->_select;
    }
}
class Model_Aluno_Table extends Zend_Db_Table_Abstract {
    protected $_name = 'aluno';
}
class Catalogo_Aluno extends Catalogo_Abstract {
    protected function _factoryTable() {
        return new Model_Aluno_Table();
    }
 
    public function daCidade($cidade) {
        $this->select()->where('aluno.cidade = ?', $cidade);
 
        return $this;
    }
 
    public function emOrdem() {
        $this->select()->order('aluno.nome ASC');
 
        return $this;
    }
}

Desta forma já é possível combinar os métodos para consulta de alunos:

$alunos = new Catalogo_Alunos();
 
$alunos->emOrdem();
/* … */
$alunos->daCidade('Rio de Janeiro');
/* … */
$alunos->emOrdem()->daCidade('Rio de Janeiro');
/* … */
$alunos->daCidade('Rio de Janeiro')->emOrdem();

As últimas duas consultas resultariam na seguinte SQL:

SELECT aluno.*
FROM aluno
WHERE (aluno.cidade = 'Rio de Janeiro')
ORDER BY aluno.nome ASC

Determinando o momento da consulta com SPL

Até então, os dois métodos do catálogo de alunos apenas configuram a consulta e retornam o próprio catálogo. Falta uma forma de executar a query.

Uma solução é adicionar um método consultar que vai utilizar a SQL gerada e retornar a lista de alunos. Esse método, contudo, não impressionaria em nada as gatinhas, sem contar que não fluiria tão bem se comparado a uma frase em português.

Tratando-se de um catálogo, espera-se dele uma lista de objetos que serão iterados em um loop. No cenário ideal, o código abaixo deve ser suficiente:

$alunos = new Catalogo_Aluno();
 
$alunos->emOrdem()
       ->daCidade('Rio de Janeiro');
 
foreach ($alunos as $aluno) {
    echo $aluno->nome, '<br />';
}

E esse de fato é o cenário real (Ohhh).

Para controlar a iteração de um objeto em um foreach, o PHP dispõe da interface Iterator da Standard PHP Library. Quando utilizada, é necessário implementar os seguintes métodos, que serão chamados nessa ordem:

  1. rewind: reinicia a iteração;
  2. valid: determina se a iteração deve continuar;
  3. current: retorna o valor da iteração atual;
  4. key: retorna o índice da iteração atual;
  5. next: avança a iteração.

A consulta no Zend_Db retorna um objeto Zend_Db_Table_Rowset_Abstract, que já implementa a interface Iterator da SPL. Como a iteração no catálogo deve representar uma iteração neste rowset, basta direcionar os métodos para o objeto da consulta.

abstract class Catalogo_Abstract implements Iterator {
    /* complementando o código anterior … */
 
    private $_rowset;
 
    public function rowset() {
        if (null === $this->_rowset) {
            $this->_rowset = $this->table()->fetchAll($this->select());
        }
 
        return $this->_rowset;
    }
 
    public function rewind() {
        return $this->rowset()->rewind();
    }
 
    public function valid() {
        return $this->rowset()->valid();
    }
 
    public function current() {
        return $this->rowset()->current();
    }
 
    public function key() {
        return $this->rowset()->key();
    }
 
    public function next() {
        return $this->rowset()->next();
    }
}

Para ficar mais claro como os métodos são chamados, vale a pena dar uma olhada nestes slides.

O problema do contexto

O filtro por cidade foi um exemplo simples para ilustrar o conceito de Fluent Interface, imaginando que exista a coluna cidade na tabela de alunos, sem chave estrangeira. No mundo real é mais complicado que isso.

No primeiro exemplo lá em cima, a proposta era filtrar os alunos matriculados no curso de Enfermagem, mas, na minha realidade, não existe vínculo direto entre curso e aluno, apenas pela tabela de matrícula.

A solução que encontrei foi criar um catálogo específico de matrículas que é instanciado pelo catálogo de alunos, já criando o relacionamento entre as duas classes, resultando em um JOIN entre as três tabelas.

Para evitar que este texto ficasse ainda mais longo, separei os detalhes em um post específico sobre problema de contexto nas Fluent Interfaces.

Domain Specific Language

Talvez ainda não pareça justificável todo o trabalho que uma Fluent Interface consistente vai gerar.

A grande vantagem só ficou clara pra mim quando o Eclipse começou a auto-completar os métodos dos catálogos. Se estou no catálogo de alunos, ele já mostra o filtro de cidade. Se eu escrevo a cidade, ele ainda permite filtrar pelos já matriculados. Uma vez na matrícula, posso filtrar pelo curso, e assim por diante.

Isso é o mínimo que se espera de uma IDE que ocupa tanta memória. Mas e se o usuário tivesse a mesma facilidade?

Ao invés de me ligarem falando “Garoto, preciso de uma lista de alunos da região norte inscritos essa semana no curso de Gestão em Saúde pelo edital da UAB que tenham blah blah blah”, a (quase) mesma fala seria digitada numa caixa de texto com auto-complete.

Não que o problema não possa ser resolvido com um formulário gigante incluindo todos os filtros de consulta possíveis, mas estamos falando de usabilidade. Interfaces amigáveis para o usuário, código amigável para o programador. Quem não achou o plugin Ubiquity minimamente interessante?

E pra quem concorda que digitar a busca nem sempre é a melhor opção, os catálogos seriam facilmente exportados para ambientes visuais. Combinados com ações específicas de cada entidade (como matricular aluno, aprovar inscrição, desativar tutor…), o código passa a ser extremamente plugável.

Para tornar isso possível, a idéia é utilizar reflexão nas classes e expor os métodos na interface do usuário. Assim que parar de fazer sol na praia e eu tiver algum tempo, vou tentar expandir estes exemplos para DSL externa. O post de Domain Specific Language externa no PHP demonstra a implementação.

De qualquer forma, no fundo vou sentir falta das tais ligações.

Exemplo completo

Uma implementação mais recente, já com interface para consulta pelo usuário, está disponível no post sobre Domain Specific Language externa.

Referências

14 Respostas

Subscreva aos comentários comRSS.

  1. Samuca said, on Outubro 29, 2008 at 9:50 pm

    Muuuuuito foda esse artigo, achei interessantíssimo, e farei meus testes aqui.

    Grande abraço.

  2. Guilherme Chapiewski said, on Outubro 29, 2008 at 9:53 pm

    Show de bola ;)

  3. Lawrence Lagerlof said, on Outubro 30, 2008 at 10:53 am

    Nossa. Estou para ver um manual tão bem escrito. Excelente didática, foi direto ao ponto, sem enrolação. Exatamente o que os programadores procuram.

    Valeu!

  4. Carlos Kazuo said, on Novembro 3, 2008 at 4:51 pm

    Muito interessante o artigo! =]

  5. Felipe Lucio said, on Novembro 10, 2008 at 4:43 pm

    Muito bom o artigo cara!
    Simples, direto e com exemplos!

    Parabéns e valeu!

  6. Leandro Silva said, on Novembro 19, 2008 at 4:45 pm

    hehehe… bem interessante… parabéns!

  7. Jonathas Scot said, on Dezembro 10, 2008 at 7:19 pm

    Diogo, como nosso amigo lawrence fala: “você foi arrasadoramente arrasador”.

    Muito bacana seu post, inteligível até para o Designer /pseudo-programador-junior-mirim aqui.

    Show de bola. O melhor foi ver fucionando na sua máquina.

    Parabéns.

  8. Bruno Viana said, on Janeiro 19, 2009 at 8:00 pm

    Muito bom seu artigo. Meu deu várias idéias para melhorar o framework interno da empresa que trabalho.

    Meus parabéns!

  9. Micael Estrázulas said, on Fevereiro 10, 2009 at 9:35 am

    Excelente artigo. Parabens pelo blog

  10. José Cláudio said, on Julho 14, 2009 at 8:32 pm

    Cara, esse teu post é praticamente o único material sobre fluent interface para php da língua portuguesa.
    Porque não cria mais uma série de posts?
    Seria legal uma classe para gerar formulá’rio..não precisa de todos os inputs, e tals. O maior problema é como saber quando terminamos as configurações, e quando o php deve retornar o resultado.
    Alguma dica/

  11. garotosopa said, on Julho 14, 2009 at 9:02 pm

    Fala, José!

    A teoria base pra fluent interface é toda essa mesmo, acho que não tem muito mais o que explorar. A questão é só parar pra implementar os melhores métodos e fluxos pra resolver cada problema.

    Nesse caso do formulário que você falou, se o que você precisa é renderizar o resultado HTML, talvez funcione com __toString() sem quebrar a lógica. Algo do tipo:

    $form = new MeuFormFluente();
    $form->comInput(‘name’)->deRotulo(‘Nome’)->iniciadoCom(‘Seu nome’)
    ->e()
    ->comTextarea(‘comment’)->deRotulo(‘Comentário’)->iniciadoCom(‘Digite aqui’)
    ->que()
    ->submetePara(‘/salvar.php’);

    Supondo que isso fique no seu controller e que você passe o objeto para o view normalmente. No view, para fazer apenas echo $form, é só colocar a lógica de renderização no método __toString() do objeto. O PHP vai chamá-lo automaticamente.

    É possível ainda colocar o echo direto ali na frente da montagem do objeto, mas não sei se isso ficaria muito claro.

    Foi suposto que os métodos comInput e comTextarea da API acima passam o contexto para objeto do tipo Elemento (ou algo assim), de forma que o chain com métodos como deRotulo e iniciadoCom ficam nessa classe Elemento, e não no Form diretamente.

    Pra continuar inserindo componentes, nessa API, entra o método e() ou que() da classe Elemento, que apenas retorna o contexto de volta para o Form. Talvez outros métodos possam ser necessários pra deixar as chamadas com cara de português.

    Isso poderia ser contornado de outras formas, mas esse pra mim é o método mais simples e eficaz tecnicamente.

    De uma forma geral, acho que o que tá acima deveria ficar numa classe especializada, por exemplo class FormComentario, e aqueles comandos ficariam num __construct. Assim, no seu controller você faria apenas $form = new FormComentario(); e ele estaria todo montado.

    Uma vez com o form todo encapsulado, já não tenho mais certeza se a (possível) complexidade de montar uma interface fluente consistente vai valer a pena. Mas de qualquer forma, vale experimentar e ver no que vai dar.

    Se você fizer a classe de formulário ou alguma outra, me avisa aqui, pode ser muito útil =)

    Abs,
    Diogo

  12. José Cláudio said, on Julho 15, 2009 at 9:12 am

    É verdade. Não havia me tocado que o echo é quem vai imprimir. Foi bobeira mesmo. hehehe
    Valeu pela luz.

  13. José Cláudio said, on Julho 16, 2009 at 7:53 pm

    Para imprimir eu uso o __tostring, que é chamado sempre que dou um echo em algum lugar. E se eu quiser só executar uma ação como essa, sem imprimir nada? Qual método mágico usaria?

    needCache()
    ->controller(‘noticias’)
    ->and()
    ->controller(‘users’)
    ->action(‘login’)
    ->action(‘register’);

    Não achei um compatível no manual do php. Poderia me dar a luz?

  14. garotosopa said, on Julho 16, 2009 at 8:04 pm

    Se o que você está montando é uma instância de Cache, e nela quer chamar um método como processar(), aí me parece que não tem mágica. Só incluindo o ->processar() no final de tudo mesmo.

    Nesta implementação de envio de e-mail em Java http://code.google.com/p/fluentmailapi/ é utilizado o método send() no final. Talvez seja o mesmo raciocínio no seu caso.


Deixe uma resposta