You are here

commerce_product.module in Commerce Core 7

Same filename and directory in other branches
  1. 8.2 modules/product/commerce_product.module

Defines the core Commerce product entity, including the entity itself, the bundle definitions (product types), and various API functions to manage products and interact with them through forms and autocompletes.

File

modules/product/commerce_product.module
View source
<?php

/**
 * @file
 * Defines the core Commerce product entity, including the entity itself, the
 * bundle definitions (product types), and various API functions to manage
 * products and interact with them through forms and autocompletes.
 */

/**
 * Implements hook_menu().
 */
function commerce_product_menu() {
  $items = array();
  $items['commerce_product/autocomplete'] = array(
    'title' => 'commerce_product autocomplete',
    'page callback' => 'commerce_product_autocomplete',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implements hook_hook_info().
 */
function commerce_product_hook_info() {
  $hooks = array(
    'commerce_product_type_info' => array(
      'group' => 'commerce',
    ),
    'commerce_product_type_info_alter' => array(
      'group' => 'commerce',
    ),
    'commerce_product_type_insert' => array(
      'group' => 'commerce',
    ),
    'commerce_product_type_update' => array(
      'group' => 'commerce',
    ),
    'commerce_product_type_delete' => array(
      'group' => 'commerce',
    ),
    'commerce_product_uri' => array(
      'group' => 'commerce',
    ),
    'commerce_product_view' => array(
      'group' => 'commerce',
    ),
    'commerce_product_presave' => array(
      'group' => 'commerce',
    ),
    'commerce_product_insert' => array(
      'group' => 'commerce',
    ),
    'commerce_product_update' => array(
      'group' => 'commerce',
    ),
    'commerce_product_can_delete' => array(
      'group' => 'commerce',
    ),
    'commerce_product_delete' => array(
      'group' => 'commerce',
    ),
  );
  return $hooks;
}

/**
 * Implements hook_entity_info().
 */
function commerce_product_entity_info() {
  $return = array(
    'commerce_product' => array(
      'label' => t('Commerce Product'),
      'controller class' => 'CommerceProductEntityController',
      'base table' => 'commerce_product',
      'revision table' => 'commerce_product_revision',
      'fieldable' => TRUE,
      'entity keys' => array(
        'id' => 'product_id',
        'bundle' => 'type',
        'label' => 'title',
        'revision' => 'revision_id',
        'language' => 'language',
      ),
      'bundle keys' => array(
        'bundle' => 'type',
      ),
      'bundles' => array(),
      'load hook' => 'commerce_product_load',
      'view modes' => array(
        'full' => array(
          'label' => t('Admin display'),
          'custom settings' => FALSE,
        ),
      ),
      'uri callback' => 'commerce_product_uri',
      'metadata controller class' => '',
      'token type' => 'commerce-product',
      'access callback' => 'commerce_entity_access',
      'access arguments' => array(
        'user key' => 'uid',
        'access tag' => 'commerce_product_access',
      ),
      'permission labels' => array(
        'singular' => t('product'),
        'plural' => t('products'),
      ),
      // Prevent Redirect alteration of the product form.
      'redirect' => FALSE,
      // Add translation support.
      'translation' => array(
        'locale' => TRUE,
        'entity_translation' => array(
          'class' => 'EntityTranslationCommerceProductHandler',
          'bundle callback' => 'commerce_product_entity_translation_supported_type',
          'default settings' => array(
            'default_language' => LANGUAGE_NONE,
            'hide_language_selector' => FALSE,
          ),
        ),
      ),
      // Add title replacement support for translations.
      'field replacement' => array(
        'title' => array(
          'field' => array(
            'type' => 'text',
            'cardinality' => 1,
            'translatable' => TRUE,
          ),
          'instance' => array(
            'label' => t('Title'),
            'required' => TRUE,
            'settings' => array(
              'text_processing' => 0,
            ),
            'widget' => array(
              'weight' => -5,
            ),
          ),
        ),
      ),
    ),
  );
  $return['commerce_product']['bundles'] = array();
  foreach (commerce_product_type_get_name() as $type => $name) {
    $return['commerce_product']['bundles'][$type] = array(
      'label' => $name,
    );
  }
  return $return;
}

/**
 * Entity uri callback: gives modules a chance to specify a path for a product.
 */
function commerce_product_uri($product) {

  // Allow modules to specify a path, returning the first one found.
  foreach (module_implements('commerce_product_uri') as $module) {
    $uri = module_invoke($module, 'commerce_product_uri', $product);

    // If the implementation returned data, use that now.
    if (!empty($uri)) {
      return $uri;
    }
  }
  return NULL;
}

/**
 * Implements hook_file_download_access().
 *
 * This hook is grants access to files based on a user's access to the entity
 * a file is attached to. For example, users with access to a product should be
 * allowed to download files attached to that product. Here we do this on a per-
 * field basis for files attached to products.
 *
 * @param $field
 *   The field to which the file belongs.
 * @param $entity_type
 *   The type of $entity; for example, 'node' or 'user' or 'commerce_product'.
 * @param $entity
 *   The $entity to which $file is referenced.
 *
 * @return
 *   TRUE if access should be allowed by this entity or FALSE if denied. Note
 *   that denial may be overridden by another entity controller, making this
 *   grant permissive rather than restrictive.
 */
function commerce_product_file_download_access($field, $entity_type, $entity) {
  if ($entity_type == 'commerce_product') {
    return field_access('view', $field, $entity_type, $entity);
  }
}

/**
 * Implements hook_field_extra_fields().
 */
function commerce_product_field_extra_fields() {
  $extra =& drupal_static(__FUNCTION__);
  if (!isset($extra)) {
    foreach (commerce_product_types() as $type => $product_type) {
      $extra['commerce_product'][$type] = array(
        'form' => array(
          'sku' => array(
            'label' => t('Product SKU'),
            'description' => t('Product module SKU form element'),
            'weight' => -10,
          ),
          'title' => array(
            'label' => t('Title'),
            'description' => t('Product module title form element'),
            'weight' => -5,
          ),
          'status' => array(
            'label' => t('Status'),
            'description' => t('Product module status form element'),
            'weight' => 35,
          ),
        ),
        'display' => array(
          'sku' => array(
            'label' => t('SKU'),
            'description' => t('The human readable identifier of the product'),
            'theme' => 'commerce_product_sku',
            'weight' => -10,
          ),
          'title' => array(
            'label' => t('Title'),
            'description' => t('Full product title'),
            'theme' => 'commerce_product_title',
            'weight' => -5,
          ),
          'status' => array(
            'label' => t('Status'),
            'description' => t('Whether the product is active or disabled'),
            'theme' => 'commerce_product_status',
            'weight' => 5,
          ),
        ),
      );
    }
  }
  return $extra;
}

/**
 * Implements hook_theme().
 */
function commerce_product_theme() {
  return array(
    'commerce_product_sku' => array(
      'variables' => array(
        'sku' => NULL,
        'label' => NULL,
        'product' => NULL,
      ),
      'path' => drupal_get_path('module', 'commerce_product') . '/theme',
      'template' => 'commerce-product-sku',
    ),
    'commerce_product_title' => array(
      'variables' => array(
        'title' => NULL,
        'label' => NULL,
        'product' => NULL,
      ),
      'path' => drupal_get_path('module', 'commerce_product') . '/theme',
      'template' => 'commerce-product-title',
    ),
    'commerce_product_status' => array(
      'variables' => array(
        'status' => NULL,
        'label' => NULL,
        'product' => NULL,
      ),
      'path' => drupal_get_path('module', 'commerce_product') . '/theme',
      'template' => 'commerce-product-status',
    ),
  );
}

/**
 * Implements hook_views_api().
 */
function commerce_product_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'commerce_product') . '/includes/views',
  );
}

