You are here

function commerce_cart_add_to_cart_form in Commerce Core 7

Builds an Add to Cart form for a set of products.

Parameters

$line_item: A fully formed product line item whose data will be used in the following ways by the form:

  • $line_item->data['context']['product_ids']: an array of product IDs to include on the form or the string 'entity' if the context array includes an entity array with information for accessing the product IDs from an entity's product reference field.
  • $line_item->data['context']['entity']: if the product_ids value is the string 'entity', an associative array with the keys 'entity_type', 'entity_id', and 'product_reference_field_name' that points to the location of the product IDs used to build the form.
  • $line_item->data['context']['add_to_cart_combine']: a boolean indicating whether or not to attempt to combine the product added to the cart with existing line items of matching fields.
  • $line_item->data['context']['show_single_product_attributes']: a boolean indicating whether or not product attribute fields with single options should be shown on the Add to Cart form.
  • $line_item->data['context']['button_text']: a string that overrides the default Add to Cart button text for submitting the form.
  • $line_item->quantity: the default value for the quantity widget if included (determined by the $show_quantity parameter).
  • $line_item->commerce_product: the value of this field will be used as the default product ID when the form is built for multiple products.

The line item's data array will be used on submit to set the data array of the product line item created by the form.

$show_quantity: Boolean indicating whether or not to show the quantity widget; defaults to FALSE resulting in a hidden field holding the quantity.

$context: Information on the context of the form's placement, allowing it to update product fields on the page based on the currently selected default product. Should be an associative array containing the following keys:

  • class_prefix: a prefix used to target HTML containers for replacement with rendered fields as the default product is updated. For example, nodes display product fields in their context wrapped in spans with the class node-#-product-field_name. The class_prefix for the add to cart form displayed on a node would be node-# with this form's AJAX refresh adding the suffix -product-field_name.
  • view_mode: a product view mode that tells the AJAX refresh how to render the replacement fields.

If no context is specified, AJAX replacement of rendered fields will not happen. This parameter only affects forms containing multiple products.

Return value

The form array.

1 call to commerce_cart_add_to_cart_form()
commerce_cart_handler_field_edit_attributes::views_form in modules/cart/includes/views/handlers/commerce_cart_handler_field_edit_attributes.inc
Returns the form which replaces the placeholder from render().
8 string references to 'commerce_cart_add_to_cart_form'
CommerceBaseTestCase::attachProductReferenceField in tests/commerce_base.test
Attach a product reference field to a given content type. Creates the field if the given name doesn't already exist. Automatically sets the display formatters to be the "add to cart form" for the teaser and full modes.
commerce_cart_field_attach_view_alter in modules/cart/commerce_cart.module
Implements hook_field_attach_view_alter().
commerce_cart_field_formatter_settings_form in modules/cart/commerce_cart.module
Implements hook_field_formatter_settings_form().
commerce_cart_field_formatter_settings_summary in modules/cart/commerce_cart.module
Implements hook_field_formatter_settings_summary().
commerce_cart_field_formatter_view in modules/cart/commerce_cart.module
Implements hook_field_formatter_view().

... See full list

File

modules/cart/commerce_cart.module, line 1876
Implements the shopping cart system and add to cart features.

Code

