garotosopa

O problema do contexto nas Fluent Interfaces

Publicado em OOP, PHP por garotosopa em Outubro 29, 2008

No post sobre Fluent Interface no PHP foi apresentada uma API que terminou assim:

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

O banco de dados estava estruturado com as tabelas de aluno, cidade, matrícula e curso. Como a relação do aluno com o curso é através da tabela de matrícula, foi necessário um catálogo extra para adicionar o critério sem quebrar o significado dos métodos.

Para o critério do nome da cidade do aluno como chave estrangeira, bastou realizar um JOIN a partir do próprio método e continuar o fluxo no catálogo de alunos.

public function daCidade($cidade) {  
    $this->select()->join('cidade', 'aluno.id_cidade = cidade.id', array())  
                   ->where('cidade.nome = ?', $cidade);  
  
    return $this;  
}

Neste caso é um critério simples, mas é comum acontecer o contrário, como no caso em que o aluno tem vínculo com a tabela de matrículas e esta tem vínculo com a tabela de cursos.

Poderia ser criado um método matriculadosNoCurso($curso) que fizesse JOIN nas duas tabelas, mas não seria uma prática adequada, já que o catálogo de alunos estaria assumindo uma responsabilidade do contexto de matrículas, o que em breve levaria à repetição de código.

O ideal é permitir que catálogos tenham referências entre si. O critério do nome do curso passa para o catálogo de matrículas que poderá ser associado com o catálogo de alunos.

class Model_Matricula_Table extends Zend_Db_Table_Abstract {
    protected $_name = 'matricula';
 
    protected $_referenceMap = array(
        'aluno=> array(
            'columns'       => 'id_aluno',
            'refTableClass=> 'Model_Aluno_Table',
            'refColumns'    => 'id',
        ),
}
 
class Catalogo_Matricula extends Catalogo_Abstract {
    protected function _factoryTable() {
        return new Model_Matricula_Table();
    }
 
    public function noCurso($curso) {
        $this->select()->join('curso', 'matricula.id_curso = curso.id', array())
                       ->where('curso.nome = ?', $curso);
 
        return $this;
    }
}

Agora falta relacionar o catálogo de matrículas com o catálogo de alunos.

Passando o contexto para outro objeto

Começando de fora pra dentro, o catálogo de alunos precisa passar o contexto para o catálogo de matrículas para que a interface possa utilizar os seus métodos.

class Catalogo_Aluno extends Catalogo_Abstract {
    /* complementando o código do post anterior */
 
    public function matriculados() {
        return $this->_reference('Catalogo_Matricula');
    }
}

O método _reference é herdado da classe Catalogo_Abstract e é responsável por instanciar e retornar o catálogo desejado.

abstract class Catalogo_Abstract {  
    /* complementando o código do post anterior */  
  
    private $_references = array();  
  
    protected function _reference($catalogo) {  
        if (!isset($this->_references[$catalogo])) {  
            $this->_references[$catalogo] = new $catalogo($this);  
        }  
  
        return $this->_references[$catalogo];  
    }  
}

Os catálogos são mapeados em um array para que a referência não seja feita mais de uma vez a partir do mesmo objeto.

Ao instanciar o objeto, o próprio catálogo é passado como argumento para o construtor, e é nessa etapa que o relacionamento será realizado.

Aceitando a referência

Ao receber uma referência no construtor, três procedimentos precisam ser feitos:

  1. guardar no array de referências o objeto passado;
  2. utilizar o mesmo select, já que a query é só uma;
  3. fazer JOIN entre as tabelas.

Para associar os catálogos é necessário saber o critério que os relaciona. Apesar de no Zend Framework essa informação estar disponível no reference map, parece que ainda não há uma forma automática de relacionar duas classes Zend_Db_Table. Como o uso do framework não é o foco, os detalhes serão dispensados.

abstract class Catalogo_Abstract {
    /* complementando o código */
 
    public function __construct(Catalogo_Abstract $reference = null)
    {
        if (null !== $reference) {
            // 1 – guarda a referência do catálogo
            $this->_references[get_class($reference)] = $reference;
 
            // 2 – utiliza o mesmo select
            $this->_select = $reference->select();
 
            // 3 – faz JOIN entre as tabelas (código relacionado ao Zend Framework)
            $fromTable = $reference->table()->info(Zend_Db_Table_Abstract::NAME);
            $fromModel = get_class($reference->table());
            
            $joinTable = $this->table()->info(Zend_Db_Table_Abstract::NAME);
 
            $referenceMap = $this->table()->getReference($fromModel);
 
            $clauses = array();
 
            foreach ($referenceMap['columns'] as $key => $column) {
                $refColumn = $referenceMap['refColumns'][$key];
                $clauses[] = "$fromTable.$refColumn = $joinTable.$column";
            }
 
            $clauses = implode(' AND ', $clauses);
 
            $this->_select->join(array($joinTable => $joinTable), $clauses, array());
        }
    }
}