/**
 * Implements hook_permission().
 */
function commerce_product_permission() {
  $permissions = array(
    'administer product types' => array(
      'title' => t('Administer product types'),
      'description' => t('Allows users to configure product types and their fields.'),
      'restrict access' => TRUE,
    ),
  );
  $permissions += commerce_entity_access_permissions('commerce_product');
  return $permissions;
}

/**
 * Implements hook_enable().
 */
function commerce_product_enable() {
  commerce_product_configure_product_types();
}

/**
 * Implements hook_modules_enabled().
 */
function commerce_product_modules_enabled($modules) {
  commerce_product_configure_product_fields($modules);
}

/**
 * Configure the product types defined by enabled modules.
 */
function commerce_product_configure_product_types() {
  foreach (commerce_product_types() as $type => $product_type) {
    commerce_product_configure_product_type($type);
  }
}

/**
 * Ensures a base price field is present on a product type bundle.
 */
function commerce_product_configure_product_type($type) {
  commerce_price_create_instance('commerce_price', 'commerce_product', $type, t('Price'), 0, 'calculated_sell_price');
}

/**
 * Configures the fields on product types provided by other modules.
 *
 * @param $modules
 *   An array of module names whose product type fields should be configured;
 *   if left NULL, will default to all modules that implement
 *   hook_commerce_product_type_info().
 */
