Estendendo Zend_Form_Element

Desenvolvimento e Consultoria  |    06/02/2010   |   6708 hits   |   J. Ricardo Simões Rodrigues

Em nossas tarefas de desenvolvimento, rotineiramente necessitamos fazer com que nossos elementos de formuláros sejam data aware. O caso mais comum é o dos selects que necessitam serem carregados com opções vindas de um banco de dados. Não raro esses valores sãoprovenientes de uma tabela que possui uma restrição do tipo chave estrangeira. Para os que estão acostumados com a utilização da biblioteca PEAR (PHP Extension and Application Repository), o componente HTML_Select tem um método loadDbResult() que carrega selects com dados vindos de um banco de dados.

Ultimamente (janeiro de 2010) iniciei o estudo do Zend Framework. Uma das primeiras coisas das quais senti necessidade foi justamente essa: popular selects a partir de um banco de dados, preferencialmente respeitando relacionamentos entre tabelas. Essa é minha primeira experiência buscado resolver tal problema meu.

O Banco de dados


O esquema utilizado no exemplo utilizará duas tabelas relacionadas por uma chave. No caso uma tabela denominada municipios que se relaciona a uma segunda entidade unidades_federadas mediante uma chave estrangeira.
O esquema, para bancos da dados PostgreSQL, é o seguinte:


/*
* Tabela unidades_federadas
*/

CREATE TABLE unidades_federadas(
uf character(2) NOT NULL,
nome character varying(30),
CONSTRAINT pk_unidades_federadas PRIMARY KEY (uf),
CONSTRAINT uf_unidades_federadas_key UNIQUE (uf)
);

/*
* Tabela municipios
*/
CREATE TABLE municipios(
id serial NOT NULL,
nome character varying(60),
uf character(2),
CONSTRAINT pk_municipios PRIMARY KEY (id),
CONSTRAINT municipios_unidades_federadas_fkey FOREIGN KEY (uf)
REFERENCES unidades_federadas (uf) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE CASCADE
);



Prosseguindo, podemos popular ambas tabelas com informações.


/*
* Inserindo dados na tabela unidades_federadas
*/
INSERT INTO unidades_federadas(uf, nome) VALUES ('RO', 'Rondônia');
INSERT INTO unidades_federadas(uf, nome) VALUES ('AM', 'Amazonas');
INSERT INTO unidades_federadas(uf, nome) VALUES ('MT', 'Mato Grosso');

/*
* Inserindo dados na tabela municipios
*/
INSERT INTO municipios(nome, uf) VALUES ('Rolim de Moura', 'RO');
INSERT INTO municipios(nome, uf) VALUES ('Porto Velho', 'RO');
INSERT INTO municipios(nome, uf) VALUES ('Manaus', 'AM');
INSERT INTO municipios(nome, uf) VALUES ('Cuiabá', 'MT');

É sempre bom ter à mão uma visualização que retorne os dados de tabelas relacionadas:


/*
* View municipios_e_ufs
*/
CREATE VIEW municipios_e_ufs AS
SELECT m.id, m.nome as nome_do_municipio, u.nome as nome_da_uf, u.uf as sigla_da_uf
FROM municipios m, unidades_federadas u
WHERE m.uf = u.uf;

Classes

Criaremos uma classe Default_Model_UnidadesFederadas estendendo Zend_Db_Table_Abstract.

Note a utilização do prefixo “Default_”. Em nosso application/Bootstrap.php criamos um autoloader e registramos o namespace:

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

return $autoloader;
}

Já a classe Default_Model_UnidadesFederadas terá a seguinte definição:

<?php
/**
* /application/models/UnidadesFederadas.php
*/
class Default_Model_UnidadesFederadas extends Zend_Db_Table_Abstract
{
/**
* $_name – nome da tabela
* @var string
*/
protected $_name='unidades_federadas';
/**
* $_id – chave primária
* @var string
*/
protected $_id='uf';
/**
* $_sequence – chave natural
* @var boolean
*/
protected $_sequence = false;
}

Agora o código da classe Default_Model_UnidadesFederadas:


<?php
/**
* /application/models/Municipios.php
*/
class Default_Model_Municipios extends Zend_Db_Table_Abstract
{
/**
* $_name – nome da tabela
* @var string
*/
protected $_name='municipios';
/**
* $_id – chave primária
* @var string
*/
protected $_id='uf';
/**
* $_referenceMap – Informações sobre as chaves estrangeiras
* @var array
*/
protected $_referenceMap = array(
'MunicipiosUnidadesFederadasFkey' => array(
'columns' => 'uf',
'refTableClass' => 'UnidadesFederadas',
'refColumns' => 'uf'
)
);
}



