Curioso depois da palestra Smarty 3 – O Céu é o Limite! na PHP Conf 2009, resolvi dar uma checada na versão desse sistema de templates que tanto gosto e costumo usar em meus projetos. Primeiro, vou falar um pouco sobre o que foi falado na palestra e que está no arquivo de apresentação, que foi disponibilizado aqui pelo autor. Depois, algumas observações e opiniões de algumas pesquisadas pela internet a fora e fuçadas.

Obs.: esse post foi feito para falar do Smarty 3 beta 5, ou seja, algumas mudanças e melhorias podem acontecer conforme o projeto ficar mais maduro.

Novidades

A maioria dessas novidades estão no slide, mas vou falar um pouco de cada:

Melhoria no sistema de plugins: só olhando para o esquema de diretórios do smarty, e podemos ver que está diferente: agora, tem uma pasta para plugins comuns, como já havia antes, e plugins do sistema. Nos plugins do sistema, podemos encontrar todo o código interno do smarty, incluindo métodos chamados no Smarty.class.php e funções internas do template como {foreach}.

Novo sistema de Cache: agora podemos especificar setar o caching como true e dizer que algumas partes não queremos que o cache seja usado, com a tag {nocache}. Chegaram até a perguntar durante a palestra se dava pra fazer o contrário, e setar apenas onde queremos que tenha cache, mas infelizmente não é possivel.

Templates como código PHP embutido: tem alguns templates em PHP puro e não quer reescrever para a linguagem smarty? Sem problemas, agora isso é possível, sem fazer nenhuma alteração se quer no template:

$smarty->display('php:template.php');

Por conta disso, a tag {php} agora foi desabilitada por padrão. Para compatibilidade com Smarty 2, ela pode ser habilitada com $smarty->allow_php_tag=true.

Total compatibilidade com Javascript, CSS, Jquery, etc.: atire a primeira pedra quem nunca ficou nervoso porque o parser do smarty dava erro quando encontrava as chaves de CSS e Javascript e precisava do {literal}{/literal} ou então mudar os delimitadores … no Smarty 3 não precisamos mais disso: agora ele só interpreta como tag própria se não tiver espaço depois e se tiver, é ignorado;

Código mais próximo da linguagem PHP: tags como {foreach} foram alteradas e usam a mesma sintaxe do PHP. Foram adicionados controles mais próximos do PHP também, como {else if}, {while} e {for}. Outro exemplo é que o {assign} não é mais necessário, já que podemos usar uma atribuição igual a que usamos no PHP. Por exemplo:

{$mensagem = "Data atual"}
{$data = $smarty.now|date_format:"%d/%m/%Y %H:%M:%S"}

{$mensagem = "$mensagem: $data"}

Inclusão de diretórios de templates: podemos agora definir diversas pastas para templates com as funções setTemplateDir() e addTemplateDir(). A primeira recebe um array de diretórios, enquanto a segunda recebe um valor único que pode ser tanto string quanto array e que é adicionado ao array de templates. Na hora de encontrar algum arquivo, ele é buscado do primeiro diretório encontrado ao último.

Funções dentro de templates: é possível agora definirmos funções em templates, para reuso de código. Um exemplo:

{function name=menu level=0}
  <ul class="level{$level}">
  {foreach $data as $entry}
    {if is_array($entry)}
      <li>{$entry@key}</li>
       {menu data=$entry level=$level+1}
    {else}
      <li>{$entry}</li>
    {/if}
  {/foreach}
  </ul>
{/function}

{$menu = ['item1','item2','item3' => ['item3-1','item3-2','item3-3' =>
  ['item3-3-1','item3-3-2']],'item4']}

{menu data=$menu}

Ou seja: {function} tem apenas o atributo name como obrigatório, e podemos ter vários adicionais; para chamar uma função, devemos usar uma tag com o mesmo nome da função (no exemplo, foi menu), e por isso deve ser usado apenas nomes não usados em tags do próprio smarty.

Herança de Templates: bem, esse não estava no slide, mas achei que é uma novidade interessante, que outros sistemas de templates já tinham. A herança de templates consiste em ter um template com blocos que pode ser alterado por outros templates filhos, assim como quando estendemos classes com o PHP normal. No Smarty 2 isso não era possível, mas essa necessidade era resolvida com o {include}.

O Smarty possibilita fazer isso de 2 maneiras. A primeira:

base.tpl:

<html>
<head>
  <title>{block name="title"}default{/block}</title>
  {block name="head"}{/block}
</head>
<body>
  {block name="body"}{/block}
</body>
</html>

page.tpl:

{extends file="base.tpl"}
{block name="title"} my title {/block}
{block name="head"} <!-- insert js/css here --> {/block}
{block name="body"} <!-- insert content here --> {/block}

content.tpl:

{extends file="page.tpl"}
{block name="body"} <h1>Some Content</h1> {/block}

Chamando o content.tpl no display() do smarty, temos como resultado:

<html>
<head>
  <title> my title </title>
   <!-- insert js/css here -->
</head>
<body>
   <h1>Some Content</h1>
</body>
</html>