function commerce_product_configure_product_fields($modules = NULL) {

  // If no modules array is passed, recheck the fields for all product types
  // defined by enabled modules.
  if (empty($modules)) {
    $modules = module_implements('commerce_product_type_info');
  }

  // Reset the product type cache to get types added by newly enabled modules.
  commerce_product_types_reset();

  // Loop through all the enabled modules.
  foreach ($modules as $module) {

    // If the module implements hook_commerce_product_type_info()...
    if (module_hook($module, 'commerce_product_type_info')) {
      $product_types = module_invoke($module, 'commerce_product_type_info');

      // Loop through and configure the product types defined by the module.
      foreach ($product_types as $type => $product_type) {
        commerce_product_configure_product_type($type);
      }
    }
  }
}

/**
 * Returns an array of product type arrays keyed by type.
 */
function commerce_product_types() {

  // First check the static cache for a product types array.
  $product_types =& drupal_static(__FUNCTION__);

  // If it did not exist, fetch the types now.
  if (!isset($product_types)) {
    $product_types = array();

    // Find product types defined by hook_commerce_product_type_info().
    foreach (module_implements('commerce_product_type_info') as $module) {
      foreach (module_invoke($module, 'commerce_product_type_info') as $type => $product_type) {

        // Set the module each product type is defined by and revision handling
        // if they aren't set yet.
        $product_type += array(
          'module' => $module,
          'revision' => 1,
        );
        $product_types[$type] = $product_type;
      }
    }

    // Last allow the info to be altered by other modules.
    drupal_alter('commerce_product_type_info', $product_types);
  }
  return $product_types;
}

/**
 * Resets the cached list of product types.
 */
function commerce_product_types_reset() {
  $product_types =& drupal_static('commerce_product_types');
  $product_types = NULL;
  entity_info_cache_clear();
}

/**
 * Loads a product type.
 *
 * @param $type
 *   The machine-readable name of the product type; accepts normal machine names
 *     and URL prepared machine names with underscores replaced by hyphens.
 */
function commerce_product_type_load($type) {
  $type = strtr($type, array(
    '-' => '_',
  ));
  $product_types = commerce_product_types();
  return !empty($product_types[$type]) ? $product_types[$type] : FALSE;
}

/**
 * Returns the human readable name of any or all product types.
 *
 * @param $type
 *   Optional parameter specifying the type whose name to return.
 *
 * @return
 *   Either an array of all product type names keyed by the machine name or a
 *     string containing the human readable name for the specified type. If a
 *     type is specified that does not exist, this function returns FALSE.
 */
function commerce_product_type_get_name($type = NULL) {
  $product_types = commerce_product_types();

  // Return a type name if specified and it exists.
  if (!empty($type)) {
    if (isset($product_types[$type])) {
      return $product_types[$type]['name'];
    }
    else {

      // Return FALSE if it does not exist.
      return FALSE;
    }
  }

  // Otherwise turn the array values into the type name only.
  $product_type_names = array();
  foreach ($product_types as $key => $value) {
    $product_type_names[$key] = $value['name'];
  }
  return $product_type_names;
}

/**
 * Wraps commerce_product_type_get_name() for the Entity module.
 */
function commerce_product_type_options_list() {
  return commerce_product_type_get_name();
}

/**
 * Returns a path argument from a product type.
 */
function commerce_product_type_to_arg($type) {
  return strtr($type, '_', '-');
}

/**
 * Returns an initialized product object.
 *
 * @param $type
 *   The machine-readable type of the product.
 *
 * @return
 *   A product object with all default fields initialized.
 */
function commerce_product_new($type = '') {
  return entity_get_controller('commerce_product')
    ->create(array(
    'type' => $type,
  ));
}

/**
 * Saves a product.
 *
 * @param $product
 *   The full product object to save.
 *
 * @return
 *   SAVED_NEW or SAVED_UPDATED depending on the operation performed.
 */
function commerce_product_save($product) {
  return entity_get_controller('commerce_product')
    ->save($product);
}

/**
 * Loads a product by ID.
 */
