O problema do contexto nas Fluent Interfaces
No post sobre Fluent Interface no PHP foi apresentada uma API que terminou assim:
$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.
$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.
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.
/* 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.
/* 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:
- guardar no array de referências o objeto passado;
- utilizar o mesmo select, já que a query é só uma;
- 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.
/* 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:
// 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();
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”.
/* 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.
/* 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
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.



