Türchen 22: Magento EAV Attribute Setup

This post on EAV (Entity-Attribute-Value) in Magento intends to help developers understand the mechanics of EAV attribute modification, starting first with a quick description of EAV and then an explanation of the two main arbiters of attribute modification: addAttribute() and updateAttribute().

An obvious first question is, "why use EAV in the first place?" Simply put, the EAV storage pattern is a strategy for handling entities with arbitrarily variable sets or properties - something which is common in eCommerce. EAV accomplishes this by decoupling entity attributes from the entity records themselves and storing their configuration metadata and entity relation separately. Developers wishing to avail themselves of the flexibility of EAV in Magento must become experts in the programmatic assignment of attributes and update of their configuration.

Magento-EAV-Attribute-Setup-Fig-00

In the above table, each row is the sole entity record, and each column represents an entity property. This is a typical entity data storage strategy (especially in web apps), and it is often implicitly coupled with the representation of the entity in the application. As implemented in the Magento 1 framework, the EAV storage pattern offers several functional advantages over the flat storage approach. To understand these advantages, compare the two entities above as they might be represented in a simple EAV storage scheme:

Magento-EAV-Attribute-Setup-Fig-01-650x400

The schema above demonstrates the decoupled storage which is possible with EAV, though it should be noted that it is not a complete diagram of Magento's EAV schema. Adding an attribute row (registering an attribute) in an EAV scheme is analogous to adding an extra column onto an entity table in a flat scheme.. While it can be easy to get lost in the enormity of Magento's EAV implementation (for example, the product entity in Magento 1 involves 18+ tables), there is an important benefit to consider beyond the decoupling of entity properties from data coordinates, and that benefit is attribute metadata. The columns in the eav_attribute table contain these metadata, and they are used for various purposes, such as determining the proper value storage table, providing value transformations for storage, etc.

This introduction brings us to the means for effecting attribute configuration. The EAV setup model - Mage_Eav_Model_Entity_Setup is responsible for both the initial installation of entity types and attributes as well as ongoing modifications to these configurations. This model is involved in the initial installation and configuration of entities and attributes as well as modifications to these configurations which may be needed later on. The two most important and oft-used post-installation functions are addAttribute() and updateAttribute().

```addAttribute()```

addAttribute() requires three arguments:

  • $entityTypeId The ```entity_type_id``` or ```entity_type_code``` from ```eav_entity_type``` table.
  • $code The ```attribute_code``` value for the / from the ```eav_attribute``` table.
  • array $attr A configuration array for attribute configuration values across multiple EAV tables

As the method name suggests, addAttribute() can be used to add an attribute (and its configuration) to a given entity type; again, it's akin to adding an additional column to a table which stores data for a flat entity. Internally, the method performs four distinct functions:

  • Function #1: Set default attribute configuration values
  • Function #2: Determine if the specified attribute exists and if so, update instead
  • Function #3: Determine if the attribute should be assigned to all sets
  • Function #4: Add attribute options if necessary

So, this method is doing quite a lot, and as it turns out this method's behavior varies greatly depending on the values which are provided in the configuration array. Before expanding on this, it's essential to understand that the primary role of this method is to update columns in the eav_attibute table, and that these columns are the metadata which drive attribute behavior.

#1: Setting default attribute config values

Magento EAV setup uses _prepareValues() for two purposes. The main purpose is the setting of default configuration values via simple array_merge():

protected function _prepareValues($attr)
{
    $data = array(
        'backend_model'   => $this->_getValue($attr, 'backend'),
        'backend_type'    => $this->_getValue($attr, 'type', 'varchar'),
        'backend_table'   => $this->_getValue($attr, 'table'),
        'frontend_model'  => $this->_getValue($attr, 'frontend'),
        'frontend_input'  => $this->_getValue($attr, 'input', 'text'),
        'frontend_label'  => $this->_getValue($attr, 'label'),
        'frontend_class'  => $this->_getValue($attr, 'frontend_class'),
        'source_model'    => $this->_getValue($attr, 'source'),
        'is_required'     => $this->_getValue($attr, 'required', 1),
        'is_user_defined' => $this->_getValue($attr, 'user_defined', 0),
        'default_value'   => $this->_getValue($attr, 'default'),
        'is_unique'       => $this->_getValue($attr, 'unique', 0),
        'note'            => $this->_getValue($attr, 'note'),
        'is_global'       => $this->_getValue($attr, 'global', 1),
    );

    return $data;
}