function commerce_product_load($product_id) {
  if (empty($product_id)) {
    return FALSE;
  }
  $products = commerce_product_load_multiple(array(
    $product_id,
  ), array());
  return $products ? reset($products) : FALSE;
}

/**
 * Loads a product by SKU.
 *
 * @param string $sku
 *    The SKU to load.
 * @param bool $reset
 *    Boolean to reset static cache.
 */
function commerce_product_load_by_sku($sku, $reset = FALSE) {
  $cache =& drupal_static(__FUNCTION__, array());
  if (!isset($cache[$sku]) || $reset) {
    $products = commerce_product_load_multiple(array(), array(
      'sku' => $sku,
    ));
    $cache[$sku] = $products ? reset($products) : FALSE;
  }
  return $cache[$sku];
}

/**
 * Loads multiple products by ID or based on a set of matching conditions.
 *
 * @see entity_load()
 *
 * @param $product_ids
 *   An array of product IDs.
 * @param $conditions
 *   An array of conditions on the {commerce_product} table in the form
 *     'field' => $value.
 * @param $reset
 *   Whether to reset the internal product loading cache.
 *
 * @return
 *   An array of product objects indexed by product_id.
 */
function commerce_product_load_multiple($product_ids = array(), $conditions = array(), $reset = FALSE) {
  if (empty($product_ids) && empty($conditions)) {
    return array();
  }
  return entity_load('commerce_product', $product_ids, $conditions, $reset);
}

/**
 * Determines whether or not the give product can be deleted.
 *
 * @param $product
 *   The product to be checked for deletion.
 *
 * @return
 *   Boolean indicating whether or not the product can be deleted.
 */
function commerce_product_can_delete($product) {

  // Return FALSE if the given product does not have an ID; it need not be
  // deleted, which is functionally equivalent to cannot be deleted as far as
  // code depending on this function is concerned.
  if (empty($product->product_id)) {
    return FALSE;
  }

  // If any module implementing hook_commerce_product_can_delete() returns FALSE
  // the product cannot be deleted. Return TRUE if none return FALSE.
  return !in_array(FALSE, module_invoke_all('commerce_product_can_delete', $product));
}

/**
 * Deletes a product by ID.
 *
 * @param $product_id
 *   The ID of the product to delete.
 *
 * @return
 *   TRUE on success, FALSE otherwise.
 */
function commerce_product_delete($product_id) {
  return commerce_product_delete_multiple(array(
    $product_id,
  ));
}

/**
 * Deletes multiple products by ID.
 *
 * @param $product_ids
 *   An array of product IDs to delete.
 *
 * @return
 *   TRUE on success, FALSE otherwise.
 */
function commerce_product_delete_multiple($product_ids) {
  return entity_get_controller('commerce_product')
    ->delete($product_ids);
}

/**
 * Checks product access for various operations.
 *
 * @param $op
 *   The operation being performed. One of 'view', 'update', 'create' or
 *   'delete'.
 * @param $product
 *   Optionally a product to check access for or for the create operation the
 *   product type. If nothing is given access permissions for all products are returned.
 * @param $account
 *   The user to check for. Leave it to NULL to check for the current user.
 */
function commerce_product_access($op, $product = NULL, $account = NULL) {
  return commerce_entity_access($op, $product, $account, 'commerce_product');
}

/**
 * Implements hook_query_TAG_alter().
 */
function commerce_product_query_commerce_product_access_alter(QueryAlterableInterface $query) {
  return commerce_entity_access_query_alter($query, 'commerce_product');
}

/**
 * Performs token replacement on a SKU for valid tokens only.
 *
 * TODO: This function currently limits acceptable Tokens to Product ID and type
 * with no ability to use Tokens for the Fields attached to the product. That
 * might be fine for a core Token replacement, but we should at least open the
 * $valid_tokens array up to other modules to enable various Tokens for use.
 *
 * @param $sku
 *   The raw SKU string including any tokens as entered.
 * @param $product
 *   A product object used to perform token replacement on the SKU.
 *
 * @return
 *   The SKU with tokens replaced or else FALSE if it included invalid tokens.
 */
function commerce_product_replace_sku_tokens($sku, $product) {

  // Build an array of valid SKU tokens.
  $valid_tokens = array(
    'product-id',
    'type',
  );

  // Ensure that only valid tokens were used.
  $invalid_tokens = FALSE;
  foreach (token_scan($sku) as $type => $token) {
    if ($type !== 'product') {
      $invalid_tokens = TRUE;
    }
    else {
      foreach (array_keys($token) as $value) {
        if (!in_array($value, $valid_tokens)) {
          $invalid_tokens = TRUE;
        }
      }
    }
  }

  // Register the error if an invalid token was detected.
  if ($invalid_tokens) {
    return FALSE;
  }
  return $sku;
}

