Domain Specific Language externa com PHP
No post sobre Fluent Interface, testei seu uso para nomear métodos de forma clara e reutilizável nas consultas ao banco de dados.
Durante o desenvolvimento, percebi que o Eclipse completava o código conforme eu digitava, tornando o uso das classes extremamente simples:

No final, a linha de código soava como uma frase em português, exceto pelos caracteres adicionais para a sintaxe do PHP.
A idéia então foi exportar a lista de classes e métodos parar criar uma linguagem própria do sistema, de forma que o usuário final pudesse usar o autocomplete pela web para formar uma frase sem as distrações da sintaxe.
A interface ficou assim:
Se o player não abrir, clique aqui para ver o vídeo de demonstração da interface no Youtube.
O autocomplete ainda não acompanha o que o usuário digita sem escolher uma opção, mas já é possível selecionar as opções com o teclado ou mouse, preencher cada parâmetro pelo prompt do Javascript e ter os métodos listados de acordo com o contexto.
A biblioteca e uma aplicação de exemplo estão no projeto DslCatalog que criei no Google Code.
Abaixo seguem os detalhes de como implementar os catálogos em um banco de dados já existente em 3 passos rápidos. Rápidos mesmo, eu juro :)
Passo 1 de 3: Criando as classes do modelo
A aplicação de exemplo segue com um banco de dados SQLite neste modelo:

