Change the Product Quantity Added to Cart – part 2: Model Rewrite

Change the Product Quantity Added to Cart – part 2: Model Rewrite

Now we want to take a look at rewriting some of Magento’s core code in a module of our own. Our aim is to set the quantity of a related product (a product warranty) added to the cart to the same quantity as the main product that we select. There are bigger lessons here too, and we want to learn those along the way. Here goes…

This post is very focussed on how to solve the problem. If you want the solution, and don’t want to follow the approach, then skip to the last section headed ‘Rewriting addProductAdvanced()’.

The starting point for this post is to investigate program execution from the point where we left off in the previous post, when Mage_Checkout_CartController addAction() is invoked. Take a look at my cut-down version of addAction() next. I’ve avoided exceptions and tried to keep just the key lines of code which get our products added to the cart. I know. It’s always debatable what to leave out, but we can’t discuss everything – we don’t have the time (or the inclination). It’s worth noting though, that the code left in provides us with all the essentials for adding products programmatically to the cart – should we desire to rewrite this controller for some requirement in the future – and maybe then replace taking product IDs and quantities from a post from ‘ADD TO CART’ with taking them from somewhere else? Enough digression, here’s the code.

174public function addAction()

175{

:

180$cart = $this->_getCart();

181$params = $this->getRequest()->getParams();

182try {

:

190$product = $this->_initProduct();

191$related = $this->getRequest()->getParam(‘related_product’);

:

201$cart->addProduct($product, $params);

201if (!empty($related)) {

203$cart->addProductsByIds(explode(‘,’, $related));

204}

205

206$cart->save();

:

217if (!$this->_getSession()->getNoCartRedirect(true)) {

218if (!$cart->getQuote()->getHasError()) {

219$message = $this->__(‘%s was added to your shopping cart.’, Mage::helper(‘core’)->escapeHtml($product->getName()));

220$this->_getSession()->addSuccess($message);

221}

222$this->_goBack();

223}

224}

:

245}

Let’s take a closer look at some of the lines of code we want to understand here:-

Although we won’t get to it just yet, the code on line 206 is obviously key, as it saves our quote to the database. And line 222 is of interest too, because that redirects program flow from addAction() through to the page rendering method indexAction(), when all is said and done here. So much for the big picture, and where we’re headed longer term, let’s now look at some of that preamble.

In line 181, we load all request parameters into the array $params. These were posted with ‘ADD-TO-CART’. I can see from my ‘variables’ window in PhpStorm that the array items include ID’s for product and related product and a quantity for product. There’s no quantity posted for related product, because, as I think I mentioned, out-of-the-box Magento doesn’t post a quantity, and as we’ll see later it hard codes the quantity for related products as 1. In line 191, we also capture the related product ID into a variable on its own.

Also in the first few lines of code we instantiate some models.

Firstly, $cart = $this->_getCart() on line 180. This calls _getCart(), which with Mage::Singleton(‘checkout/cart’) instantiates Mage_Checkout_Model_Cart with an empty _data property at this stage (_data = null).

Secondly, $product = $this->_initProduct() on line 190 calls _initProduct() which through

$product = Mage::getModel(‘catalog/product’)->setStoreId(Mage::app()->getStore()->getId())->load($productId);

instantiates Mage_Catalog_Model_Product where $productId is the parameter for the main product posted with ‘ADD TO CART’. (We’ll need product models for related products too. That comes later.)

Note we use Mage::getSingleton(‘checkout/cart’) to instantiate the cart model. The getSingleton() method allows us to use this same object throughout processing with each subsequent invocation of getSingleton(‘checkout/cart’). The method getModel(), on the other hand, will always returns a new object. We’ll need multiple product objects, for main product and related products.

From getModel() to Instantiated Model

For those perhaps still a little uncomfortable in seeing a call to the Mage class static methods, getSingleton() and getModel(), to instantiate model classes, I’ll give a brief word of explanation. For those already familiar, please feel free to skip this section.

