Solving Magento

Solutions for Magento E-Commerce Platform

by Oleg Ishenko

Product Type Logic Implementation in Magento

Depending on what merchandise your shop specializes in, you may need different product types. The basic, simple product type logic is straightforward: a product has a price, it has product information like pictures and descriptions, it has inventory that can be linked to a warehouse management system. Products of this type can be placed into a shopping cart, paid, packed and sent. But it is not always that simple. You may need products that are not physical, but are files that can be downloaded by customers who bought them. Your products may be still physical, but consist of several options, which customers must choose from before adding their chosen combination to the shopping cart. Several simple products could be grouped together and sold as a single one. Or a product may be just an entry in your catalog whose details page has a link redirecting to the shop of your partner. We are talking about product types that have certain behaviour and require functionality sometimes similar and sometimes distinctly different from each other.

Magento stores product data in database tables and uses complex EAV models to retrieve and manipulate that data. The product functionality mentioned above is not a part of the EAV system. It is implemented in a set of classes that all extend one abstract class, Mage_Catalog_Model_Product_Type_Abstract. These classes are regular Magento models inheriting all the way up from Varien_Object. If necessary, these models can be connected to resource models and through them to database data. Magento core offers the following product types:

  • Simple
  • Configurable
  • Grouped
  • Virtual
  • Bundle
  • Downloadable

The classes for the first four types are implemented in the Mage_Catalog module. Bundle and Downloadable types require much more infrastructure and have their own modules: Mage_Bundle and Mage_Downloadable. Still all of them inherit the shared product type functionality defined in the abstract class.

Product Type Configuration

Each product type must be declared in the Magento configuration. The corresponding configuration node is global/catalog/product/type. Some of the Magento product types are defined in the config file of the Mage_catalog module:

<config>
        <!--code omitted for brevity-->
        <global>
        <!--code omitted for brevity-->
                <catalog>
                        <product>
                                <type>
                                        <simple translate="label" module="catalog">
                                                <label>Simple Product</label>
                                                <model>catalog/product_type_simple</model>
                                                <composite>0</composite>
                                                <index_priority>10</index_priority>
                                        </simple>
                                        <grouped translate="label" module="catalog">
                                                <label>Grouped Product</label>
                                                <model>catalog/product_type_grouped</model>
                                                <price_model>catalog/product_type_grouped_price</price_model>
                                                <composite>1</composite>
                                                <allow_product_types>
                                                        <simple/>
                                                        <virtual/>
                                                </allow_product_types>
                                                <index_priority>50</index_priority>
                                                <price_indexer>catalog/product_indexer_price_grouped</price_indexer>
                                        </grouped>
                                        <configurable translate="label" module="catalog">
                                                <label>Configurable Product</label>
                                                <model>catalog/product_type_configurable</model>
                                                <price_model>catalog/product_type_configurable_price</price_model>
                                                <composite>1</composite>
                                                <allow_product_types>
                                                        <simple/>
                                                        <virtual/>
                                                </allow_product_types>
                                                <index_priority>30</index_priority>
                                                <price_indexer>catalog/product_indexer_price_configurable</price_indexer>
                                        </configurable>
                                        <virtual translate="label" module="catalog">
                                                <label>Virtual Product</label>
                                                <model>catalog/product_type_virtual</model>
                                                <composite>0</composite>
                                                <index_priority>20</index_priority>
                                        </virtual>
                                </type>
                                <!--code omitted for brevity-->
                        </product>
                        <!--code omitted for brevity-->
                </catalog>
                <!--code omitted for brevity-->
        </global>
        <!--code omitted for brevity-->
</config>

The configurations for the Bundle and Downloadable type you can find in their respective module’s config files.

Bundle:


 <config>
        <!--code omitted for brevity-->
        <global>
        <!--code omitted for brevity-->
                <catalog>
                        <product>
                                <type>
                                        <bundle translate="label" module="bundle">
                                                <label>Bundle Product</label>
                                                <model>bundle/product_type</model>
                                                <composite>1</composite>
                                                <allowed_selection_types>
                                                        <simple/>
                                                        <virtual/>
                                                </allowed_selection_types>
                                                <price_model>bundle/product_price</price_model>
                                                <index_data_retriever>bundle/catalogIndex_data_bundle</index_data_retriever>
                                                <index_priority>40</index_priority>
                                                <price_indexer>bundle/indexer_price</price_indexer>
                                                <stock_indexer>bundle/indexer_stock</stock_indexer>
                                        </bundle>
                                </type>
                                <!--code omitted for brevity-->
                        </product>
                        <!--code omitted for brevity-->
                </catalog>
                <!--code omitted for brevity-->
        </global>
        <!--code omitted for brevity-->
</config>

Downloadable:

<config>
        <!--code omitted for brevity-->
        <global>
        <!--code omitted for brevity-->
                <catalog
                        <product>
                                <type>
                                        <downloadable translate="label" module="downloadable">
                                                <label>Downloadable Product</label>
                                                <model>downloadable/product_type</model>
                                                <is_qty>1</is_qty>
                                                <price_model>downloadable/product_price</price_model>
                                                <index_data_retriever>downloadable/catalogIndex_data_downloadable</index_data_retriever>
                                                <composite>0</composite>
                                                <price_indexer>downloadable/indexer_price</price_indexer>
                                                <can_use_qty_decimals>0</can_use_qty_decimals>
                                        </downloadable>
                                </type>
                                <!--code omitted for brevity-->
                        </product>
                        <!--code omitted for brevity-->
                </catalog>
                <!--code omitted for brevity-->
        </global>
        <!--code omitted for brevity-->
</config>

As you can see the name of the node corresponds to the product type name. What are the other possible configuration options?

  • label – self-evidently a descriptive name of the product type used in the user interface.
  • model – contains a configuration path to the product type model file.
  • composite – tells if a product of this type can have child products (like Configurable, Grouped, and Bundle)
  • price_model – contains a configuration path the product type’s price model. Product prices require functionality that goes beyond simple “getPrice()” calls, such as special prices, tier prices, or prices specific to a customer group. This functionality is defined in price models which can also differ from one product type to another.
  • can_use_qty_decimals – defines if product of this type can be sold in whole numbers or fractional quantities. For example buying 3.5 kg sand requires a product type using decimal quantities,
  • index_priority – defines the indexing priority for product types. Uses a numeric value to sort types in ascending order. If no value is provided,the default value 0 is used. Composite types, no matter what their priority is, are sorted separately and added to the end of the sorted types list.
  • price_indexer – references to the price indexer resource model used by type. If no value is provided, the default price indexer is used, Mage_Catalog_Model_Resource_Product_Indexer_Price_Default.
  • stock_indexer – references to the type’s stock indexer resource model. If the type has no indexer specified, the system uses the default one, Mage_CatalogInventory_Model_Resource_Indexer_Stock_Default.
  • index_data_retriever – contains a reference to the type-specific retriever model. Retriever models are used to access product data during indexing.

Some product types can use their own configuration entries, like Grouped and Configurable’s node allow_product_types, which lists product types that can be used as child products. The configuration of a product type can also be extended in other modules. For example, Downloadable extends the configurations of Grouped and Configurable by adding itself into their allowed product types list (app/code/core/Mage/Downloadable/etc/config.xml):

<configurable>
    <allow_product_types>
         <downloadable/>
    </allow_product_types>
</configurable>
<grouped>
    <allow_product_types>
        <downloadable/>
    </allow_product_types>
</grouped>

The product type configuration entries are converted into type model properties when they are instantiated. Product type models are instantiated by a factory class Mage_Catalog_Model_Product_Type. This class’s function factory receives a product object, reads its type Id and creates an instance of its type model. The instance can be singleton if the call to product model’s function getTypeInstance passes true as a parameter:

$typeInstance = $product->getTypeInstance(true); // returns a singleton

Basic Product Type Functionality

The functions declared and implemented in the basic product type model Mage_Catalog_Model_Product_Type_Abstract make up the fundamental functionally expected from a product type model. It can be classified in the following groups.

The first group of functionality provided by a product type model can be called descriptive. It tells the system what sort of product type it deals with. Here are some examples:

  • isComposite method returns true for Configurable, Grouped, and Bundle
  • isVirtual would return true for the Virtual product type as well as for Configurable and Bundle products whose options are Virtual products
  • canConfigure checks if a product of this type can be configured, which means it will return true for Configurable, Grouped, Bundle, and also Downloadable if such product has download links that can be purchased separately.

Another group offers some product type-specific procedures performed before the product data is saved to the database:

  • beforeSave. The product model class calls this method from its _beforeSave function, i.e. before saving the product data. The beforeSave method calls a protected function _removeNotApplicableAttributes and sets the product model’s property _canAffectOptions to true (this property plays a role in setting or un-setting product options when a product is saved). The configurable, downloadable, and bundle product types override this function and add more functionality to it. This modifications will be discussed in the overview of the respective product types.
  • _removeNotApplicableAttributes($product = null). When a product is being saved this method is called before the data is written to the database. The purpose of this method is to purge all the values set for attributes that do not apply to the given product type. Every attribute has a list of product types to which it can apply. It is possible, however, to set a value for an attribute using a standard setter method on the product model. To prevent confusion of saving this value to a database, it is unset in the _removeNotApplicableAttributes method.
  • save is also called from the product model, after the product data is saved, i.e. from its _afterSave function. The abstract type class has no implementation for it but its children, Configurable, Grouped, Bundle, and Downloadable – all use it to perform some database writing operations on their specific data. For example, a Configurable product would save current set of configurable attributes and relations to its children products.