/**
 * Checks to see if a given SKU already exists for another product.
 *
 * @param $sku
 *   The string to match against existing SKUs.
 * @param $product_id
 *   The ID of the product the SKU is for; an empty value represents the SKU is
 *     meant for a new product.
 *
 * @return
 *   TRUE or FALSE indicating whether or not the SKU exists for another product.
 */
function commerce_product_validate_sku_unique($sku, $product_id) {

  // Look for an ID of a product matching the supplied SKU.
  if ($match_id = db_query('SELECT product_id FROM {commerce_product} WHERE sku = :sku', array(
    ':sku' => $sku,
  ))
    ->fetchField()) {

    // If this SKU is supposed to be for a new product or a product other than
    // the one that matched...
    if (empty($product_id) || $match_id != $product_id) {
      return FALSE;
    }
  }
  return TRUE;
}

/**
 * Checks to ensure a given SKU does not contain invalid characters.
 *
 * @param $sku
 *   The string to validate as a SKU.
 *
 * @return
 *   TRUE or FALSE indicating whether or not the SKU is valid.
 */
function commerce_product_validate_sku($sku) {

  // Do not allow commas in a SKU.
  if (strpos($sku, ',') !== FALSE) {
    return FALSE;
  }
  return TRUE;
}

/**
 * Callback for getting product properties.
 * @see commerce_product_entity_property_info()
 */
function commerce_product_get_properties($product, array $options, $name) {
  switch ($name) {
    case 'creator':
      return $product->uid;
    case 'edit_url':
      return url('admin/commerce/products/' . $product->product_id . '/edit', $options);
  }
}

/**
 * Callback for setting product properties.
 * @see commerce_product_entity_property_info()
 */
function commerce_product_set_properties($product, $name, $value) {
  if ($name == 'creator') {
    $product->uid = $value;
  }
}

/**
 * Returns output for product autocompletes.
 *
 * The values returned will be keyed by SKU and appear as such in the textfield,
 * even though the preview in the autocomplete list shows "SKU: Title".
 */
function commerce_product_autocomplete($entity_type = 'commerce_product', $field_name = NULL, $bundle = NULL, $string = '') {
  $matches = array();

  // Extract the SKU string from the URL while preserving support for SKUs that
  // contain slashes. Whereas we previously relied on the $string parameter, its
  // value was being truncated when any SKUs in the autocomplete widget had a /.
  // @see http://drupal.org/node/1962144
  $args = explode('/', request_path());
  if (count($args) <= 5) {
    drupal_json_output($matches);
    return;
  }

  // Trim off leading arguments from other modules such as Locale.
  $offset = array_search('commerce_product', $args);
  array_splice($args, 0, 5 + $offset);
  $string = implode('/', $args);

  // The user enters a comma-separated list of tags. We only autocomplete the last tag.
  $tags_typed = drupal_explode_tags($string);
  $tag_last = drupal_strtolower(array_pop($tags_typed));
  if (!empty($tag_last)) {
    $prefix = count($tags_typed) ? implode(', ', $tags_typed) . ', ' : '';
    $field = field_info_field($field_name);
    $instance = field_info_instance($entity_type, $field_name, $bundle);

    // Determine the type of autocomplete match to use when searching for products.
    $match = isset($field['widget']['autocomplete_match']) ? $field['widget']['autocomplete_match'] : 'contains';

    // Get an array of matching products.
    $products = commerce_product_match_products($field, $instance, $tag_last, $match, array(), 10, TRUE);

    // Loop through the products and convert them into autocomplete output.
    foreach ($products as $product_id => $data) {
      $matches[$prefix . $data['sku']] = $data['rendered'];
    }
  }
  drupal_json_output($matches);
}

