commerce_checkout.pages.inc in Commerce Core 7
The page and form callbacks for use in the checkout form.
File
modules/checkout/includes/commerce_checkout.pages.incView source
<?php
/**
* @file
* The page and form callbacks for use in the checkout form.
*/
/**
* Redirects invalid checkout attempts or displays the checkout form if valid.
*/
function commerce_checkout_router($order, $checkout_page = NULL) {
// Ensure this function is routing the latest revision of the given order in
// case some hook invoked before this page callback is executed updated some
// value on the order that would impact routing.
if (!commerce_order_is_latest_revision($order)) {
entity_get_controller('commerce_order')
->resetCache(array(
$order->order_id,
));
$order = commerce_order_load($order->order_id);
}
$checkout_pages = commerce_checkout_pages();
// If no checkout page is specified, default to the first one.
if (empty($checkout_page)) {
$checkout_page = reset($checkout_pages);
}
// If the user does not have access to checkout the order, return a 404. We
// could return a 403, but then the user would know they've identified a
// potentially valid checkout URL.
if (!commerce_checkout_access($order)) {
return MENU_NOT_FOUND;
}
// If the user is attempting to access an inaccessible page for their order,
// redirect them to the proper page.
if (!commerce_checkout_page_access($checkout_page, $order)) {
$target_uri = commerce_checkout_order_uri($order);
// Only redirect if the target page is different from the page the user was
// trying to access. Otherwise give a 403 error.
if (!empty($target_uri) && $target_uri !== $_GET['q']) {
drupal_goto($target_uri);
}
else {
return MENU_ACCESS_DENIED;
}
}
// Ensure the order can proceed to checkout; if not, redirect away.
if (!commerce_checkout_order_can_checkout($order)) {
drupal_goto('<front>');
}
// Prior to displaying the checkout form, allow other modules to route the
// checkout form.
module_invoke_all('commerce_checkout_router', $order, $checkout_page);
// Update the page title if specified.
if (!empty($checkout_page['title'])) {
drupal_set_title($checkout_page['title']);
}
return drupal_get_form('commerce_checkout_form_' . $checkout_page['page_id'], $order, $checkout_page);
}
/**
* Builds the checkout form for the given order on the specified checkout page.
*
* @param $order
* The fully loaded order object being checked out.
* @param $checkout_page
* The checkout page object representing the current step in checkout.
*/
function commerce_checkout_form($form, &$form_state, $order, $checkout_page) {
global $user;
// Ensure this include file is loaded when the form is rebuilt from the cache.
$form_state['build_info']['files']['form'] = drupal_get_path('module', 'commerce_checkout') . '/includes/commerce_checkout.pages.inc';
$form['#attached']['css'][] = drupal_get_path('module', 'commerce_checkout') . '/theme/commerce_checkout.base.css';
$form['#attached']['css'][] = drupal_get_path('module', 'commerce_checkout') . '/theme/commerce_checkout.theme.css';
$form['#attached']['js'][] = drupal_get_path('module', 'commerce_checkout') . '/commerce_checkout.js';
// Refresh the order object in case it has been altered outside the checkout
// process so cache_form has a stale version.
$order = commerce_order_load($order->order_id);
$form_state['order'] = $order;
$form_state['checkout_page'] = $checkout_page;
$form_state['account'] = clone $user;
// Add any help text that has been defined for this checkout page.
$help = filter_xss($checkout_page['help']);
if (!empty($help)) {
$form['help'] = array(
'#markup' => theme('commerce_checkout_help', array(
'help' => $help,
)),
);
}
// Restore form errors.
if (!empty($form_state['storage']['errors'])) {
$form_errors =& drupal_static('form_set_error', array());
$form_errors = $form_state['storage']['errors'];
}
$form['#after_build'][] = 'commerce_checkout_form_process_errors';
// Catch and clear already pushed messages.
$previous_messages = drupal_get_messages();
$show_errors_message = FALSE;
$visible_panes = 0;
// Add any enabled checkout panes for this page to the form.
foreach (commerce_checkout_panes(array(
'enabled' => TRUE,
'page' => $checkout_page['page_id'],
)) as $pane_id => $checkout_pane) {
if ($callback = commerce_checkout_pane_callback($checkout_pane, 'checkout_form')) {
// Generate the pane form.
$pane_form = $callback($form, $form_state, $checkout_pane, $order);
// Refresh the order object in case the checkout pane altered it outside
// of the passed reference.
$order = commerce_order_load($order->order_id);
$form_state['order'] = $order;
// Combine the messages that were created during this pane's validation or
// submit process with any that were created during the pane generation
// and merge them into the session's current messages array.
if (!empty($form_state['storage']['messages'][$pane_id])) {
$_SESSION['messages'] = array_merge_recursive($form_state['storage']['messages'][$pane_id], drupal_get_messages());
}
// If there are messages in the session right now for this pane, theme
// them into the form right above the pane itself.
if (!empty($_SESSION['messages'])) {
// If there are error messages and this is not the first pane on the
// form, then indicate we need to show an error message at the top of
// the page.
if ($visible_panes > 0 && !empty($_SESSION['messages']['error'])) {
$show_errors_message = TRUE;
}
// Rendering status messages clears the session of messages, so they
// will not be visible if the user is redirected. We can at least not
// render here when we detect the global variable added by Rules to
// handle redirects, though modules implementing redirects will still
// encounter the same problem of "lost" messages.
if (!isset($GLOBALS['_rules_action_drupal_goto_do'])) {
$form_state['storage']['themed_messages'][$pane_id] = theme('status_messages');
$pane_form[$pane_id . '_messages'] = array(
'#markup' => $form_state['storage']['themed_messages'][$pane_id],
'#weight' => -50,
);
}
}
// Create a fieldset for the pane and add the form data defined in the
// pane's form callback.
if ($pane_form) {
$form[$pane_id] = $pane_form + array(
'#type' => $checkout_pane['fieldset'] ? 'fieldset' : 'container',
'#title' => check_plain($checkout_pane['title']),
'#collapsible' => $checkout_pane['collapsible'],
'#collapsed' => $checkout_pane['collapsed'],
'#attributes' => array(
'class' => array(
$pane_id,
),
),
'#tree' => TRUE,
);
$visible_panes++;
}
}
}
// Restore general messages to the current session's messages array.
$_SESSION['messages'] = array_merge_recursive(array_filter($previous_messages), drupal_get_messages());
// If there are errors on the form, add a message to the top of the page.
if ($show_errors_message) {
$form['error_message'] = array(
'#markup' => theme('commerce_checkout_errors_message', array(
'label' => t('Errors on form'),
'message' => t('There are errors on the page. Please correct them and resubmit the form.'),
)),
'#weight' => -10,
);
}
// Only add buttons to the form if the checkout page hasn't disabled them.
if ($checkout_page['buttons']) {
$form['buttons'] = array(
'#type' => 'fieldset',
'#attributes' => array(
'class' => array(
'checkout-buttons',
),
),
);
$form['buttons']['continue'] = array(
'#type' => 'submit',
'#value' => $checkout_page['submit_value'],
'#attributes' => array(
'class' => array(
'checkout-continue',
),
),
'#suffix' => '<span class="checkout-processing element-invisible"></span>',
'#validate' => array(
'commerce_checkout_form_validate',
),
'#submit' => array(
'commerce_checkout_form_submit',
),
);
// Add the cancel or back button where appropriate. We define button level
// submit handlers because we're using hook_forms() to use this form builder
// function and to avoid issues if other modules implement button level submit
// handlers on these or custom checkout buttons.
$button_operator = '<span class="button-operator">' . t('or') . '</span>';
if (!$checkout_page['prev_page'] && !empty($checkout_page['back_value'])) {
// Add an empty "Back" button value to avoid submission errors.
$form['buttons']['back'] = array(
'#type' => 'value',
'#value' => '',
);
// Store the cancel redirect in the form so modules can modify it easily.
$form_state['cancel_redirect'] = '<front>';
$form['buttons']['cancel'] = array(
'#type' => 'submit',
'#value' => t('Cancel'),
'#attributes' => array(
'class' => array(
'checkout-cancel',
),
),
'#submit' => array(
'commerce_checkout_form_cancel_submit',
),
'#limit_validation_errors' => array(),
'#prefix' => $button_operator,
);
}
elseif ($checkout_page['prev_page'] && !empty($checkout_page['back_value'])) {
$form['buttons']['back'] = array(
'#type' => 'submit',
'#value' => $checkout_page['back_value'],
'#attributes' => array(
'class' => array(
'checkout-back',
),
),
'#submit' => array(
'commerce_checkout_form_back_submit',
),
'#limit_validation_errors' => array(),
'#prefix' => $button_operator,
);
}
}
// Remove form level validate and submit handlers.
$form['#validate'] = array();
$form['#submit'] = array();
return $form;
}
/**
* After build callback for the checkout form.
*/
function commerce_checkout_form_process_errors($form, $form_state) {
// Do this only on form rebuild (when the form will not be validated anymore):
if (!empty($form_state['storage']['errors']) && !empty($form_state['rebuild'])) {
foreach (array_keys($form_state['storage']['errors']) as $element_name) {
// Look for all elements which have $element_name as parents, and
// restore their #validated property (so _form_set_class() will set
// the error class even though the rebuilt form is not validated).
// We can't simply use drupal_array_get_nested_value(), since the #parents
// property may have been changed and not match the form structure.
_commerce_checkout_set_validated($form, $element_name);
}
}
return $form;
}
/**
* Set '#validated' on elements which have the specified parents.
*/
function _commerce_checkout_set_validated(&$element, $imploded_parents) {
// Recurse to child elements if the current element is a container.
foreach (element_children($element) as $key) {
_commerce_checkout_set_validated($element[$key], $imploded_parents);
}
// This will also set #validated on all elements where #needs_validation would
// be FALSE, but that doesn't hurt anything.
if (!empty($element['#parents']) && strpos($imploded_parents, implode('][', $element['#parents'])) === 0) {
$element['#validated'] = TRUE;
}
}
/**
* Validate handler for the continue button of the checkout form.
*
* This function calls the validation function of each pane, followed by the
* submit function if the validation succeeded. As long as one pane fails
* validation, we then ask for the form to be rebuilt. Once all the panes are
* happy, we move on to the next page.
*/
function commerce_checkout_form_validate($form, &$form_state) {
$checkout_page = $form_state['checkout_page'];
// Load a fresh copy of the order stored in the form.
$order = commerce_order_load($form_state['order']->order_id);
// Catch and clear already pushed messages.
$previous_messages = drupal_get_messages();
// Load any pre-existing validation errors for the elements.
$form_errors = form_get_errors();
$errors = array();
// If we found any errors at all on the page, go ahead and mark the form as
// failing validation. This will ensure that even form elements on the page
// that aren't part of a checkout pane trigger validation failure.
$form_validate = TRUE;
if (!empty($form_errors)) {
$form_validate = FALSE;
}
foreach ((array) $form_errors as $element_path => $error) {
list($pane_id, ) = explode('][', $element_path, 2);
$errors[$pane_id][$element_path] = $error;
}
// Loop through the enabled checkout panes for the current page.
foreach (commerce_checkout_panes(array(
'enabled' => TRUE,
'page' => $checkout_page['page_id'],
)) as $pane_id => $checkout_pane) {
$validate = TRUE;
// If any element in the pane failed validation, we mark the pane as
// unvalidated and replay the validation messages on top of it.
if (!empty($errors[$pane_id])) {
$validate = FALSE;
foreach ($errors[$pane_id] as $element_path => $message) {
if ($message) {
drupal_set_message($message, 'error');
}
}
if (isset($previous_messages['error'])) {
$previous_messages['error'] = array_values(array_diff($previous_messages['error'], $errors[$pane_id]));
}
}
// If the pane has defined a checkout form validate handler...
if ($callback = commerce_checkout_pane_callback($checkout_pane, 'checkout_form_validate')) {
// Give it a chance to process the submitted data.
$validate &= $callback($form, $form_state, $checkout_pane, $order);
}
// Catch and clear panes' messages.
$pane_messages = drupal_get_messages();
// Submit the pane if it validated.
if ($validate && ($callback = commerce_checkout_pane_callback($checkout_pane, 'checkout_form_submit'))) {
$callback($form, $form_state, $checkout_pane, $order);
}
// Generate status messages.
$form_state['storage']['messages'][$pane_id] = array_merge_recursive($pane_messages, drupal_get_messages());
// A failed pane makes the form fail.
$form_validate &= $validate;
}
// Restore messages and form errors.
$_SESSION['messages'] = array_merge_recursive(array_filter($previous_messages), drupal_get_messages());
$form_errors =& drupal_static('form_set_error', array());
$form_state['storage']['errors'] = $form_errors;
$form_errors = array();
// Save the updated order object and reset the order in the form cache to
// ensure rebuilt forms use the updated order.
commerce_order_save($order);
$form_state['build_info']['args'][0] = $order;
// If a pane failed validation or the form state has otherwise been altered to
// initiate a rebuild, return without moving to the next checkout page.
if (!$form_validate || $form_state['rebuild']) {
$form_state['rebuild'] = TRUE;
}
}
/**
* Submit handler for the continue button of the checkout form.
*/
function commerce_checkout_form_submit($form, &$form_state) {
$checkout_page = $form_state['checkout_page'];
// Load a fresh copy of the order stored in the form.
$order = commerce_order_load($form_state['order']->order_id);
// If we are going to redirect with checkout pane messages stored in the form
// state, they will not be displayed on a subsequent form build like normal.
// Move them out of the form state messages array and into the current
// session's general message array instead.
if (!empty($form_state['storage']['messages'])) {
foreach ($form_state['storage']['messages'] as $pane_id => $pane_messages) {
$_SESSION['messages'] = array_merge_recursive($_SESSION['messages'], $pane_messages);
}
}
// If the form was submitted via the continue button...
if (end($form_state['triggering_element']['#array_parents']) == 'continue') {
// If there is another checkout page...
if ($checkout_page['next_page']) {
// Update the order status to reflect the next checkout page.
$order = commerce_order_status_update($order, 'checkout_' . $checkout_page['next_page'], FALSE, NULL, t('Customer continued to the next checkout page via a submit button.'));
// If it happens to be the complete page, process completion now.
if ($checkout_page['next_page'] == 'complete') {
commerce_checkout_complete($order);
}
// Redirect to the next checkout page.
$form_state['redirect'] = 'checkout/' . $order->order_id . '/' . $checkout_page['next_page'];
}
}
}
/**
* Special submit handler for the back button to avoid processing orders.
*/
function commerce_checkout_form_back_submit($form, &$form_state) {
// If there is a previous page...
if ($previous_page = commerce_checkout_page_load($form_state['checkout_page']['prev_page'])) {
$order = commerce_order_load($form_state['order']->order_id);
// Move the form back to that page.
if ($previous_page['prev_page']) {
$form_state['redirect'] = 'checkout/' . $order->order_id . '/' . $previous_page['page_id'];
}
else {
$form_state['redirect'] = 'checkout/' . $order->order_id;
}
// Update the order status for the checkout step.
$form_state['order'] = commerce_order_status_update($order, 'checkout_' . $previous_page['page_id'], FALSE, NULL, t('Customer returned to the previous checkout page via a submit button.'));
}
}
/**
* Special submit handler for the cancel button to avoid processing orders.
*/
function commerce_checkout_form_cancel_submit($form, &$form_state) {
$order = commerce_order_load($form_state['order']->order_id);
// Set the order status back to the first checkout page's status.
$order_state = commerce_order_state_load('checkout');
$form_state['order'] = commerce_order_status_update($order, $order_state['default_status'], TRUE);
// Skip saving in the status update and manually save here to force a save
// even when the status doesn't actually change.
if (variable_get('commerce_order_auto_revision', TRUE)) {
$form_state['order']->revision = TRUE;
$form_state['order']->log = t('Customer manually canceled the checkout process.');
}
commerce_order_save($form_state['order']);
drupal_set_message(t('Checkout of your current order has been canceled and may be resumed when you are ready.'));
$form_state['redirect'] = $form_state['cancel_redirect'];
}
/**
* Themes the optional checkout review page data.
*/
function theme_commerce_checkout_review($variables) {
$form = $variables['form'];
// Turn the review data array into table rows.
$rows = array();
foreach ($form['#data'] as $pane_id => $data) {
// First add a row for the title.
$rows[] = array(
'data' => array(
array(
'data' => $data['title'],
'colspan' => 2,
),
),
'class' => array(
'pane-title',
'odd',
),
);
// Next, add the data for this particular section.
if (is_array($data['data'])) {
// If it's an array, treat each key / value pair as a 2 column row.
foreach ($data['data'] as $key => $value) {
$rows[] = array(
'data' => array(
array(
'data' => $key . ':',
'class' => array(
'pane-data-key',
),
),
array(
'data' => $value,
'class' => array(
'pane-data-value',
),
),
),
'class' => array(
'pane-data',
'even',
),
);
}
}
else {
// Otherwise treat it as a block of text in its own row.
$rows[] = array(
'data' => array(
array(
'data' => $data['data'],
'colspan' => 2,
'class' => array(
'pane-data-full',
),
),
),
'class' => array(
'pane-data',
'even',
),
);
}
}
return theme('table', array(
'rows' => $rows,
'attributes' => array(
'class' => array(
'checkout-review',
),
),
));
}
Functions
Name | Description |
---|---|
commerce_checkout_form | Builds the checkout form for the given order on the specified checkout page. |
commerce_checkout_form_back_submit | Special submit handler for the back button to avoid processing orders. |
commerce_checkout_form_cancel_submit | Special submit handler for the cancel button to avoid processing orders. |
commerce_checkout_form_process_errors | After build callback for the checkout form. |
commerce_checkout_form_submit | Submit handler for the continue button of the checkout form. |
commerce_checkout_form_validate | Validate handler for the continue button of the checkout form. |
commerce_checkout_router | Redirects invalid checkout attempts or displays the checkout form if valid. |
theme_commerce_checkout_review | Themes the optional checkout review page data. |
_commerce_checkout_set_validated | Set '#validated' on elements which have the specified parents. |