ORM (como pode ser feito)

Artigo que explica como é possível implementar um ORM - Object Relational Mapping - na linguagem PHP.

Ontem escrevi sobre ORM, mas apenas apresentei o conceito. Hoje vamos ver o que se espera de uma camada ORM e algumas dicas de como implementá-la.

Uma das coisas que se espera de uma camada ORM é que ela não gere retrabalho. Um tipo de retrabalho comum em aplicações tradicionais é: criar a estrutura do banco de dados e, depois, precisar criar classes que referenciam as tabelas. Como foi apresentado ontem, existe muita semelhança entre a estrutura do BD e as classes que utilizam o conceito de ORM (chamo estas classes de "classes entidades" ou, simplesmente, "entidades", por se tratarem de classes com características especiais).

Já que os atributos da classe serão semelhantes às colunas da tabela, pra que especificá-las duas vezes? E se precisar mudar em um lugar, ter que mudar no outro. A solução é que sua camada ORM seja capaz de realizar a chamada "reflexão" em um dos lados e aplicar as regras do outro (vou explicar).

Existem, então, duas possibilidades:

  1. A camada ORM da aplicação ser capaz de ler a estrutura das tabelas e montar os atributos dinamicamente; ou
  2. A camada ORM da aplicação ser capaz de ler a estrutura de classes entidades e montar a estrutura das tabelas dinamicamente.

Ambas as soluções podem ser boas. A primeira, para ser mais otimizada, deve ler a estrutura do BD uma vez e criar as classes dinamicamente (geração automática de código-fonte). A segunda, também, pode ser mais otimizada se ler a estrutura das entidades uma única vez e montar a estrutura do BD (durante a instalação do sistema, por exemplo). Uma alternativa de menor performance é sempre comparar a estrutura das entidades com as estruturas das tabelas e realizar atualizações automáticas no BD, porém, isso é bastante complexo e, em alguns casos, arriscado. Por exemplo, é difícil uma aplicação detectar, automaticamente, que o nome de determinado atributo de uma entidade apenas mudou de nome. A princípio, a aplicação pode identificar, com facilidade, que um novo atributo foi criado e outro foi apagado.

O PHP oferece alguns elementos interessantes para criação de uma camada ORM. A primeira delas é a possibilidade de criação de atributos virtuais que são acessados com métodos mágicos. A segunda delas é utilizar a extensão responsável por fazer a "reflexão" (ou "reflection"), que obtem características do código-fonte em tempo de execução. Vamos tentar montar um código com trechos abstratos que ilustrem a situação:

/**
 * Classe base para todas as entidades
 */
abstract class entidade {

    /**
     * Array que guarda os valores dos atributos de uma instancia
     * @var array[string => mixed] Array indexado pelo nome do atributo
     *                             e aponta para o valor do atributo
     */
    private $valores = array();

    /**
     * Atributo que guarda a definicao da entidade e da tabela
     * @var estrutura_entidade Estrutura da entidade e da tabela
     */
    private $estrutura = null;

    /**
     * Metodo que devolve um objeto com a estrutura da classe entidade
     * @return estrutura_entidade Estrutura da classe entidade
     */
    abstract public function get_estrutura_entidade();

    /**
     * Construtor: cria uma entidade vazia
     */
    final public function __construct() {
        $this->estrutura = $this->get_estrutura_entidade();
        foreach ($this->estrutura->get_atributos() as $atributo) {
            $this->valores[$atributo->get_nome()] = $atributo->get_valor_nulo();
        }
    }

    /**
     * Obtem o valor de um atributo
     * @param string $nome_atributo Nome do atributo desejado
     * @param mixed Valor do atributo desejado
     */
    final public function __get($nome_atributo) {
        if ($this->estrutura->possui_atributo($nome_atributo)) {
            return $this->valores[$nome_atributo];
        }
    }