Atualmente o único adapter de banco de dados para uso no DSL Catalog é a Zend Db Table. Mesmo que o projeto já utilize algum outro ORM ou qualquer abstração, as tabelas deverão ser mapeadas com a Zend_Db_Table_Abstract. O que não significa mudar de biblioteca. Apenas o mapeamento das tabelas deve ser feito.
Utilizando a Zend Db Table, que já está inclusa no pacote para download, a classe da tabela de cidades do modelo acima fica assim:
class Model_Cidade_Table extends Zend_Db_Table_Abstract
{
protected $_name = 'cidade';
protected $_rowClass = 'Model_Cidade_Row';
}
Para as tabelas que têm chave estrangeira, é preciso mapear as colunas e a classe que cada chave referencia:
class Model_Matricula_Table extends Zend_Db_Table_Abstract
{
protected $_primary = array('id_aluno', 'id_matricula');
protected $_name = 'matricula';
protected $_rowClass = 'Model_Matricula_Row';
protected $_referenceMap = array(
'aluno' => array(
'columns' => 'id_aluno',
'refTableClass' => 'Model_Aluno_Table',
'refColumns' => 'id',
),
'curso' => array(
'columns' => 'id_curso',
'refTableClass' => 'Model_Curso_Table',
'refColumns' => 'id',
),
);
}
A propriedade $_rowClass descreve qual classe representará cada linha da tabela e é onde os métodos que atuam individualmente nos objetos devem ser implementados.
Para nós isso é importante porque precisamos de uma forma genérica de representar na tela cada objeto do catálogo. Nos exemplos, a representação é feita apenas como string com o método mágico __toString que o PHP utiliza, mas com um pouco de criatividade pode-se evoluir facilmente para widgets mais elaborados.
Cada cidade será objeto dessa classe, e sua representação como string é o próprio nome da cidade:
class Model_Cidade_Row extends Zend_Db_Table_Row_Abstract
{
public function __toString()
{
return $this->nome;
}
}
O mapeamento das classes deve ocorrer para cada tabela que estará disponível através dos catálogos.
Passo 2 de 3: Criando os catálogos
Nas imagens anteriores a consulta era alunos matriculados no curso letras atualmente com status cursando. Quando esta frase é interpretada pelo parser, os tokens são separados e a árvore de execução fica dessa forma:
- catálogo de alunos
- critério matriculados
- catálogo de matrículas
- critério noCurso com argumento letras
- critério atualmenteComStatus com argumento cursando
Para permitir esta gramática, o catálogo de alunos fica assim:
class Catalogo_Alunos extends DslCatalog_Abstract
{
/**
* Instancia e retorna tabela de alunos
*
* @return DslCatalog_Database_Adapter_Zend
*/
protected function _factoryAdapter()
{
return new DslCatalog_Database_Adapter_Zend(new Model_Aluno_Table());
}
/**
* Retorna ligação destes alunos com suas matrículas
*
* @return Catalogo_Matriculas
*/
public function matriculados()
{
return $this->_reference('Catalogo_Matriculas');
}
}
O método _factoryTable deve ser sempre implementado e retornar o adaptador para a tabela que o catálogo representa.
Já o método matriculados faz parte da gramática do usuário. Ele utiliza o _reference para instanciar e retornar o catálogo relacionado, passando o nome do catálogo como argumento. Para realizar o JOIN, é importante que a relação entre as tabelas esteja mapeada (no Zend Db, através da array _referenceMap).
Como no PHP não existe type hint de retorno, é fundamental que a tag @return do phpDoc seja sempre incluída corretamente, pois é através dela que a interface determina o contexto dos critérios.
E então o catálogo de matrículas:
class Catalogo_Matriculas extends DslCatalog_Abstract
{
/**
* Instancia e retorna tabela de matrículas
*
* @return DslCatalog_Database_Adapter_Zend
*/
protected function _factoryAdapter()
{
return new DslCatalog_Database_Adapter_Zend(new Model_Matricula_Table());
}
/**
* Filtra matrículas pelo nome do curso
*
* @param string $curso
* @return Catalogo_Matriculas
*/
public function noCurso($curso)
{
$cursos = $this->_reference('Catalogo_Cursos');
$cursos->chamados($curso);
return $this;
}
/**
* Filtra matrículas pelo status atual
*
* @param string $status
* @return Catalogo_Matriculas
*/
public function atualmenteComStatus($status)
{
$criterio = 'matricula.id_aluno = matricula_status.id_aluno'
. ' AND matricula.id_curso = matricula_status.id_curso'
. ' AND CURRENT_TIMESTAMP BETWEEN matricula_status.data_inicio'
. ' AND matricula_status.data_fim';
$this->criteria()->join('matricula_status', $criterio, array())
->where('matricula_status.status LIKE ?', $status);
return $this;
}
}
O interessante neste caso é que o método noCurso referencia e utiliza um critério do catálogo de cursos, mas retorna o próprio catálogo de matrículas. Isso foi necessário por conveniência na gramática para manter o fluxo no mesmo contexto. Mais detalhes podem ser vistos no antigo post sobre problema do contexto nas Fluent Interfaces.
E finalmente o método atualmenteComStatus adiciona um critério à query. Esse ponto tem mais a ver com a Zend Db do que com a lógica da DSL em si.
Os demais catálogos estão disponíveis na demonstração.
Uma vez com os catálogos criados, é preciso reuní-los para montar a gramática. Como o conjunto de catálogos tende a ser o mesmo em todo o sistema, parece interessante estender a classe dessa forma:
class Catalogo_Grammar extends DslCatalog_Parser_Grammar
{
public function __construct()
{
$this->addCatalog('Catalogo_Alunos');
$this->addCatalog('Catalogo_Cidades');
$this->addCatalog('Catalogo_Cursos');
$this->addCatalog('Catalogo_Matriculas');
}
}
Passo 3 de 3: Criando a interface do usuário
Considerando a consulta do usuário em uma string qualquer (por exemplo vinda de um formulário), basta instanciar o parser e adicionar a ele a gramática que criamos logo acima.
A partir daí é possível interpretar a consulta e receber um catálogo que pode ser iterado e ter seus objetos mostrados na tela:
$consulta = 'alunos matriculados no curso letras';
$gramatica = new Catalogo_Grammar();
$parser = new DslCatalog_Parser();
$parser->setGrammar($gramatica);
$catalogo = $parser->parse($consulta);
foreach ($catalogo as $item) {
echo $item, '<br />';
}
Para deixar a caixa de texto da consulta com autocomplete, o script dslcatalog.js deve ser incluído junto com a estrutura da gramática:
<form method="get" action="index.php">
<p>
<input type="text" id="q" name="q" value="" />
<input type="submit" />
</p>
</form>
<script type="text/javascript" src="dslcatalog.js"></script>
<script type="text/javascript">
var gramatica = <?php echo $gramatica->getJson() ?>;
new DslCatalog.AutoComplete(document.getElementById('q'), gramatica);
</script>
Na aplicação de exemplo, a gramática foi deixada em um script separado com o objetivo de ficar no cache do navegador. Falando em cache, atualmente nenhuma camada da biblioteca tem essa preocupação, mas com o tempo isso tende a melhorar.
Download
A biblioteca com a aplicação de exemplo na revisão 39 está disponível no link abaixo, já incluindo as classes necessárias do Zend Framework. É só baixar e acessar o caminho demo/public/index.php.
http://dslcatalog.googlecode.com/files/dslcatalog-r39.zip
ou
http://dslcatalog.googlecode.com/files/dslcatalog-r39.tar.gz
Para a demonstração, o único requisito é que o PDO esteja habilitado com o driver SQLite. Caso receba a mensagem The sqlite driver is not currently installed, basta instalar a extensão pelo pecl:
Uma vez instalada, algum dos arquivos do php.ini deve conter extension=pdo_sqlite.so. Depois é só reiniciar o Apache.
Testes
Se alguém puder testar para tentar levantar possíveis sugestões seria legal. E se der algum problema é só falar.
Alguns dos recursos já planejados incluem:
- outra camada de acesso ao banco quando a Zend Db se mostrar limitada;
- catálogos de agregação com possibilidade de agrupamento para consultas como total de alunos matriculados por curso e cidade;
- parâmetros por widgets configuráveis (consulta de datas em um calendário, consulta de cursos em um combo pré-definido, etc);
- item dos catálogos mostrados em seus respectivos widgets;
- melhorar o autocomplete;
- e por aí vai…
E por enquanto eu fico devendo os testes de unidade, foi pura falta de habilidade mesmo. Mas já serão providenciados.




