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