garotosopa

Domain Specific Language externa com PHP

Publicado em Javascript, OOP, PHP por garotosopa em Novembro 19, 2008

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:

Eclipse autocompletando métodos do catálogo de alunos

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:

Tabelas cidade (id, nome), aluno (id, nome, nascimento, id_cidade), matricula (id_aluno, id_curso), curso (id, nome), matricula_status (id_aluno, id_curso, data_inicio, data_fim, status)

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:

  1. catálogo de alunos
  2. critério matriculados
  3. catálogo de matrículas
  4. critério noCurso com argumento letras
  5. 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:

# pecl install pdo_sqlite

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.

7 Respostas

Subscribe to comments with RSS.

  1. Guilherme Chapiewski said, on Novembro 22, 2008 at 9:54 pm

    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

  2. garotosopa said, on Novembro 24, 2008 at 6:25 pm

    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

  3. murilo said, on Dezembro 23, 2008 at 12:12 am

    Legal esse esquema hein??!

    ^^

  4. Bruno Coelho said, on Março 11, 2009 at 2:49 pm

    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.

  5. garotosopa said, on Março 11, 2009 at 5:53 pm

    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

  6. Bruno Coelho said, on Março 25, 2009 at 10:19 am

    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.

  7. garotosopa said, on Março 25, 2009 at 2:32 pm

    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


Deixe um comentário