Change the Product Quantity Added to Cart – part 3: Create Observer

Change the Product Quantity Added to Cart – part 3: Create Observer

In this post, we take a different approach. Part 2 provided a solution based on a rewrite. Here, our approach is light touch. We track down a suitable event and build an observer to provide the solution. Again, as we did in part 2, we’ll focus on how we got there, as well as on the end goal itself…

The Magento application is teeming with events we can tap into. My search across the application with my IDE revealed over 400 usages of the term ‘dispatchEvent’. (You could also use grep recursively at the command line and save the results with line numbers into a file: grep -r -n “Mage::dispatchEvent” app/ > grep_results)

The approach is a simple one. Find an event ‘near’ to the code update we require and build an observer to listen to when the event is fired to execute the required code. As an aside, we could create our own event, but this eliminates the light-touch advantages we’re looking for, as we’d then need to rewrite existing code to provide the new event!

Here’s an example of an event:-

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

We can listen for it by name (‘sales_quote_product_add_after’) and specify what method should execute when it fires, within tags in the config.xml file of a custom module. And we also need an observer class model containing the method to be executed. Note the event typically passes local data through to the method as in this example (array(‘items’ => $items))). We’ll get to the coding a little later, but let’s begin by searching for an appropriate event.

Remember, our aim is to ensure that for the number of main products added to the cart, there are an equal number of warranties added. Warranties have been setup as related products to main products. Out of the box, when ‘ADD TO CART’ is selected, Magento adds only one related product to the cart, regardless of the number of main products added. We’ll begin by finding the event that appears to meet the requirement best and then build the method.

We’ve the deduced the following criteria for the ‘right’ event:

(1) We’d prefer an event that only fires when adding products to the cart. If it also fires, when changing cart quantities, then we’ll need additional coding to ensure that these changes are unaffected. Remember according to the requirement here, the customer must be free to change quantities after products have been added to the cart.

(2) The event must occur after native code has set the quantity for the related product. Obvious really, as we can’t have native code simply undoing the change made by our observer method.

(3) We’d like the event to pass through as much useful data as possible to the observer method. This will lessen the amount of coding required to make this data accessible in the method.

Are there more?

If you’re getting the impression that picking the ‘right’ event can be as much of an art as a science, you’re catching on.

Let’s review the path that we take through the code when adding related products, noting events as we encounter them. If you read part 2 of this series, this path should be familiar, as we looked at it in more depth there.

The following table shows key methods along this path. Again, a good deal has been left out to simplify our analysis. Our focus is the derivation of product quantity and then adding it to the quote. Step numbers have been added to emphasise the sequence and for reference purposes. Key for now, events have been added, and these are shown in yellow. These were found using ‘find’ in my IDE and then tracking through the code in debug mode to see which events were hit.

Code Path for Adding a Related Product

CodePathPart3

The rewrite we did in part 2 to derive related product quantity was for step 4. You can see in summary here how objects are instantiated, for product (step 2), session and quote (step 3), product type (step 5), quote item collection (step 10) and quote item (step 11). The events that we want to talk about in this post are steps 6, 14, 19 and 20.

The Event catalog_product_type_prepare_full_options

The event dispatched in step 6 may meet our requirement. In our example its event name is ‘catalog_product_type_prepare_full_options’ where ‘full’ is the value of $processMode, inserted into the ‘%s’ of ‘catalog_product_type_prepare_%s_options’ by the sprintf() PHP function. We’ve also identified this as event A to simplify the way we reference it. It’s fired after step 4, which sets the quantity for the related product in the Varien_Object $request as 1. So in theory, all we need do is reset it to the quantity for the main product.

Before we code the observer method, we need to set up the module. Again, if you need a refresher on module setup please take a look at earlier posts.

We’ll use the namespace of the previous post with a module name of RelatedObserver. Here’s Blessthemoon_RelatedObserver.xml:

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

