Zend_Db_Table #3 – Lógica de Persistência

O componente Zend_Db_Table possui diversos mecanismos interessantes que poucas pessoas conhecem. Neste post irei falar da classe Zend_Db_Table_Row e a lógica de persistência.

Para isto é necessário extender a classe Zend_Db_Table_Row e configura-la na table pelo atributo $_rowClass ou usando o método $table->setRowClass($className). A row fornece 6 métodos em branco para serem sobrescritos de acordo com a necessidade. Para cada uma das operações insert, update e delete, existe um método que é executado antes e outro depois.

_insert
executado antes do insert
_postInsert
executado após o insert
_update
executado antes do update
_postUpdate
executado após o update
_delete
executado antes do delete
_postDelete
executado após o delete
 
class User extends Zend_Db_Table_Row 
{
    /**
     * Criptografa a senha do usuário.
     *
     * @param string $password
     * @return string
     */
    static public function makePassword($password)
    {
        return md5($password);
    }    
    
    /**
     * Executa antes de inserir a row.
     *
     */
    protected function _insert()
    {
        // define o grupo de usuario padrao
        $this->role = 'member';
        
        // seta a data de registro usando uma expressao do banco de dados
        $this->register_date = new Zend_Db_Expr('NOW()');
        
        // armazena a senha com md5
        $this->password = self::makePassword($this->password);
    }
    
    /**
     * Executa antes de atualizar a row.
     *
     */
    protected function _update()
    {
        /**
         * Se a senha foi alterada, precisa criptografar novamente.
		 * Verifica isso usando o array $_cleanData que contém os dados do banco
		 * sem alterações.
         */

        // a senha atual é diferente da que estava no banco? 
        if ($this->_cleanData['password'] != $this->password) {
            // criptografa a nova senha
            $this->password = self::makePassword($this->password);
        }
    }    
}

class Users extends Zend_Db_Table 
{
    protected $_rowClass = 'User';
}

Com a lógica definida, basta usar tranquilamente de qualquer forma que a table permite. Seja utilizando os métodos da table ou da row, a lógica sempre será executada.

$users = new Users();

// criando um usuario
$user = $users->createRow();
$user->username = 'deco';
$user->password = '12345';
$user->save(); // insert

// atualizando
$user->password = 'novasenha';
$user->save(); // update

// inserindo usando metodos da table
$user2 = array(
    'password' => '4321',
    'username' => 'joselito'
);
$users->insert($user2);

Utilizar este componente corretamente ajuda a diminuir os riscos de bugs da aplicação. Estas operações são garantidas também no relacionamento de tabelas, assunto que irei falar no próximo post da série.

Zend_Db_Table #2 – Consultando com Zend_Db_Table_Select

O componente Zend_Db_Table possui um mecanismo de consultas orientado a objetos que torna muito mais prático recuperar registros do banco. Esse mecanismo utliza o objeto Zend_Db_Table_Select e alguns métodos da própria table. Veja a assinatura do método fetchAll:

/**
 * Fetches all rows.
 *
 * Honors the Zend_Db_Adapter fetch mode.
 *
 * @param string|array|Zend_Db_Table_Select $where  OPTIONAL An SQL WHERE clause or Zend_Db_Table_Select object.
 * @param string|array                      $order  OPTIONAL An SQL ORDER clause.
 * @param int                               $count  OPTIONAL An SQL LIMIT count.
 * @param int                               $offset OPTIONAL An SQL LIMIT offset.
 * @return Zend_Db_Table_Rowset_Abstract The row results per the Zend_Db_Adapter fetch mode.
 */
public function fetchAll($where = null, $order = null, $count = null, $offset = null);

São 4 parâmetros e mesmo assim, em várias situações você precisaria de mais alguns. Repare que o primeiro parâmetro $where também aceita o objeto Zend_Db_Table_Select, que será a forma que iremos ver a seguir. Ignore todos os parâmetros deste método pois são deprecated. Considere o método fetchAll como abaixo:

/**
 * Fetches all rows.
 *
 * Honors the Zend_Db_Adapter fetch mode.
 *
 * @param Zend_Db_Table_Select $select  OPTIONAL Zend_Db_Table_Select object.
 * @return Zend_Db_Table_Rowset_Abstract The row results per the Zend_Db_Adapter fetch mode.
 */
public function fetchAll(Zend_Db_Table_Select $select = null);

Para criar o objeto Zend_Db_Table_Select basta utilizar o método select() da table.

$table = new MyTable();
$select = $table->select();

O objeto select possui diversos métodos para criar uma consulta, veja os principais.

public function where($cond, $value = null, $type = null);
public function orWhere($cond, $value = null, $type = null);
public function order($spec);
public function limit($count = null, $offset = null);
public function limitPage($page, $rowCount);

Na prática:

class Users extends Zend_Db_Table 
{ }

$users = new Users();

$countryId = 1;
$select = $users->select()
                ->where('country_id = ?', $countryId)
                ->order('username asc')
                ->limit(0, 10);

$rowset = $users->fetchAll($select);

Repare que o objeto select utiliza Fluent Interface. O código acima é equivalente a SQL:

SELECT * FROM `users`
WHERE country_id = '1'
ORDER BY `username` ASC
LIMIT 0,10

A classe table possui outro método de pesquisa chamado fetchRow que funciona parecido com o fetchAll, a única diferença é que retorna apenas uma row ou NULL ao invés do rowset.

$select = $users->select()
                ->where('email = ?', $email);

$user = $users->fetchRow($select);

if ($user != null) {
    echo $user->username; 
}

Apesar dos métodos fetchAll e fetchRow serem públicos, é mais coerente escrever códigos com select apenas na table.

class Users extends Zend_Db_Table 
{
    
    public function fetchRowByEmail($email)
    {
        $select = $this->select()
                       ->where('email = ?', $email);

        return $this->fetchRow($select);        
    }
}

Consulte o manual do Zend Framework sobre Zend_Db_Table e Zend_Db_Select para ver outros comandos. No próximo post irei escrever sobre lógica de persistência com Zend_Db_Table.

Zend_Db_Table #1 – Introdução

Zend_Db_Table é um componente do Zend Framework que implementa a pattern Table Data Gateway.

Vejamos um exemplo de tabela no MySQL:

CREATE TABLE `test`.`category` (
    `id` INT NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(60) NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE (`name`)
)

Agora a representação da tabela no PHP com Zend_Db_Table:

require_once 'Zend/Db/Table.php';

class Categories extends Zend_Db_Table 
{
    protected $_name = 'category';
}

Desta forma você já pode utilizar os diversos métodos da classe Zend_Db_Table. Além da pattern Table Data Gateway o pacote implementa a pattern Active Record para representar os registros da tabela com a classe Zend_Db_Table_Row. Veja abaixo alguns exemplos da utilização da Table e Row:

// iniciando o banco
$config = array(
    'dbname'     => 'test',
    'host'		 => 'localhost',
    'username'	 => 'deco',
    'password'	 => '123'
); 
require_once 'Zend/Db.php';
$db = Zend_Db::factory('MYSQLI', $config);

// setando o banco para ser utilizado em todas as table's
require_once 'Zend/Db/Table.php';
Zend_Db_Table::setDefaultAdapter($db);

// criando a tabela de categorias
$categories = new Categories();

// procurando uma categoria pelo ID
$cat = $categories->find(1)->current();
if ($cat) {
    echo $cat->name;
}

// procurando várias categorias pelos ID's
$cats = $categories->find(array(1, 2, 3));
foreach ($cats as $cat) {
    echo $cat->name;
}

// Criando uma nova categoria
$cat = $categories->createRow();
$cat->name = 'Programação';
$cat->save();

// atualizando os dados de uma categoria
$cat = $categories->find(1)->current();
if ($cat) {
    $cat->name = 'Programming';
    $cat->save();
}

// excluindo uma categoria
$cat = $categories->find(1)->current();
if ($cat) {
    $cat->delete();
}

No próximo post irei falar sobre os métodos de busca e Zend_Db_Table_Select.

Compactando a Saída do PHP com GZIP