The most comprehensive set of functionality deals with preparing products to be added to the shopping cart (quote) or to a wishlist. A product can have a set of custom options, e.g. a text for a print of a T-Shirt. Sometimes such options can be defined as required and customer must provide it before adding a product to the shopping cart. Some product types, like Configurable, cannot be added to the cart before one (or several) of its configurable options is selected. The following functions perform such checks and prepare products:

  • checkProductConfiguration. This function is called in the process of displaying a product configured and ready to be added to cart (that is on a product details page or in the backed order form). The configuration check includes running a clone of he product object through the process of preparing for the cart without actually adding it to the cart. if any errors are discovered they are communicated to the system and the check fails.
  • prepareForCartAdvanced. This function is used to prepare a product to be added to shopping cart (i.e, quote object) and is called from method Mage_Sales_Model_Quote::addProductAdvanced, or from a shop API class Mage_Checkout_Model_Api_Resource_Product. Apart from executing the _prepareProduct function which returns a product in a form ready to be converted into a quote item, this method also calls function processFileQueue, which processes custom options of type “file”, if the product has any.
  • _prepareProduct. As said above, this function processes a product object and returns it ready to be added to a quote. The “readiness” can vary from type to type. For example, Configurable type must ensure that the request submitted by the customer contains a selection of configurable options that allows to determine a child product, which in turn must be prepared so that both parent and the selected child can be added to cart. Also, in the process of “preparing” a product the _prepareOptions method is called.
  • _prepareOptions(Varien_Object $buyRequest, $product, $processMode) – this method is not overridden and can be used for every product type. The task is to read the options from the request passed to the shopping cart and to add the option values to the product. Here the process “mode” plays some role. Process mode can be defined as “full” or “lite” – in full mode the system checks if all required options are set, in the lite mode this check is skipped. The added options are later transferred to a quote item object.
  • checkProductBuyState. This function checks if the product can be sold. This check is performed on a quote item’s product whenever it is added or updated in the quote. The system in this case makes sure that required options of a product are set, if the product has any required options. This check is extended in the configurable and downloadable product types.
  • isSalable. This function checks if the product can be sold (a bit different from checking if a product is ready to be added to cart, which the functions above do). The process for non-composite product types includes the product status check (must not be disabled) and checking if the product’s property is_salable is not false (this property can be set dynamically elsewhere in the code to make the product not available). The downloadable product type model extends this function to also check if there are links available for the product. Composite product types make additional checks discussed in the respective sections. Function Mage_Catalog_Model_Product::isAvailable() makes a call to this method. The result of this call can be ignored if the catalog product helper is set to skip the salable check:
public function isAvailable()
{
    return $this->getTypeInstance(true)->isSalable($this)
        || Mage::helper('catalog/product')->getSkipSaleableCheck();
}

The following simplified diagram displays the role payed by type model when adding a simple product to the cart:

Product Type Instance functions in the process of adding a product to cart

Figure 1. Product Type Instance functions in the process of adding a product to cart

The process is initiated by a quote object (Mage_Sales_Model_Quote) that calls its method addProductAdvanced, to which a product object (Mage_Catalog_Model_Product) is passed. In this function the product model gets an instance of its type, which is a child class inheriting from Mage_Catalog_Model_Product_Type_Abstract. This type instance object returns to quote so called “cart candidates”. “Cart candidates” are an array of product objects prepared to be converted to quote items. It is an array, which can contain multiple elements even if the customer request contains only one product. The multiple candidates could be returned if a composite product is being added to cart. In this case the main product and its relevant children must be converted into quote items.

In the process of preparing the requested product for cart the type instance checks if required options (if any) are set and processes file options (also, if any). If everything is fine, the product is returned as a cart candidate.

One more group of methods defined in the product type model is tasked with processing custom options of type “file”:

  • _addFileQueue($queueOptions). File is a kind of a custom option which allow customers to upload a file to be included into the product order. Examples can be a T-shirt with a custom print uploaded by customer, or an image file to be made into a canvas print.
  • processFileQueue(). This function is called whenever there is a need to upload a file option or to move the file to the destination folder. The file name, file source and destination folders are found in the elements of the fileQueue array.

Most of the functions listed above are extended by the child classes, especially the ones that need quite distinctive functionality required by the nature of those product types. There are two product types which do not override the functions defined in the abstract type model. They are Simple and Virtual. This basic functionality is sufficient for them. The only exception is the isVirtual function overridden by the Virtual type: it returns true, naturally.

In the next posts we will discuss in more details the complex product types: Configurable, Bundle, Grouped, and Downloadable.

3 thoughts on “Product Type Logic Implementation in Magento

  1. Pingback: Magento Configurable Product Type (Part 1) | Solving Magento

  2. Pingback: Creating a Custom Product Type in Magento | Solving Magento

Leave a Reply

Your email address will not be published. Required fields are marked *

Theme: Esquire by Matthew Buchanan.