2<config>

3<modules>

4<Blessthemoon_RelatedObserver>

5<active>true</active>

6<codePool>local</codePool>

7</Blessthemoon_RelatedObserver>

8</modules>

9</config>

and here’s config.xml:

1<?xml version=”1.0″?>

2<config>

3<modules>

4<Blessthemoon_RelatedObserver>

5<version>0.1.0</version>

6</Blessthemoon_RelatedObserver>

7</modules>

8<global>

9<models>

10<relatedobserver>

11<class>Blessthemoon_RelatedObserver_Model</class>

12</relatedobserver>

13</models>

14<events>

15<catalog_product_type_prepare_full_options>

16<observers>

17<relatedobserver>

18<type>model</type>

19<class>Blessthemoon_RelatedObserver_Model_Observer</class>

20<method>updateQuantity</method>

21</relatedobserver>

22</observers>

23</catalog_product_type_prepare_full_options>

24</events>

25</global>

26</config>

Note the event that we’re targeting is named immediately after the <events> tag. The class and method executed when the event fires are between the tags for module name <relatedobserver>. Note we also must configure the new model, and we’ve identified the model class prefix (line 11) after <models> and <relatedobserver> tags. Here’s the code for the model class and method we need:

1

2class Blessthemoon_RelatedObserver_Model_Observer

3{

4public function updateQuantity($observer)

5{

6If ($observer->getProduct()->getAttributeSetId() == “4”) {

7$items = Mage::getSingleton(‘checkout/session’)->getQuote()->getAllItems();

8foreach ($items as $item) {

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

10$observer->getBuyRequest()->setQty($item->getQty());

11}

12}

13}

14}

15}

Our strategy here is simply to change the product quantity (qty) for the Varien_Object, buy_request, to the required quantity. This object is then used later on by the code to set the quantity for this quote item. As we’ll see later, when we look at tapping into events further downstream, our strategy may be different. We may have to reset the quantity in the quote item, because by then the quantity may already been applied to the quote.

Here we’ve used the data passed from the event dispatch into $observer to best advantage. As you can see from the screen shot of the IDE (next), we’ve got access to the product object, from which we can get the attribute_set_id for our condition in line 6, and we can set the product quantity (qty) in the Varien_Object, buy_request, in line 10.

But $observer gives us no access to product quantities for the main product, so in line 7 we’ve instantiated Mage_Checkout_Model_Session as a singleton object. The use of getSingleton() ensures this is the same object that Magento instantiated back in step 3 of the code path table above. We’re using getAllItems() to get the array again, as we did in part 2 of this series for the class rewrite.

eventA

We loop through the items in the array starting on line 8. If we already have items in the cart, there will be more than one, otherwise, if the cart is empty, then there’s only the main product which has already been processed. Remember the fixed sequence of the addAction() method in CartController, first the main product is added, followed by the related product. So when we get to adding the related product, the main product is already in the array.

In line 9, we’re checking that this is in fact the main product of interest. Remember, for this client’s requirement, products are identified by their attribute set. So we’re looking for an attribute set ID of 9. We also check that this isn’t a product from the cart, which will already have an item_id.

Finally, line 10 gets the quantity from $item and sets the quantity of buy_request accordingly.

So that’s a working solution. We could stop here. This beats the rewrite of addProductAdvanced() in part 2 of this series, because even if the method changes in future Magento releases, we don’t care. We also side step problems associated with two or more modules rewriting the same code. As long as any future Magento release fires this event, we should be fine. So if you’ve seen enough, you could quit here.

But I’d like to take a look at other events encountered further downstream, and explore whether they meet the requirement too (glutton for punishment?).

The Event sales_quote_add_item

Take a look at the event shown in the table above in step 14 (ref B). This is ‘sales_quote_add_item’.