Uma maneira de reduzir o tempo de carregamento de uma página é reduzindo seu tamanho. Entre tantas maneiras, a melhor é usar o mod deflate do apache. Mas só é válido se o seu servidor tiver o mod instalado. Caso contrário, umas das melhores alternativas seria utilizar o ob_gzhandler do PHP.

A maneira mais fácil de ativar é colocando a linha abaixo no .htaccess:
php_value output_handler ob_gzhandler

Caso seu servidor não suporte arquivos .htaccess, basta incluir o código abaixo no início do arquivo PHP:

ob_start('ob_gzhandler');

Para testar se está funcionando corretamente visite a ferramenta online GIDZipTest.

Veja também a função ob_gzhandler no manual PHP.net.

Problemas com caracteres e Zend_Form

Um problema comum para quem está começando a usar o Zend_Form é o escape de aspas. Na verdade o Zend_Form não vem com nenhum filtro ativado por padrão. Isso ocorre por causa da configuração magic quotes que vem habilitada no PHP. Segundo o manual do php é uma feature deprecated e será removida na versão 6.0. Para que o Zend_Form funcione corretamente você deve desativar esta funcionalidade em seu PHP.ini ou adicionar php_value magic_quotes_gpc off no seu arquivo .htaccess e os dados do Zend_Form serão retornados corretamente.

Como usar o Zend_Loader Corretamente

Ao contrário que muitos imaginam, o componente Zend_Loader não foi feito para substituir o required_once. Ele serve para carregar arquivos e classes dinamicamente de forma segura. Os casos normais de includes com os nomes de arquivos estáticos devem ser mantidos como abaixo.

require_once 'Zend/Controller/Front.php';
include 'hello.phtml';

O Zend_Loader deve ser usado quando o nome do arquivo for variável.

require_once 'Zend/Loader.php';

$className = 'Db_' . $_GET['adapter'];
Zend_Loader::loadClass($className);

$hello = $_GET['hello'] . '.phtml';
Zend_Loader::loadFile($hello);

Você pode passar um segundo parâmetro $dirs como string ou array de strings contendo diretórios para pesquisar.

$dirs = 'my/lib/';
Zend_Loader::loadClass($className, $dirs);

E para o método loadFile você pode passar um terceiro parâmetro $once como bool para utilizar como include_once.

Existe também o método registerAutoload para carregar as classes automaticamente de acordo com o padrão de classes e diretórios (My_Class => My/Class.php).

require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();

$front = Zend_Controller_Front::getInstance();

Você pode criar sua classe de autoload bastando adicionar o método estático autoload e registrar a classe no Zend_Loader.

class MyLoader
{
    static public function autoload($class)
    {
        
    }
}

require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload('MyLoader');

O autoload é recomendado para a parte concreta do projeto, pois ajuda no desenvolvimento e tráz melhorias de performance. Na implementação de componentes deve se mander o uso do require_once.

Lançamento do Zend Framework 1.6

Principais novidades que eu vi até agora:

  • File upload Form Element
  • Captcha Form Element
  • Zend_Session save handler para banco de dados
  • Integração com o Dojo
  • SOAP
  • Componente de paginação
  • Sistema de log com integração ao plugin FireBug do Firefox (veja este post)

Várias outras features foram adicionadas ao framework. Para baixa-lo visite a página de download do zend framework.

Hospedagem Grátis PHP 5 + MySQL #2

O 000webhost.com é um host gratuito com PHP 5, mysql, cPanel, 250 MB de espaço e 100 GB de transferência mensal. Além de aceitar várias configurações que os outros servidores não aceitam, incluindo arquivos .htaccess para usar rewrite do apache, PHP mail() e SendMail, sockets, possibilidade de utilizar vários domínios e muito mais. Tudo isso sem qualquer anúncio, vale a pena conferir.

Fluent Interfaces no PHP

Fluent Interfaces é um conceito de programação que deixa o código mais próximo da linaguagem humana, facilitando a leitura e escrita.

Exemplo de código normal:


function criarTarefa(TodoList $todo)
{
	$task = new Task();
	$task->setTitle('Postar sobre interfaces fluentes');
	$task->setPriority(Task::PRIORITY_NORMAL);
	
	$note = new Note();
	$note->setDescription('Com exemplos');
	$task->addNote($note);
	
	$note2 = new Note();
	$note->setDescription('Outra nota');
	$task->addNote($note);
	
	$todo->addTask($task);
}

Com Fluent Interfaces:


function criarTarefa(TodoList $todo)
{
    $todo->createTask()
         ->setTitle('Postar sobre interfaces fluentes')
         ->setPriority(Task::PRIORITY_NORMAL)
         ->addNote('Com exemplos')
         ->addNote('Outra nota');
}

Algumas linguagens utilizam o comando with para fazer algo parecido, mas não é a mesma coisa. O uso de Fluent Interfaces possibilita o retorno do próprio objeto ou de outro.

Abaixo um exemplo de métodos com retornos diferentes:

public function setTitle($title)
{
    $this->_title = $title;
    return $this;
}

public function createTask()
{
    $task = new Task();
    $this->_tasks[] = $task;
    return $task;
}

No Zend Framework diversos componentes usam Fluent Interfaces para facilitar a programação.

$table = new Zend_Db_Table();
$select = $table->select()
                ->where('priority = ?', Task::PRIORITY_HIGH)
                ->order('title')
                ->limit(10);
$rowset = $table->fetchAll($select);
                

$form = new Zend_Form();
$form->setAction('/usuarios/registrar')
     ->setMethod('post')
     
     ->createElement('text', 'username')
     ->setLabel('Nome de usuário')
     ->setAttrib('size', 25);

Este recurso foi adicionado no PHP5 e não é possível utiliza-lo no PHP4. Leia também Fluent Interfaces in PHP para saber mais.

Tratando Erros Exceptions e 404 no Zend Framework

Na versão 1.5 do Zend Framework existe o plugin Zend_Controller_Plugin_ErrorHandler. Que captura exceções de controller ou action não encontrados e exceções geradas pela aplicação do usuário que não foram tratadas. Este plugin já vem ativo por padrão.

Para utiliza-lo basta criar um controller chamado ErrorController com a action errorAction. Se você estiver utilizando o sistema de módulos, deverá criar o controller no módulo padrão.

O exemplo abaixo mostra um controller de erro que escolhe a view de acordo com o erro:

Arquivo controllers/ErrorController.php


class ErrorController extends Zend_Controller_Action 
{
    public function errorAction()
    {
        // limpa o conteúdo gerado antes do erro
        $this->getResponse()->clearBody();
        
        // pega a exceção e manda para o template
        $errors = $this->_getParam('error_handler');
        $this->view->exception = $errors->exception;
        
        // escolhe a view de acordo com o erro
        switch ($errors->type) {
            
            // página não encontrada (404)
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
            case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
                
                $this->getResponse()->setRawHeader('HTTP/1.1 404 Not Found');
                
                // renderiza a view "error/404.phtml" no lugar da view padrão
                $this->render('404');
                break;
            
            // erro no programa, exceção não tratada
            // deixa renderizar o template padrão (error/error.phtml)
            default:
            
        }
    }
}

Neste controller você pode aproveitar para gerar logs de erros no sistema das exceções que você
desejar.

Os arquivos de view não tem muito mistério. Mas eu gosto de colocar no meu arquivo de bootstrap (index.php) a constante DEV.

define('DEV', true);

E na minha view de erro, coloco um código para debugar a aplicação quando está rodando no modo DEV:

Arquivo views/scripts/error/error.phtml

<h2>Ops!</h2>

<p>Desculpe, ocorreu algum erro no sistema, tente novamente mais tarde.</p>

<? if (DEV): ?>
    <h3><?= get_class($this->exception); ?></h3>
    <p><?= $this->exception->getMessage(); ?></p>
    
    <h3>Stack Trace</h3>
    <pre><?= $this->exception->getTraceAsString(); ?></pre>
<? endif; ?>

Não esqueça de colocar define('DEV', false) quando for publicar a aplicação para desativar o modo de desenvolvimento.