Discussions

Checkout Form: Pages and Panes

The Checkout module is purely a UI module designed to present a form that allows a user to fill out the details of an order and submit payment. It is based on the never released UC Advanced Checkout module I wrote for a client on Drupal 6. It defines several menu paths used for displaying and administering the checkout form, including:

checkout - the URL of the checkout form
checkout/complete/%commerce_order - the URL of the checkout completion page for the given order
admin/commerce/config/checkout - checkout form / behavior settings form
admin/commerce/config/checkout/form - drag and drop checkout form builder
admin/commerce/config/checkout/form/pane/%commerce_checkout_pane - settings form for the given checkout pane

The lone permission defined by the module is Administer checkout, and it governs access to the various checkout settings / form building pages.

The Checkout module defines two new data objects, checkout page and checkout pane. The checkout form is a multistep form made of multiple checkout pages, each comprised of one or more checkout panes, and followed by an optional review page. The checkout page object contains information pertaining to the form's construction for its particular page:

  • page_id -> the numeric ID of the page or the string "review" for the optional review page
  • title -> the Drupal page title for the page
  • help -> the Drupal help text displayed at the top of the page above the form components
  • next_page -> the numeric / string ID of the next page if the "continue" button is used to submit the form; FALSE for the final step of the form
  • prev_page -> the numeric ID of the previous page if the "back" button is used to submit the form; FALSE for the first step of the form
  • back_value -> the string used as the value of the "back" button on the form
  • submit_value -> the string used as the value of the "continue" button on the form

Checkout pages are not defined in their own hook but are dynamically generated based on the current setting on the checkout settings form. However, checkout page objects may still be altered using hook_commerce_checkout_pages_alter($checkout_pages, $review), which is invoked when the function commerce_checkout_pages($review = TRUE) is called to return an array of checkout pages. The altered array may or may not include the review page depending on the first argument when it is called. Checkout pages may also be loaded individually with the function commerce_checkout_page_load($page_id), which will load the full list and only return the checkout page object specified by the first argument.

In the Checkout module and in any other module using its API, the checkout page object will always be a stdClass PHP object. A full page object should use the variable name $checkout_page, and an array of checkout page objects should use the variable name $checkout_pages. The ID on its own may be referred to as $page_id.

As mentioned above, each checkout page is comprised of one or more checkout panes. Checkout panes are represented as fieldsets on the checkout form, and they must be defined by modules using hook_commerce_checout_pane_info(). This hook should return an array of checkout pane objects keyed by a unique pane_id and include the following properties and callbacks for building, displaying, and handling input values of the checkout pane:

  • title => a string used as the title of the fieldset on the checkout form
  • name => Optional. A string used as the name of the checkout pane on administrative forms (for distinguishing between fieldsets that might share the same title on the checkout form); defaults to the title.
  • file => Optional. A filepath, relative from the module's root directory, to include when the checkout pane is loaded; this allows you to put callback functions for the pane in a separate file that is only loaded when necessary.
  • page => Optional. A numeric page ID on which the pane should appear; defaults to 1.
  • collapsible => Optional. Boolean indicating whether or not the pane's fieldset should be collapsible; defaults to FALSE.
  • collapsed => Optional. Boolean indicating whether or not the pane's fieldset should be collapsed by default; defaults to FALSE.
  • weight => Optional. Numeric weight value for the fieldset on the checkout form; defaults to 0.
  • enabled => Optional. Boolean indicating whether or not the pane should be included on the checkout form at all; defaults to TRUE.
  • review => Optional. Boolean indicating whether or not the pane should be represented on the checkout review page; defaults to TRUE.
  • callbacks => An associative array of callback functions for the pane using the callback keys described below.

Checkout pane objects may be altered upon load by modules implementing hook_commerce_checkout_pane_info_alter($checkout_pane). The checkout pane object will include the pane_id. This hook is invoked by the function commerce_checkout_panes() on each checkout pane individually before it is included in an array of checkout pane objects. To load a single pane, an appropriate array of filter arguments can be passed to that function or the function commerce_checkout_pane_load($pane_id) may be used. Because some settings of a checkout pane are configurable via the checkout form builder (page, collapsible / collapsed, weight, enabled, and review), there is also an API function to save checkout pane, commerce_checkout_pane_save($checkout_pane). An additional function is included to reset a checkout pane to its module defined state, commerce_checkout_pane_reset($pane_id).

The callback functions are used in different context and therefore receive different parameters and are expected to have different types of return values. A single API function should be used to check for the availability of a callback function, because it will automatically include the pane's optional include file. It is commerce_checkout_pane_callback($checkout_pane, $callback) and receives the full checkout pane object and the key of the particular callback you want to check for:

settings_form - this callback is used to build the pane's settings form accessible through the checkout form builder UI; it receives the $checkout_pane as a parameter and is expected to return an array of form elements that will be saved in the manner of Drupal's system_settings_form().

checkout_form - this callback is used to populate the pane's fieldset on the checkout form; it receives the $form_state, $checkout_pane, and checkout $order as parameters and is expected to return an array of form elements that will be added to the fieldset; the array may also alter the pane's fieldset array by including top level #attribute keys in the form array.

