
Parte I
Parte II
Nas partes anteriores do nosso tutorial já vimos como manipulamos certos tipos de dados no BD.
Na verdade, vimos a parte simples, onde os dados estão persistidos em uma única tabela. Mas e o caso do EAV?
Bem, acredito que na maioria das vezes que você precisar criar uma tabela específica para um módulo, você irá optar pelo jeito mais simples, onde cada “objeto” reflete o registro de uma única tabela.
Mas, mesmo que você não for criar um conjunto de tabelas EAV para representar um objeto, você precisa saber o básico para conseguir lidar bem com os Produtos.
Como buscar produtos no BD que tenham apenas uma determinada cor? Busco da mesma forma que estivesse buscando um registro qualquer?
É isso que veremos nessa parte do tutorial.
Bem, mas o que seria o modelo EAV (Entidade – Atributo – Valor)?
Para quem “arranha” no inglês, você poder ver uma discussão sobre o assunto aqui.
Não vou me aprofundar muito, mas seria algo mais ou menos assim:
Quando você tem um “objeto” que pode ter uma quantidade muito grande de dados, é interesse você dividir essas informações em várias tabelas.
Vamos usar o exemplo do Produto, dentro do próprio Magento.
Você pode criar quantos atributos você quiser para um Produto e cada atributo pode ser de N tipos (string, inteiro, texto, decimal, data e etc). Qual a forma mais fácil de modelar esse objeto Produto no nosso BD?
Bem, seria interessente criar uma tabela que define o nosso produto (nossa entidade, vamos assim dizer), tabelas que definam os atrbutos que essa entidade irá ter, e por fim, tabelas com os valores desse atributo.
É mais ou menos assim que estão estruturadas as tabelas no nosso BD do Magento: Há uma tabela de definição do produto, outra que define quais atributos este produto tem, e tabelas próprias para cada tipo (texto, data, inteiro, etc) que irão guardar os valores do atributo.