Legal o seu estudo, continue :)
Acho que você deveria se esforçar para fazer os testes unitários antes do código, pois você veria que o resultado seria um pouco diferente em alguns pontos.
Dei uma olhada no código e está muito bem organizado, parabéns ;)
[ ]s, gc
Valeu =)
Como eu não tenho prática em escrever testes preferi deixar pra depois, senão a idéia podia acabar morrendo e no final das contas eu ia ficar sem teste e sem idéia…
Mas é prioridade na lista de coisas pra estudar :P
Legal esse esquema hein??!
^^
Boa Tarde Garotosopa,
Achei muito interessante sua dsl, eu estou fazendo meu trabalho de conclusão de curso sobre dsl, mas queria fazer a mesma coisa que vc fez mas em java, gostaria de saber se tem alguma coisa ja implementada em java.
Outra coisa é se vc tem tambem p passo a passo (tutorial ) de como colocar rodar essa dsl, pois o link http://garotosopa.wordpress.com/dslcatalog-r39/demo/public/index.php não está mais disponivel.
Muito obrigada.
agradeço desde já!
Abs.
Olá, Bruno!
Realmente a demonstração não está mais online, mas você pode baixar o código pelo repositório do projeto.
Se você estiver familiarizado com SVN, basta fazer checkout de http://dslcatalog.googlecode.com/svn/trunk/
Caso prefira, acabei de criar um pacote zip com a revisão 48. É só baixar: http://dslcatalog.googlecode.com/files/dslcatalog-r48.zip
Depois de baixar, coloque em algum diretório acessível pelo servidor web e teste o acesso em dslcatalog/php/demo/public/index.php.
Você vai precisar do PHP 5 com PDO e o driver de SQLite para o PDO para este teste. Se ainda não estiverem instalados, no Linux, basta fazer “sudo pecl install pdo” e “sudo pecl install pdo_sqlite”.
Em relação ao Java, eu comecei a fazer algo com Hibernate recentemente, mas não tenho mexido e nem tenho pretensão de publicar algo em breve. Acho que dá pra fazer algo bem legal.
Abs,
Diogo
Diogo,
Bom Dia,
Estou querendo pegar sua ideia (excelente!!) e fazer meu Trabalho de Conclusão de Curso, gostaria de saber se vc pode me ajudar, puderia me mandar um passo a passo de como implementou a dsl (tanto na linguagem como o Banco)?
Meu email e msn é brunoanalise@hotmail.com.
Desde já agradeço Diogo.
Valeu!
Abs.
Olá, Bruno.
Você pode dar uma olhada no código que está no Google Code.
Se estiver familiarizado com SVN, basta fazer checkout de
http://dslcatalog.googlecode.com/svn/trunk/
Caso prefira, tem um pacote zip com a revisão 48. É só baixar:
http://dslcatalog.googlecode.com/files/dslcatalog-r48.zip
Depois de baixar, coloque em algum diretório acessível pelo servidor
web e teste o acesso em dslcatalog/php/demo/public/index.php.
Você vai precisar do PHP 5 com PDO e o driver de SQLite para o PDO
para este teste.
Se estiver interessado na linha de raciocínio, talvez o post
http://garotosopa.wordpress.com/2008/10/29/fluent-interface-php/ seja
interessante. Algumas coisas mudaram na versão atual, mas a idéia é
essa.
Abs,
Diogo
Rapazzz!
Muito louco esse negócio de DSL, cara que foda, achei genial!
Só tenho uma dúvida..
Qual é a lógica pra saber que o script tá lendo o ultimo método?
Ex:
$alunos->matriculados()
->noCurso(“tal”)
->comStatus(“formado”);
Como o script faz pra saber que o método “comStatus” foi o ultimo método daquela sequência pra fechar a query?
Porque tem que ter algo que feche a query , se não fica pela metade..
Eu só não entendi como o código fez isso..
Na verdade, neste ponto ele não sabe ainda quem foi o último método, e nem executa a query. Você poderia ter parado em noCurso(), ou nem ter aplicado filtro nenhum.
O grande lance é que esse objeto $alunos implementa a interface Iterator do PHP, possibilitando que o objeto possa ser usado assim: foreach ( $alunos as $aluno ) { … }.
Quando o foreach inicia, ele automaticamente chama $alunos->reset(), e é nesse momento que a query é executada, usando todos os critérios que foram configurados anteriormente. Depois o PHP chama $alunos->valid(), pra saber se deve continuar a iteração, $alunos->current() para pegar o valor do $aluno desta iteração, $alunos->next() para avançar a iteração, e depois recomeça no valid() até não ser mais válido.
O post Voltas e mais voltas com SPL explica detalhadamente o uso dessas interfaces de iterator (tão detalhado que é bastante chato hehe). Mas é bastante útil de vez em quando :)
Abs,
Diogo
Nossa véio,
Passei mó cara lendo esse guia aqui:
http://www.php.net/~helly/php/ext/spl/
Só pra poder entender esse bolo todo!
Cara, muito insano!
Mas me diz uma coisa, se você souber: Porque o método rewind executa com o foreach e não executa com o for?
Tipo assim:
for($it = new ArrayIterator($array);$it->valid();$it->next())
{
echo $it->current() . “”;
}
Eu fiz os testes, e a única maneira que consegui fazer executar, foi colocando o método rewind dentro de uma classe que extendia a ArrayIterator :O
Que mágica é essa !
Porque o for() realmente não faz nenhuma consideração quanto à interface Iterator. Nesse caso você teria mesmo que chamar o rewind() antes.
A diferença é que o foreach(), internamente no PHP, respeita a interface e chama seus métodos. Basicamente o que você fez aí no for() manualmente.
Se for pra usar o iterator, acho que o grande lance é usar foreach() e deixar o PHP se virar com a interface mesmo.
Então, é porque assim, o grande motivo deu ter procurado saber mais sobre o Fluent Interfaces, era o meu objetivo de criar um simples helper que montasse um select a partir de object chaining, usando a seguinte sintaxe fluente:
$select = new Select();
$select->option(“nome”,”valor”)
->option(“nome2″,”valor2″)
->option(“nome3″,”valor3″)
->option(“nome4″,”valor4″);
E ai vem o grande problema, porque o new tem o método construtor que cria a tag “select”, o option cria as tags “option”, e ai deveria ter algum método automático que fechasse o select.
Mas por essa teoria, se eu não vou usar um foreach, então é impossível fechar o select, porque o rewind nunca vai ser chamado.
Entendeu meu dilema?
Porque se não der desse jeito, a única forma de realizar essa tarefa é criar um método que feche o select, tipo end()..
(escrevendo comentário pela segunda vez já que o WordPress perdeu meu texto anterior só porque eu dei esc)
Mas você está dando print da tag select direto no constructor e print da tag option direto nos métodos?
Porque do contrário você poderia apenas configurar os elementos e fazer o print depois. Algo como:
$select = new Select();
$select->option(”nome”,”valor”)
->option(”nome2″,”valor2″);
echo $select;
Dessa forma, o constructor apenas configura a tag, provavelmente o name do elemento; o método option() apenas adiciona os options em algo como $this->options[]; e no final, com o echo, o método __toString() é automaticamente chamado, e então retorna o html das tags – basta ler o $this->options.
Com essa estrutura você tem a possibilidade de montar o select pelo controller e mandá-lo já montado para o template, que apenas exibe a variável.
Outra possibilidade é deixar montado selects que se repetiriam, como em class SelectStatus extends Select, e no constructor você adiciona opções de status. No código final você apenas faria echo new SelectStatus();
Sem ser assim, não vejo como você saber quando fechar a tag.
heaeaiuhaeui que zica, aconteceu isso quando fui postar da primeira vez, dá muita raiva!
Mas meu, verdade, eu não tinha pensado nessa possibilidade de usar o __toString!
Agora ficou bem melhor mesmo.
valeu!