Na segunda classe é informada a dependência da tabela, ou seja, a chave estrangeira que referencia a tabela unidades_federadas.
Com essas classes criadas já podemos, em um controller da aplicação, consultar nossos dados:


$model = new Default_Model_Municipios();
$this->view->municipios = $model->fetchAll();

Form

De modo geral, sempre criamos formulários estendendo Zend_Form ou ZendX_JQuery_Form.
Uma possível classe para manipulação de registros da tabela municipios seria a seguinte:


<?php
/**
* Default Municipios Form Model
*/
class Default_Form_Municipios extends Zend_Form
{
function init() {
$this->setMethod('post');
$this->setAttribs(array('name'=>'Municipios'));
/**
* id
*/
$this->addElement('text', 'id', array('label'=>'Id:'));
/**
* nome
*/
$this->addElement('text', 'nome', array('label'=>'Nome:'));
/**
* uf
*/
$this->addElement('text', 'uf', array('label'=>'Uf:'));
/**
* Form Submit
*/
$this->addElement('submit', 'submit', array(
'ignore' => true,
'label' => 'Salvar',
));
}
}


Nosso formulário poderá ter o seguinte aspecto:

Pronto. É só termos o cuidado de remover o elemento id (chave primária gerada pelo banco de dados) e já poderemos invocar o método insert:

$model = new Default_Model_Municipios();
$model->insert($form->getValues());


O problema é que temos que saber quais valores entrar no campo Uf, vez que, dada a restrição imposta na criação da tabela, essa coluna somente aceitará valores presentes na tabela referenciada, ora unidades_federadas.

Criando um elemento personalizado

A maneira mais fácil de criar elementos personalizados e reutilizáveis em formulários é estendendo a classe Zend_Form_Element. No caso a ideia é criar um controle do tipo select (drop down) que, a partir de um formulário Zend_Form, seja populado com os valores da tabela unidades_federadas.
Veja no código comentado da classe maiores detalhes:


<?php
/*
* /library/My/Form/Element/DbSelect.php
*/
class My_Form_Element_DbSelect extends Zend_Form_Element_Select
{
/*
* Model da entidade referenciada
*/
private $_model;
/*
* Coluna referenciada
*/
private $_identityColumn = 'id';
/*
* Coluna a ser exibida
*/
private $_valueColumn = '';

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

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

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

public function render(Zend_View_Interface $view = null)
{
$this->_performSelect();
return parent::render($view);
}

/*
* A partir das linhas retornadas, monta o select
*/
private function _performSelect()
{
$model = new $this->_model;
$results = $model->fetchAll()->toArray();
$options = array();
$options[0] = '----- Selecione -----';
foreach($results as $r) {
if(!isset($r[$this->_identityColumn])) {
throw new Zend_Form_Element_Exception(
'Coluna (' . $this->_identityColumn . ') não presente no resultado.');
}

if(!isset($r[$this->_valueColumn])) {
throw new Zend_Form_Element_Exception(
'Coluna (' . $this->_valueColumn . ') não presente no resultado.');
}

$options[$r[$this->_identityColumn]] = $r[$this->_valueColumn];
}

$this->setMultiOptions($options);
}

}


Agora podemos atualizar nosso formulário Default_Form_Municipios substituindo o campo uf do tipo texto pelo nosso recentemente criado:


//$this->addElement('text', 'uf', array('label'=>'Uf:'));

$el = new My_Form_Element_DbSelect(array(
'name' => 'uf',
'model' => 'Default_Model_UnidadesFederadas',
'identityColumn' => 'uf',
'valueColumn' => 'nome',
'label' => 'Uf:'
));
$el->setRegisterInArrayValidator(false);
$this->addElement($el);


Note que passamos o nome da classe (Default_Model_UnidadesFederadas) referenciada para o construtor do elemento My_Form_Element_DbSelect, bem como as colunas a serem consultadas e utilizadas na montagem do select.
Lembramos que, para que sua biblioteca possa ser carregada automaticamente (sem includes ou requires), seu prefixo deverá ser registrado. Comumente isso é feito no application/Bootstrap.php ou no public/index.php (prefiro o segundo):


$autoloader = Zend_Loader_Autoloader::getInstance();
$autoloader->registerNamespace('My_');

Agora, ao instanciarmos nosso formulário, teremos os dados vindos de unidades_federadas populando nosso select:

Em uma próxima oportunidade, contarei minha experiência utilizando Helper Views do Zend Framework.

Última edição concluída em 06/02/2010 por J. Ricardo Simões Rodrigues.