The universal metadata values which are set via this method are as follows:

  • type = varchar (attribute will be stored in the entity *_varchar table
  • input = text (<input type="text" /> will be rendered for editing this value)
  • required = 1 (true)
  • user_defined = 0 (false)
  • unique = 0 (false)
  • global = 1 (true, although this scope is overridden in extended metadata tables)

So, this is the main purpose for the _prepareValues() method. The second "purpose" for _prepareValues() is the aliasing of metadata column names. For example, backend_model is aliased to backend, frontend_label becomes label, etc. While this functionality is deliberate, the aliasing of metadata column names seems unjustifiable and should be considered (at best) as framework esoterica. In other words, it's nuts - don’t try to figure it out!

There is a further consideration which developers should keep in mind when adding attributes to extant EAV entities. Certain modules (Mage_Catalog, Mage_Customer, and Enterprise_Rma actually extend the set of metadata for their attributes! For example, the Mage_Catalog entities catalog_product and catalog_category are configured in the eav_entity_type table with an additional_attribute_table value which points to the catalog_eav_attribute table, where these additional metadata columns are linked to the canonical eav_attribute records by foreign key. Dizzying, right? While this is tough to describe and comprehend in natural language, it is easy to visualize in a query browser as the difference between the results of the following two queries:

SELECT * FROM eav_attribute e WHERE e.entity_type_id
IN (
    SELECT entity_type_id
    FROM eav_entity_type
    WHERE entity_type_code
    IN('catalog_category','catalog_product')
)

versus

SELECT * FROM eav_attribute e, catalog_eav_attribute ce
WHERE e.attribute_id = ce.attribute_id

The latter of these will be much wider and represents the relationship of the extended attribute metadata table. Essentially, the eav_attribute table contains generic attribute metadata which applies to all EAV attributes, and each entity has the ability to add additional metadata as appropriate. The reason why this is important is that these additional metadata may be subject to a similar default value scenario as those in the main eav_attribute table. This is typically accomplished via a custom, module-specific EAV setup resource class which subclasses the Mage_Eav_Model_Entity_Setup and merges its _prepareValues() column values (and column aliases, damnit!) with those from the parent class; ref. Mage_Catalog_Model_Resource_Setup:

protected function _prepareValues($attr)
{
    $data = parent::_prepareValues($attr);
    $data = array_merge($data, array(
        'frontend_input_renderer'=> $this->_getValue($attr, 'input_renderer'),
    //snip…
    return $data;
}

It is therefore proper to use the appropriate module setup resource for the entity which is being modified. For example, when adding an attribute to the product or category entities, use an instance of Mage_Catalog_Model_Resource_Setup regardless of the module which is actually triggering the migration:

/* @var $installer Mage_Catalog_Model_Resource_Setup */
$installer = Mage::getResourceModel('catalog/setup','catalog_setup');

$installer->startSetup();

$installer->addAttribute(
    'catalog_product',
    'custom_attrib',
    array(/* ... */)
);

$installer->endSetup();

This will ensure that the proper default values additional metadata values will be set. While it might seem that failing to use the proper setup class for a given entity's module would result in the omission of additional attribute metadata, the _insertAttributeAdditionalData() method will in fact check the entity type configuration and ensure that any additional metadata are properly inserted to the additional attribute metadata tables.

#2: Determine if the specified attribute exists and if so, update instead

One question which should occur to developers is, "What if the entity already has an associated attribute with the same attribute_code as the one being added?” Well, rather than ignoring the operation or throwing an exception, addAttribute() checks for this condition and performs an update instead:

protected function _prepareValues($attr)
{
    //snip...
    $attributeId = $this->getAttribute($entityTypeId, $code, 'attribute_id');
    if ($attributeId) {
        $this->updateAttribute($entityTypeId, $attributeId, $data, null, $sortOrder);
    } else {
        $this->_insertAttribute($data);
    }
    //snip...
}

While it might seem appealing to just always use addAttribute() even when updating attributes, it should be that if an addAttribute() call results in an attribute update, any attribute metadata with default values which are not explicitly supplied to the method will be set to the default value as a result of _prepareValues()! This could result in unintended overwrites of these attribute configuration values. It is therefore recommended to use addAttribute() only when adding an attribute. Mage_Eav_Model_Entity_Setup::getAttribute() can be used to test for attribute existence.

#3: Determine if the attribute should be assigned to all sets:

addAttribute() checks two conditions to determine if an attribute should be added to all sets:

protected function _prepareValues($attr)
{
    //snip...
    if (!empty($attr['group']) || empty($attr['user_defined'])) {
        //miscellaneous logic to add to sets & groups
    }
    //snip...
}

The group field is simple: if group is specified, the attribute is placed "in" that attribute group in all attribute sets - ref. the eav_entity_attribute table. If the specified group does not exist in a given attribute set, the group is first created. This is the most functionally diverse use of addAttribute().

The user_defined field is less obvious. Whereas the admin GUI in Magento potentially provides admin users with the ability to manipulate attribute configuration, the EAV config uses the is_user_defined flag (user_defined in an addAttribute() call) to determine whether users can remove an attribute from attribute sets. This flag should be set to 0 (for false) when the presence of an attribute in sets is imperative.

#4: Add attribute options if necessary

The fourth and final function of addAttribute() is to install attribute options, generally useful (functionally required) for attributes which are have select or multiselect as the ```frontend_input value:``````

protected function _prepareValues($attr)
{
    //snip...
        if (isset($attr['option']) && is_array($attr['option'])) {
            $option = $attr['option'];
            $option['attribute_id'] = $this->getAttributeId($entityTypeId, $code);
            $this->addAttributeOption($option);
        }
    //snip...
}

I will leave it to you to figure out the array structure which should be used to provide the label-to-value association, sort order, and scoping for option values, or you can wait for the book I'm writing with a friend of mine.

```updateAttribute()```

updateAttribute() is much less varied in function compared to addAttribute(). It requires three arguments, is really useful with four, and a fifth is optional:

  • $entityTypeId The ```entity_type_id``` or ```entity_type_code``` from the ```eav_entity_type``` table
  • $id The ```attribute_id``` or ```attribute_code``` from the ```eav_attribute table```
  • $field The column name (or array of column names) from the ```eav_attribute``` table or "additional" attribute table
  • $value The value (or array of values) to be updated
  • $sortOrder An integer value which is used in the ```eav_entity_attribute.sort_order``` column, which may affect the placement of the attribute edit field in admin views

The essential footnote here is that when using updateAttribute() the attribute metadata column names are not passed through _prepareValues(), meaning that the aliasing does not apply! A simple code sample demonstrates the difference:

/* @var $installer Mage_Catalog_Model_Resource_Setup */
$installer = Mage::getResourceModel('catalog/setup','catalog_setup');

$installer->startSetup();

$installer->addAttribute(
    'catalog_product',
    'custom_attrib',
    array('label' => 'Initial Label') //note the column alias must be used!
);

$installer->endSetup();

versus

/* @var $installer Mage_Catalog_Model_Resource_Setup */
$installer = Mage::getResourceModel('catalog/setup','catalog_setup');

$installer->startSetup();

$installer->updateAttribute(
    'catalog_product',
    'custom_attrib',
    'frontend_label', //note the full column name must be used!
    'Initial Label'
);

$installer->endSetup();

Requiring the full column name is an unfortunate difference between updateAttribute() and addAttribute(). If the Magento core team wants to give a Christmas present to the developer community, they could remove the column aliasing altogether!



Ein Beitrag von Ben Marks
Ben's avatar

Ben Marks - I do lots of Magento.

Alle Beiträge von Ben

Kommentare
Torsten Brieskorn am

great.

Daniel Kratohvil am

Thank you Ben this helped me. I have noticed that some default installations of Magento do not have the "thumbnail" attribute on catalog_category that should be created by default. I need to be sure that the attribute exist on every installation (for a custom theme that uses that attribute) and I was wondering whether Magento would throw an error if I try to create that attribute on my install script and it already exists. Now i see the attribute will be updated if it exists. Thanks!

Adi am

What about removing an attribute and all its values from all tables in the database? This code: "$installer->removeAttribute('catalog_category', 'custom_attrib');" removes the attribute from the "eav_attribute" table but all values of this attribute are still stored in other tables (e.g. in "catalog_category_entity_text", assuming that this was a category attribute of type "text"). How to completely clean the database and remove attribute and all its values?

Ben Marks am

Heh, I read this to my wife (she had trouble sleeping).

The book will be our before Magento 2 :-D

Matthias Zeis am

Thanks Ben for the great article. I'll read it to my family under the Christmas tree. :P

When is your book due? Or was this just a joke?

Dein Kommentar