4

Sometimes it is convenient to save a category directly from the collection it was loaded. For instance, for all categories of which the name attribute starts with foo, I want to update the meta_title attribute with the name followed by bar as follows:

/** @var Mage_Catalog_Model_Resource_Category_Collection $categoryCollection */
$categoryCollection = Mage::getResourceModel('catalog/category_collection');
$categoryCollection->setDisableFlat(true);
$categoryCollection->addAttributeToFilter('name', ['like' => 'foo%']);
foreach ($categoryCollection as $category) {
    /** @var Mage_Catalog_Model_Category $category */
    $category->setData('meta_title', $category->getData('name') . ' bar');
    $category->save();
}

This does work, but as side-effect: the include_in_menu attribute is reset to 1.

What is the cause of this and how can this be circumvented?

4

It turns out that when saving an EAV entity, the beforeSave and afterSave methods of the backend models of all attributes are executed, even when they are not part of the collection: the method Mage_Eav_Model_Entity_Abstract::save calls the methods _beforeSave and _afterSave, which call the method walkAttributes with backend/beforeSave or backend/afterSave as the first arguments.

The method Mage_Eav_Model_Entity_Attribute_Backend_Abstract::beforeSave, which is used by most attributes, sets the default value of the attribute on the supplied object in the following circumstances:

  1. The object does not have an entry in its internal $data array.
  2. The default value of the attribute is non-empty.

The first condition applies to most attributes when loading via a collection. The second condition only applies to the include_in_menu attribute (in a standard Magento setup).

To work around this issue, simply add the include_in_menu attribute to the collection query:

$categoryCollection->addAttributeToSelect('include_in_menu');

Another issue that the afterLoad methods of the attribute backend models are not called (thanks to Marius for the warning). On a default Magento installation, this causes issues with the following attributes:

  • created_at: the value is not converted from UTC to the current store time zone on load. On save, the value is converted back to UTC (which it already was), leading to incorrect created_at dates.
  • available_sort_by (if it is part of the collection and its value is not NULL): the comma-separated value is not replaced by an array. On save, the value is reset to NULL.

To resolve this, you can trigger the afterLoad methods by adding the following to the loop body:

$category->getResource()->loadAllAttributes($category)->walkAttributes('backend/afterLoad', array($category));

BTW This does not seem to harm performance that much: all category attributes are also loaded when saving the category, and Magento caches the attribute instances in the Mage_Eav_Model_Config singleton. So it seems that when loading and saving multiple categories, each attribute is only loaded once.

Similar to the issue that the beforeLoad method is not called for any attribute, the method Mage_Eav_Model_Entity_Abstract::_loadModelAttributes is not called. This method does two things for every attribute:

  1. Call $object->setData($attributeCode, $attributeValue) where $attributeValue is loaded from the database.
  2. Call $attribute->getBackend()->setEntityValueId($object, $valueId) where $valueId is the primary key of the attribute value in the corresponding attribute table.

When loading EAV collections, the similarly named method Mage_Eav_Model_Entity_Collection_Abstract::_loadAttributes is called, which does the former but not the latter. As a result, the corresponding getEntityValueId method always returns an empty value. This only causes issues when deleting attribute values from the backend table. For categories (and also products) this only happens when setting an the value of an attribute with global scope to false.

To avoid the issues with not being able to delete values of attributes with global scope, do not call $object->setData($attributeCode, false) for such an attribute. When it is necessary to clear an attribute, you can use null instead of false, which results in an UPDATE query on the attribute backend table instead of a DELETE query.

1

You should never call save if you did not call load before on the same object.
Even if you add the attributes to the collection, the beforeLoad and afterLoad methods are not called and they could have an impact on the object state.

Adding

$categoryCollection->addAttributeToSelect('include_in_menu');

may solve your problem for this particular case, but it may damage something else.

  • Thanks for the answer! Although I know that it is better to call load on an object before save, the problem in general is that when I need to update many categories at once, performance becomes a real issue. I have checked the methods you mentioned: beforeLoad does not exist, but afterLoad causes issues with the attributes created_at (the value is converted from current store time zone to UTC) and available_sort_by (replace comma-separated value by an array). This can be resolved by the calling afterLoad on the backend models of these attributes. I will include this as a warning to my answer. – Aad Mathijssen May 25 '16 at 16:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.