The Mage::getModel(‘checkout/cart’) method calls the getModelInstance() method in Mage_Core_Model_Config. It’s in getModelInstance() that the class is instantiated with

$obj = new $className($constructArguments);

where $className() in this example is Mage_Checkout_Model_Cart. But how do we get from the string ‘checkout/cart’ to ‘Mage_Checkout_model_Cart’? And how does the file get loaded where the class Mage_Checkout_Model_Cart resides?

OK, within Mage_Core_Model_Config, getModelInstance() calls getModelClassName() and that method in turn calls getGroupedClassName(), and it’s there that the string conversion takes place from ‘checkout/cart’ to ‘Mage_Checkout_Model_Cart’. Isn’t this why ‘checkout/cart’ is called a grouped class name?

It’s also in getGroupedClassName() that Magento checks in the ‘global config’ for the existence of a model rewrite. If there is one for this class, then Magento uses the rewrite class instead. You may be aware that the global config is assembled from the config.xml files from all Magento modules (and from other files) with each HTTP request.

And how does the file containing the class Mage_Checkout_Model_Cart get loaded? When the root directory’s Index.php ‘requires’ the file app/Mage.php, code is run to achieve 2 things:

(1) set include paths with PHP’s set_include_path(), directing Magento where to look when including or requiring a file. The 4 paths set and the order of searching are the local, community and core directories under ‘app/code/’ and lastly the lib directory.

(2) set autoloading of a class’s file with the PHP function spl_autoload_register() which names the function to be run when ‘including’ the file needed for the class instantiation. In Magento’s case, Varien_Autoload::register() in the Mage class calls register(), where ‘autoload()’ is named as the function to be run when a class is instantiated. It’s autoload() in the Varien_Autoload class which converts the class name, for example, ‘Mage_Checkout_Model_Cart’, to the file path ‘Mage/Checkout/Model/Cart.php’, and then includes the file Cart.php.

Adding products to our quote with addProduct() & addProductByIds()

You’ve skipped to here, if you avoided that detour into the realms of how Magento instantiates classes.

The code for adding the main product to our quote is line 201, addProduct(). After the main product is added – in our case just one product as this site sells only simple products – then we add related products with addProductsByIds() on line 203. The fixed sequence here, main product then related, is important for us, as we’ll see later when we do some coding. Note related products are added only if an ID for at least one related product was was found in the request parameters and stored in $related.

We expect both routes to be similar, i.e. adding a main product will be similar to adding a related product, which is indeed the case. The starting point is slightly different for each but sooner rather than later, they’ll be using the same code. Let’s take a look at each of the preambles before the code meets up. And let’s not lose sight of the aim here, lest we forget, to find some code that we can rewrite which adds the same number of related products to the cart as main ones.

Route One – addProduct() to add the main product

The code in line 201 ‘$cart->addProduct($product, $params)’ calls Mage_Checkout_Model_Cart’s addProduct() method, which with the code

$request = $this->_getProductRequest($requestInfo);

calls the _getProductRequest() method, where $requestInfo is $params. That method checks if $params is an instance of Varien_object (it isn’t, it’s an array). If it isn’t, the method creates a Varien_Object named $request and sets its _data property to the $requestInfo array. The method also checks that $request has a quantity, and if it doesn’t, sets the quantity to 1.

My initial thought having tracked through to this point was ‘good’. I bet that when Magento adds a related product, it will tread through this same code as well, and I can rewrite this section to add a quantity for the related product that is the same as the main product. I was wrong Magento doesn’t go here for related products, so we’ll need to continue our search.

Next up, Magento runs

$result = $this->getQuote()->addProduct($product, $request);

back in Mage_Checkout_Model_Cart’s addProduct() method. Remember the argument $product is the instantiated product model for the main product. The important calls here are firstly to getQuote() in Mage_Checkout_Model_Cart and then addProduct() in Mage_Sales_Model_Quote. Don’t you just love cross-class method chaining.

