In this post we will look into the functionality of Mage_Catalog_Model_Product_Type_Configurable, in what way it differs from that of the parent class Mage_Catalog_Model_Product_Type_Abstract and will see what unique features does Configurable type have. This is the second part of the discussion started in post Magento Configurable Product Type (Part 1).
We have already reviewed the role played by product type models and talked about functions implemented in the base product type class Mage_Catalog_Model_Product_Type_Abstract, and you can read about it in my post Product Type Logic Implementation in Magento. I am going to follow the structure laid out previously and describe the functionality provided by the configurable product type class in groups.
Saving configurable product information
The first group of functions is responsible for saving configurable products to database. Here Mage_Catalog_Model_Product_Type_Configurable overrides the following functions:
- beforeSave – this function extends its parent one to ensure that a configurable product when saved does not retain values for its configurable attributes. Only children products of a configurable can (and must) have values for attributes designated as configurable. The following code sets these to null before the configurable product data is written to database:
foreach ($this->getConfigurableAttributes($product) as $attribute) { $this->getProduct($product)->setData($attribute->getProductAttribute()->getAttributeCode(), null); }
Listing 1. /app/code/core/Mage/Catalog/Model/Product/Type/Configurable.php, line 228.
Consider an example of a configurable product, a shoe. The shoe can have multiple sizes, which means that its configurable attribute is shoe_size. Every child product has a value for attribute shoe_size: 5, 6, 7, or 8. The configurable shoe product, however, must not have a value for shoe_size. You won’t find an input for this attribute in the configurable shoe product’s edit page in the back-end. Even if a value is set programmatically, it will be stripped by the above code.
- save – this function is called from the product model’s method _afterSave, which happens after the model’s data is saved to database. The save method of the configurable type makes sure that the information on configurable attributes and associated products also get written to database. In section Mage_Catalog_Model_Product_Type_Configurable_Attribute Model we will talk more about how the assignment of configurable attributes to a product is stored.
Working with configurable attributes
With the help of these functions you can access the configurable attribute collection of a configurable product. And since not every attribute can be used as configurable, the following method provides necessary checks:
- canUseAttribute – this method decides if an attribute can be used to create a configurable product. It is called from a back-end form, which is displayed when you are creating a new configurable product. The requirements to a configurable attribute are:
- it must be defined with scope global,
- it must be visible (is_visible == 1),
- its property is_configurable must be set to yes,
- its front-end input type must be either “select”, “multiselect”, or its property source_model must not be empty,
- it must be “user defined”, i.e. not a system attribute.
- getConfigurableAttributeCollection and getConfigurableAttributes return a collection of configurable attributes for a product. The latter function also orders the attributes by their position parameter. This result is saved to a property of the product object to avoid repeated database queries. If you call the getConfigurableAttributesAsArray it will return these attributes in a form of a handy array complete with attribute codes, labels, and front-end names.
Relationships between configurable products and their children
The third group is comprised of methods that give you access to the associated products of a configurable:
- getUsedProducts – this function returns a collection of children products used by the current configurable. In essence, this is a result of a query joining the product entity and the parent-child relationships tables:
$collection = $this->getUsedProductCollection($product) ->addAttributeToSelect('*') ->addFilterByRequiredOptions();
Listing 2. /app/code/core/Mage/Catalog/Model/Product/Type/Configurable.php, line 337.
Or in SQL:
SELECT `e` . *, `link_table`.`parent_id` FROM `catalog_product_entity` AS `e` INNER JOIN `catalog_product_super_link` AS `link_table` ON link_table.product_id = e.entity_id WHERE (link_table.parent_id = 83) AND (((`e`.`required_options` != '1') OR (`e`.`required_options` IS NULL)))
Listing 3. Query returning child products of a configurable.
Note the required_options condition. Configurable (and bundle) products have this property set to 1. This condition ensures that no such product can be used as a configurable child.
The used product collection can be extended with more filters if the function parameter $requiredAttributeIds is an array of IDs of configurable attributes. In this case, the query may look like this:
SELECT `e` . *, `link_table`.`parent_id`, `at_gender`.`value` AS `gender`, `at_shoe_size`.`value` AS `shoe_size` FROM `catalog_product_entity` AS `e` INNER JOIN `catalog_product_super_link` AS `link_table` ON link_table.product_id = e.entity_id INNER JOIN `catalog_product_entity_int` AS `at_gender` ON (`at_gender`.`entity_id` = `e`.`entity_id`) AND (`at_gender`.`attribute_id` = '501') AND (`at_gender`.`store_id` = 0) INNER JOIN `catalog_product_entity_int` AS `at_shoe_size` ON (`at_shoe_size`.`entity_id` = `e`.`entity_id`) AND (`at_shoe_size`.`attribute_id` = '502') AND (`at_shoe_size`.`store_id` = 0) WHERE (link_table.parent_id = 83) AND (((`e`.`required_options` != '1') OR (`e`.`required_options` IS NULL))) AND (at_gender.value IS NOT NULL) AND (at_shoe_size.value IS NOT NULL)
Listing 4. Query returning child products of a configurable with attribute filters.
As you can see, the required attributes with IDs 501 and 502 have added two more joins to the query ensuring that the returned products do have values for configurable attributes “Gender” and “Size”.
If you run your debugger through the code of getUsedProducts you may wonder, why the following condition block returns the used product collection without even reaching the lines where the collection is initialized:
if (!$this->getProduct($product)->hasData($this->_usedProducts)) { if (is_null($requiredAttributeIds) and is_null($this->getProduct($product)->getData($this->_configurableAttributes))) { // If used products load before attributes, we will load attributes. $this->getConfigurableAttributes($product); // After attributes loading products loaded too. Varien_Profiler::stop('CONFIGURABLE:'.__METHOD__); return $this->getProduct($product)->getData($this->_usedProducts); }
Listing 5. /app/code/core/Mage/Catalog/Model/Product/Type/Configurable.php, line 326.
The answer lies in this line:
$this->getConfigurableAttributes($product);
This code generates a collection of configurable attributes for the given product. But this collection model has a method _afterLoad which calls the getUsedProduct function again and produces the used product collection that is then saved into the configurable product object and is ready to be returned.
- getProductByAttributes – this method’s first parameter, $attributeInfo, is an array containing attribute IDs as keys and attribute values as elements. The system attempts to find a child product whose attributes match values in the $attributeInfo array. This method is used to locate an associated product based on an attribute selection passed from, for example, an add-to-cart form.
- getParentIdsByChild – this function takes an ID (entity_id) of a child product and returns a list of its parent products. The relationships between configurable products and their children are stored in table catalog_product_super_link. This information is also saved in table catalog_product_relation, which also contains parent-child relationships for other product types, such as grouped and bundle products. Table catalog_product_super_link, however, is used exclusively by the configurable type.
Saleable checks and preparing products for cart
The methods of this group check if a configurable product can be sold and prepare product objects to be converted into quote items:
- isSalable – the saleable status of a configurable product is true if at least one of its children can be sold.
- checkProductBuyState – the purpose of this method is to check if a product object can be added to a quote as an item. If a product has required options they must be set before the product is added to a shopping cart. This is partially taken care of by the parent method in class Mage_Catalog_Model_Product_Type_Abstract. The configurable type extends this method to make an additional check: a request passed from the add-to-cart-form must have values for configurable attributes (customers must select required configuration attributes).
- _prepareProduct – the purpose of this function is to process the configurable product in such a way that it and its selected simple product can be added to cart (converted to quote items). Let’s go through the lines of this method to see how it is done.It starts with the system receiving a buying request, which contains information about selected attributes. This information is used to determine which of the configurable products children is selected. You can see this array of attributes from the $buyRequest object in the first line of the _prepareProduct method.
protected function _prepareProduct(Varien_Object $buyRequest, $product, $processMode) { $attributes = $buyRequest->getSuperAttribute();
Listing 6. /app/code/core/Mage/Catalog/Model/Product/Type/Configurable.php, line 567.
If a configurable product has two selectable attributes, e.g. “Gender” and “Size”, then the attribute array would look like this:
array(2) ( [501] => (string) 35 [502] => (string) 46 )
The array’s keys correspond to IDs of the product’s configurable attributes, gender and shoe_size. Array elements are the values, which a customer has selected in the add-to-cart form. Configurable attributes must be of “select” type, that is why these values refer to data in tables eav_attribute_option and eav_attribute_option_value. Since I am using Magento sample data, the 501 attribute is “Gender” and value “35” means “Women”; 502 is “Size” and “46” is “size 3”.
If you keep following the lines of this method’s code next you’ll see the configurable product object being prepared for cart by calling the _prepareProduct method of the parent Mage_Catalog_Model_Product_Type_Abstract class. It is expected that the returned value is an array, otherwise there must have been an error, in which case the function would return an error message:
$result = parent::_prepareProduct($buyRequest, $product, $processMode); if (is_array($result)) { $product = $this->getProduct($product);
Listing 7. /app/code/core/Mage/Catalog/Model/Product/Type/Configurable.php, line 579.
After that the system checks if the configurable attributes of the product have matches in the attribute data passed from the add-to-cart form. If yes, then the system can load the selected child product; if not, then the function returns an error message (if the processing mode is strict) or an array containing the configurable product only (if product is being added to a wish list).
$subProduct = true; if ($this->_isStrictProcessMode($processMode)) { foreach($this->getConfigurableAttributes($product) as $attributeItem){ /* @var $attributeItem Varien_Object */ $attrId = $attributeItem->getData('attribute_id'); if(!isset($attributes[$attrId]) || empty($attributes[$attrId])) { $subProduct = null; break; } } } if( $subProduct ) { $subProduct = $this->getProductByAttributes($attributes, $product); }
Listing 8. /app/code/core/Mage/Catalog/Model/Product/Type/Configurable.php, line 586.
The selected sub-product is loaded using the getProductByAttributes function, which we already mentioned before.
The found matching sub-product in turn goes through the _prepareProduct method. If there are any custom options to include, they are processed as well and are set to the sub-product object.
Next, the quantity specified in the $buyRequest object is set to the child product object.
Finally, both configurable and sub-product are returned as an array of “cart candidates” ready to be added to a quote object.
Mage_Catalog_Model_Product_Type_Configurable_Attribute model
Our discussion of the Configurable product type would not be complete if I didn’t mention model Mage_Catalog_Model_Product_Type_Configurable_Attribute. This class plays an important role in handling of configurable products. It represents a configurable attribute of a configurable product. It is coupled with resource model Mage_Catalog_Model_Resource_Product_Type_Configurable_Attribute, which takes care of saving the attribute model data into tables catalog_product_super_attribute, catalog_product_super_attribute_label, and catalog_product_super_attribute_pricing. The first table contains relationships between products and attributes, i.e. which configurable product has which configurable attributes. The labels of the said attributes are saved into the second table, catalog_product_super_attribute_label. The configured attribute labeling has scope “store”, which means that for the same attribute you can define different labels in different stores, such as “Manufacturer” for store “English” and “Hersteller” for store “German”. Table catalog_product_super_attribute_pricing contains option prices for configurable attribute values. If a product has configurable attribute size and selecting size “9” increases the total price by $1 then this table will contain an entry like this:
value_id | product_super_attribute_id | value_index | is_percent | pricing_value | website_id |
---|---|---|---|---|---|
13 | 3 | 40 | 0 | 1.0000 | 0 |
Table 1. A sample entry in the catalog_product_super_attribute_pricing table.
The value in column product_super_attribute_id refers to an entry in table catalog_product_super_attribute that connects the attribute size to the current product. The value_index column references to attribute option ID 40, which stands for “Size 9”. Our increment of $1 is saved in column pricing_value. Column is_percent has value 0, which means that we are using an absolute price increment (if it was 1 then the price increments would have been “by 1%” and not by “$1”). Configurable option prices have scope “website”, for which the last column is used. The value 0 stands for the “Main Website”.
Configurable type and its effect on shop performance
The objects of type Mage_Catalog_Model_Product_Type_Configurable_Attribute are most commonly requested as a collection. Collection model Mage_Catalog_Model_Resource_Product_Type_Configurable_Attribute_Collection is engaged wherever a configurable product needs access to its configurable attributes. Take a look at its method _afterLoad. The collection represents all configurable attributes defined for the product. To make a better use of this collection each of its items is extended with data like attribute model instances (added by function addProductAttributes), attribute labels (_loadLabels), and information on prices of each configurable option (_loadPrices). This generation of attribute collection is in my experience one of the most significant performance critical pieces of code in Magento. Why? Take a closer look at the code. It has multiple lines starting and stopping the built-in Magento profiler:
protected function _afterLoad() { parent::_afterLoad(); Varien_Profiler::start('TTT1:'.__METHOD__); $this->_addProductAttributes(); Varien_Profiler::stop('TTT1:'.__METHOD__); Varien_Profiler::start('TTT2:'.__METHOD__); $this->_addAssociatedProductFilters(); Varien_Profiler::stop('TTT2:'.__METHOD__); Varien_Profiler::start('TTT3:'.__METHOD__); $this->_loadLabels(); Varien_Profiler::stop('TTT3:'.__METHOD__); Varien_Profiler::start('TTT4:'.__METHOD__); $this->_loadPrices(); Varien_Profiler::stop('TTT4:'.__METHOD__); return $this; }
Listing 9. /app/code/core/Mage/Catalog/Model/Resource/Product/Type/Configurable/Attribute/Collection.php, line 586.
Enable Magento profiler by uncommenting Varien_Profiler::enable(); in index.php and open a catalog category page containing many configurable products (such as apparel.html if you are using the sample data). The results will be like the ones in the table below (I am using the Mgt Developer Toolbar extension to produce the table):
Name | Time | Cnt | Emalloc | RealMem |
---|---|---|---|---|
mage | 7.939271 | 1 | 0 | 0.00 KB |
mage::dispatch::routers_match | 6.169161 | 1 | 0 | 0.00 KB |
mage::dispatch::controller::action::catalog_category_view | 5.983179 | 1 | 0 | 0.00 KB |
layout/db_update: default | 5.472315 | 1 | 0 | 0.00 KB |
layout/db_update: STORE_default | 5.466694 | 1 | 0 | 0.00 KB |
layout/db_update: THEME_frontend_default_default | 5.462283 | 1 | 0 | 0.00 KB |
layout/db_update: catalog_category_view | 5.457826 | 1 | 0 | 0.00 KB |
layout/db_update: MAP_popup | 5.449864 | 1 | 0 | 0.00 KB |
layout/db_update: SHORTCUT_popup | 5.445135 | 1 | 0 | 0.00 KB |
layout/db_update: SHORTCUT_uk_popup | 5.439426 | 1 | 0 | 0.00 KB |
layout/db_update: catalog_category_layered | 5.434973 | 1 | 0 | 0.00 KB |
layout/db_update: CATEGORY_18 | 5.430586 | 1 | 0 | 0.00 KB |
layout/db_update: customer_logged_out | 5.424732 | 1 | 0 | 0.00 KB |
mage::dispatch::controller::action::catalog_category_view::layout_render | 4.812617 | 1 | 0 | 0.00 KB |
frontend/base/default/template/page/3columns.phtml | 4.807388 | 1 | 0 | 0.00 KB |
frontend/base/default/template/catalog/category/view.phtml | 3.723822 | 1 | 13,018,6 | 12.42 MB |
frontend/base/default/template/catalog/product/list.phtml | 3.577364 | 1 | 10,686,7 | 10.19 MB |
CONFIGURABLE:Mage_Catalog_Model_Product_Type_Configurable::getConfigurableAttributes | 2.988455 | 9 | 5,141,2 | 4.90 MB |
CONFIGURABLE:Mage_Catalog_Model_Product_Type_Configurable::getUsedProducts | 1.732344 | 105 | 3,715,6 | 3.54 MB |
TTT2:Mage_Catalog_Model_Resource_Product_Type_Configurable_Attribute_Collection::_afterLoad | 1.732193 | 9 | 3,064,3 | 2.92 MB |
TTT1:Mage_Catalog_Model_Resource_Product_Type_Configurable_Attribute_Collection::_afterLoad | 1.026179 | 9 | 1,206,02 | 1.15 MB |
config/load-db | 0.808559 | 1 | 254,4 | 248.45 KB |
config/load-modules | 0.719633 | 1 | 4,968 | 4.85 KB |
Table 2. Profiler output of a category page with 12 configurable products.
The total number of lines in this table is 401. I am showing only the first 24, which take the longest time. As you can see, the code tagged with TTT2 and TTT1 takes a significant proportion of the total time of 7.94 seconds (the cache is disabled). And this is with only 12 configurable products! Many real shops have to deal with product collections thousands times as big. If the number of configurable attributes is also high (not a typical “color” and “size”, but dozens of configurable options, like in jewelry stores) the time required by function _addProductAttributes (tagged TTT1 in the profiler results) will also increase. But it is the call to function _addAssociatedProductFilters that potentially can cause performance problem. This function gets all children items for a configurable product by calling Mage_Catalog_Model_Product_Type_Configurable::getUsedProducts. You can find it in the table above. It was called 105 times for a collection of 12 products! I’ve worked on shops where this problem turned out to be especially bad and forced me and my colleagues to look hard for ways around it (adding custom indexes, Varnish caching, Solr).
Conclusion
In this two-part overview of the Configurable product type we have discussed how products of this type are created and managed in the back-end and looked at the way configurable products are displayed in the front-end. We have also learned about functions defined in the configurable product type class that provide access to the attributes and associated products of a configurable product. Finally, we touched the topic of performance risks that usage of large collections of configurable products may bring. In the next post I will present a brief tutorial covering some practical aspects of using configurable product type functionality in a custom Magento extension.
Excellent article. One question, how come there were no /Mage/Catalog/Model/Resource/Product/Type/Configurable/Attribute/Collection.php when I was on 1.5.0.2 and when I upgraded to 1.7.2 this new code actually made performance much worse. What did varien achieved with this new code that they didn’t have before?
Hello Ryan,
Thank you for your appreciation. File
/Mage/Catalog/Model/Resource/Product/Type/Configurable/Attribute/Collection.php
isn’t really new and is present in earlier versions. It only changed name because of Varien’s trend to move resource models from “Mysql4” folders to “Resource”. You can locate this collection file in Magento 1.5 atMage/Catalog/Model/Resource/Eav/Mysql4/Product/Type/Configurable/Attribute/Collection.php
. As of to the performance penalty, unfortunately I can’t tell – I never worked with 1.5 before.Hi Oleg – Thanks for your fantastic blog, it is a great reference.
I have one comment in relation to the requirements for configurable attributes – you mentioned that they can either be dropdown or multiselect but when creating an attribute in the Admin if you select multiselect there is no “Use To Create Configurable Product” option and therefore is_configurable is 0 and when multiselect is selected source_model is NULL. Maybe I have missed something on this ?
Hello,
Can you tell us about t he use of price promotions in configurable products? Until now, when using a catalog price promotion, we see the promotion applied to the main product and not to its configurations in the cases when the configurations have a price different from that of the main products.
Is there a way to solve this?
Thanks and kind regards,
Isabelle
How can show configurable product on product list page with their attributes and with support of configurable.js in magento 2.