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.

15 Respostas

Subscreva aos comentários comRSS.

  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

  8. Renato said, on Setembro 16, 2009 at 4:34 pm

    Rapazzz!

    Muito louco esse negócio de DSL, cara que foda, achei genial!

  9. Renato said, on Setembro 16, 2009 at 4:39 pm

    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..

  10. garotosopa said, on Setembro 16, 2009 at 4:52 pm

    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

  11. Renato said, on Setembro 17, 2009 at 5:24 pm

    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 !

  12. garotosopa said, on Setembro 18, 2009 at 9:52 am

    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.

  13. Renato said, on Setembro 18, 2009 at 10:00 am

    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()..

  14. garotosopa said, on Setembro 18, 2009 at 10:22 am

    (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.

  15. Renato said, on Setembro 18, 2009 at 11:56 am

    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!


Deixe uma resposta