It’s at this point that what I’m calling route 1 and route 2 meet up and use the same code, i.e. code for adding the main product and for adding the related product(s) is the same from here on in. Let’s see how the code for adding the related product(s) arrives at this same point next.

Route Two – addProductsByIds() to add related products

Adding a related product starts its journey thus:- $cart->addProductsByIds(explode(‘,’, $related)) on line 203 above. This line explodes the array of related product IDs and passes them to addProductsByIds(). (Remember in our case we have only one related product, but this could change in a later requirement!) As you might suspect addProductsByIds() takes each ID in the array through a ‘foreach’ loop.

In the loop, Magento uses

$product = $this->_getProduct($productId);

to instantiate a new object for the related product ID. The code in _getProduct() uses getModel() (see our brief discussion above about getSingleton() versus getModel()) thus

$product = Mage::getModel(‘catalog/product’)->setStoreId(Mage::app()->getStore()->getId())->load($productInfo);

Remember too that above in Mage_Checkout_CartController’s addAction() method we instantiated a model for the main product. Well now this gives us one for this, and, each time through the loop, an instantiation for other related products as well.

In the heart of the loop there’s this code, which may strike you as familiar, because we just saw a variation of it for adding our main product:

$this->getQuote()->addProduct($product);

This code puts the addition of a related product on the same journey now as the journey for the main product. Let’s explore that journey now with a trusted IDE, so that we can decide where to rewrite the quantity for the related product.

Getting the quote with getQuote()

The main and related products now use the same code, i.e. after the main product is added we then use the code again to add the related product. We saw how adding a main product uses

$result = $this->getQuote()->addProduct($product, $request);

and a related product uses

$this->getQuote()->addProduct($product);

In the getQuote() method, we find these lines of code

116public function getQuote()

117{

118if (!$this->hasData(‘quote’)) {

119$this->setData(‘quote’, $this->getCheckoutSession()->getQuote());

120}

121return $this->_getData(‘quote’);

122}

The $this here is the object Mage_Checkout_Model_Cart. Line 118 checks if Mage_Checkout_Model_Cart has a quote property. First time through when adding the main product, it doesn’t, so three things happen in line 119:

(1) getCheckoutSession() is called which instantiates Mage_Checkout_Model_Session as a singleton object,

(2) a method of Mage_Checkout_Model_Session is called, getQuote(). This method is key. It checks to see whether Mage_Checkout_Model_Session’s _quote property is null and, if it is, it instantiates Mage_Sales_Model_Quote, as we can see in line 122 in the first part of the Mage_Checkout_Model_Session’s getQuote() method next. Now it’s instantiated, property values can be added ready for saving later.

116public function getQuote()