checkout_form_validate - this callback should be a form validate handler that will be added to the checkout form's #validate array when the checkout pane is added to the current page of the form. It should function as any other form validate handler.

checkout_form_submit - this callback should be a form submit handler that will be called from the checkout form's own submit handler; it receives the $form, $form_state, and checkout $order as parameters and is not expected to return anything. It should function as and adhere to the best practices of any other form submit handler, but it should not alter the redirect or rebuild values of the $form_state.

review - this callback is used to get review data for the input collected by the checkout pane; it receives the $form_state and checkout $order as parameters and is expected to return an string or an array of review data that will be formatted into a table for review on the optional checkout review page. A string of review data will be displayed in a single cell row in the review table, like the cart contents review. An array of review data should be an associative array of (human-readable) key / value pairs that will be formatted into two column rows in the review table.

In the Checkout module and in any other module using its API, the checkout pane object will always be a stdClass PHP object. A full pane object should use the variable name $checkout_pane, and an array of checkout pane objects should use the variable name $checkout_panes. The ID on its own may be referred to as $pane_id.

The checkout form is constructed by the function commerce_checkout_form() in commerce_checkout.pages.inc. It works as a multistep form and will always include a hidden element for the uid, a value element for the full account object of the current user, a value element for the full order object being checked out, a value element for the full checkout page object of the current step, and a markup element for Drupal help text of the current step. Checkout pane fieldsets will be added for every pane on the current page, and a set of buttons will be shown depending on the current page.

The submit handler checks which button was used to submit the form (it will either be a Cancel, Back, Continue, or Submit Order button). The page of the form will be updated in the $form_state['storage'] array, and the form will be marked for rebuilding. This will continue until the Submit Order button is clicked (which will either be on the optional review page or on the final checkout page if the review page is turned off).

What happens upon checkout completion is still being worked out, but it's the basics of user account creation, order status updating, redirection to the checkout completion page, and an event invocation for Rules.

To see example implementations of hook_commerce_checkout_pane_info() and the related callbacks, look in the order and cart modules.

Major changes from Ubercart include multi-step capability, an optional review page, the existence of a single checkout $order object throughout the entire process, drag-and-drop checkout form building, and the absence of any ability to alter strings on things like buttons and fieldset titles (this should be done at the module level, particularly using an appropriate translation interface).

Ryan Szrama
Posted: May 11, 2010

Comments

Ryan Ryan Szrama on May 13, 2010

Got some good feedback through other channels from mfer and will be changing the $filter argument in the Checkout API and other modules to $conditions to line up with core usage.

masu0105 on September 5, 2011

Hi!

I have been trying to implement hook_commerce_checkout_pages_alter($checkout_pages, $review) in a custom module but i doesn't seem to be called. Is it still there in Commerce 7.x-1.0?

zurazura on August 8, 2012

Think the signature is also as follows...argument should be passed as reference (&) so the change actually kicks in.

hook_commerce_checkout_pane_info_alter(&$checkout_pane)

jmljunior on August 24, 2012

Right, so is there an easy way to add text to the checkout panes? Or am I looking at a form_alter. I just want to add some text in the "Account Information" pane notifying the user that an account will automatically be created upon checkout completion.

tmsimont on December 10, 2012

I just wasted a few hours on an issue that comes from undocumented, non-standard form validation:

This page reads
checkout_form_validate - this callback should be a form validate handler that will be added to the checkout form's #validate array when the checkout pane is added to the current page of the form. It should function as any other form validate handler.

what this does not mention is that checkout_form_validate MUST return TRUE if you want your checkout process to move forward on successful validation. This is not in accordance with standard Drupal form API: http://api.drupal.org/api/drupal/developer%21topics%21forms_api_referenc..., so this doesn't function quite like "any other form validate handler"

Hope this info helps others..

Otherwise this is a great API -- thanks Ryan.

andyg5000 Andy Giles on October 4, 2013

I just had the same problem as tmsimont. Updating this post with that information would have been helpful as I didn't read the comments until after I figured out the issue.

tester1 on October 10, 2013

Can you tell me what I'm missing? My goal is to perform custom validation on several panes on a page -- only once after the page has been submitted. Currently my pane validation logic is working correctly, but it's being performed MANY times -- like if the checkout page is refreshed or even if the user tabs between fields on the page.

I've setup:

<?php
my_module_commerce_checkout_pane_info_alter
(&$checkout_pane)
{
 
// For each pane I want to validate
 
$checkout_pane['my_pane_id']['callbacks'] = array(
   
'checkout_form_validate' => 'my_module_checkout_review_pane_checkout_form_validate',
  );
 
// etc
}

my_module_checkout_review_pane_checkout_form_validate($form, &$form_state, $checkout_pane, $order)
{
 
$valid = TRUE;
 
$pane_id = $checkout_pane['pane_id'];
  if(
$pane_id == 'my_pane_id' )
  {
   
// If theres a problem, set valid to FALSE
 
}
return
$valid;
}
?>

Generally this is works as desired from the user's point of view, but behind the scenes the validation is being performed many times -- and since my validation is expensive involving some remote validation services I need to avoid that.

Thanks!