Acompanhando o código:

$alunos = new Catalogo_Aluno();
// SELECT `aluno`.* FROM `aluno`
 
$alunos->emOrdem()
// ORDER BY `aluno`.`nome` ASC
 
       ->daCidade('Rio de Janeiro')
// INNER JOIN `cidade` ON aluno.id_cidade = cidade.id
// WHERE (cidade.nome = 'Rio de Janeiro')
 
       ->matriculados()
// instancia e retorna catálogo de matrículas
// passando o catálogo de alunos como parâmetro pro construtor
 
// __construct de Catalogo_Matricula adiciona à query:
// INNER JOIN `matricula` ON aluno.id = matricula.id_aluno
 
// A partir daqui, os métodos são do catálogo de matrículas
 
       ->noCurso('Enfermagem');
// INNER JOIN `curso` ON matricula.id_curso = curso.id
// WHERE (curso.nome = 'Enfermagem')
 
echo $alunos->select();
SELECT aluno.*
FROM aluno
INNER JOIN cidade ON aluno.id_cidade = cidade.id
INNER JOIN matricula ON aluno.id = matricula.id_aluno
INNER JOIN curso ON matricula.id_curso = curso.id
WHERE (cidade.nome = 'Rio de Janeiro')
  AND (curso.nome = 'Enfermagem')
ORDER BY aluno.nome ASC

Voltando ao contexto anterior (até porque voltar ao próximo é mais complicado)

O catálogo de alunos já se relaciona com o de matrículas, mas uma vez no contexto de matrículas, ainda não é possível voltar a utilizar os critérios de alunos.

Observando a consulta em português puro, é correto afirmar que a lista se trata de “alunos matriculados no curso Enfermagem em que os alunos sejam da cidade do Rio de Janeiro”.

Assim como o método “matriculados” serve como ligação semântica entre os alunos e suas matrículas, nada mais justo que criar um método para a ligação “em que os alunos sejam”.

class Catalogo_Matricula extends Catalogo_Abstract {
    /* complementando o código */
 
    public function dosAlunos() {
        return $this->_reference('Catalogo_Aluno');
    }
 
    public function emQueOsAlunosSejam() {
        return $this->dosAlunos();
    }
 
    public function emQueOsAlunosEstejam() {
        return $this->dosAlunos();
    }
}

Os três novos métodos fazem a mesma coisa e existem para deixar a chamada mais natural. Dessa forma, fica transparente iniciar a consulta pelo catálogo de matrículas e filtrar por $matriculas-> dosAlunos()-> daCidade(‘Rio de Janeiro’), ou voltar para o contexto de alunos e aplicar um método de estado, como $matriculas-> emQueOsAlunosEstejam()-> desativados().

De certa forma a escolha das palavras foi tendenciosa. A frase talvez devesse ser “alunos matriculados no curso Enfermagem que sejam da cidade Rio de Janeiro”, mas nesse caso seria difícil determinar o contexto a qual “que sejam” se refere, principalmente se a consulta iniciar pelo catálogo de matrículas.

Quando as chamadas começam pelo catálogo de alunos e seguem pelo método matriculados, o método dosAlunos não precisa instanciar o catálogo de alunos, porque o construtor já o adicionou como referência no array _references.

Quando mudar de contexto

Ao filtrar pela cidade do aluno ou pelo curso da matrícula, o JOIN foi feito no mesmo método e o contexto não mudou. Esta decisão foi totalmente baseada na prática.

Olhando a API, não era necessário ter algo como $alunos-> daCidade(‘Rio de Janeiro’)-> emQueOsAlunosEstejam()-> emOrdem(), porque a partir dos alunos não haveria nenhum critério específico das cidades.

De qualquer forma, pode ser interessante criar um catálogo de cidades e utilizá-lo ao filtrar os alunos, mesmo não mudando de contexto.

class Catalogo_Aluno extends Catalogo_Abstract {
    /* apenas alterando o método daCidade */
 
    public function daCidade($cidade) {
        $cidades = $this->_reference('Catalogo_Cidade');
        $cidades->chamadas($cidade);
 
        return $this;
    }
}

Com um catálogo próprio fica natural iniciar a consulta pelo contexto de cidades, possibilitando algo como $cidades-> comAlunos()-> matriculados()-> noCurso(‘Psicologia’).

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.

Publiquei os testes no link abaixo, incluindo as classes necessárias do Zend Framework e SQL de amostra utilizando SQLite na memória. É só baixar e executar o index.php.

Código fonte navegável
Exemplo para download

Deixe uma resposta