/**
 * Fetches an array of all products matching the given parameters.
 *
 * This info is used in various places (allowed values, autocomplete results,
 * input validation...). Some of them only need the product_ids, others
 * product_id + titles, others yet product_id + titles + rendered row (for
 * display in widgets).
 *
 * The array we return contains all the potentially needed information,
 * and lets calling functions use the parts they actually need.
 *
 * @param $field
 *   The field description.
 * @param $string
 *   Optional string to filter SKUs and titles on (used by autocomplete).
 * @param $match
 *   Operator to match filtered SKUs and titles against, can be any of:
 *   'contains', 'equals', 'starts_with'
 * @param $ids
 *   Optional product ids to lookup (used when $string and $match arguments are
 *   not given).
 * @param $limit
 *   If non-zero, limit the size of the result set.
 * @param $access_tag
 *   Boolean indicating whether or not an access control tag should be added to
 *   the query to find matching product data. Defaults to FALSE.
 *
 * @return
 *   An array of valid products in the form:
 *   array(
 *     product_id => array(
 *       'product_sku' => The product SKU,
 *       'title' => The product title,
 *       'rendered' => The text to display in widgets (can be HTML)
 *     ),
 *     ...
 *   )
 */
function commerce_product_match_products($field, $instance = NULL, $string = '', $match = 'contains', $ids = array(), $limit = NULL, $access_tag = FALSE) {
  $results =& drupal_static(__FUNCTION__, array());

  // Create unique id for static cache.
  $cid = implode(':', array(
    $field['field_name'],
    $match,
    $string !== '' ? $string : implode('-', $ids),
    $limit,
  ));
  if (!isset($results[$cid])) {

    // If the variable commerce_product_match_using_efq has been set, then
    // process the match using an EntityFieldQuery instead of db_select(). EFQ
    // was made the only matching query builder in Commerce 1.10, but this
    // introduced a scalability concern as it required the use of a multiple
    // product load to build the return value instead of directly querying the
    // title / SKU from the local database.
    // Commerce 1.12 then introduced a breaking change by attempting to resolve
    // the scalability issue by changing the options list limit from unlimited
    // to 10 by default. With no clear path to resolve all concerns, we opted to
    // make the routine alterable via configuration for Commerce 1.14.
    // The recommended method to select the EFQ based callback instead of this
    // one is to set the variable directly in the $conf array in your
    // settings.php file. Additionally, you may need to set the product
    // reference options list limit variable as explained in the comments
    // inline in commerce_product_reference_options_list().
    if (variable_get('commerce_product_match_products_efq', FALSE)) {
      $matches = _commerce_product_match_products_efq($instance, $string, $match, $ids, $limit, $access_tag);
    }
    else {
      $matches = _commerce_product_match_products_standard($instance, $string, $match, $ids, $limit, $access_tag);
    }

    // Store the results.
    $results[$cid] = !empty($matches) ? $matches : array();
  }
  return $results[$cid];
}

/**
 * Helper function for commerce_product_match_products().
 *
 * Returns an array of products matching the specific parameters.
 */
function _commerce_product_match_products_standard($instance, $string = '', $match = 'contains', $ids = array(), $limit = NULL, $access_tag = FALSE) {

  // Build the query object with the necessary fields.
  $query = db_select('commerce_product', 'cp');

  // Add the access control tag if specified.
  if ($access_tag) {
    $query
      ->addTag('commerce_product_access');
  }

  // Add a global query tag so anyone can alter this query.
  $query
    ->addTag('commerce_product_match');
  $product_id_alias = $query
    ->addField('cp', 'product_id');
  $product_sku_alias = $query
    ->addField('cp', 'sku');
  $product_title_alias = $query
    ->addField('cp', 'title');
  $product_type_alias = $query
    ->addField('cp', 'type');

  // Add a condition to the query to filter by matching product types.
  if (!empty($instance['settings']['referenceable_types'])) {
    $types = array_diff(array_values($instance['settings']['referenceable_types']), array(
      0,
      NULL,
    ));

    // Only filter by type if some types have been specified.
    if (!empty($types)) {
      $query
        ->condition('cp.type', $types, 'IN');
    }
  }
  if ($string !== '') {
    $args = array();

    // Build a where clause matching on either the SKU or title.
    switch ($match) {
      case 'contains':
        $where = '(cp.sku LIKE :sku_match OR cp.title LIKE :title_match)';
        $args['sku_match'] = '%' . $string . '%';
        $args['title_match'] = '%' . $string . '%';
        break;
      case 'starts_with':
        $where = '(cp.sku LIKE :sku_match OR cp.title LIKE :title_match)';
        $args['sku_match'] = $string . '%';
        $args['title_match'] = $string . '%';
        break;
      case 'equals':
      default:
        $where = '(cp.sku = :sku_match OR cp.title = :title_match)';
        $args['sku_match'] = $string;
        $args['title_match'] = $string;
        break;
    }
    $query
      ->where($where, $args);
  }
  elseif ($ids) {

    // Otherwise add a product_id specific condition if specified.
    $query
      ->condition($product_id_alias, $ids, 'IN', $ids);
  }

  // Order the results by SKU, title, and then product type.
  $query
    ->orderBy($product_sku_alias)
    ->orderBy($product_title_alias)
    ->orderBy($product_type_alias);

  // Add a limit if specified.
  if ($limit) {
    $query
      ->range(0, $limit);
  }

  // Execute the query and build the results array.
  $result = $query
    ->execute();
  $matches = array();
  foreach ($result
    ->fetchAll() as $product) {
    $matches[$product->product_id] = array(
      'sku' => $product->sku,
      'type' => $product->type,
      'title' => $product->title,
      'rendered' => t('@sku: @title', array(
        '@sku' => $product->sku,
        '@title' => $product->title,
      )),
    );
  }
  return $matches;
}

