DAO personalizado


1Apresentação

Como você viu na introdução e na documentação sobre o DAO genérico, o Temma fornece um mecanismo que permite manipular facilmente os dados de uma tabela, usando métodos genéricos.

Porém, no contexto de um projeto "real", você vai querer ir mais longe. Você pode escolher duas direções, que podem se complementar:

  • Em vez de usar os métodos get(), search(), update(), etc., você pode querer usar seus próprios métodos, que serão mais fáceis de usar.
  • Você pode precisar manipular dados armazenados em várias tabelas e, portanto, fazer junções (joins).

Em ambos os casos, o primeiro passo será criar seu próprio objeto DAO, que derivará do objeto padrão \Temma\Dao\Dao.


2Tabela única

2.1Criação

Aqui está um exemplo de objeto DAO personalizado, que simplesmente fornece um método adicional:

class ArticleDao extends \Temma\Dao\Dao {
    /**
     * Retorna a lista de artigos escritos há menos de um dia.
     * @return  array  Lista de arrays associativos.
     */
    public function getLastArticles() {
        // data e hora correspondentes a 24 horas atrás
        $yesterday = date('c', time() - 86400);

        // define o critério de busca, usando essa data
        $criteria = $this->criteria()
                    ->greaterThan('date', $yesterday);

        // executa a busca usando esse critério
        $articles = $this->search($criteria);

        // retorna o resultado dessa busca
        return ($articles);
    }
}

Você pode ver que esse objeto estende o objeto \Temma\Dao\Dao de forma bem "leve", adicionando apenas um método, e que ele usa os critérios de busca propostos pelo Temma.

Para ser utilizável, esse objeto deve ser salvo em um arquivo chamado ArticleDao.php, colocado no diretório lib/ do projeto.


2.2Configuração

Se você precisar especificar um ou mais parâmetros de criação do DAO, pode adicionar os seguintes atributos:

  • _disableCache: Defina como true para desativar o uso do cache pelo DAO.
  • _tableName: Nome da tabela que o DAO usará.
  • _dbName: Nome do banco de dados que contém a tabela.
  • _idField: Nome do campo que contém a chave primária.
  • _fields: Array associativo contendo a lista de campos a serem recuperados do banco de dados. Se os campos precisarem ser renomeados, basta declarar um par associativo cuja chave é o nome do campo na tabela, e o valor associado é o nome pelo qual ele deve ser renomeado.
  • _criteriaObject: veja abaixo

Todos esses atributos são opcionais e devem ser declarados com visibilidade protected.

Exemplo de um objeto DAO usando esses parâmetros:

class ArticleDao extends \Temma\Dao\Dao {
    // desativa o cache
    protected $_disableCache = true;
    // configura o nome do banco de dados
    protected $_dbName = 'cms';
    // configura o nome da tabela a ser usada
    protected $_tableName = 'content';
    // configura o nome do campo que contém a chave primária
    protected $_idField = 'cid';
    // lista de campos a recuperar, alguns sendo renomeados
    protected $_fields = [
        'cid'     => 'contentId',
                     'title',
                     'login',
        'content' => 'text',
        'date'    => 'creationDate',
                     'status'
    ];

    /* o resto do código */
}

Neste exemplo, podemos ver que o cache está desativado, que é a tabela content do banco de dados cms que será usada, cujo campo contendo a chave primária se chama cid. Podemos ver que as consultas get() e search() obterão 6 colunas para cada linha da tabela (a tabela pode ter outras colunas, mas elas não serão lidas), e que três dessas colunas serão renomeadas.


2.3Uso

Para usar o DAO que acabamos de criar, você deve especificar seu nome no controlador, usando o atributo _temmaAutoDao já visto anteriormente:

class Article extends \Temma\Web\Controller {
    /** Indica que o framework deve criar automaticamente
      * uma instância de ArticleDao. */
    protected $_temmaAutoDao = 'ArticleDao';

    /** Ação que exibe os últimos artigos. */
    public function showLast() {
        $data = $this->_dao->getLastArticles();
        $this['articles'] = $data;
    }
}

3Critérios avançados

No exemplo que vimos anteriormente, deve-se entender que ainda é possível usar os métodos padrão do objeto \Temma\Dao\Dao: get(), search(), update(), e assim por diante.

Por exemplo, poderíamos escrever o controlador assim:

class Article extends \Temma\Web\Controller {
    /** Indica que o framework deve criar automaticamente
     *  uma instância de ArticleDao. */
    protected $_temmaAutoDao = 'ArticleDao';

    /** Ação que exibe os últimos artigos. */
    public function showLast() {
        $data = $this->_dao->getLastArticles();
        $this['articles'] = $data;
    }

    /** Ação que exibe os 5 artigos mais antigos
     *  que não estão vazios. */
    public function showOlders() {
        $crit = $this->_dao->criteria()->different('content', '');
        $data = $this->_dao->search($crit, sort: 'date', limitOffset: null, limit: 5);
        $this['articles'] = $data;
    }
}

Você pode ver que a primeira ação usa o novo método getLastArticles(), enquanto a segunda ação usa o método usual search().

Criar métodos específicos (como o método getLastArticles() em nosso exemplo) pode ser bastante trabalhoso. Em vez disso, podemos preferir outra forma de fazer as coisas, que é estender o objeto de gerenciamento de critérios. Ao fazer isso, podemos continuar usando os métodos padrão (search(), update()) do objeto \Temma\Dao\Dao, mas escrevendo critérios de busca mais explícitos, por estarem mais próximos da lógica de negócio.


3.1Criando critérios

Vamos criar um objeto ArticleDaoCriteria, que herda do objeto \Temma\Dao\Criteria, e adicionar critérios específicos a ele:

class ArticleDaoCriteria extends \Temma\Dao\Criteria {
    /** Critério que pega os artigos que foram escritos
     *  há menos de 24 horas. */
    public function mostRecent() {
        // cria um critério simples
        $yesterday = date('c', time() - 86400);
        $this->greaterThan('date', $yesterday);

        // sempre retorna a instância do objeto atual
        return ($this);
    }

    /** Critério que pega os artigos que falam sobre PHP ou
     *  sobre Linux. */
    public function aboutRealStuff() {
        // no caso de critérios combinados por um "OU" lógico
        // (e não um "E" lógico), é preciso criar um
        // sub-objeto de critérios, especificando o tipo de
        // combinação entre os critérios
        $subCrit = $this->subCriteria('or')
                   ->like('title', '%PHP%')
                   ->like('title', '%Linux%');

        // em seguida adicionamos esse subcritério (combinado com "E")
        $this->and($subCrit);

        return ($this);
    }
}

Para ser utilizável, esse objeto deve ser salvo em um arquivo chamado ArticleDaoCriteria.php, colocado no diretório lib/ do projeto.


3.2Configuração e uso de critérios

Para usar o novo objeto de critérios, precisamos especificar seu nome ao objeto DAO personalizado:

class ArticleDao extends \Temma\Dao\Dao {
    // configuração do critério
    protected $_criteriaObject = 'ArticleDaoCriteria';
}

Agora que esse critério foi criado, podemos usá-lo em nosso controlador:

class Article extends \Temma\Web\Controller {
    /** Indica que o framework deve criar automaticamente
      * uma instância de ArticleDao. */
    protected $_temmaAutoDao = 'ArticleDao';

    /** Ação que exibe os últimos artigos. */
    public function execShowLast() {
        $data = $this->_dao->search(
            $this->_dao->criteria()->mostRecent()
        );
        $this['articles'] = $data;
    }

    /** Ação que exibe artigos recentes que falam
      * sobre PHP ou Linux. */
    public function execGoodNews() {
        $data = $this->_dao->search(
            $this->_dao->criteria()
            ->mostRecent()
            ->aboutRealStuff()
        );
        $this['articles'] = $data;
    }
}
  • Linha 4: Configuramos o DAO para usar o objeto DAO personalizado ArticleDao.
  • Linha 8: Usamos o método usual search(), mas com o novo critério mostRecent(), para recuperar os artigos novos.
  • Linhas 17 a 20: Ainda usamos o método search(), com os novos critérios mostRecent() e aboutRealStuff().

A vantagem dessa abordagem é óbvia. Quando você usa o critério aboutRealStuff(), você não precisa se preocupar com os detalhes de sua implementação.


3.3Configuração sem objeto DAO personalizado

Caso você não tenha criado um objeto DAO personalizado, você pode configurar o objeto de critérios diretamente no nível do controlador.