    /**
     * Define um novo valor para um atributo
     * @param string $nome_atributo Nome do atributo a ser atualizado
     * @param mixed $valor Novo valor a ser definido para o atributo
     * @return bool Indicacao se a atribuicao foi feita ou nao
     */
    final public function __set($nome_atributo, $valor) {
        if (!$this->estrutura->possui_atributo($nome_atributo)) {
            return false;
        }
        return $this->estrutura->get_atributo($nome_atributo)->set_valor(
            $this->valores[$nome_atributo],
            $valor
        );
    }

    // Outros metodos gerais para qualquer entidade aqui...
}

/**
 * Classe entidade usuario
 */
class usuario extends entidade {

    /**
     * {@inherit}
     */
    public function get_estrutura_entidade() {
        return new estrutura_entidade('usuario.xml');
    }
}

O exemplo acima eu inventei agora, mas exemplifica bem o conceito de ORM. A primeira classe é a base para a criação das entidades (chamei de classe "entidade"). Ela define um construtor padronizado que guarda a "definição da entidade" em um atributo e já define valores padrão para os atributos da instância ($this). Em seguida, foram implementados os métodos mágicos "__get" e "__set", que analisam a estrutura da entidade e realizam operações de atribuição e consulta genéricas. Note, no entanto, que não utilizei a extensão Reflection. Ao invés disso, obriguei as classes que herdam a classe "entidade" a implementar o método "get_estrutura_entidade", que precisa devolver a estrutura da entidade (o que a extensão Reflection conseguiria fazer).

Veja, na segunda classe (a classe "usuario"), que não foi necessário implementar quase nada, apenas um método que devolve a definição da entidade. Uma forma de fazer isso é guardando esta definição em um arquivo XML. O framework JPA (de Java) guarda definições em XML, o que torna a definição da entidade legivel não apenas para a aplicação, mas para outras aplicações. Outra alternativa é gerar a estrutura da entidade através de código (criando objetos da classe "estrutura_entidade" e chamando métodos que "constróem" esta estrutura). No caso do armazenamento em XML, é interessante ter uma classe que converte este XML em uma estrutura da aplicação, como um objeto. No exemplo, isso é representado pela classe "estrutura_entidade", que não cheguei a implementar aqui. Esta classe é uma peça chave da camada ORM que apresentei, já que oferece vários métodos úteis para a classe "entidade".

Bom, resumindo: a classe "entidade" e "estrutura_entidade" seriam as classes de suporte da camada ORM. Elas poderiam ser oferecidas por bibliotecas de terceiros. O que o programador precisaria fazer é criar as classes que extendem a classe "entidade", como a classe "usuario" do exemplo. Para isso, é importante conhecer relativamente bem o funcionamento da base (classe "entidade").

Com isso, perceba que ao instanciar um objeto "usuario", já temos os métodos "Getters" e "Setters" genéricos e prontos para utilização. Não cheguei a implementar, a parte mais complexa do ORM, mas vamos deixar isso para uma próxima oportunidade.

Utilizar os "getters" e "setters" é simples em PHP:

$u = new usuario();
$u->login = 'rubens';  // chama o método __set('login', 'rubens')
echo $u->login;        // chama o método __get('login')

E tudo isso é a base para a criação de um bom framework de desenvolvimento de Sistema de Informação Orientado a Objetos suportado por uma Base Relacional.

3 comentários

Ramon Filipe Sanches disse...

Fala Rubens, parabéns pelo artigo cara, ficou muito bom mesmo. Comecei a pouco tempo a trabalhar com orientação a objetos e padrões MVC no PHP e acredito que esse seu artigo será de grande valia.

Parabéns denovo, um abraço!

Silas disse...

Olá, estive olhando seu blog e como atualmente trabalho desenvolvendo um framework de persistencia para PHP gostaria de saber se voce teria interesse em conversar sobre o assunto.

Silas.

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá Silas, com certeza eu gostaria de conversar e trocar idéias sobre o assunto. Em 2006, comecei um framework que implementa a própria camada de persistência e ele evoluiu bastante de lá pra cá. Porém, se eu fosse começar hoje, teria feito bem diferente. Inclusive é um dos meus objetivos criar um novo framework.