/**
 * Helper function for commerce_product_match_products().
 *
 * Returns an array of products matching the specific parameters via EFQ.
 */
function _commerce_product_match_products_efq($instance, $string = '', $match = 'contains', $ids = array(), $limit = NULL, $access_tag = FALSE) {
  $query = new EntityFieldQuery();
  $query
    ->entityCondition('entity_type', 'commerce_product');

  // Add the access control tag if specified.
  if ($access_tag) {
    $query
      ->addTag('commerce_product_access');
  }

  // Add a global query tag so anyone can alter this query.
  $query
    ->addTag('commerce_product_match');

  // Add a condition to the query to filter by matching product types.
  if (!empty($instance['settings']['referenceable_types'])) {
    $types = array_diff(array_values($instance['settings']['referenceable_types']), array(
      0,
      NULL,
    ));

    // Only filter by type if some types have been specified.
    if (!empty($types)) {
      $query
        ->propertyCondition('type', $types, 'IN');
    }
  }
  if ($string !== '') {

    // EntityFieldQuery cannot do OR clauses, so we use hook_query_TAG_alter.
    $query
      ->addTag('commerce_sku_or_title_match');
    $sku_title_meta = new stdClass();
    $sku_title_meta->properties = array(
      'sku',
      'title',
    );
    $sku_title_meta->string = $string;
    $sku_title_meta->match = $match;
    $query
      ->addMetaData('commerce_sku_or_title_match', $sku_title_meta);
  }
  elseif ($ids) {

    // Otherwise add a product_id specific condition if specified.
    $query
      ->propertyCondition('product_id', $ids, 'IN');
  }

  // Order the results by SKU, title, and then product type.
  $query
    ->propertyOrderBy('sku')
    ->propertyOrderBy('title')
    ->propertyOrderBy('type');

  // Add a limit if specified.
  if ($limit) {
    $query
      ->range(0, $limit);
  }
  $entities = $query
    ->execute();
  $matches = array();
  if (isset($entities['commerce_product'])) {
    $pids = array_keys($entities['commerce_product']);

    // EntityFieldQuery doesn't return sku and title, so we have to load again.
    $products = commerce_product_load_multiple($pids);
    foreach ($products as $product) {
      $matches[$product->product_id] = array(
        'sku' => $product->sku,
        'type' => $product->type,
        'title' => $product->title,
        'rendered' => t('@sku: @title', array(
          '@sku' => $product->sku,
          '@title' => $product->title,
        )),
      );
    }
  }
  return $matches;
}

/**
 * Implements hook_query_TAG_alter.
 *
 * EntityFieldQuery used in _commerce_product_match_products_standard() does not
 * allow OR clauses. Alter the SQL query to string match on sku OR title.
 *
 * @param QueryAlterableInterface $query
 */
function commerce_product_query_commerce_sku_or_title_match_alter(QueryAlterableInterface $query) {
  $string = $query->alterMetaData['commerce_sku_or_title_match']->string;
  $match = $query->alterMetaData['commerce_sku_or_title_match']->match;
  if (isset($string, $match)) {

    // Build a where clause matching on either the SKU or title.
    switch ($match) {
      case 'contains':
        $or = db_or()
          ->condition('sku', '%' . $string . '%', 'LIKE')
          ->condition('title', '%' . $string . '%', 'LIKE');
        break;
      case 'starts_with':
        $or = db_or()
          ->condition('sku', $string . '%', 'LIKE')
          ->condition('title', $string . '%', 'LIKE');
        break;
      case 'equals':
      default:
        $or = db_or()
          ->condition('sku', $string, '=')
          ->condition('title', $string, '=');
        break;
    }
    $query
      ->condition($or);
  }
}

