Saving category from collection resets include_in_menu
-
29-09-2020 - |
Frage
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?
Lösung
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:
- The object does not have an entry in its internal
$data
array. - 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 incorrectcreated_at
dates.available_sort_by
(if it is part of the collection and its value is notNULL
): the comma-separated value is not replaced by an array. On save, the value is reset toNULL
.
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:
- Call
$object->setData($attributeCode, $attributeValue)
where$attributeValue
is loaded from the database. - 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.
Andere Tipps
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.