Esclarecendo, temos 2 novas tags envolvidas: {extends} e {block}. O {extends} serve que nem o extends para classes php, você indica ao filho qual o template que ele herdará. Já o {block} é um pedaço de código no qual o conteúdo do template que está sendo gerado é sobreposto sempre que um filho tiver um conteúdo diferente. Analisando o resultado final isso fica evidente: sempre que o filho teve um {block} com o mesmo nome, o conteúdo foi trocado. Além disso, pudemos ver no content.tpl que não é necessário que todos os blocos sejam alterados.

Porém, como eu falei anteriormente, podemos fazer de outro jeito:

$smarty->display('extends:base.tpl|page.tpl|content.tpl');

Nesse caso, não precisaríamos usar o {extends} nos tpls, definindo dinâmicamente a herança dentro do próprio PHP.

Primeiras Impressões

Gostei de algumas facilidades que a nova versão proporciona, como não precisar mais do {literal}, a atualização do próprio sistema de templates, e a compatibilidade com a versão 2.

Mas nem tudo também me agradou. A grande expectativa minha para essa versão era justamente a melhora de performance. Ok, eu não criei benchmarks para poder provar com números, mas fiquei até preocupado quando li esse post do fórum do Smarty e alguns poucos comentários pela web a fora de que o Smarty 3 é mais lento ou pelo menos igual ao Smarty 2 nesse sentido. A esperança é que, como ainda é uma versão beta, isso ainda possa ser melhorado.

Outra coisa que eu esperava do smarty era que ele passasse a ficar mais parecido com uma linguagem de marcação, como me parece ser a tendência, ao invés de se aproximar cada vez mais do que já temos no PHP. Quero dizer, será que é realmente vantajoso tornar a compilação mais lenta com a interpretação do PHP no Smarty para continuar escrevendo códigos imperativos dentro da parte de visualização?

E por fim, temos a falta de documentação da nova versão no site. A única coisa que achei deles foi um txt que descreve algumas funcionalidades da versão beta.

Por enquanto é isso, está bom, mas acho que poderia ser melhor. Quando o Smarty 3 for lançado oficialmente e tiver uma documentação, espero ter algumas surpresas quanto as melhorias da nova versão. E você, o que achou do Smarty 3?

Posted in php at dezembro 18th, 2009. 3 Comments.

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.

No PHP, usar var_dump(), print_r() e afins no meio do código não só é uma maneira pouco eficiente como pouco elegante de encontrar e reduzir erros no código. Depurar é uma boa maneira de fazer isso e temos uma ótima extensão do PHP chamada xDebug para nos ajudar.

Instalação

No Windows, a maneira mais simples de instalar o xdebug é baixar a extensão como dll no site oficial da extensão e configurar o php.ini para que o PHP possa reconhecer a extensão:

zend_extension_ts="C:/caminho/para/o/php/ext/php_xdebug.dll"

Para as versões até 5.2.x deve ser usado zend_extension_ts, enquanto que para a 5.3, zend_extension.

Além disso, podemos setar algumas configurações da própria extensão no arquivo. Algumas específicas, para permitir saber mais sobre o que acontece na execução:

xdebug.collect_includes = On
xdebug.collect_params = On
xdebug.collect_return = On
xdebug.collect_vars = On
xdebug.dump_globals = On
xdebug.show_local_vars = On
xdebug.dump.GET=*
xdebug.dump.POST=*
xdebug.dump.COOKIE=*
xdebug.dump.ENV=*
xdebug.dump.FILES=*
xdebug.dump.REQUEST=*
xdebug.dump.SERVER=*
xdebug.dump.SESSION=*

Dando um restart no Apache, o xdebug deve estar listado no phpinfo() com todas as configurações setadas.

Usos do xdebug

Podemos conseguir diversos tipos de informações sobre o que acontece em nossa aplicação. O mais comum é o xdebug mostrar no html quais erros acontecem. Faça o teste:

<?php

$teste = array("um", "teste", "qualquer");

include("umarquivonaoexistente.php");

var_dump($var);

?>

Rodando a página, ele nos diz que não encontrou o arquivo não existe e nos mostra os valores das variáveis até dar que o erro ocorresse. Além disso, podemos observar que o var_dump() foi sobrescrito pelo xdebug, tornando mais fácil a leitura dos valores.

Trace

Podemos também coletar informações sobre a execução do arquivo, guardando em um arquivo separado. O Trace pode ser feito tanto pelo arquivo de configuração:

xdebug.auto_trace=1
xdebug.trace_output_dir=/caminho

quanto individualmente, no arquivo:

<?php

xdebug_start_trace("/caminho/para/o/trace");

$soma = soma(1,2);

xdebug_stop_trace();

?>

Xdebug Remoting

Podemos também configurar o xdebug para ser usado com IDEs, permitindo assim acompanhar os valores de variáveis, colcoar pontos de parada como é normalmente feito no debug com outras linguagens.

Para permitir que isso aconteça, tem que adicionar nas configurações:

xdebug.remote_enable=1
xdebug.remote_host=127.0.0.1
xdebug.remote_port=9000
xdebug.remote_handler=dbgp

Diversas IDEs e editores já funcionam com o xdebug: Netbeans, Eclipse, Notepad++, o plugin para o VIM, entre outros.

Posted in php at dezembro 5th, 2009. No Comments.