Para entrarmos a fundo nessa questão da modelagem, vamos mudar um pouco o exemplo (módulo para meucomentario) que temos seguido até aqui. Acho que ficará mais fácil assimilar o “drama”.
Vamos utilizar um exemplo real, de um módulo que iremos disponibilizar logo logo aqui no blog: Forma de Pagamento – Financiamento.
Um cliente uma vez solicitou um módulo que permitisse que o cliente preenchesse uma ficha de financiamento, em vez de simplesmente pagar com boleto ou etc.
Ou seja, no momento do checkout o cliente escolheria o método “Financiamento”, preencheria um formulário que posteriormente seria avaliado.
A primeira pergunta que tive foi a seguinte: Mas quais campos esse formulário vai ter?
A resposta foi: “Depende”.
Na verdade o cliente poderia escolher entre Pessoa Física e Jurídica e de acordo com sua escolha, o formulário teria itens diferenciados.
Além disso, para que o módulo em questão fosse utilizado por mais de uma loja, seria interessante que esses campos fossem configuráveis.
Mas como fazer isso em relação banco de dados, já que cada campo poderia ser de um tipo diferente (data, texto, inteiro e etc)?
O jeito seria criar uma tabela para campos do tipo texto, outra para data e assim por diante.
Bem, o cadastro se parece muito com o cadastro de um produto certo? Pq então não utilizar o mesmo modelo de dados?
Assim foi definida a utilização do EAV!
Como esse tutorial não tem como objetivo a criação de novos módulos, só vou citar as configurações específicas para o modelo de dados utilizado.
Se achar interessante, modifique o exemplo anterior para salvar os comentários em um modelo EAV.
CONFIG.XML
<!-- ... -->
<global>
<models>
<financiamentooffline>
<class>Webgp_FinanciamentoOffline_Model</class>
<resourceModel>financiamentooffline_resource_eav_mysql4</resourceModel>
</financiamentooffline>
<financiamentooffline_resource_eav_mysql4>
<class>Webgp_FinanciamentoOffline_Model_Resource_Eav_Mysql4</class>
<entities>
<proposta>
<table>financiamentooffline_proposta</table>
</proposta>
<eav_attribute><table>financiamentooffline_eav_attribute</table></eav_attribute>
</entities>
</financiamentooffline_resource_eav_mysql4>
</models>
<resources>
<financiamentooffline_setup>
<setup>
<module>Webgp_FinanciamentoOffline</module>
<class>Webgp_FinanciamentoOffline_Model_Entity_Setup</class>
</setup>
<connection>
<use>core_setup</use>
</connection>
</financiamentooffline_setup>
<financiamentooffline_write>
<connection>
<use>core_write</use>
</connection>
</financiamentooffline_write>
<financiamentooffline_read>
<connection>
<use>core_read</use>
</connection>
</financiamentooffline_read>
</resources>
<!-- ... -->
<global>
<!-- ... -->
No nossa definição de modelo notamos que a diferença está no nosso resourceModel, que passa a utilizar a estrutura resource_eav_mysql4.
Na configuração do nosso novo resourceModel, temos que listar nossas entidades. Nesse caso, temos uma entidade chamada proposta (será chamada como Mage::getModel(‘financiamentooffline/proposta’)), que representa o nosso Model principal (seria como Produto, por exemplo) e também uma entidade chamada eav_attribute, que irá guardar as definições dos novos atributos.
Caso você queira utilizar alguma tabela FLAT para representar uma entidade, configure-a normalmente no config.xml. A diferenciação do seu real resource iremos fazer na hora de definir a classe.
Uma diferença também nessa configuração é a definição de uma classe específica para o nosso Setup (Webgp_FinanciamentoOffline_Model_Entity_Setup), que veremos adianta para que ela serve!
mysql4-install-0.1.0.php
<?php
$installer = $this;
$installer->startSetup();
$installer->run("
CREATE TABLE `{$installer->getTable('financiamentooffline/eav_attribute')}` (
`attribute_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`frontend_input_renderer` varchar(255) DEFAULT NULL,
`is_global` tinyint(1) unsigned NOT NULL DEFAULT '1',
`is_visible` tinyint(1) unsigned NOT NULL DEFAULT '1',
`position` int(11) NOT NULL,
PRIMARY KEY (`attribute_id`),
CONSTRAINT `FK_FINANCIAMENTOOFFILINE_EAV_ATTRIBUTE_ID` FOREIGN KEY (`attribute_id`) REFERENCES `{$installer->getTable('eav/attribute')}` (`attribute_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;");
$installer->endSetup();
$installer->addEntityType('financiamentooffline_proposta',
Array(
'entity_model' =>'financiamentooffline/proposta',
'attribute_model' =>'financiamentooffline/resource_eav_attribute',
'table' =>'financiamentooffline/proposta',
'increment_model' =>'eav/entity_increment_numeric',
'increment_per_store' =>'1'
));
<p>$installer->createEntityTables($this->getTable('financiamentooffline/proposta'));
$installer->installEntities();
$describe = $installer->getConnection()->describeTable($installer->getTable('financiamentooffline/eav_attribute'));
foreach ($describe as $columnData) {
if ($columnData['COLUMN_NAME'] == 'attribute_id') {
continue;
}
$installer->getConnection()->dropColumn($installer->getTable('eav/attribute'), $columnData['COLUMN_NAME']);
}
O nosso arquivo de setup cria a tabela que definimos em config.xml para a entidade ‘financiamentooffline/eav_attribute’.
Além disso, ele cria todas as tabelas para a modelagem EAV:
Antes de tudo precisamos criar um novo tipo de entidade. Como, por exemplo, iremos saber se um conjunto de atributos faz parte de Produtos ou Propostas?
Você pode ver os tipos já cadastrados na tabela eav_entity_type.
Depois que criamos o nosso novo tipo, precisamos criar as tabelas para os tipos de dados (date, varchar, int e etc). Para isso chamamos o método createEntityTables(..).
Quando instalamos o magento, vemos que os produtos já possuem alguns atributos pré cadastrados certo? Na nossa classe de Setup iremos definir os atributos que devem ser instalados inicialmente. Para cadastrá-los no banco precisamos chamar o método installEntities().
Pronto!
Quando nosso módulo for instalado, você verá que as seguintes tabelas foram criadas:

Setup.php
<?php
class Webgp_FinanciamentoOffline_Model_Entity_Setup extends Mage_Eav_Model_Entity_Setup {
public function getDefaultEntities()
{
return array (
'financiamentooffline_proposta' => array(
'entity_model' => 'financiamentooffline/proposta',
'attribute_model' => 'financiamentooffline/resource_eav_attribute',
'table' => 'financiamentooffline/proposta',
'increment_model' =>'eav/entity_increment_numeric',
'additional_attribute_table' => 'financiamentooffline/eav_attribute',
'entity_attribute_collection' => 'financiamentooffline/eav_attribute',
'attributes' => array(
'nome' => array(
'type' => 'varchar',
'backend' => '',
'frontend' => '',
'label' => 'Nome',
'input' => 'text',
'class' => '',
'source' => '',
// store scope == 0
// global scope == 1
// website scope == 2
'global' => 0,
'visible' => true,
'required' => true,
'user_defined' => true,
'default' => '',
'unique' => false,
),
),
)
);
}
Neste ponto do tutorial você já deve ser capaz de identificar a localização desse arquivo apenas olhando o nome da classe (Webgp/FinanciamentoOffline/Model/Entity/Setup.php).
Quando chamamos o método installEntities() no arquivo mysql4-install-0.1.0.php estamos mandando que as entidades definidas em getDefaultEntities() sejam instaladas.
No nosso exemplo, estamos definindo a entidade financiamento_proposta. O conteúdo do array é bem intuitivo. Apenas como exemplo, estamos cadastrando o atributo NOME.
Vamos mostrar agora as classes referentes ao nosso modelo Proposta e seus respectivos resources.
<?php
class Webgp_FinanciamentoOffline_Model_Resource_Eav_Mysql4_Proposta extends Mage_Eav_Model_Entity_Abstract
{
public function _construct()
{
$resource = Mage::getSingleton('core/resource');
$this->setType('financiamentooffline_proposta');
$this->setConnection(
$resource->getConnection('financiamentooffline_read'),
$resource->getConnection('financiamentooffline_write')
);
}
/**
* Default proposta attributes
*
* @return array
*/
protected function _getDefaultAttributes()
{
return array('entity_id', 'entity_type_id', 'attribute_set_id', 'order_id', 'is_active', 'created_at', 'updated_at');
}
<?php
class Webgp_FinanciamentoOffline_Model_Resource_Eav_Mysql4_Proposta_Attribute_Collection extends Mage_Eav_Model_Mysql4_Entity_Attribute_Collection
{
/**
* Resource model initialization
*/
public function _construct()
{
$this->_init('financiamentooffline/resource_eav_attribute', 'eav/entity_attribute');
}
protected function _initSelect()
{
$this->getSelect()->from(array('main_table' => $this->getResource()->getMainTable()))
->where('main_table.entity_type_id=?', Mage::getModel('eav/entity')->setType('financiamentooffline_proposta')->getTypeId())
->join(
array('additional_table' => $this->getTable('financiamentooffline/eav_attribute')),
'additional_table.attribute_id=main_table.attribute_id'
);
return $this;
}
/**
* Specify attribute entity type filter
*
* @param int $typeId
*/
public function setEntityTypeFilter($typeId)
{
return $this;
}
/**
* Return array of fields to load attribute values
*
* @return array
*/
protected function _getLoadDataFields()
{
$fields = parent::_getLoadDataFields();
$fields = array_merge($fields, array('additional_table.is_global'));
return $fields;
}
/**
* Specify filter by "is_visible" field
*
*/
public function addVisibleFilter()
{
$this->getSelect()->where('additional_table.is_visible=?', 1);
return $this;
}
}
<?php
class Webgp_FinanciamentoOffline_Model_Resource_Eav_Mysql4_Proposta_Collection extends Mage_Eav_Model_Entity_Collection_Abstract
{
protected function _construct()
{
$this->_init('financiamentooffline/proposta');
}
}
<?php
class Webgp_FinanciamentoOffline_Model_Resource_Eav_Attribute extends Mage_Eav_Model_Entity_Attribute
{
const SCOPE_STORE = 0;
const SCOPE_GLOBAL = 1;
const SCOPE_WEBSITE = 2;
const MODULE_NAME = 'Webgp_FinanciamentoOffline';
const ENTITY = 'financiamentooffline_eav_attribute';
protected $_eventPrefix = 'financiamentooffline_entity_attribute';
protected $_eventObject = 'attribute';
/**
* Array with labels
*
* @var array
*/
static protected $_labels = null;
protected function _construct()
{
$this->_init('financiamentooffline/attribute');
}
/**
* Processing object before save data
*
* @return Mage_Core_Model_Abstract
*/
protected function _beforeSave()
{
$this->setData('modulePrefix', self::MODULE_NAME);
if (isset($this->_origData['is_global'])) {
if (!isset($this->_data['is_global'])) {
Mage::throwException('0_o');
}
}
return parent::_beforeSave();
}
/**
* Processing object after save data
*
* @return Mage_Core_Model_Abstract
*/
protected function _afterSave()
{
/**
* Fix saving attribute in admin
*/
Mage::getSingleton('eav/config')->clear();
return parent::_afterSave();
}
/**
* Init indexing process after attribute data commit
*
*/
public function afterCommitCallback()
{
parent::afterCommitCallback();
return $this;
}
/**
* Register indexing event before delete financiamentooffline eav attribute
*
*/
protected function _beforeDelete()
{
return parent::_beforeDelete();
}
/**
* Init indexing process after catalog eav attribute delete commit
*
*/
protected function _afterDeleteCommit()
{
parent::_afterDeleteCommit();
}
/**
* Return is attribute global
*
* @return integer
*/
public function getIsGlobal()
{
return $this->_getData('is_global');
}
/**
* Retrieve attribute is global scope flag
*
* @return bool
*/
public function isScopeGlobal()
{
return $this->getIsGlobal() == self::SCOPE_GLOBAL;
}
/**
* Retrieve attribute is website scope website
*
* @return bool
*/
public function isScopeWebsite()
{
return $this->getIsGlobal() == self::SCOPE_WEBSITE;
}
/**
* Retrieve attribute is store scope flag
*
* @return bool
*/
public function isScopeStore()
{
return !$this->isScopeGlobal() && !$this->isScopeWebsite();
}
/**
* Retrieve store id
*
* @return int
*/
public function getStoreId()
{
if ($dataObject = $this->getDataObject()) {
return $dataObject->getStoreId();
}
return $this->getData('store_id');
}
/**
* Retrieve source model
*
* @return Mage_Eav_Model_Entity_Attribute_Source_Abstract
*/
public function getSourceModel()
{
$model = $this->getData('source_model');
if (empty($model)) {
if ($this->getBackendType() == 'int' && $this->getFrontendInput() == 'select') {
return 'eav/entity_attribute_source_table';
}
}
return $model;
}
/**
* Retrieve don't translated frontend label
*
* @return string
*/
public function getFrontendLabel()
{
return $this->_getData('frontend_label');
}
/**
* Get Attribute translated label for store
*
* @deprecated
* @return string
*/
protected function _getLabelForStore()
{
return $this->getFrontendLabel();
}
/**
* Initialize store Labels for attributes
*
* @deprecated
* @param int $storeId
*/
public static function initLabels($storeId = null)
{
if (is_null(self::$_labels)) {
if (is_null($storeId)) {
$storeId = Mage::app()->getStore()->getId();
}
$attributeLabels = array();
$attributes = Mage::getResourceSingleton('financiamentooffline/proposta')->getAttributesByCode();
foreach ($attributes as $attribute) {
if (strlen($attribute->getData('frontend_label')) > 0) {
$attributeLabels[] = $attribute->getData('frontend_label');
}
}
self::$_labels = Mage::app()->getTranslator()->getResource()->getTranslationArrayByStrings($attributeLabels, $storeId);
}
}
/**
* Get default attribute source model
*
* @return string
*/
public function _getDefaultSourceModel()
{
return 'eav/entity_attribute_source_table';
}
/**
* Check is an attribute used in EAV index
*
* @return bool
*/
public function isIndexable()
{
$backendType = $this->getBackendType();
$frontendInput = $this->getFrontendInput();
if ($backendType == 'int' && $frontendInput == 'select') {
return true;
} else if ($backendType == 'varchar' && $frontendInput == 'multiselect') {
return true;
} else if ($backendType == 'decimal') {
return true;
}
return false;
}
/**
* Retrieve index type for indexable attribute
*
* @return string|false
*/
public function getIndexType()
{
if (!$this->isIndexable()) {
return false;
}
if ($this->getBackendType() == 'decimal') {
return 'decimal';
}
return 'source';
}
}
<?php
class Webgp_FinanciamentoOffline_Model_Proposta extends Mage_Core_Model_Abstract {
protected function _construct()
{
$this->_init('financiamentooffline/proposta');
}
public function getAttributesBySetAndGroup($groupId = 0, $setId = 0)
{
return $this->getAttributesBySet($setId)->setAttributeGroupFilter($groupId);
}
public function getAttributesBySet($setId = 0)
{
return Mage::getResourceModel('financiamentooffline/proposta_attribute_collection')->setAttributeSetFilter($setId);
}
/**
*
* @param int $attributeId
* @return Mage_Eav_Model_Entity_Attribute_Abstract
*/
public function getAttributeById($attributeId, $setId = 0)
{
foreach ($this->getAttributesBySet($setId) as $attribute) {
if ($attribute->getId() == $attributeId) {
return $attribute;
}
}
return null;
}
Vamos lembrar que cada atributo pertence a um grupo e a um conjunto de atributos. Ao instalar as entidades, automaticamente o Magento já cria um Conjunto chamado Default e um grupo também chamado Default.
O Conjunto seria os conjuntos de atributos que você cria para os produtos.
Essa parte de EAV é muito extensa e o assunto parece não ter fim!
Espero que tenha dado para vocês entenderem! Só para fechar..
//instanciar uma proposta com id = 21
$proposta = Mage::getModel('financiamentooffline/proposta')->load(21);
//recuperar conjunto de atributos cadastrados para as propostas
$collection = Mage::getResourceModel('financiamentooffline/proposta_attribute_collection');
//salvar um atributo
$model = Mage::getModel('financiamentooffline/resource_eav_attribute');
$model->setEntityTypeId(Mage::getModel('eav/entity')->setType('financiamentooffline_proposta')->getTypeId());
...
$model->save();