class Article extends \Temma\Web\Controller {
    /** Informa que o framework deve usar o objeto
      * de critérios ArticleDaoCriteria.*/
    protected $_temmaAutoDao = [
        'criteria' => 'ArticleDaoCriteria',
    ];

    /** resto do código... */
}

Usar o atributo _temmaAutoDao permite adicionar outros parâmetros específicos (veja a documentação do DAO genérico).


4Múltiplas tabelas

Caso você precise fazer junções (joins), chegamos aos limites da simplificação permitida pelo DAO. Recomendamos que você escreva suas próprias consultas SQL; assim você terá acesso a total liberdade.

Nesse caso, também é recomendável desativar o uso do cache, para evitar o risco de ter grandes inconsistências de dados.

Aqui está um exemplo simples de DAO fazendo consultas na tabela article com uma junção na tabela user:

class ArticleDao extends \Temma\Dao\Dao {
    // desativa o cache
    protected $_disableCache = true;

    /**
     * Retorna a lista de artigos, com informações sobre
     * seus autores, mais recentes primeiro.
     * @return  array  Lista de arrays associativos.
     */
    public function getArticles() {
        $sql = 'SELECT *
                FROM article
                    INNER JOIN user ON (article.user_id = user.id)
                ORDER BY article.date DESC';
        $articles = $this->_db->queryAll($sql);
        return ($articles);
    }

    /**
     * Retorna os artigos que pertencem a um usuário,
     * a partir do seu endereço de email.
     * @param  string  $email  Endereço de email do autor.
     * @return array   Lista de arrays associativos.
     */
    public function getArticlesFromEmail($email) {
        $sql = "SELECT *
                FROM article
                    INNER JOIN user ON (article.user_id = user.id)
                WHERE user.email = " . $this->_db->quote($email) . "
                ORDER BY article.date DESC";
        $articles = $this->_db->queryAll($sql);
        return ($articles);
    }
}

O controlador então é bem simples:

class Article extends \Temma\Web\Controller {
    /** Informa que o framework deve criar automaticamente
      * uma instância de ArticleDao. */
    protected $_temmaAutoDao = 'ArticleDao';

    /** Ação que exibe todos os artigos. */
    public function showAll() {
        $data = $this->_dao->getArticles();
        $this['articles'] = $data;
    }

    /**
     * Ação que busca os artigos de um usuário.
     * @param  string  $email  Endereço de email do autor.
     */
    public function search($email) {
        $data = $this->_dao->getArticlesFromEmail($email);
        $this['articles'] = $data;
    }
}

5Carregar múltiplos DAOs

É comum que um controlador precise usar vários objetos DAO durante seu processamento. O método _loadDao() foi feito para isso; ele espera um parâmetro de configuração, que pode ser de dois tipos:

  • Uma string contendo o nome do objeto DAO personalizado a ser carregado.
  • Um array associativo contendo todos os parâmetros, conforme visto na página anterior.

Aqui está um exemplo de criação de múltiplos DAOs, usando DAOs personalizados:

class Article extends \Temma\Web\Controller {
    /** Informa que o framework deve criar automaticamente
      * uma instância de ArticleDao. */
    protected $_temmaAutoDao = 'ArticleDao';
    /** Atributo que conterá uma instância de \MyApp\UserDao. */
    private $_userDao = null;

    /** Inicialização do controlador. */
    public function init() {
        $this->_userDao = $this->_loadDao('\MyApp\UserDao');
    }

    /**
     * Ação que busca os artigos de um usuário.
     * @param  string  $email  Endereço de email do usuário.
     */
    public function execSearch($email) {
        // verifica o endereço de email
        if ($this->_userDao->checkEmail($email)) {
            // recuperação dos dados
            $data = $this->_dao->getArticlesFromEmail($email);
            $this['articles'] = $data;
        }
    }
}

Aqui está um segundo exemplo, usando um array associativo de parâmetros:

class Article extends \Temma\Web\Controller {
    /** Informa que o framework deve criar automaticamente
      * uma instância de ArticleDao. */
    protected $_temmaAutoDao = 'ArticleDao';
    /** Atributo que conterá uma instância de \MyApp\UserDao. */
    private $_userDao = null;

    /** Inicialização do controlador. */
    public function init() {
        $this->_userDao = $this->_loadDao([
            'cache'  => false,
            'base'   => 'cms',
            'table'  => 'content',
        ]);
    }

    /** ... resto do código ... */
}