sexta-feira, 24 de junho de 2011

Cálculo de tempo decorrido com ZF

Não é muito raro vermos em alguns sites, principalmente no rodapé, a informação do tempo decorrido para exibir uma página. E nos sites em PHP, geralmente é usado a função microtime para calcular esse tempo, chamando-a antes e depois do trecho de código que queremos calcular o tempo de execução, e depois subtraindo o primeiro do último.

Isso é muito útil, principalmente para trabalharmos na melhora da performance do código. Foi daí que me deu a idéia de criar um plugin para Zend Framework para facilitar o uso dessa função, e eis o resultado:

class My_Controller_Plugin_ElapsedTime 
extends Zend_Controller_Plugin_Abstract
{
/*
* Constantes que serão usadas para identificar a partir de
* que parte da execução deseja-se calcular o tempo decorrido.
* O nome das constantes é baseado no nome das funções que
* inicializam os valores correspondentes na propriedade
* $_startTime, logo abaixo
*/
const EVENT_ROUTE_STARTUP = 1;
const EVENT_ROUTE_SHUTDOWN = 2;
const EVENT_DISPATCH_LOOP_STARTUP = 3;
const EVENT_PRE_DISPATCH = 4;
const EVENT_POST_DISPATCH = 5;
const EVENT_DISPATCH_LOOP_SHUTDOWN = 6;

// Guarda o tempo de inicialização de cada parte da execução
static protected $_startTime = array(
self::EVENT_ROUTE_STARTUP => null,
self::EVENT_ROUTE_SHUTDOWN => null,
self::EVENT_DISPATCH_LOOP_STARTUP => null,
self::EVENT_PRE_DISPATCH => null,
self::EVENT_POST_DISPATCH => null,
self::EVENT_DISPATCH_LOOP_SHUTDOWN => null
);

// Define o tempo de inicialização antes de as rotas serem
// inicializadas
public function routeStartup(
Zend_Controller_Request_Abstract $request)
{
self::$_startTime[self::EVENT_ROUTE_STARTUP] =
$this->microtime();
}

// Define o tempo de inicialização depois de as rotas serem
// inicializadas
public function routeShutdown(
Zend_Controller_Request_Abstract $request)
{
self::$_startTime[self::EVENT_ROUTE_SHUTDOWN] =
$this->microtime();
}

// Define o tempo de inicialização antes de iniciar o loop
// de dispatch
public function dispatchLoopStartup(
Zend_Controller_Request_Abstract $request)
{
self::$_startTime[self::EVENT_DISPATCH_LOOP_STARTUP] =
$this->microtime();
}

// Define o tempo de inicialização antes do dispatch de
// uma action
public function preDispatch(
Zend_Controller_Request_Abstract $request)
{
self::$_startTime[self::EVENT_PRE_DISPATCH] =
$this->microtime();
}

// Define o tempo de inicialização depois do dispatch de
// uma action
public function postDispatch(
Zend_Controller_Request_Abstract $request)
{
self::$_startTime[self::EVENT_POST_DISPATCH] =
$this->microtime();
}

// Define o tempo de inicialização depois de finalizar o
// loop de dispatch
public function dispatchLoopShutdown()
{
self::$_startTime[self::EVENT_DISPATCH_LOOP_SHUTDOWN] =
$this->microtime();
}

/**
* Retorna o tempo de inicialização de cada evento do plugin,
* ou de um evento específico, que pode ser passado como
* parâmetro baseado no valor das contantes desta classe
*
* @param int $event
* @return mixed Float se receber $event, senão, array
*/
public function getStartTime($event=null)
{
if ($event) {
if (!isset(self::$_startTime[$event])) {
throw new Zend_Exception('Invalid value for $event: '
. $event);
}
return self::$_startTime[$event];
}
return self::$_startTime;
}

/**
* Calcula o tempo decorrido de um trecho e código.
*
* @param int $event
* @return float
*/
public function getElapsedTime(
$event=self::EVENT_ROUTE_STARTUP)
{
$startTime = $this->getStartTime($event);

// Lança uma excessão Caso o evento não tenha sido
// inicializado ainda
if (!$startTime) {
throw new Zend_Exception('Event has not been '
. 'initialized yet');
}

// Pega o microtime atual e substrai dele o tempo
// inicial para calcular o tempo decorrido
$endTime = $this->microtime();
$elapsedTime = $endTime - $startTime;

return $elapsedTime;
}

/**
* Pega o microtime em formato float, levando em conta as
* variações sofridas pela função de acordo com a versão do
* PHP
*/
private function _microtime()
{
if (version_compare(PHP_VERSION, '5.0.0', '>=')) {
// O parâmetro já converte o restulado da função para
// float a partir da versão 5.0.0
return microtime(true);
} else {
// Antes da versão 5.0.0, a conversão para float tinha
// que ser feita manualmente
list($usec, $sec) = explode(" ", microtime());
return ((float)$usec + (float)$sec);
}
}
}
Ufa, parece coisa demais pra uma coisa tão simples, não é? Mas tudo isso nos dá a possibilidade de medir o tempo de execução a partir de várias partes diferentes do código, que é uma facilidade que os plugins do ZF nos oferece, então, por que não aproveitar isso pra fazer um plugin mais completo, não é?

