The Magento checkout flow involves templates, block classes and JavaScript to work together in order for the customer to successfully place an order. This article will explain what files are involved and how the checkout works.
To start with, it is worth looking at the checkout.xml
layout file within the RWD theme and specifically looking for the checkout_onepage_index
layout handle.
This layout handle defines the blocks and templates that are used on the checkout page.
<?xml version="1.0"?> <layout version="0.1.0"> .... <checkout_onepage_index translate="label"> <label>One Page Checkout</label> <!-- Mage_Checkout --> <remove name="left"/> <reference name="root"> <action method="setTemplate"><template>page/2columns-right.phtml</template></action> </reference> <reference name="right"> <action method="unsetChildren"></action> <block type="page/html_wrapper" name="checkout.progress.wrapper" translate="label"> <label>Checkout Progress Wrapper</label> <action method="setElementId"><value>checkout-progress-wrapper</value></action> <block type="checkout/onepage_progress" name="checkout.progress" before="-" template="checkout/onepage/progress.phtml"> <block type="checkout/onepage_progress" name="billing.progress" template="checkout/onepage/progress/billing.phtml"></block> <block type="checkout/onepage_progress" name="shipping.progress" template="checkout/onepage/progress/shipping.phtml"></block> <block type="checkout/onepage_progress" name="shippingmethod.progress" template="checkout/onepage/progress/shipping_method.phtml"></block> <block type="checkout/onepage_progress" name="payment.progress" template="checkout/onepage/progress/payment.phtml"></block> </block> </block> </reference> <reference name="content"> <block type="checkout/onepage" name="checkout.onepage" template="checkout/onepage.phtml"> <block type="checkout/onepage_login" name="checkout.onepage.login" as="login" template="checkout/onepage/login.phtml"> <block type="page/html_wrapper" name="checkout.onepage.login.before" as="login_before" translate="label"> <label>Login/Registration Before</label> <action method="setMayBeInvisible"><value>1</value></action> </block> </block> <block type="checkout/onepage_billing" name="checkout.onepage.billing" as="billing" template="checkout/onepage/billing.phtml"/> <block type="checkout/onepage_shipping" name="checkout.onepage.shipping" as="shipping" template="checkout/onepage/shipping.phtml"/> <block type="checkout/onepage_shipping_method" name="checkout.onepage.shipping_method" as="shipping_method" template="checkout/onepage/shipping_method.phtml"> <block type="checkout/onepage_shipping_method_available" name="checkout.onepage.shipping_method.available" as="available" template="checkout/onepage/shipping_method/available.phtml"/> <block type="checkout/onepage_shipping_method_additional" name="checkout.onepage.shipping_method.additional" as="additional" template="checkout/onepage/shipping_method/additional.phtml"/> </block> <block type="checkout/onepage_payment" name="checkout.onepage.payment" as="payment" template="checkout/onepage/payment.phtml"> <block type="checkout/onepage_payment_methods" name="checkout.payment.methods" as="methods" template="checkout/onepage/payment/info.phtml"> <action method="setMethodFormTemplate"><method>purchaseorder</method><template>payment/form/purchaseorder.phtml</template></action> </block> <block type="core/template" name="checkout.onepage.payment.additional" as="additional" /> <block type="core/template" name="checkout.onepage.payment.methods_additional" as="methods_additional" /> </block> <block type="checkout/onepage_review" name="checkout.onepage.review" as="review" template="checkout/onepage/review.phtml"/> </block> </reference> </checkout_onepage_index> </layout>
The main points to take away from here can be seen below.
2columns-right
templatecontent
area that contains the actual steps, and a right
area that contains the progress steps.content
area has a onepage.phtml
wrapper template file, and the right
area has a progress.phtml
wrapper template file.Looking at the layout configuration and pinpointing the onepage.phtml
template file, it has a block type of checkout/onepage
.
If you’re unfamiliar with Magento blocks, this means that the template file has a corresponding block class located in app/code/core/Mage/Checkout/Block/Onepage.php
.
The Onepage.php
file consists of the following code.
class Mage_Checkout_Block_Onepage extends Mage_Checkout_Block_Onepage_Abstract { /** * Get 'one step checkout' step data * * @return array */ public function getSteps() { $steps = array(); $stepCodes = $this->_getStepCodes(); if ($this->isCustomerLoggedIn()) { $stepCodes = array_diff($stepCodes, array('login')); } foreach ($stepCodes as $step) { $steps[$step] = $this->getCheckout()->getStepData($step); } return $steps; } /** * Get active step * * @return string */ public function getActiveStep() { return $this->isCustomerLoggedIn() ? 'billing' : 'login'; } }
Taking a closer look at the _getSteps()
method, we can see it makes a call to _getStepCodes()
and returns an array of step codes.
<?php class Mage_Checkout_Block_Onepage extends Mage_Checkout_Block_Onepage_Abstract { .... protected function _getStepCodes() { return array('login', 'billing', 'shipping', 'shipping_method', 'payment', 'review'); } .... }
Each checkout step will also have its own corresponding block class, such as the billing step has a block class of Mage_Checkout_Block_Onepage_Billing
.
<block type="checkout/onepage_billing" name="checkout.onepage.billing" as="billing" template="checkout/onepage/billing.phtml"/>
When the checkout page is first loaded, within the indexAction()
method, a check to see whether the onepage checkout is enabled and whether the customer’s quote can be obtained are made.
<?php class Mage_Checkout_OnepageController.php extends Mage_Checkout_Controller_Action { .... public function indexAction() { if (!Mage::helper('checkout')->canOnepageCheckout()) { Mage::getSingleton('checkout/session')->addError($this->__('The onepage checkout is disabled.')); $this->_redirect('checkout/cart'); return; } $quote = $this->getOnepage()->getQuote(); if (!$quote->hasItems() || $quote->getHasError()) { $this->_redirect('checkout/cart'); return; } } .... }
When a customer progresses through the checkout steps, the checkout page does not reload. Because of this, we need to have a method that doesn’t just check if the checkout is enabled or disabled, or the quote still exists within the indexAction()
method. Luckily, there is a expireAjax()
method that is used at the top of each “Action” method when moving through the checkout.
This is present when a customer continues through the checkout steps, and each step calls their own action, e.g. completing the billing step will call the saveBillingAction()
method.
public function saveBillingAction() { if ($this->_expireAjax()) { return; }
The _expireAjax()
method consists of the following code.
protected function _expireAjax() { if (!$this->getOnepage()->getQuote()->hasItems() || $this->getOnepage()->getQuote()->getHasError() || $this->getOnepage()->getQuote()->getIsMultiShipping() ) { $this->_ajaxRedirectResponse(); return true; } $action = strtolower($this->getRequest()->getActionName()); if (Mage::getSingleton('checkout/session')->getCartWasUpdated(true) && !in_array($action, array('index', 'progress')) ) { $this->_ajaxRedirectResponse(); return true; } return false; }
There are a few checks made in the code.
The JavaScript file responsible for guiding the customer through the checkout steps is the opcheckout.js
file.
Within this file, the checkout step codes are also defined here.
var Checkout = Class.create(); Checkout.prototype = { initialize: function(accordion, urls){ this.accordion = accordion; this.progressUrl = urls.progress; this.reviewUrl = urls.review; this.saveMethodUrl = urls.saveMethod; this.failureUrl = urls.failure; this.billingForm = false; this.shippingForm= false; this.syncBillingShipping = false; this.method = ''; this.payment = ''; this.loadWaiting = false; this.steps = ['login', 'billing', 'shipping', 'shipping_method', 'payment', 'review']; .... }, .... }
When the customer gets to the review step and finally places the order, a few things happen. Firstly, the save method of the Review
class in opcheckout.js
gets called.
var Review = Class.create(); Review.prototype = { .... save: function(){ if (checkout.loadWaiting!=false) return; checkout.setLoadWaiting('review'); var params = Form.serialize(payment.form); if (this.agreementsForm) { params += '&'+Form.serialize(this.agreementsForm); } params.save = true; var request = new Ajax.Request( this.saveUrl, { method:'post', parameters:params, onComplete: this.onComplete, onSuccess: this.onSave, onFailure: checkout.ajaxFailure.bind(checkout) } ); }, .... }
A file submits an AJAX request using the url this.saveUrl
, which is set at the top of the Review
class.
We can see that the URL gets passed in as a parameter in the initialize method. This is seen within some inline JavaScript in the review template file.
<script type="text/javascript"> //<![CDATA[ review = new Review('<?php echo $this->getUrl('checkout/onepage/saveOrder', array('form_key' => Mage::getSingleton('core/session')->getFormKey())) ?>', '<?php echo $this->getUrl('checkout/onepage/success') ?>', $('checkout-agreements')); //]]> </script>
This is the same in ever checkout step. The template files contain script tags that pass in the save URLs to the opcheckout.js
file.
So we can see that the saveOrderAction()
method gets called when the customer places an order.
public function saveOrderAction() { if (!$this->_validateFormKey()) { $this->_redirect('*/*'); return; } if ($this->_expireAjax()) { return; } $result = array(); try { $requiredAgreements = Mage::helper('checkout')->getRequiredAgreementIds(); if ($requiredAgreements) { $postedAgreements = array_keys($this->getRequest()->getPost('agreement', array())); $diff = array_diff($requiredAgreements, $postedAgreements); if ($diff) { $result['success'] = false; $result['error'] = true; $result['error_messages'] = $this->__('Please agree to all the terms and conditions before placing the order.'); $this->getResponse()->setBody(Mage::helper('core')->jsonEncode($result)); return; } } $data = $this->getRequest()->getPost('payment', array()); if ($data) { $data['checks'] = Mage_Payment_Model_Method_Abstract::CHECK_USE_CHECKOUT | Mage_Payment_Model_Method_Abstract::CHECK_USE_FOR_COUNTRY | Mage_Payment_Model_Method_Abstract::CHECK_USE_FOR_CURRENCY | Mage_Payment_Model_Method_Abstract::CHECK_ORDER_TOTAL_MIN_MAX | Mage_Payment_Model_Method_Abstract::CHECK_ZERO_TOTAL; $this->getOnepage()->getQuote()->getPayment()->importData($data); } $this->getOnepage()->saveOrder(); $redirectUrl = $this->getOnepage()->getCheckout()->getRedirectUrl(); $result['success'] = true; $result['error'] = false; } .... }
This calls the saveOrder()
method in the Mage_Checkout_Model_Type_Onepage
class.
public function saveOrder() { $this->validate(); $isNewCustomer = false; switch ($this->getCheckoutMethod()) { case self::METHOD_GUEST: $this->_prepareGuestQuote(); break; case self::METHOD_REGISTER: $this->_prepareNewCustomerQuote(); $isNewCustomer = true; break; default: $this->_prepareCustomerQuote(); break; } $service = Mage::getModel('sales/service_quote', $this->getQuote()); $service->submitAll(); .... }
The switch statements checks the checkout method used by the customer when going through the checkout, and then a call is made to the submitAll()
method of the Mage_Sales_Model_Service_Quote
class.
public function submitAll() { // don't allow submitNominalItems() to inactivate quote $shouldInactivateQuoteOld = $this->_shouldInactivateQuote; $this->_shouldInactivateQuote = false; try { $this->submitNominalItems(); $this->_shouldInactivateQuote = $shouldInactivateQuoteOld; } catch (Exception $e) { $this->_shouldInactivateQuote = $shouldInactivateQuoteOld; throw $e; } // no need to submit the order if there are no normal items remained if (!$this->_quote->getAllVisibleItems()) { $this->_inactivateQuote(); return; } $this->submitOrder(); }
The submitAll()
method delegates to the submitOrder()
method. Quite a few thing happen within this method.
First of all, the order is validated by checking the shipping information. The customer and quote objects are added to the transaction model.
public function submitOrder() { $this->_deleteNominalItems(); $this->_validate(); $quote = $this->_quote; $isVirtual = $quote->isVirtual(); $transaction = Mage::getModel('core/resource_transaction'); if ($quote->getCustomerId()) { $transaction->addObject($quote->getCustomer()); } $transaction->addObject($quote); $quote->reserveOrderId(); .... }
Note that also the Order ID gets reserved here.
We then have the area where quote data gets converted into order data using the Mage_Sales_Model_Convert_Quote
model.
public function submitOrder() { .... if ($isVirtual) { $order = $this->_convertor->addressToOrder($quote->getBillingAddress()); } else { $order = $this->_convertor->addressToOrder($quote->getShippingAddress()); } $order->setBillingAddress($this->_convertor->addressToOrderAddress($quote->getBillingAddress())); if ($quote->getBillingAddress()->getCustomerAddress()) { $order->getBillingAddress()->setCustomerAddress($quote->getBillingAddress()->getCustomerAddress()); } if (!$isVirtual) { $order->setShippingAddress($this->_convertor->addressToOrderAddress($quote->getShippingAddress())); if ($quote->getShippingAddress()->getCustomerAddress()) { $order->getShippingAddress()->setCustomerAddress($quote->getShippingAddress()->getCustomerAddress()); } } $order->setPayment($this->_convertor->paymentToOrderPayment($quote->getPayment())); foreach ($this->_orderData as $key => $value) { $order->setData($key, $value); } foreach ($quote->getAllItems() as $item) { $orderItem = $this->_convertor->itemToOrderItem($item); if ($item->getParentItem()) { $orderItem->setParentItem($order->getItemByQuoteItemId($item->getParentItem()->getId())); } $order->addItem($orderItem); } $order->setQuote($quote); .... }
Finally, we save the transaction, deactivate the quote by setting the is_active
column to 0
, and we dispatch a few events.
public function submitOrder() { .... Mage::dispatchEvent('checkout_type_onepage_save_order', array('order'=>$order, 'quote'=>$quote)); Mage::dispatchEvent('sales_model_service_quote_submit_before', array('order'=>$order, 'quote'=>$quote)); try { $transaction->save(); $this->_inactivateQuote(); Mage::dispatchEvent('sales_model_service_quote_submit_success', array('order'=>$order, 'quote'=>$quote)); } catch (Exception $e) { .... }
Some of the events are quite interesting to look at. The sales_model_service_quote_submit_before
event gets dispatched and the observer found in the Mage_CatalogInventory
module has a subtractQuoteInventory()
method which gets executed.
This, as the name suggests, decreases the stock of the product(s) ordered.
public function subtractQuoteInventory(Varien_Event_Observer $observer) { $quote = $observer->getEvent()->getQuote(); // Maybe we've already processed this quote in some event during order placement // e.g. call in event 'sales_model_service_quote_submit_before' and later in 'checkout_submit_all_after' if ($quote->getInventoryProcessed()) { return; } $items = $this->_getProductsQty($quote->getAllItems()); /** * Remember items */ $this->_itemsForReindex = Mage::getSingleton('cataloginventory/stock')->registerProductsSale($items); $quote->setInventoryProcessed(true); return $this; }
The event sales_model_service_quote_submit_success
has a different method found in the same observer file as above that reindexes the stock inventory.
public function reindexQuoteInventory($observer) { .... if (count($productIds)) { Mage::getResourceSingleton('cataloginventory/indexer_stock')->reindexProducts($productIds); } .... }
To learn how to add a checkout step into Magento, click here to read the Magento Add Checkout Step article.
Note: This article is based on Magento Community/Open Source version 1.9.