117{

118Mage::dispatchEvent(‘custom_quote_process’, array(‘checkout_session’ => $this));

119

120if ($this->_quote === null) {

121/** @var $quote Mage_Sales_Model_Quote */

122$quote = Mage::getModel(‘sales/quote’)->setStoreId(Mage::app()->getStore()->getId());

123if ($this->getQuoteId()) {

124if ($this->_loadInactive) {

125$quote->load($this->getQuoteId());

126} else {

127$quote->loadActive($this->getQuoteId());

128}

(3) Line 127 is called if Magento can’t find a quote_id in the _data property of Mage_Checkout_Model_Session, which it would find if the cart session were active, i.e. typically when there are items already in the cart and not checked out, or items have been removed and the session is still open. (Cookies expire after an hour, placing a time limit on a checkout session.) Line 127 uses the Mage_Sales_Model_Quote method loadActive() to get the last active record (is_active = “1”) from the sales_flat_quote table and store it in the Mage_Sales_Model_Quote _data property. Thereafter, these properties in the Mage_Sales_Model_Quote object are updated before saving the data back to the sales_flat_quote table later.

Of course steps 1 though 3 don’t happen if Mage_Checkout_Model_Cart already has a quote property. And this is the case when a related product is added as this occurs after the main product is added. So for related products the already instantiated objects Mage_Checkout_Model_Session and Mage_Sales_Model_Quote are used.

As I mentioned, the sales_flat_quote table is the table where each record is a quote model. It’s also worth mentioning two other key related tables in the database: the sales_flat_quote_item table which stores each cart item, and sales_flat_quote_address which stores each address for a quote. Expect to see new rows for both of these tables after quote is saved.

Adding the product with Mage_Sales_Model_Quote’s addProduct()

We now continue with the call to addProduct() from either

$result = $this->getQuote()->addProduct($product, $request);

for the main product, or for the related product

$this->getQuote()->addProduct($product);

Note that while addProduct() for both the main and the related products pass $product onwards (where $product is the product object for that product), only addProduct() for the main product passes the Varien_Object $request onwards. Remember, Varien_Object holds the request parameters including the quantity requested for the main product.

Mage_Sales_Model_Quote’s method addProduct() looks like this:-

116public function addProduct(Mage_Catalog_Model_Product $product, $request = null)

117{

118return $this->addProductAdvanced(

119$product,

120$request,

121Mage_Catalog_Model_Product_Type_Abstract::PROCESS_MODE_FULL

122);

123}

As we see, the addProduct() method returns the call to addProductAdvanced(), passing through not only $product and $request (null for related products), but also a third parameter which specifies the ‘process mode’ either ‘full’ or ‘lite’.  Suffice it to say in this post that ‘process mode’ relates to the checking of product options before adding them to the quote object.

Processing the array of products to be added to the quote with addProductAdvanced()

The called addProductAdvanced() method is important and a condensed version is reproduced here:

956public function addProductAdvanced(Mage_Catalog_Model_Product $product, $request = null, $processMode = null)

957{

958if ($request === null) {

959$request = 1;

960}

961if (is_numeric($request)) {

962$request = new Varien_Object(array(‘qty’=>$request));

963}

964if (!($request instanceof Varien_Object)) {

965Mage::throwException(Mage::helper(‘sales’)->__(‘Invalid request for adding product to quote.’));

966}

967

968$cartCandidates = $product->getTypeInstance(true)

969->prepareForCartAdvanced($request, $product, $processMode);

:

988foreach ($cartCandidates as $candidate) {

:

992$item = $this->_addCatalogProduct($candidate, $candidate->getCartQty());

:

1011$item->addQty($candidate->getCartQty());

:

1023}

1024

1025Mage::dispatchEvent(‘sales_quote_product_add_after’, array(‘items’ => $items));

1026

1027return $item;

1028}

The most important part of this code from the perspective of this post is lines 958 through 963, because we can rewrite this to ensure that we add the same number of related products as main products to the quote. At last we’ve hit pay dirt! We’ll come back to this in the next section, when we’ve first had chance to consider the bigger purpose of this method.

As suggested by the title to this section, this method is where products as product models are added to the quote object Mage_Sales_Model_Quote. Each product is added as a Mage_Sales_Model_Quote_Item object. There’s a loop running here, starting at line 988

foreach($cartCandidates as $candidate){

where $cartCandidates is an array of products to be added to the quote.

How come, you may be asking, do we have an array of products to be processed, as first the main product and then each related product is passed into the addProductAdvanced() method, i.e. we process one product at a time through this method. Our array comprises only one item.

Well, not all products are simple products as in this example. The Mage_Catalog_Model_Product’s getTypeInstance() method, called on line 968 in

$cartCandidates = $product->getTypeInstance(true)->prepareForCartAdvanced($request, $product, $processMode);

instantiates the current product type with

$this->_typeInstanceSingleton = Mage::getSingleton(‘catalog/product_type’)->factory($this, true);

While in our case, this code instantiates Mage_Catalog_Model_Product_Type_Simple and sets the _typeInstanceSingleton property of Mage_Catalog_Model_Product to this object, the code could also accommodate and process composite products comprising multiple product elements, which would pass one at a time through the foreach loop. Composite products are products like the product types configurable, grouped and bundle.

All product type classes, including of course Mage_Catalog_Model_Product_Type_Simple, extend the class Mage_Catalog_Model_Product_Type_Abstract.

Line 968/969 is also responsible for calling the prepareForCartAdvanced() method of Mage_Catalog_Model_Product_Type_Abstract (more cross-class method chaining). Having added product type to Mage_Catalog_Model_Product with getTypeInstance(), now with prepareForCartAdvanced(), Magento adds the product quantity. It does this as the prepareForCartAdvanced() method calls _prepareProduct() with this line

$_products = $this->_prepareProduct($buyRequest, $product, $processMode);

The parameter $buyRequest is the Varien_Object object containing product quantity which has been handed off from method to method this far. We can see in the selected code for _prepareProduct() how Magento gets the quantity (comprising both cart_qty and qty) from the Varien_Object’s _data property on lines 371 and 373 and sets these values in $product.

316protected function _prepareProduct(Varien_Object $buyRequest, $product, $processMode)

317{

:

369// set quantity in cart

370if ($this->_isStrictProcessMode($processMode)) {

371$product->setCartQty($buyRequest->getQty());

372}

373$product->setQty($buyRequest->getQty());

374

375return array($product);

376}

The method _prepareProduct() returns on line 375 into prepareForCartAdvanced(), which returns into $cartCandidates in addProductAdvanced() ready to be processed in the addProductAdvanced() foreach loop. Each $cartCandidate is a product object with quantities set.

Within the loop two code lines are key:

(1) Line 992: $item = $this->_addCatalogProduct($candidate, $candidate->getCartQty();. Within _addCatalogProduct(), the following is achieved (and again, tracking this in an IDE to display the evolution of variables is essential).

(a) The Mage_Sales_Model_Quote_Item_Collection is added to the quote as the _items property.  The called method getItemsCollection() instantiates this with:-  $this->_items = Mage::getModel(‘sales/quote_item’)->getCollection();

(b) The Mage_Sales_Model_Quote_Item is added to the _items array within Mage_Sales_Model_Quote_Item_Collection (which itself is within quote). Mage_Sales_Model_Quote_Item has a _data array with array items including product_id, sku and product, where product is the object Mage_Catalog_Model_Product, including product quantities as provided for by _prepareProduct(), as explained above.  The called method addItem() is ultimately responsible for adding Mage_Sales_Model_Quote_item to the quote.

(2) Line 1011: $item->addQty($candidate->getCartQty());.  And within Mage_Sales_Model_Quote_Item’s addQty() method, Magento  sets product quantities in Mage_ Sales_Model_Quote_Item’s _data property (within Mage_Sales_Model_Quote_Item_Collection within Mage_Sales_Model_Quote).

Now that product quantities have been added to the item model, the model is ready to be saved to the sales_flat_quote_item table within the database. We’ll revisit some of this code in the next post when our objective is a little different: to find an event we can tap into to code an observer to do the same job as the rewrite we’re planning here.

We mentioned at the beginning of this section that we can rewrite lines 958 through 963 in addProductAdvanced() to have the same number of related products as main products added to the quote. Let’s do that now and wrap up.

Rewriting addProductAdvanced()

We’ve seen how the quantity of the main product has been passed down the line from addAction() as an array to addProduct() in Mage_Checkout_Model_Cart, and then getProductRequest() has converted the array to a Varien_Object. Lastly, in addProduct() of Mage_Sales_Model_Quote, Magento has passed the Varien_Object to the addProductAdvanced() method.

Of course for the related product no data has been passed, and addProductAdvanced() passes in a quantity of null. Lets show lines 958 through 963 in addProductAdvanced() again.

956public function addProductAdvanced(Mage_Catalog_Model_Product $product, $request = null, $processMode = null)

957{

958if ($request === null) {

959$request = 1;

960}

961if (is_numeric($request)) {

962$request = new Varien_Object(array(‘qty’=>$request));

963}

964if (!($request instanceof Varien_Object)) {

965Mage::throwException(Mage::helper(‘sales’)->__(‘Invalid request for adding product to quote.’));

966}

When the main product is added, $request as a Varien_Object doesn’t trip on any of the conditions on lines 958, 961 and 964, so the main product quantity stored in Varien_object is used later to update the main product quantity.

On the other hand, when a related product is added, $request is null, so $request is set to 1 (line 959). And as is_numeric($request) is true (line 961), $request is made a Varien_Object with an array item in it’s _data property of ‘qty’ equal to 1 (line 962). It won’t trip on the condition in line 964 either, because it’s now a Varien_Object.

Going forward now, the Varien_Object regardless of whether it’s for main product or related product can supply the product quantity needed.

Now we are going to rewrite this method. We’ll extend the class Mage_Sales_model_Quote and ensure that addProductAdvanced() is the only method that we’ll rewrite.

Rewriting addProductAdvanced() suits our purpose well. Other important CartController methods don’t hand off to addProductAdvanced(). We can amend product quantities in the cart, for example, invoking updatePostAction() or we can remove items invoking deleteAction(). These don’t touch addProductAdvanced() so the client requirement to allow customers to amend quantities after products are in the cart is met.

We won’t cover the basics of setting up a module here. See earlier posts if you’d like a refresher.

I’ve created the module with namespace ‘Blessthemoon’ and module name ‘RelatedQuantity. The xml for our module xml file, Blessthemoon_RelatedQuantity.xml looks like this:

1<?php xml version=”1.0″?>

2<config>

3<modules>

4<Blessthemoon_RelatedQuantity>

5<active>true</active>

6<codePool>local</codePool>

7</Blessthemoon_RelatedQuantity>

8</modules>

9</config>

and config.xml like this:

1<?xml version=”1.0″?>

2<config>

3<modules>

4<blessthemoon_relatedquantity>

5<version>0.1.0</version>

6</blessthemoon_relatedquantity>

7</modules>

8<global>

9<models>

10<sales>

11<rewrite>

12<quote>Blessthemoon_RelatedQuantity_Model_Quote</quote>

13</rewrite>

14</sales>

15</models>

16</global>

17</config>

Note how we rewrite Mage_Sales_Model_Quote here:

(1) Model from Mage_Sales_Model_Quote is represented after <global> as <models>.

(2) Sales and Quote from Mage_Sales_Model_Quote are either side of the <rewrite> as <sales><rewrite><quote>. If we had a subdirectory (or subdirectories), as in the case of a rewrite for Mage_Sales_Model_Quote_Item, for example, then Quote and Item would be on the right side of <rewrite> thus <sales><rewrite><quote_item>. It’s worth noting that resource models are a little different. When overwriting Mage_CatalogSearch_Model_Resource_Fulltext, for example, CatalogSearch and Resource are placed to the left of <rewrite> thus <catalogsearch_resource><rewrite><fulltext>.

(3) And lastly, the class extension is placed between the <quote> tags thus <quote>Blessthemoon_RelatedQuantity_Model_Quote</quote>.

Helpers and Blocks follow the same setup, substituting <blocks> or <helpers> for <models> of course. Controllers are different again.

The class rewrite Blessthemoon/RelatedQuantity/Model/Quote.php looks like this:-

10class Blessthemoon_RelatedQuantity_Model_Quote extends Mage_Sales_Model_Quote

11{

12public function addProductAdvanced(Mage_Catalog_Model_Product $product, $request = null, $processMode = null)

13{

14if ($request === null) {

15if ($product->getAttributeSetId() == “4”) {

16$allItems = $this->getAllItems();

17foreach ($allItems as $item) {

18if ($item->getProduct()->getAttributeSetId() == “9” && !($this->getItemId())){

19$request = $item->getQty();

20} else {

21$request = 1;

22}

23}

24} else {

25$request = 1;

26}

27}

28if (is_numeric($request)) {

29$request = new Varien_Object(array(‘qty’=>$request));

30}

31if (!($request instanceof Varien_Object)) {

32Mage::throwException(Mage::helper(‘sales’)->__(‘Invalid request for adding product to quote.’));

33}

34

35$cartCandidates = $product->getTypeInstance(true)

36->prepareForCartAdvanced($request, $product, $processMode);

:

95}

96

97}

Not all lines are shown. Those not shown are unchanged from the original method. New lines are shown in red.

This is how it works. When a related product is added, $request is null, and lines 15 through 26 determine what happens next.

For this client, product types are defined by the attribute set assigned to them. Main products have an attribute set ID of 9, whereas the related product ‘warranty’ has an attribute set ID of 4. As a related product comes through the code, a check is made in line 15 as to whether this is a warranty with attribute set of 4.

Varien_Object implements PHP’s magic __call() method, and as Magento’s objects inherit from Varien_Object, we can use the magic getter method, getAttributeSetId() to get the value of the attribute_set_id stored in the product model (and in the catalog_product_entity table in the database). The __call() method uses PHP’s preg_replace() function to replace upper case characters in the camel case string ‘AttributeSetId’ (from the getAttributeSetId() method) with underscores (to give attribute_set_id as a result). This screen shot from the PhpStorm IDE helps with a clear view of the contents of the related product object’s _data property including attribute_set_id.

product_getattributesetid

Next in line 16, we want to get all the items already in the quote, so that we can loop through them and find the main product associated with this related product. In this particular example, I’m adding a main product with its related product to the cart, and I already have 2 items in the cart which were added previously. These were another main product and its related product. If you’re having difficulty envisaging, here’s a screen shot showing the two items already in the cart, and a main product with related product (warranty) that I’m adding now having clicked ‘ADD TO CART’.

screen_2itemsalready

Back to line 16, I’m using getAllItems() to store the items as an array in $allItems. Where did getAllItems() come from? The next screen shot of the IDE shows a listing of methods in the $this object provided by PhpStorm’s code completion functionality, where $this is Blessthemoon_RelatedQuantity_Model_Quote (which extends Mage_Sales_Model_Quote). It’s easy enough, just to select getAllItems() from the list!

getAllItems

Of course, second best, if we really don’t have a suitable IDE with code completion, we could use the following code to get methods for $this:

$class = get_class($this);
$methods = get_class_methods($class);
foreach($methods as $method) {
var_dump($method);
}

or substitute var_dump() with Mage::log() and track the contents of var/log/system.log, ensuring firstly that logging is enabled (Admin Panel > System > Configuration > Advanced/Developer > Log Settings > Enabled = Yes). But why struggle without a solid IDE and debug mode if you don’t have to?

OK, so line 16 provides us with all the items in quote stored in $allItems. So how many are there, the first time we hit this code. In my example, we’ve added the main product as an item to the product model, and, this time through, we’re adding the related product. In the screen shot in the ‘variables window’, we can see 3 items in the _items array, 404, 405 and 406. The first two are items that were already in the cart, as mentioned. The last, 406, is the main product just added, and now we are adding its related product.

Let’s loop through the items, pick out the main product just added, get its quantity, and set this related product’s quantity to that. The main product item is always stored in the quote object before its related product – we saw in addAction() at the start of this post where Magento calls addProduct() for the main product before it calls addProductsByIds() for related products.

Line 18, tests to see whether the item is a main product – main products have an attribute_set_id of 9. We also test whether the item has an item id. Only products already in the quote have an item id, so the main product just added is the only one meeting both conditions, and in line 19, $request is set to the quantity (qty) for the main product.

That’s it! Line 29 converts $request to a Varien_Object with the new quantity as an item in its _data property, and as described already, further downstream the related product is added as an item to the quote with this quantity.

I hope this post has been useful, not only in providing a solution to a not-too-complex problem, but also in showing how I got there.

In the next post, we’ll provide another solution to the problem. But, much lighter touch, we’ll use an observer and won’t rewrite any code. See you soon.


david.mann

Leave a Reply

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