Já falei bastante aqui sobre o Zend Framework, mas estava fazendo muita falta ao menos um que falasse sobre banco de dados, não é mesmo? Nesse artigo vou falar sobre a integração do framework com o MySQL, por ser um dos mais utilizados com o PHP.

Requisitos

Para conectar ao banco, utilizarei o pdo_mysql. Para isso, é necessário habilitar no php.ini as extensões php_pdo e php_pdo_mysql.

Configurações do banco

Tudo ok no php, é necessário agora indicar pro framework como ele conectará ao mysql. Para facilitar o processo, já tem criada uma classe de Resource (parte do módulo Zend_Application, para carregar funcionalidades com o bootstrap) pronta para que o Zend_Db crie a conexão, bastando então alterar o arquivo de configurações (se criado pelo Zend_Tool, estará em application/configs/application.ini), adicionando com as suas informações:

resources.db.adapter = "pdo_mysql"
resources.db.params.host = "localhost"
resources.db.params.username = "username"
resources.db.params.password = "password"
resources.db.params.dbname = "database"

Pronto, temos uma conexão com o banco funcionando.

O Banco de Dados

Como a idéia é mostrar o funcionamento do Zend_Db com o MySQL, farei um exemplo simples com um crud de usuários. Então, será um banco simples:

CREATE TABLE IF NOT EXISTS `user` (
  `usr_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `usr_name` varchar(255) NOT NULL,
  `usr_register` date NOT NULL,
  PRIMARY KEY (`usr_id`)
)

Models

Antes de ir para a parte realmente importante, é recomendável usar o Module_Autoloader para não precisar ficar cheio de includes em todas as nossas classes tanto de banco quanto de negócio. Então, inclua no arquivo application/Bootstrap.php:

protected function _initAutoload()
    {
        $autoloader = new Zend_Application_Module_Autoloader(array(
            'namespace' => 'Default_',
            'basePath'  => dirname(__FILE__),
        ));
        return $autoloader;
    }

Para usar o Zend_Db, usarei uma abordagem semelhante ao manual do framework, de forma que separaremos as classes de negócio das classes de tabela com o uso do pattern Data Mapper. Da forma que criei, as classes de negócio devem ficar na pasta application/models, os mappers na pasta application/models/Mapper e as classes de tabela em application/models/Table.

Primeiro, a classe para a tabela de usuários (application/models/Table/User.php):

<?php

class Default_Model_Table_User extends Zend_Db_Table_Abstract {

    protected $_name = 'user';
    protected $_primary = 'usr_id';

}

Vamos às explicações:

  1. o nome: por conta do autoloader que foi colocado no Bootstrap, é necessário nomear que nem as próprias classes do framework, com base nas pastas… como o arquivo está em application/models/Table/User.php, então deve ficar Default_Model_Table_User
  2. todas as classes de tabela devem estender Zend_Db_Table_Abstract
  3. o nome da tabela deve ser obrigatoriamente setado em $_name. A chave primária não é obrigatória pois o Zend é capaz de descobrir, mas por via das dúvidas eu coloquei.

Em seguida, temos que configurar o mapper, a classe que faz a comunicação entre as classes de negócio e de tabela. Primeiro, uma classe abstrata que eu fiz para evitar repetições em sistemas maiores, e contém o que todos os mappers deveriam ter:

<?php

abstract class Default_Model_Mapper_Abstract
{

    // liga o mapper à classe de tabela
    protected $_dbTable;
    // indica o nome da entidade (ex.: User), que é usado tanto no
    // modelo quanto na classe de tabela
    protected $_model;

    public function getDbTable()
    {
        if (null === $this->_dbTable)
        {
            $this->setDbTable("Default_Model_Table_".$this->getModel());
        }
        return $this->_dbTable;
    }   

    public function getModel()
    {
        if (null === $this->_model)
        {
            // ok, eu sei que isso daria erro... mas é justamente para forçar o erro
            // para saber que faltou setar o modelo no mapper
            $this->setModel('Abstract');
        }
        return $this->_model;
    }

    public function setDbTable($dbTable)
    {
        if (is_string($dbTable))
        {
            $dbTable = new $dbTable();
        }
        if (!$dbTable instanceof Zend_Db_Table_Abstract)
        {
            throw new Exception('Invalid table data gateway provided');
        }
        $this->_dbTable = $dbTable;
        return $this;
    }

    public function setModel($model)
    {
        $this->_model = $model;
    }

}

Não vou comentar muito porque acho que os comentários já explicam… agora uma implementação para o mapper do usuario:

<?php

class Default_Model_Mapper_User extends Default_Model_Mapper_Abstract {

    public function __construct()
    {
        $this->setModel("User");
    }

    public function add( Default_Model_User $user )
    {

        $data = array(
            'usr_name'     => $user->getName(),
            'usr_register' => new Zend_Db_Expr('NOW()')
        );

        return $this->getDbTable()->insert($data);

    }

    public function edit( Default_Model_User $user )
    {

        $data = array(
            'usr_name'   => $user->getName()
        );

        return $this->getDbTable()->update($data, array('usr_id = ?' => $user->getId()));

    }

    public function remove( $id )
    {

        return $this->getDbTable()->delete( array('usr_id = ?' => $id) );

    }

    public function find($id, Default_Model_User $user)
    {
        $result = $this->getDbTable()->find($id);
        if (0 == count($result)) {
            return;
        }
        $row = $result->current();
        $user->setId($row->usr_id);
        $user->setName($row->usr_name);
        $user->setRegister($row->usr_register);

        return $user;
    }

    public function fetchAll()
    {

        $table = $this->getDbTable();

        $resultSet = $this->getDbTable()->fetchAll();
        $entries   = array();

        foreach ($resultSet as $row) {
            $entry = new Default_Model_User();
            $entry->setId($row->usr_id);
            $entry->setName($row->usr_name);
            $entry->setRegister($row->usr_register);
            $entry->setMapper($this);
            $entries[] = $entry;
        }
        return $entries;

    }

}

Vendo o código, podemos perceber algumas coisas:

  1. através do método getDbTable(), podemos fazer várias operações de bancos de dados, tanto as da classe Zend_Db_Table quanto as do Zend_Db_Select (que no caso eu não usei)
  2. ao invés de fazer esse tipo de operação dentro da própria classe de negócio, é retornado um objeto da mesma: isso é muito útil em questão de manutenção, já que se precisarmos alterar algo no banco de dados ou até mesmo substituí-lo por outro componente, não precisaremos mexer na classe de negócio
  3. Graças a indução de tipo, o serviço do item passado torna-se bem mais fácil!

Agora só falta a classe de negócios: assim como os mappers, eu fiz uma classe abstrata. Ela ficou assim:

>?php

abstract class Default_Model_Abstract {

    // define a classe de mapper ligada ao model
    protected $_mapper;
    // indica qual o modelo usado
    protected $_model;

    public function __construct(array $options = null)
    {
        if (is_array($options))
        {
            $this->setOptions($options);
        }
    }

    public function __get($name)
    {
        $method = 'get' . $name;
        if (('mapper' == $name) || !method_exists($this, $method))
        {
            throw new Exception('Invalid property');
        }
        return $this->$method();
    }

    public function getMapper()
    {
        if (null === $this->_mapper)
        {
            $model = "Default_Model_Mapper_".$this->getModel();
            $this->setMapper(new $model());
        }
        return $this->_mapper;
    }

    public function getModel()
    {
        if (null === $this->_model)
        {
            $this->setModel('Abstract');
        }
        return $this->_model;
    }

    public function __set($name, $value)
    {
        $method = 'set' . $name;
        if (('mapper' == $name) || !method_exists($this, $method))
        {
            throw new Exception('Invalid property');
        }
        $this->$method($value);
    }

    public function setOptions(array $options)
    {
        $methods = get_class_methods($this);
        foreach ($options as $key => $value)
        {
            $method = 'set' . ucfirst($key);
            if (in_array($method, $methods))
            {
                $this->$method($value);
            }
        }
        return $this;
    }

    public function setMapper($mapper)
    {
        $this->_mapper = $mapper;
        return $this;
    }

    public function setModel($model)
    {
        $this->_model = $model;
    }

    public function add()
    {
        $this->getMapper()->add($this);
    }

    public function edit()
    {
        $this->getMapper()->edit($this);
    }

    public function remove()
    {
        $this->getMapper()->remove($this->getId());
    }

    public function find($id)
    {
        $this->getMapper()->find($id, $this);
        return $this;
    }

    public function fetchAll()
    {
        return $this->getMapper()->fetchAll();
    }

}

Acho que não há muito o que comentar, é a mesma idéia da classe abstrata do mapper. A única observação a se fazer é que os métodos abaixo dos setters e getters poderiam estar na classe que estenderá, mas eu preferi já deixar aí para não dar mais trabalho depois.

A última classe (finalmente!) é a de usuários:

<?php

class Default_Model_User extends Default_Model_Abstract {

    protected $id;
    protected $name;
    protected $register;

    public function __construct(array $options = null)
    {
        $this->setModel("User");

        parent::__construct($options);
    }

    public function getId() {
        return $this->id;
    }

    public function getName() {
        return $this->name;
    }

    public function getRegister() {
        return $this->register;
    }

    public function setId($id) {
        $this->id = $id;
    }

    public function setName($name) {
        $this->name = $name;
    }

    public function setRegister($register) {
        $this->register = $register;
    }

}

Simples não? uma classe apenas com os atributos básicos, setters e getters.

Concluindo

É isso ae, foi apenas uma de tantas possibilidades que deve haver para conectar ao banco. Acredito que essa seja uma boa opção para quem quer aproveitar os recursos que o framework implementou no Zend_Application.

Posted in php at dezembro 13th, 2009. 4 Comments.