Let’s take a look at how far downstream this is fired. It’s fired once, when we add a main product, and then again, when the related product treads through the same code. At this point, the quote item has been instantiated, but a product quantity hasn’t been added yet. This event doesn’t appear to meet the criteria established earlier, i.e. it doesn’t occur after native code has set the quantity for the related product, so we run the risk of native code simply undoing any changes we make within our observer method. Let’s see how this pans out.

We’ll use the existing module, replace the event name in config.xml with the name ‘sales_quote_add_item’ and recode the updateQuantity() method in Observer.php.

Here’s the new Observer code:

1

2class Blessthemoon_RelatedObserver_Model_Observer

3{

4public function updateQuantity($observer)

5{

6If ($observer->getQuoteItem()->getProduct()->getAttributeSetId() == “4”) {

7$items = $observer->getQuoteItem()->getQuote()->getAllItems();

8foreach ($items as $item) {

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

10$mainQuantity = $item->getQty();

11}

12if ($item->getProduct()->getAttributeSetId() == “4” && !($item->getItemId())){

13$item->setData(‘qty’,$mainQuantity);

14}

15}

16}

17}

18}

We’re using what’s been passed into $observer to full effect. Firstly, we’re accessing the quote item and obtaining the attribute set ID from its product object to ensure we have a product which is a warranty ( = 4 ). We’ve also accessed the quote object stored within quote item, which allows us to access all quote items already added, including their quantities. Let me emphasise how without debug and an easy-to-use IDE, this would have been heavy weather.

Note that I’ve used setData(‘qty’,$mainQuantity) in line 13 rather than the Magento setter setQty($mainQuantity). The reason is there’s a setQty() method in the Mage_Sales_Model_Quote_Item class which overrides Varien_Object’s setter method, and I’ve decided not to use it.

In the next screen shot, in the variables window of my IDE, you can see that when we process the related product, we have 4 quote items available to us (414 – 417). Two of these, 414 and 415, are items already in the cart, saved within the database. The item 416 is the main product added to quote as an item before this one. The item 417 is the related product that we are currently adding.

screenB

The next image shows the contents of _data for the main product (416) compared with those of the related product (417).

mainVrelated_eventB

Whereas 416 already has a quantity that we can access, 417 does not. Steps 15 through 18 in the table above add the quantity after this event has been fired.

OK, so what happens if we ‘ADD TO CART’ with this observer in place? Not what I initially expected. Here’s the screen shot showing 3 main products to add, with the box for related product ticked.

addToCartEventB

Click on ‘ADD TO CART’ and hey presto: 3 main products (Blessthemoon 1 x 3) and 4 related products (Warranty 1 x 4)! So why 4 related products in the cart?

Refer back to the code path table above. In step 14, our observer adds a quantity (qty) for related product to the object Mage_Sales_Model_Quote_Item.

In step 15, Magento calls addQty() passing through the value 1 ($candidate->getCartQty()) into $qty. This is the value for product quantity for a related product set in step 4, added to the product object in step 7, and taken through the loop as $candidate in step 8.

Step 16 is: –

$oldQty = $this->getQty(); // where $this is the object Mage_Sales_Model_Quote_Item

which gets the quantity we just added within the observer method (the value 3 in this example), and sets it equal to $oldQty. And what’s the purpose of the variable $oldQty? Well, capturing ‘qty’ into $oldQty is relevant where we’re adding a product which already exists in the cart. Not the case here though, for the products we’re adding, I should hasten to mention.

Then in step 18, we set the quantity in $this (Mage_Sales_Model_Quote_Item) to $oldQty (with a value of 3) plus $qty (with a value of 1). We get a total of 4!

So this is not an event, we’d choose to trigger our observer method. That’s one success and one failure this far. Let’s take a look at two events which are fired after quantities are added to the quote item, ‘sales_quote_item_qty_set_after’ and ‘sales_quote_product_add_after’.

The Event sales_quote_item_qty_set_after