function commerce_cart_add_to_cart_form($form, &$form_state, $line_item, $show_quantity = FALSE, $context = array()) {
  global $user;

  // Store the context in the form state for use during AJAX refreshes.
  $form_state['context'] = $context;

  // Store the line item passed to the form builder for reference on submit.
  $form_state['line_item'] = $line_item;
  $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
  $default_quantity = $line_item->quantity;

  // Retrieve the array of product IDs from the line item's context data array.
  $product_ids = commerce_cart_add_to_cart_form_product_ids($line_item);

  // If we don't have a list of products to load, just bail out early.
  // There is nothing we can or have to do in that case.
  if (empty($product_ids)) {
    return array();
  }

  // Add a generic class ID.
  $form['#attributes']['class'][] = drupal_html_class('commerce-add-to-cart');

  // Store the form ID as a class of the form to avoid the incrementing form ID
  // from causing the AJAX refresh not to work.
  $form['#attributes']['class'][] = drupal_html_class(commerce_cart_add_to_cart_form_id($product_ids));

  // Disable autocomplete on Add to Cart form elements.
  $form['#attributes']['autocomplete'] = 'off';

  // Store the customer uid in the form so other modules can override with a
  // selection widget if necessary.
  $form['uid'] = array(
    '#type' => 'value',
    '#value' => $user->uid,
  );

  // Load all the active products intended for sale on this form.
  $products = commerce_product_load_multiple($product_ids, array(
    'status' => 1,
  ));

  // If no products were returned...
  if (count($products) == 0) {
    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Product not available'),
      '#weight' => 15,
      // Do not set #disabled in order not to prevent submission.
      '#attributes' => array(
        'disabled' => 'disabled',
      ),
      '#validate' => array(
        'commerce_cart_add_to_cart_form_disabled_validate',
      ),
    );
  }
  else {

    // If the form is for a single product and displaying attributes on a single
    // product Add to Cart form is disabled in the form context, store the
    // product_id in a hidden form field for use by the submit handler.
    if (count($products) == 1 && empty($line_item->data['context']['show_single_product_attributes'])) {
      $form_state['default_product'] = reset($products);
      $form['product_id'] = array(
        '#type' => 'hidden',
        '#value' => key($products),
      );
    }
    else {

      // However, if more than one products are represented on it, attempt to
      // use smart select boxes for the product selection. If the products are
      // all of the same type and there are qualifying fields on that product
      // type, display their options for customer selection.
      $qualifying_fields = array();
      $same_type = TRUE;
      $type = '';
      foreach ($products as $product_id => $product) {

        // Store the first product type.
        if (empty($type)) {
          $type = $product->type;
        }

        // If the current product type is different from the first, we are not
        // dealing with a set of same typed products.
        if ($product->type != $type) {
          $same_type = FALSE;
        }
      }

      // Find an array of matching products based on attribute selections so we
      // can find the default product and add a selection widget if multiple
      // matching products are found.
      $matching_products = _commerce_cart_matching_products($products, $form_state);

      // Set the default product.
      if (empty($matching_products)) {

        // If a product ID value was passed in, use that product if it exists.
        if (!empty($form_state['values']['product_id']) && !empty($products[$form_state['values']['product_id']])) {
          $default_product = $products[$form_state['values']['product_id']];
        }
        elseif (empty($form_state['values']) && !empty($line_item_wrapper->commerce_product) && !empty($products[$line_item_wrapper->commerce_product
          ->raw()])) {

          // If this is the first time the form is built, attempt to use the
          // product specified by the line item.
          $default_product = $products[$line_item_wrapper->commerce_product
            ->raw()];
        }
        else {
          reset($products);
          $default_product = $products[key($products)];
        }
      }
      else {

        // If the product selector has a value, use that.
        if (!empty($form_state['values']['attributes']['product_select']) && !empty($products[$form_state['values']['attributes']['product_select']]) && in_array($products[$form_state['values']['attributes']['product_select']], $matching_products)) {
          $default_product = $products[$form_state['values']['attributes']['product_select']];
        }
        else {
          reset($matching_products);
          $default_product = $matching_products[key($matching_products)];
        }
      }

      // Remove any attribute input values from the form state that aren't
      // associated with the default product so that attribute widgets can use
      // the default values.
      if (!empty($form_state['input']['attributes'])) {
        foreach ($form_state['input']['attributes'] as $attribute_name => $attribute_value) {
          $default_product_attribute_items = field_get_items('commerce_product', $default_product, $attribute_name);
          if (!empty($default_product_attribute_items)) {
            $default_product_attribute_value = reset($default_product_attribute_items[0]);
            if ($default_product_attribute_value != $attribute_value) {
              unset($form_state['input']['attributes'][$attribute_name]);
            }
          }
          else {
            unset($form_state['input']['attributes'][$attribute_name]);
          }
        }
      }
      $form_state['default_product'] = $default_product;

      // If all the products are of the same type...
      if ($same_type) {

        // Loop through all the field instances on that product type.
        foreach (field_info_instances('commerce_product', $type) as $field_name => $instance) {
          $field = field_info_field($field_name);

          // If the instance is of a field type that is eligible to function as
          // a product attribute field and if its attribute field settings
          // specify that this functionality is enabled...
          if (commerce_cart_field_attribute_eligible($field) && commerce_cart_field_instance_is_attribute($instance)) {

            // Get the options properties from the options module for the
            // attribute widget type selected for the field, defaulting to the
            // select list options properties.
            $commerce_cart_settings = commerce_cart_field_instance_attribute_settings($instance);
            switch ($commerce_cart_settings['attribute_widget']) {
              case 'checkbox':
                $widget_type = 'onoff';
                break;
              case 'radios':
                $widget_type = 'buttons';
                break;
              default:
                $widget_type = 'select';
            }
            $properties = _options_properties($widget_type, FALSE, TRUE, TRUE);

            // Try to fetch localized names.
            $allowed_values = NULL;

            // Prepare translated options if using the i18n_field module.
            if (module_exists('i18n_field')) {
              if ($translate = i18n_field_type_info($field['type'], 'translate_options')) {
                $allowed_values = $translate($field);
                _options_prepare_options($allowed_values, $properties);
              }
            }

            // Otherwise just use the base language values.
            if (empty($allowed_values)) {
              $allowed_values = _options_get_options($field, $instance, $properties, 'commerce_product', $default_product);
            }

            // Only consider this field a qualifying attribute field if we could
            // derive a set of options for it.
            if (!empty($allowed_values)) {
              $qualifying_fields[$field_name] = array(
                'field' => $field,
                'instance' => $instance,
                'commerce_cart_settings' => $commerce_cart_settings,
                'options' => $allowed_values,
                'weight' => $instance['widget']['weight'],
                'required' => $instance['required'],
              );
            }
          }
        }
      }

      // Otherwise for products of varying types, display a simple select list
      // by product title.
      if (!empty($qualifying_fields)) {
        $used_options = array();
        $field_has_options = array();

        // Sort the fields by weight.
        uasort($qualifying_fields, 'drupal_sort_weight');
        foreach ($qualifying_fields as $field_name => $data) {

          // Build an options array of widget options used by referenced products.
          foreach ($products as $product_id => $product) {

            // Only add options to the present array that appear on products that
            // match the default value of the previously added attribute widgets.
            foreach ($used_options as $used_field_name => $unused) {

              // Don't apply this check for the current field being evaluated.
              if ($used_field_name == $field_name) {
                continue;
              }

              // Don't apply this check if the previously used field we'd
              // compare against was unset from the qualifying fields array,
              // meaning that we only added NULL values to its used options
              // array because it was an unused optional attribute field.
              if (!isset($qualifying_fields[$used_field_name])) {
                continue;
              }

              // However, if the form does include an element for the previous
              // attribute field, ensure the current product's attribute field
              // value matches the selected value on that element.
              if (isset($form['attributes'][$used_field_name]) && array_key_exists('#default_value', $form['attributes'][$used_field_name])) {
                $used_field_items = field_get_items('commerce_product', $product, $used_field_name);
                if (!empty($used_field_items[0])) {
                  $used_field_value = reset($used_field_items[0]);
                }
                else {
                  $used_field_value = NULL;
                }
                if ($used_field_value != $form['attributes'][$used_field_name]['#default_value']) {
                  continue 2;
                }
              }
            }

            // With our hard dependency on widgets provided by the Options
            // module, we can make assumptions about where the data is stored.
            $field_items = field_get_items('commerce_product', $product, $field_name);
            if (!empty($field_items[0])) {
              $field_value = reset($field_items[0]);
            }
            else {
              $field_value = NULL;
            }
            if ($field_value != NULL) {
              $field_has_options[$field_name] = TRUE;
            }
            $used_options[$field_name][] = $field_value;
          }

          // If for some reason no options for this field are used, remove it
          // from the qualifying fields array.
          if (empty($field_has_options[$field_name]) || empty($used_options[$field_name])) {
            unset($qualifying_fields[$field_name]);
          }
          else {
            $default_product_field_items = field_get_items('commerce_product', $default_product, $field_name);
            if (!empty($default_product_field_items[0])) {
              $default_product_field_value = reset($default_product_field_items[0]);
            }
            else {
              $default_product_field_value = NULL;
            }
            $form['attributes'][$field_name] = array(
              '#type' => $data['commerce_cart_settings']['attribute_widget'],
              '#title' => commerce_cart_attribute_widget_title($data['instance']),
              '#options' => array_intersect_key($data['options'], drupal_map_assoc($used_options[$field_name])),
              '#default_value' => $default_product_field_value,
              '#weight' => $data['instance']['widget']['weight'],
              '#ajax' => array(
                'callback' => 'commerce_cart_add_to_cart_form_attributes_refresh',
              ),
            );

            // Add the empty value if the field is not required and products on
            // the form include the empty value.
            if (!$data['required'] && in_array('', $used_options[$field_name])) {
              $form['attributes'][$field_name]['#empty_value'] = '';
            }
            $form['unchanged_attributes'][$field_name] = array(
              '#type' => 'value',
              '#value' => $default_product_field_value,
            );
          }
        }
        if (!empty($form['attributes'])) {
          $form['attributes'] += array(
            '#tree' => 'TRUE',
            '#prefix' => '<div class="attribute-widgets">',
            '#suffix' => '</div>',
            '#weight' => 0,
          );
          $form['unchanged_attributes'] += array(
            '#tree' => 'TRUE',
          );

          // If the matching products array is empty, it means this is the first
          // time the form is being built. We should populate it now with
          // products that match the default attribute options.
          if (empty($matching_products)) {
            foreach ($products as $product_id => $product) {
              $match = TRUE;
              foreach (element_children($form['attributes']) as $field_name) {
                $product_field_items = field_get_items('commerce_product', $product, $field_name);
                if (!empty($product_field_items[0])) {
                  $product_field_value = reset($product_field_items[0]);
                }
                else {
                  $product_field_value = NULL;
                }
                if ($product_field_value != $form['attributes'][$field_name]['#default_value']) {
                  $match = FALSE;
                }
              }
              if ($match) {
                $matching_products[$product_id] = $product;
              }
            }
          }

          // If there were more than one matching products for the current
          // attribute selection, add a product selection widget.
          if (count($matching_products) > 1) {
            $options = array();
            foreach ($matching_products as $product_id => $product) {
              $options[$product_id] = $product->title;
            }

            // Note that this element by default is a select list, so its
            // #options are not sanitized here. Sanitization will occur in a
            // check_plain() in the function form_select_options(). If you alter
            // this element to another #type, such as 'radios', you are also
            // responsible for looping over its #options array and sanitizing
            // the values.
            $form['attributes']['product_select'] = array(
              '#type' => 'select',
              '#title' => t('Select a product'),
              '#options' => $options,
              '#default_value' => $default_product->product_id,
              '#weight' => 40,
              '#ajax' => array(
                'callback' => 'commerce_cart_add_to_cart_form_attributes_refresh',
              ),
            );
          }
          $form['product_id'] = array(
            '#type' => 'hidden',
            '#value' => $default_product->product_id,
          );
        }
      }

      // If the products referenced were of different types or did not possess
      // any qualifying attribute fields...
      if (!$same_type || empty($qualifying_fields)) {

        // For a single product form, just add the hidden product_id field.
        if (count($products) == 1) {
          $form['product_id'] = array(
            '#type' => 'hidden',
            '#value' => $default_product->product_id,
          );
        }
        else {

          // Otherwise add a product selection widget.
          $options = array();
          foreach ($products as $product_id => $product) {
            $options[$product_id] = $product->title;
          }

          // Note that this element by default is a select list, so its #options
          // are not sanitized here. Sanitization will occur in a check_plain() in
          // the function form_select_options(). If you alter this element to
          // another #type, such as 'radios', you are also responsible for looping
          // over its #options array and sanitizing the values.
          $form['product_id'] = array(
            '#type' => 'select',
            '#options' => $options,
            '#default_value' => $default_product->product_id,
            '#weight' => 0,
            '#ajax' => array(
              'callback' => 'commerce_cart_add_to_cart_form_attributes_refresh',
            ),
          );
        }
      }
    }

    // Render the quantity field as either a textfield if shown or a hidden
    // field if not.
    if ($show_quantity) {
      $form['quantity'] = array(
        '#type' => 'textfield',
        '#title' => t('Quantity'),
        '#default_value' => $default_quantity,
        '#datatype' => 'integer',
        '#size' => 5,
        '#weight' => 45,
      );
    }
    else {
      $form['quantity'] = array(
        '#type' => 'hidden',
        '#value' => $default_quantity,
        '#datatype' => 'integer',
        '#weight' => 45,
      );
    }

    // Add the line item's fields to a container on the form.
    $form['line_item_fields'] = array(
      '#type' => 'container',
      '#parents' => array(
        'line_item_fields',
      ),
      '#weight' => 10,
      '#tree' => TRUE,
    );
    field_attach_form('commerce_line_item', $form_state['line_item'], $form['line_item_fields'], $form_state);

    // Loop over the fields we just added and remove any that haven't been
    // marked for inclusion on this form.
    foreach (element_children($form['line_item_fields']) as $field_name) {
      $info = field_info_instance('commerce_line_item', $field_name, $form_state['line_item']->type);
      $form['line_item_fields'][$field_name]['#commerce_cart_settings'] = commerce_cart_field_instance_access_settings($info);
      if (empty($form['line_item_fields'][$field_name]['#commerce_cart_settings']['field_access'])) {
        $form['line_item_fields'][$field_name]['#access'] = FALSE;
      }
    }

    // Do not allow products without a price to be purchased.
    $values = commerce_product_calculate_sell_price($form_state['default_product']);
    if (is_null($values) || is_null($values['amount']) || is_null($values['currency_code'])) {
      $form['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Product not available'),
        '#weight' => 50,
        // Do not set #disabled in order not to prevent submission.
        '#attributes' => array(
          'disabled' => 'disabled',
        ),
        '#validate' => array(
          'commerce_cart_add_to_cart_form_disabled_validate',
        ),
      );
    }
    else {

      // If the context contains custom button text, filter it and attempt to
      // translate it.
      if (!empty($line_item->data['context']['button_text'])) {
        $button_text = function_exists('locale') ? locale(filter_xss($line_item->data['context']['button_text'])) : filter_xss($line_item->data['context']['button_text']);
      }
      else {
        $button_text = t('Add to cart');
      }
      $form['submit'] = array(
        '#type' => 'submit',
        '#value' => $button_text,
        '#weight' => 50,
      );
    }
  }

  // Add the handlers manually since we're using hook_forms() to associate this
  // form with form IDs based on the $product_ids.
  $form['#validate'][] = 'commerce_cart_add_to_cart_form_validate';
  $form['#submit'][] = 'commerce_cart_add_to_cart_form_submit';
  $form['#after_build'][] = 'commerce_cart_add_to_cart_form_after_build';
  return $form;
}