/**
 * Access callback: determines access to a product's translation tab.
 */
function commerce_product_entity_translation_tab_access($product) {
  return entity_translation_tab_access('commerce_product', $product);
}

/**
 * Returns whether the given product type has support for translations.
 *
 * @param $type
 *   The machine-name of the product type to check for translation support.
 *
 * @return
 *   TRUE or FALSE indicating translation support for the requested type.
 */
function commerce_product_entity_translation_supported_type($type) {
  return variable_get('language_product_type_' . $type, 0) == ENTITY_TRANSLATION_ENABLED;
}

/**
 * Sanitizes the product SKU before output.
 */
function template_preprocess_commerce_product_sku(&$variables) {
  $variables['sku'] = check_plain($variables['sku']);
}

/**
 * Sanitizes the product title before output.
 */
function template_preprocess_commerce_product_title(&$variables) {
  $variables['title'] = check_plain($variables['title']);
}

/**
 * Returns the options list for the product status property.
 */
function commerce_product_status_options_list() {
  return array(
    0 => t('Disabled'),
    1 => t('Active'),
  );
}

/**
 * Converts the product status integer to a string before output.
 */
function template_preprocess_commerce_product_status(&$variables) {
  $variables['status'] = empty($variables['status']) ? t('Disabled') : t('Active');
}

Functions

Namesort descending Description
commerce_product_access Checks product access for various operations.
commerce_product_autocomplete Returns output for product autocompletes.
commerce_product_can_delete Determines whether or not the give product can be deleted.
commerce_product_configure_product_fields Configures the fields on product types provided by other modules.
commerce_product_configure_product_type Ensures a base price field is present on a product type bundle.
commerce_product_configure_product_types Configure the product types defined by enabled modules.
commerce_product_delete Deletes a product by ID.
commerce_product_delete_multiple Deletes multiple products by ID.
commerce_product_enable Implements hook_enable().
commerce_product_entity_info Implements hook_entity_info().
commerce_product_entity_translation_supported_type Returns whether the given product type has support for translations.
commerce_product_entity_translation_tab_access Access callback: determines access to a product's translation tab.
commerce_product_field_extra_fields Implements hook_field_extra_fields().
commerce_product_file_download_access Implements hook_file_download_access().
commerce_product_get_properties Callback for getting product properties.
commerce_product_hook_info Implements hook_hook_info().
commerce_product_load Loads a product by ID.
commerce_product_load_by_sku Loads a product by SKU.
commerce_product_load_multiple Loads multiple products by ID or based on a set of matching conditions.
commerce_product_match_products Fetches an array of all products matching the given parameters.
commerce_product_menu Implements hook_menu().
commerce_product_modules_enabled Implements hook_modules_enabled().
commerce_product_new Returns an initialized product object.
commerce_product_permission Implements hook_permission().
commerce_product_query_commerce_product_access_alter Implements hook_query_TAG_alter().
commerce_product_query_commerce_sku_or_title_match_alter Implements hook_query_TAG_alter.
commerce_product_replace_sku_tokens Performs token replacement on a SKU for valid tokens only.
commerce_product_save Saves a product.
commerce_product_set_properties Callback for setting product properties.
commerce_product_status_options_list Returns the options list for the product status property.
commerce_product_theme Implements hook_theme().
commerce_product_types Returns an array of product type arrays keyed by type.
commerce_product_types_reset Resets the cached list of product types.
commerce_product_type_get_name Returns the human readable name of any or all product types.
commerce_product_type_load Loads a product type.
commerce_product_type_options_list Wraps commerce_product_type_get_name() for the Entity module.
commerce_product_type_to_arg Returns a path argument from a product type.
commerce_product_uri Entity uri callback: gives modules a chance to specify a path for a product.
commerce_product_validate_sku Checks to ensure a given SKU does not contain invalid characters.
commerce_product_validate_sku_unique Checks to see if a given SKU already exists for another product.
commerce_product_views_api Implements hook_views_api().
template_preprocess_commerce_product_sku Sanitizes the product SKU before output.
template_preprocess_commerce_product_status Converts the product status integer to a string before output.
template_preprocess_commerce_product_title Sanitizes the product title before output.
_commerce_product_match_products_efq Helper function for commerce_product_match_products().
_commerce_product_match_products_standard Helper function for commerce_product_match_products().