The event ‘sales_quote_item_qty_set_after’ in step 19 would not be an ideal choice as it’s fired not only when we add an item to the quote but also when we either change item quantities in the cart (with ‘UPDATE SHOPPING CART’) or remove an item from the cart (with the Magento out-of-the-box trash can icon). We discussed these two controller methods, updatePostAction() and deleteAction() in part 2 of this series.

As an aside there was also another event much further downstream that I thought of early on in this project, ‘checkout_cart_save_before’. Again, the issue with it is that it’s used by multiple controller methods.

Notwithstanding, unnecessary firing of sales_quote_item_qty_set_after, we could code an observer to ignore the item if it’s already in the cart. We’ve done this when coding observer methods already, using this condition,

!($item->getItemId())

because we said that only items already in the cart (saved in the database) have a quote item ID.

Here then is the coded solution, which again makes good use of the data passed into $observer in the updateQuantity() method.

1

2class Blessthemoon_RelatedObserver_Model_Observer

3{

4public function updateQuantity($observer)

5{

6if ($observer->getItem()->getProduct()->getAttributeSetId() == “4” && !($observer->getItem()->getItemId())) {

7$items = $observer->getItem()->getQuote()->getAllItems();

8foreach ($items as $item) {

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

10$mainQty = $item->getQty();

11$observer->getItem()->setQtyToAdd($mainQty);

12$observer->getItem()->setData(‘qty’,$mainQty);

13}

14}

15}

16}

17}

Again, as we’ve used the existing module, don’t forget to replace the event name in config.xml with the name ‘sales_quote_item_qty_set_after’.

Let’s finally take a look at one more event to see if it is suitable.

The Event sales_quote_product_add_after

The advantage this event has over the event just tried (sales_quote_item_qty_set_after) is that it is not fired by either changing or removing a product in the cart, i.e. just fired by adding a product.

This event passes all quote items through to the observer method in an array. Items have product quantities at this stage. So our strategy is simple to take each item through the loop, and check if it’s a warranty (attribute set ID = 4). If it is, loop through the quote items for that item, and find the main product for this warranty (attribute set ID = 9). Finally, apply the quantity of main product to the quantities for the warranty (related product).

Again, replace the event name in config.xml with the name ‘sales_quote_product_add_after’ and change the code in Observer.php to this:

1

2class Blessthemoon_RelatedObserver_Model_Observer

3{

4public function updateQuantity($observer)

5{

6$items = $observer->getItems();

7foreach ($items as $item) {

8if ($item->getProduct()->getAttributeSetId() == “4”){

9$quoteItems = $item->getQuote()->getAllItems();

10foreach ($quoteItems as $quoteItem) {

11if ($quoteItem->getProduct()->getAttributeSetId() == “9” && !($quoteItem->getItemId())){

12$item->setQtyToAdd($quoteItem->getQty());

13$item->setQty($quoteItem->getQty());

14}

15}

16}

17}

18}

19}

Endnote

We’ve sought an event in this post that we could use in conjunction with an observer instead of the rewrite proposed in part 2. Inherent problems in rewrites are potential multiple rewrites of the same method, and potential issues when it comes to a Magento upgrade (as is at some point inevitable, if we continue to trade).

We wanted an event that ensured that coding changes would not be undone by subsequent native code and allowed simple coding exploiting to the full data passed through from the event to the method.

Arguably, ‘catalog_product_type_prepare_full_options’ and ‘sales_quote_product_add_after’ met this requirement best and ‘ sales_quote_add_item’ not at all.

My preference would be to use ‘catalog_product_type_prepare_full_options’ because unlike the others it doesn’t attempt to put the quantity ‘right’ after it has already been coded ‘wrong’.

The parts of this series have been a little long, hopefully not too long, because along the way I wanted to show how I arrived at the solution. The main tool in getting me there has been a solid IDE with debug mode.

That really is it!


david.mann

Leave a Reply

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