Feito o plugin, agora vamos ao View Helper, que vai se encarregar de pegar o valor definido no plugin e subtraí-lo de um novo microtime para exibir na página:
class My_View_Helper_ElapsedTime 
extends Zend_View_Helper_Abstract
{
/**
* Exibe o tempo decorrido de um trecho de código
* baseado nos valores inicializados no plugin
* 'My_Controller_Plugin_ElapsedTime'
*
* @param int $precision
* @param string|Zend_Locale $locale
* @return float
* @throws Zend_Exception
*/
public function elapsedTime($precision = null,
$locale = null, $event =
My_Controller_Plugin_ElapsedTime::EVENT_ROUTE_STARTUP)
{
$pluginClass = 'My_Controller_Plugin_ElapsedTime';

// Verifica se o plugin foi registrado
if (!Zend_Controller_Front::getInstance()
->hasPlugin($pluginClass)) {
throw new Exception("Plugin '{$_pluginClass}' is not '
. 'registered");
}

// Primeiro vamos pegar o plugin lá do front controller,
// que já foi inicializado
$elapsedTimePlugin = Zend_Controller_Front::getInstance()
->getPlugin($pluginClass);

// Agora vamos pegar o valor calculado do tempo decorrido,
// passando como parâmetro uma daquelas constantes definidas
// na classe do plugin, pra dizer a partir de que ponto do
// código onde calcular o tempo de execução. O padrão é de
// antes da inicialização das rotas.
$elapsedTime = $elapsedTimePlugin->getElapsedTime($event);

// Retorna o tempo decorrido formatado, levando em conta o
// número de decimais a ser exibido e a localização, para
// apresentar os símbolos de separação de milhares e de
// decimais corretos
return Zend_Locale_Format::toFloat($elapsedTime, array(
'precision' => $precision, 'locale' => $locale));
}

}
Agora é só chamar o view helper lá no script pra exibir o tempo? Não, ainda falta uma coisa. É preciso inicializar o plugin primeiro. Basta colocar lá no application.ini uma linhazinha de código, que ele será inicializado automaticamente:

resources.frontController.plugins[] = My_Controller_Plugin_ElapsedTime

Agora sim, vamos ao script phtml chamar nosso view helper e ver ele funcionando:

Essa página foi carregada em <?php echo $this->elapsedTime(2) ?> segundos
Agora vamos experimentar alterar os parâmetros pra ver os tempo decorrido a partir de diferentes trechos do código:

<ul><?php 
$events = array(
1 => 'routeStartup',
2 => 'routeShutdown',
3 => 'dispatchLoopStartup',
4 => 'preDispatch',
5 => 'postDispatch',
// 6 => 'dispatchLoopShutdown'
);
foreach($events as $eventId => $eventName) {
echo '<li>' . $eventName . ': '
. $this->elapsedTime(2, 'pt_BR', $eventId)
. ' segundos</li>';
}
?></ul>
O último tipo de evento (dispatchLoopShutdown) ficou comentado por uma boa razão. Quando a view é executada, esse ponto do código ainda não foi alcançado, portanto será lançada uma excessão, como foi definido na classe do plugin. Esse caso é usado mais para fins de debug mesmo, e para ver o resultado desse cálculo, é preciso chamar o plugin diretamente do front controller. Um exemplo, é chamá-lo no index.php no fim do arquivo:

$application->bootstrap()->run();
echo Zend_Controller_Front::getInstance()
->getPlugin('Case_Controller_Plugin_ElapsedTime')
->getElapsedTime(6);
Prontinho. Façam bom proveito!

segunda-feira, 20 de junho de 2011

Filtro de transliteração para o ZF

Já por várias vezes trabalhando em sites, tive a necessidade de converter strings para um formato sem acentos, cedilha, espaços, caracteres especiais, ou letras maiúsculas. Seja pra renomear um arquivo, renomear o título de uma notícia para a url ou criar um alias para um nome de usuário, a idéia é sempre a mesma, ou no mínimo muito semelhante. Isso é chamado transliteração, ou mais comumente, em inglês transliteration.

Procurando na internet achei várias expressões regulares que fazem o trabalho, mas nenhuma dela tinha tudo que eu queria, então resolvi fazer uma classe seguindo o modelo de classes do Zend Framework, e acabei fazendo duas: uma de filtro, e um view helper, que usa o filtro.
class My_Filter_Transliterate implements Zend_Filter_Interface 
{
public function filter($string)
{
// Lista de caracteres que devem ser substituídos
$a = 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ$ßàáâãäåæ@çèéêë&'
. 'ìíîïðñòóôõöøùúûüýýþÿŔŕ°ºª,.;:\|/"^~*%# ()[]{}=!?`‘’'
. "'";

// Lista que irá substituir os caracteres acima
$b = 'aaaaaaaceeeeiiiidnoooooouuuuybssaaaaaaaaceeeee'
. 'iiiidnoooooouuuuyybyRrooa--------------------------'
. '-';

// Efetua a substituição
$string = strtr($string, $a, $b);

// Deixa tudo minúsculo
$string = strtolower($string);

// Evita hífens repetidos
$string = preg_replace('/--+/', '-', $string);
return $string;
}
}

Pronto, tá feito o filtro. Agora pra usar em qualquer lugar, é só instanciar o filtro e chamar a função filter(). Como no exemplo:
$filterTransliterate = new My_Filter_Transliterate();
$string = 'Já não era sem tempo de Jaime lançar um post novo '
. 'nesse blog!!!';
echo $filterTransliterate->filter($string);
// ja-nao-era-sem-tempo-de-jaime-lancar-um-post-novo-nesse-blog-

Agora, só pra facilitar ainda mais, vamos criar um view helper pra usarmos nos nossos scripts.
class My_View_Helper_Transliterate 
extends Zend_View_Helper_Abstract
{
public function transliterate($value)
{
$filterTransliterate = new My_Filter_Transliterate();
return = $filterAlias->filter($value);
}
}

Simples assim... Agora, dentro dos arquivos de layout ou view, é só chamar como no exemplo abaixo:
<?php echo transliterate($this->post->title) ?>

quarta-feira, 10 de novembro de 2010

Melhorando o Zend_Db_Table - Parte 4: Fazendo JOIN automático com tabelas relacionadas

Uma das coisas chatas em se usar o Zend_Db_Table é quando você precisa de informações que se encontram em tabelas associadas à tabela que você está usando. Para isso, o Zend_Db_Table oferece a opção de você configurar a variável $_referenceMap para definir os relacionamentos entre as tabelas, e usar métodos como findDependentRowset(), ou algo do tipo find<tableclass>By<rule>(), que são na verdade métodos de Zend_Db_Table_Row (Saber mais).

A utilização desses métodos pode ser boa no caso de poucos dados, mas se você estiver querendo pegar as informações completas das tabelas associadas de uma lista muito grande de registros, a quantidade de acessos ao banco para pegar essas informações vai ser proporcional à quantidade registros. Veja o seguinte cenário como exemplo:

Você quer pegar de uma tabela Usuarios a lista dos usuários com o nome da cidade em que vivem. Sendo que em Usuarios Você só tem o código da cidade, que é chave estrangeira para a tabela Cidades, onde está o nome.

Agora imagine que você quer pegar essa informação de todos os 300 usuários da sua lista. Você teria que fazer uma consulta ao banco para pegar a lista de usuários, iterar sobre a lista e fazer uma consulta para cada usuário para pegar o nome da cidade usando os métodos de Zend_Db_Table_Row, totalizando 301 acessos ao banco de dados e muito trabalho de código.

Por esse motivo, eu sempre me perguntei porque a classe Zend_Db_Table não tinha métodos para trazer as informações das tabelas associadas juntamente com a tabela em questão. E isso é o que vai ser mostrado neste post como ser feito.

A idéia é aproveitar a estrutura original da Zend_Db_Table, sem alterá-la, para que se necessário possa ser usada da forma usual. Então, para começar, vamos criar o método principal, que vai montar o select com as joins baseadas em $_referenceMap.

Vou colocar logo o código completo e explicando com comentários:
/**
* Gera automaticamente um Zend_Db_Table_Select fazendo join nas tabelas
* baseado nas informações do atributo $_referenceMap
*
* @param array $columns Lista de campos a serem retornados
* do banco de dados (usado para ganho de performance).
* Se for null, todos as colunos serão retornados.
* @param array $referenceTables Lista de "rules" das tabelas a serem
* acrescentadas na query (usado para ganho de performance)
* Se null, todas as tabelas serão acrescentadas na query.
* return Zend_Db_Table_Select
*/

public function selectWithJoins(array $columns = null,
array $referenceTables = null)
{
$select = $this->select();

// Verifica se há restrições nos campos a serem acrescentados
$retrieveColumns = '*';
if($columns) {
$tableCols = $this->info('cols');
$retrieveColumns = array_intersect($tableCols, $columns);
}
$select->from($this->_name, $retrieveColumns);

// Verifica se há tabelas a serem acrescentadas com join na query
// baseado no $_referenceMap

if (count($this->_referenceMap) > 0) {
$select->setIntegrityCheck(false);
if (!isset($tableCols)) {
$tableCols = $this->info('cols');
}
foreach($this->_referenceMap as $rule=>$rm) {
// Verifica se há restrições nas tabelas a serem
// acrescentadas

if (null === $referenceTables ||
in_array($rule, $referenceTables))
{
$refTableClass = new $rm['refTableClass'];

// Campos das tabelas a serem acrescentadas
// são renomeadas
// para "NomeDaRule_NomeDoCampo"

$refTableColumns = array();
foreach ($refTableClass->info('cols') as $col) {
$colAlias = "{$rule}_{$col}";
// Verifica se há restrições nos campos a
// serem acrescentados

if (!$columns || in_array($colAlias, $columns)) {
$refTableColumns[$colAlias] = $col;
}
}

// Se não há campos a serem acrescentados,
// a tabela não será acrescentada

if (count($refTableColumns)) {
$refTableName = $refTableClass->info(self::NAME);
if (is_string($rm['refColumns'])) {
// Se a chave primária é única
$joinOnString =
"{$refTableName}.{$rm['refColumns']}"
. " = {$this->_name}.{$rm['columns']}";
} else if (is_array($rm['refColumns'])) {
// Se a chave primária é concatenada
$joinOnArray = array();
foreach($rm['refColumns'] as $key=>$rc) {
$joinOnArray[] = "{$refTableName}.{$rc} = "
. "{$this->_name}.{$rm['columns'][$k]}";
}
$joinOnString = implode(' AND ', $joinOnArray);
}

$select->joinLeft(
$refTableName,
$joinOnString,
$refTableColumns
);
}
}
}
}

return $select;
}

Pronto, feito o select, já é possível gerar a query com os joins e usar, por exemplo, em um Zend_Paginator, ou fazer um fetchAll() e usar o resultado normalmente.
Mas, pra ficar ainda mais prático, vamos acrescentar o método fetch lá na nossa classe e também um método find pra retornar um único registro, baseado na chave primária.

/**
* Faz uma busca no banco de dados com joins nas tabelas relacionadas
* baseado no atributo $_referenceMap
*
* @param string|array $where OPTIONAL An SQL WHERE clause.
* @param string|array $order OPTIONAL An SQL ORDER BY clause.
* @param array $columns OPTIONAL Campos a serem retornados
* @param array $referenceTables OPTIONAL Tabelas a serem acrescentadas
* @param mixed $fetchMode OPTIONAL Define o formato do resultado da busca
* @return mixed Depende do valor de $fetchMode.
*/

public function fetchWithJoins($where = null, $order = null,
array $columns = null, array $referenceTables = null,
$fetchMode = Zend_Db::FETCH_OBJ)
{
$select = $this->selectWithJoins($columns, $referenceTables);

if ($where !== null) {
$this->_where($select, $where);
}

if ($order) {
$select->order($order);
}

return $this->getAdapter()->fetchAll($select, null, $fetchMode);
}

/**
* Faz uma busca usando a chave primária no banco de dados com joins
* nas tabelas relacionadas baseado no atributo $_referenceMap
*
* @param string|array $pkey Valor da chave primária
* @param array $columns OPTIONAL Campos a serem retornados
* @param array $referenceTables OPTIONAL Tabelas a serem acrescentadas
* @param mixed $fetchMode OPTIONAL Define o formato do resultado da busca
* @return mixed Depende do valor de $fetchMode.
*/

public function findWithJoins($pkey, array $columns = null,
array $referenceTables = null, $fetchMode = null)
{
// Ver posts anteriores para entender este método
$where = $this->_generateRestrictionsFromPrimaryKeys($pkey);

$result = $this->fetchWithJoins($where, null, $columns,
$referenceTables, $fetchMode);
return count($result) == 1 ? current($result) : $result;
}
Agora fica fácil fazer uma busca com joins nas tabelas. Vamos reaproveitar o exemplo de Usuarios e Cidades.
Digamos que essas sejam as respectivas classes:

class Cidades extends My_Db_Table_Abstract
{
protected $_name = 'cidades';
protected $_primary = 'id_cidade';

protected $_dependentTables = array('Usuarios');
}

class Usuarios extends My_Db_Table_Abstract
{
protected $_name = 'usuarios';
protected$_primary = 'id_usuario';

protected $_referenceMap = array(
'Cidade' => array(
'columns' => 'id_cidade',
'refTableClass' => 'Cidades',
'refColumns' => 'id_cidade'
),
)
}
Perceba que os relacionamentos estão bem definidos em $_referenceMap. Agora vamos fazer a busca que no interessa.

$idUsuario = 1;
$tbUsuarios = new Usuarios();
$usuario = $tbUsuarios->findWithJoins($idUsuario);

Se considerarmos que a tabela usuarios só tem os campos id_usuario, nome e id_cidade, e a tabela cidades tem id_cidade, nome e estado, teremos um resultado como o seguinte:
$usuarios = StdClass(
'id_usuario' => 1,
'nome' => 'Fulano de Tal',
'id_cidade' => 40,
'Cidade_id_cidade' => 40,
'Cidade_nome' => 'João Pessoa',
'Cidade_estado' => 'PB'
);
Veja que os campos de cidade ganharam um prefixo com o nome da rule que foi definida no atributo $_referenceMap. Isso é pra evitar conflito entre nomes de campos iguais, como ocorre neste exemplo com id_cidade e nome, por exemplo.
Para obter mais resultados, você pode usar da mesma forma o método fetchWithJoins().

No caso de uma classe que faça raferência a muitas outras tabelas, essa consulta pode ficar muito pesada, e muitas vezes nem nos interessa num certo momento todos aqueles dados, então, pode ser passado como parâmetro apenas as tabelas que temos interesse, ou mesmo os campos que nos interessa, diminuindo, dessa forma, a carga sobre o banco de dados, já que diminuirá significativamente a quantidade de dados a serem retornados na consulta.

No exemplo seguinte, a query retornará apenas os campos nome e Cidade_nome de todos os usuários do estado da Paraíba ordenados pelo nome da cidade:

$where = array('estado = ?' => 'PB');
$order = array('cidades.nome ASC');
$usuarios = $tbUsuarios->fetchWithJoins($where, $order, array('nome', 'Cidade_nome'));


Imaginando que a tabela Usuarios faça referências a outras tabelas, e para a consulta só me interessam os dados da tabela de cidades (além das de usuarios, claro), o exemplo seguinte mostra como limitar os joins apenas as tabelas de interesse, utilizando as rules definidas em $_referenceMap:

$order = array('usuarios.nome ASC');
$usuarios = $tbUsuarios->fetchWithJoins(null, $order, null, array('Cidade'));


Perceba que a classe como está aqui apresentada só faz join em um nível de relacionamento. Por exemplo, se a tabela Cidades fizesse referência a uma tabela Estados, os dados de estados não entrariam no resultado dos exemplos citados. Quem sabe numa versão futura...

Ok. Agora ficou fácil fazer joins sem precisar ficar montando os selects toda hora, nem ficar fazendo centenas de acessos desnecessários ao banco.

Ainda assim, isso deve ser usado com cautela, já que consultas com join também pesam no banco, devendo-se optar sempre que possível pela utilização de Views de banco de dados ou tabelas tamporárias. Vale também uma olhada nas dicas de performance no site do Zend Framework a respeito da utilização da Zend_Db_Table (Saber mais).

Espero que tenham gostado! ^__^

sábado, 30 de janeiro de 2010

Melhorando o Zend_Db_Table - Parte 3: Contando registros da tabela

Tá... o Zend_Db_Table já me dá métodos pra inserir, alterar, excluir e fazer busca de registros. Mas... e se eu só quiser saber quantos registros tenho na tabela? Ele não tem por padrão um método para me dizer isso. Eu precisaria fazer um fetchAll() e depois um count() no resultado, ou montar uma query pra me devolver esse resultado, ou passar como parâmetro no bind uma string 'COUNT(*)'... Mas tudo isso parece pouco prático, quando poderíamos ter um método count() pra fazer isso pra nós...

Então é isso que fiz... Implementei na minha classe My_Db_Table_Abstract a interface Coutable, que ficou dessa forma:

class My_Db_Table_Abstract extends Zend_Db_Table_Abstract
implements Countable
{
/* Aqui ficam os métodos citados nos posts anteriores */

public function count($where = null)
{
$select = $this->select();

if ($where !== null) {
$this->_where($select, $where);
}

$select->from($this->_name, array('count' => 'COUNT(*)'));

$count = $this->getAdapter()->fetchRow($select);
return intval($count['count']);
}
}

Pra ficar ainda mais completo, o método pode receber como parâmetro algumas restrições, caso não se queria contar a quantidade total, e sim algo mais específico. Agora, fica bem mais fácil:

$tbUsuarios = new Usuarios();
$qtty = $tbUsuarios->count();

Se quisermos, por exemplo, saber quantos usuários usam gmail:

$qtty = $tbUsuarios->count("email LIKE '%@gmail.com'");

Simples assim...

Melhorando o Zend_Db_Table - Parte 2: Simplificando UPDATE e DELETE

Continuando as melhorias na utilização da Zend_Db_Table, do Zend Framework. Outra melhoria a ser feita é nos métodos update() e delete(). O que há de comum entre esses dois métodos é que ambos recebem como parâmetro uma ou mais restrições para identificar em que registro a alteração ou exclusão será feita. Sendo que a grande maioria das vezes que você usa esses métodos, o registro é relativo à chave primária da tabela, e mais raramente à alguma outra coluna. Como no exemplo:

$id = $this->_request->getPost('id');

$tbUsuarios = new Usuarios();
// Imaginemos que esses dados vieram de um formulário
$dados = array(
'nome' => 'Fulaninho de Tal',
'email' => 'fulaninho@email.com'
);
$where = $tbUsuarios->getAdapter()->quoteInto('id = ?', $id);
// Vamos atualizar o usuário
$tbUsuarios->update($dados, $where);

// E agora excluímos
$tbUsuarios->delete($where);

Pra não ter que toda vez montar a restrição para esses métodos, adicionei à minha classe My_Db_Table_Abstract. métodos de update e delete para um registro específico, onde o parâmetro de restrição é sempre a chave primária. Vejamos:

class My_Db_Table_Abstract extends Zend_Db_Table_Abstract
{
protected function _filterData(array $data)
{
/* Permanece a mesma coisa */
}

/**
* Gera restricoes SQL de acordo com a(s) chave(s) primária(s)
* @param String|array $pkey chave(s) primária(s)
* [nome_do_campo => valor]
* @return String|array SQL where
**/

protected function _generateRestrictionsFromPrimaryKeys($pkey)
{
$where = array();
if (is_array($this->_primary)) {
foreach($this->_primary as $key) {
if (is_array($pkey)) {
$where[] = $this->getAdapter()
->quoteInto($this->_name.'.'.$key . ' = ?',
$pkey[$key]);
} else {
$where = $this->getAdapter()
->quoteInto($this->_name.'.'.$key . ' = ?',
$pkey);
}
}
} else {
$where = $this->getAdapter()
->quoteInto($this->_name.'.'.$this->_primary . ' = ?',
$pkey);
}
return $where;
}

public function insert(array $data)
{
/* Permanece a mesma coisa */
}

public function update(array $data, $where)
{
$data = $this->_filterData($data);
return parent::update($data, $where);
}

/**
* Update de um registro único, baseado na chave primária
* @param array $data Dados a serem atualizados
* @param string|array $pkey Valor da chave primária
**/

public function updateRow(array $data, $pkey)
{
$where = $this->_generateRestrictionsFromPrimaryKeys($pkey);
return $this->update($data, $where);
}

/**
* Delete de um registro único, baseado na chave primária
* @param string|array $pkey Valor da chave primária
**/

public function deleteRow($pkey)
{
$where = $this->_generateRestrictionsFromPrimaryKeys($pkey);
return $this->delete($where);
}
}

Veja que o método updateRow() é uma variação do método update(), com a diferença de que este recebe apenas o valor da chave primária, assim como o método deleteRow(). Esse valor pode ser um array, caso a tabela tenha chave primária concatenada.

O código agora resume-se a:

// para atualizar um usuário
$tbUsuarios->updateRow($dados, $id);
// para excluí-lo
$tbUsuarios->deleteRow($id);

Nada mais de ficar montando os "wheres" toda vez que for atualizar ou excluir um registro. Agora é só jogar o valor da chave primária lá e pronto.

Melhorando o Zend_Db_Table - Parte 1: Evitando chaves que não correspondem à colunas

Faz muito tempo que não posto nada no blog, então resolvi começar a compartilhar algumas soluções que venho desenvolvendo para Zend Framework, que pode ser útil pra muita gente que o utiliza.

Uma das grandes críticas que vejo sobre o ZF é o uso do Zend_Db_Table, por diversos motivos, então, para começar vou postar algumas soluções que venho desenvolvendo para facilitar meu trabalho no uso dessa abstração de banco de dados.

Para começar, uma das coisas que me aborrecia sempre que ia dar um insert ou update na tabela, era quando eu passava os dados do formulário para um desses métodos, e o ZF me jogava uma excessão dizendo que não existe uma coluna de nome tal, que foi passada no array de dados. Pra deixar mais claro, vamos pegar o seguinte cenário:

Temos uma tabela "Usuarios", com os campos "id", "nome" e "email", por exemplo. E uma classe modelo Usuarios que herda de Zend_Db_Table_Abstract. Ao inserirmos os dados de um usuário, o formulário está mandando também o nome do botão "Salvar".

$tbUsuarios = new Usuarios();
$dados = $this->_request->getPost();
/*
Digamos que esse seja o valor de $dados:
$dados = array(
'nome' => 'Fulano de Tal',
'email' => 'fulano@email.com',
'salvar' => 'Salvar'
)
*/

unset($dados['salvar']);
$tbUsuarios->insert($dados);

Veja que foi necessário um unset() para remover a chave que não corresponde à colunas da tabela. Óbvio que esse é um exemplo simplificado, e pode parecer uma bobagem, mas imagine um cenário maior e mais complicado onde cada chave não correspondente à uma coluna teria que ser excluída do array a fim de evitar a excessão lançada pelo Zend_Db_Table. E, acredite, isso pode se tornar um pé-no-saco, principalmente quando você tem que fazer alterações no formulário e acaba colocando mais uma chave que não corresponde à uma coluna, daí vem mais um unset().

Então, entendido o problema, vejamos a forma que encontrei para que não seja mais necessário nos preocuparmos com dados não correspondentes à colunas:
require_once 'Zend/Db/Table/Abstract.php';

class My_Db_Table_Abstract extends Zend_Db_Table_Abstract
{
/**
* Filtra os itens do array de dados, verificando se as
* chaves correspondem a campos da tabela
*
* @param array $data Array de dados
* @return array Array de dados filtrados
**/

protected function _filterData(array $data)
{
$cols = $this->info('cols');
$filteredData = array();
if($data) {
foreach($data as $key=>$value) {
if (in_array($key, $cols)) {
$filteredData[$key] = $value;
}
}
}
return $filteredData;
}

public function insert(array $data)
{
$data = $this->_filterData($data);
return parent::insert($data);
}

public function update(array $data, $where)
{
$data = $this->_filterData($data);
return parent::update($data, $where);
}
}

Agora é só fazer com que a classe Usuarios herde de My_Db_Table_Abstract e pronto.
Veja que, desta forma, as chaves do array não correspondentes à colunas da tabela serão automaticamente filtrados, e não será mais exibida excessão relativa a esse caso específico.

sábado, 13 de junho de 2009

Qual o navegador mais rápido?

O Peacekeeper é uma ferramenta online para verificação e comparação de desempenho de browsers. Funciona de forma bastante simples, você entra no site e clica em 'Benchmark Your Browser'. Se não tiver java instalado, ele vai pedir para instalar. Depois é só mandar rodar e esperar uns 5 minutos, no máximo. Ele vai medir o desempenho do browser que você está usando e irá fornecer um link para você usar com outros browsers e fazer o comparativo.

Eu fiz a experiência, e eis o resultado:



O Safari 4.0 e o Chrome estão de parabéns em desempenho. Isso não fez com que eu trocasse o firefox por eles, já que nenhum oferece todos os recursos (complementos) que eu gosto de usar no firefox, mas a diferença é impressionante.

Fiz o teste antes de atualizar o firefox para a nova versão (3.11) e fiz novamente depois da atualização, pra minha surpresa o desempenho caiu bastante...

O Opera que usei era uma versão que tinha instalado no meu pen drive U3. Não sei se isso influenciou muito no resultado do desempenho, mas me desapontou bastante... mais lento que o IE8!? Não dá, né...

Ainda assim, para mim o firefox permanece como a melhor opção, pois facilita muito meu trabalho graças aos complementos úteis que tenho instalados: Firebug, FirePHP, HTML Validator, Web Developper, Fireshot, IE Tab, Yslow, LoremIpsum Content Generator entre outros... Isso sem falar dos outros que não estão relacionados a meu trabalho, mas que são bastante úteis: Xmarks, TwitterFox, Gmail Manager, Better Gmail 2, Smart Bookmarks Bar, Cooliris, entre outros...