You are here

function commerce_product_bundle_add_to_cart_form in Commerce Product Bundle 7

Same name and namespace in other branches
  1. 7.2 commerce_product_bundle.module \commerce_product_bundle_add_to_cart_form()

Builds the add to cart form, for product bundles.

Therefore it takes the add-to-cart-forms of each of the subproducts and put them together in one form.

@ToDo This are tons of duplicated code from 'commerce_cart_add_to_cart_form'. We should finde a way to reduce code duplication.

Parameters

array &$form: The standard commerce add to cart form for the bundle product.

array &$form_state: The form_state array for the bundle product.

int $parent_product_id: The ID of the bundle product.

array $product_ids: The ID's of the subproducts, aka the products which are part of the bundle.

array $field_instance: Document the field instance.

array $settings: Settings for this instance of the field.

array $context: Document Context array.

Return value

array The add to cart form with the bundle product.

1 call to commerce_product_bundle_add_to_cart_form()
commerce_product_bundle_form_alter in ./commerce_product_bundle.module
Implements of hook_bundle_form_alter().

File

./commerce_product_bundle.module, line 222
Allows the bundling of products in Drupal Commerce.

Code

function commerce_product_bundle_add_to_cart_form(&$form, &$form_state, $parent_product_id, $product_ids, $field_instance, $settings = array(), $context = array()) {
  global $user;

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

  // Get display settings.
  if (isset($field_instance['display']['node_full'])) {
    $display = $field_instance['display']['node_full'];
  }
  else {
    if (isset($field_instance['display']['default'])) {
      $display = $field_instance['display']['default'];
    }
    else {
      $display = array();
    }
  }

  // Skip if we are not using the commerce_bundle_product_add_to_cart_form.
  if (!isset($display['type']) || $display['type'] != 'commerce_bundle_product_add_to_cart_form') {
    return array();
  }

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

  // If no products were returned, go home.
  // @todo: Commerce has a more user friendly approach, which uses a disabled submit button.
  //     Steal that and implement here.
  if (count($products) == 0) {
    return array();
  }
  if (!isset($display['settings'])) {
    $display['settings'] = array();
  }
  $settings = $display['settings'] + commerce_product_bundle_field_formatter_default_settings();
  $weight = $display['weight'];
  $ref_field_name = $field_instance['field_name'];
  $id = $parent_product_id . '__' . $ref_field_name;

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

  // Load data from line item id.
  if (isset($form_state['line_item']) && !isset($form_state['values']['bundle'][$id]['product_id'])) {
    $sub_line_items = commerce_product_bundle_get_sub_line_items($form_state['line_item']);

    // We assume that we have never the same product in one bundle.
    // This means that we let the customer never choose the same product
    // in the same bundle.
    foreach ($sub_line_items as $sub_line_item) {
      $sub_line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $sub_line_item);
      $sub_product_id = $sub_line_item_wrapper->commerce_product->product_id
        ->value();
      if (isset($products[$sub_product_id])) {
        $form_state['values']['bundle'][$id]['product_id'] = $sub_product_id;
        $settings['default_quantity'] = (double) $sub_line_item_wrapper->quantity
          ->value();
        break;
      }
    }
  }
  if (!isset($form['bundle'])) {
    $form['bundle'] = array();
  }
  if (!isset($form['bundle'][$id])) {
    $form['bundle'][$id] = array();
  }
  $form['bundle'] += array(
    '#tree' => TRUE,
  );
  $form['bundle'][$id] += array(
    '#tree' => TRUE,
    '#prefix' => '<div class="bundle-widgets">',
    '#suffix' => '</div>',
    '#weight' => $weight,
  );

  // If the form is for a single product, store the product_id in a hidden
  // form field for use by the submit handler.
  if (count($products) == 1) {
    $form['bundle'][$id]['product_id'] = array(
      '#type' => 'hidden',
      '#value' => key($products),
    );
  }
  if ($settings['show_fieldset']) {
    $form['bundle'][$id] += array(
      '#type' => 'fieldset',
      '#title' => $field_instance['label'],
    );
  }
  $form_state['bundle_products'][$id] = $products;

  // 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.
  $same_type = TRUE;
  $qualifying_fields = array();
  $type = '';

  // Find the default product so we know how to set default options on the
  // various Add to Cart form widgets and an array of any matching product
  // based on attribute selections so we can add a selection widget.
  $matching_products = array();
  $default_product = NULL;
  $attribute_names = array();
  $unchanged_attributes = array();
  $defaults = array();
  foreach ($products as $product_id => $product) {
    $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
    $defaults[$product_id] = $product_id;

    // 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;
    }

    // If the form state contains a set of attribute data, use it to try
    // and determine the default product.
    $changed_attribute = NULL;
    if (!empty($form_state['values']['bundle'][$id]['attributes'])) {
      $match = TRUE;

      // Set an array of checked attributes for later comparison against the
      // default matching product.
      if (empty($attribute_names)) {
        $attribute_names = (array) array_diff_key($form_state['values']['bundle'][$id]['attributes'], array(
          'product_select' => '',
        ));
        $unchanged_attributes = $form_state['values']['bundle'][$id]['unchanged_attributes'];
      }
      foreach ($attribute_names as $key => $value) {

        // If this is the attribute widget that was changed...
        if ($value != $unchanged_attributes[$key]) {

          // Store the field name.
          $changed_attribute = $key;

          // Clear the input for the "Select a product" widget now if it
          // exists on the form since we know an attribute was changed.
          unset($form_state['input']['bundle'][$id]['attributes']['product_select']);
        }

        // If a field name has been stored and we've moved past it to
        // compare the next attribute field...
        if (!empty($changed_attribute) && $changed_attribute != $key) {

          // Wipe subsequent values from the form state so the attribute
          // widgets can use the default values from the new default product.
          unset($form_state['input']['bundle'][$id]['attributes'][$key]);

          // Don't accept this as a matching product.
          continue;
        }
        if ($product_wrapper->{$key}
          ->raw() != $value) {
          $match = FALSE;
        }
      }

      // If the changed field name has already been stored, only accept the
      // first matching product by ignoring the rest that would match. An
      // exception is granted for additional matching products that share
      // the exact same attribute values as the first.
      if ($match && !empty($changed_attribute) && !empty($matching_products)) {
        reset($matching_products);
        $matching_product = $matching_products[key($matching_products)];
        $matching_product_wrapper = entity_metadata_wrapper('commerce_product', $matching_product);
        foreach ($attribute_names as $key => $value) {
          if ($product_wrapper->{$key}
            ->raw() != $matching_product_wrapper->{$key}
            ->raw()) {
            $match = FALSE;
          }
        }
      }
      if ($match) {
        $matching_products[$product_id] = $product;
      }
    }
  }

  // Set the default product now if it isn't already set.
  if (empty($matching_products)) {

    // If a product ID value was passed in, use that product if it exists.
    if (!empty($form_state['values']['bundle'][$id]['product_id']) && !empty($products[$form_state['values']['bundle'][$id]['product_id']])) {
      $default_product = $products[$form_state['values']['bundle'][$id]['product_id']];
    }
    else {
      reset($products);
      $default_product = $products[key($products)];
    }
  }
  else {

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

  // Wrap the default product for later use.
  $default_product_wrapper = entity_metadata_wrapper('commerce_product', $default_product);
  $form_state['bundle'][$id]['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 $name => $instance) {

      // A field qualifies if it is single value, required and uses a widget
      // with a definite set of options. For the sake of simplicity, this is
      // currently restricted to fields defined by the options module.
      $field = field_info_field($instance['field_name']);

      // Get the array of Cart settings pertaining to this instance.
      $commerce_cart_settings = commerce_cart_field_instance_attribute_settings($instance);
      if (commerce_cart_field_attribute_eligible($field) && $commerce_cart_settings['attribute_field']) {

        // Get the options properties from the options module for the
        // attribute widget type selected for the field, defaulting to the
        // select list options properties.
        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);
          }

          // Translate the field title if set.
          if (!empty($instance['label'])) {
            $instance['label'] = i18n_field_translate_property($instance, 'label');
          }
        }

        // 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[$name] = array(
            'field' => $field,
            'instance' => $instance,
            'commerce_cart_settings' => $commerce_cart_settings,
            'options' => $allowed_values,
            'weight' => $instance['widget']['weight'],
            'required' => $instance['required'],
          );
        }
      }
    }
  }

  // Generate the select form items if we have only one product type,
  // which implies that all products has the same fields / attributes.
  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 $name => $data) {

      // Build an options array of widget options used by referenced products.
      foreach ($products as $product_id => $product) {
        $product_wrapper = entity_metadata_wrapper('commerce_product', $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 == $name) {
            continue;
          }
          if (isset($form['bundle'][$id]['attributes'][$used_field_name]['#default_value'])) {
            if ($product_wrapper->{$used_field_name}
              ->raw() != $form['bundle'][$id]['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.
        if ($product_wrapper->{$name}
          ->raw() != NULL) {
          $field_has_options[$name] = TRUE;
        }
        $used_options[$name][] = $product_wrapper->{$name}
          ->raw();
      }

      // If for some reason no options for this field are used, remove it
      // from the qualifying fields array.
      if (empty($field_has_options[$name]) || empty($used_options[$name])) {
        unset($qualifying_fields[$name]);
      }
      else {
        $form['bundle'][$id]['attributes'][$name] = array(
          '#type' => $data['commerce_cart_settings']['attribute_widget'],
          '#title' => check_plain($data['instance']['label']),
          '#options' => array_intersect_key($data['options'], drupal_map_assoc($used_options[$name])),
          '#default_value' => $default_product_wrapper->{$name}
            ->raw(),
          '#weight' => $data['instance']['widget']['weight'],
          '#ajax' => array(
            'callback' => 'commerce_product_attributes_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[$name])) {
          $form['bundle'][$id]['attributes'][$name]['#empty_value'] = '';
        }
        $form['bundle'][$id]['unchanged_attributes'][$name] = array(
          '#type' => 'value',
          '#value' => $default_product_wrapper->{$name}
            ->raw(),
        );
      }
    }
    if (!empty($form['bundle'][$id]['attributes'])) {
      $form['bundle'][$id]['attributes'] += array(
        '#tree' => 'TRUE',
        '#prefix' => '<div class="attribute-widgets">',
        '#suffix' => '</div>',
        '#weight' => 0,
      );
      $form['bundle'][$id]['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) {
          $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
          $match = TRUE;
          foreach (element_children($form['bundle'][$id]['attributes']) as $name) {
            if ($product_wrapper->{$name}
              ->raw() != $form['bundle'][$id]['attributes'][$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] = check_plain($product->title);
        }
        $form['bundle'][$id]['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_product_attributes_add_to_cart_form_attributes_refresh',
          ),
        );
      }
      $form['bundle'][$id]['product_id'] = array(
        '#type' => 'hidden',
        '#value' => $default_product->product_id,
      );
    }
  }

  // If the products referenced were of different types or did not posess
  // any qualifying attribute fields, add a product selection widget.
  if (!$same_type || empty($qualifying_fields)) {

    // For a single product form, just add the hidden product_id field.
    if (count($products) == 1) {
      $form['bundle'][$id]['product_id'] = array(
        '#type' => 'hidden',
        '#value' => $default_product->product_id,
      );
    }
    else {
      $options = array();
      $defaults = array();
      foreach ($products as $product_id => $product) {
        $options[$product_id] = check_plain($product->title);
        $defaults[$product_id] = $product_id;
      }
      if ($settings['bundle_type'] == 'hidden') {
        foreach ($defaults as $default) {
          $form['bundle'][$id]['product_id'][$default] = array(
            '#type' => 'hidden',
            '#value' => $default,
          );
        }
      }
      else {
        if ($settings['bundle_type'] == 'multiple') {
          $form['bundle'][$id]['product_id'] = array(
            '#type' => 'select',
            '#options' => $options,
            '#default_value' => $defaults,
            '#weight' => 0,
            '#multiple' => true,
            '#ajax' => array(
              'callback' => 'commerce_product_attributes_add_to_cart_form_attributes_refresh',
            ),
          );
        }
        else {
          $form['bundle'][$id]['product_id'] = array(
            '#type' => 'select',
            '#options' => $options,
            '#default_value' => $default_product->product_id,
            '#weight' => 0,
            '#ajax' => array(
              'callback' => 'commerce_product_attributes_add_to_cart_form_attributes_refresh',
            ),
          );
        }
      }
    }
  }

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

  // Do not allow bundle 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 {

    // Remove the default submit handler, we need our own handler.
    foreach ($form['#submit'] as $handler_id => $handler) {
      if ($handler == 'commerce_cart_add_to_cart_form_submit') {
        unset($form['#submit'][$handler_id]);
      }
      elseif ($handler == 'commerce_product_attributes_add_to_cart_form_submit') {
        unset($form['#submit'][$handler_id]);
      }
      elseif ($handler == 'commerce_product_bundle_add_to_cart_form_submit') {
        unset($form['#submit'][$handler_id]);
      }
    }
  }

  // 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_product_bundle_add_to_cart_form_validate';
  $form['#submit'][] = 'commerce_product_bundle_add_to_cart_form_submit';
  return $form;
}