commerce_product.module

  1. commerce
    1. 7

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.

Functions & methods

NameDescription
commerce_product_accessChecks product access for various operations.
commerce_product_autocompleteReturns output for product autocompletes.
commerce_product_build_contentGenerate an array for rendering the given product.
commerce_product_can_deleteDetermines whether or not the give product can be deleted.
commerce_product_configure_product_fieldsConfigures the fields on product types provided by other modules.
commerce_product_configure_product_typeEnsures a base price field is present on a product type bundle.
commerce_product_configure_product_typesConfigure the product types defined by enabled modules.
commerce_product_deleteDeletes a product by ID.
commerce_product_delete_multipleDeletes multiple products by ID.
commerce_product_enableImplements hook_enable().
commerce_product_entity_infoImplements hook_entity_info().
commerce_product_entity_translation_supported_typeReturns whether the given product type has support for translations.
commerce_product_entity_translation_tab_accessAccess callback: determines access to a product's translation tab.
commerce_product_field_extra_fieldsImplements hook_field_extra_fields().
commerce_product_file_download_accessImplements hook_file_download_access().
commerce_product_get_propertiesCallback for getting product properties.
commerce_product_hook_infoImplements hook_hook_info().
commerce_product_loadLoads a product by ID.
commerce_product_load_by_skuLoads a product by SKU.
commerce_product_load_multipleLoads multiple products by ID or based on a set of matching conditions.
commerce_product_match_productsFetches an array of all products matching the given parameters.
commerce_product_menuImplements hook_menu().
commerce_product_modules_enabledImplements hook_modules_enabled().
commerce_product_newReturns an initialized product object.
commerce_product_permissionImplements hook_permission().
commerce_product_query_commerce_product_access_alterImplementation of hook_query_commerce_product_access_alter().
commerce_product_replace_sku_tokensPerforms token replacement on a SKU for valid tokens only.
commerce_product_saveSaves a product.
commerce_product_set_propertiesCallback for setting product properties.
commerce_product_themeImplements hook_theme().
commerce_product_typesReturns an array of product type arrays keyed by type.
commerce_product_types_resetResets the cached list of product types.
commerce_product_type_get_nameReturns the human readable name of any or all product types.
commerce_product_type_loadLoads a product type.
commerce_product_type_options_listWraps commerce_product_type_get_name() for the Entity module.
commerce_product_type_to_argReturns a path argument from a product type.
commerce_product_uriEntity uri callback: gives modules a chance to specify a path for a product.
commerce_product_validate_skuChecks to ensure a given SKU does not contain invalid characters.
commerce_product_validate_sku_uniqueChecks to see if a given SKU already exists for another product.
commerce_product_views_apiImplements hook_views_api().
_commerce_product_match_products_standardHelper function for commerce_product_match_products().
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_uri' => array(
      'group' => 'commerce',
    ),
    'commerce_product_view' => array(
      'group' => 'commerce',
    ),
    '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_presave' => array(
      'group' => 'commerce',
    ),
    'commerce_product_insert' => array(
      'group' => 'commerce',
    ),
    'commerce_product_update' => 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',
      'fieldable' => TRUE,
      'entity keys' => array(
        'id' => 'product_id',
        'bundle' => 'type',
        'label' => 'title',
      ),
      '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'),
      ),

      // Add translation support.
      'translation' => array(
        'locale' => TRUE,
        'entity_translation' => array(
          'class' => 'EntityTranslationCommerceProductHandler',
        ),
      ),

      // 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,
            ),
          ),
        ),
      ),
    ),
  );

  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 = array();

  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,
        ),
      ),
    );
  }

  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');
  }

  // 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 if it isn't set yet.
        $product_type += array('module' => $module);
        $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;
}

/**
 * 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.
  foreach ($product_types as $key => $value) {
    $product_types[$key] = $value['name'];
  }

  return $product_types;
}

/**
 * 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.
 */
function commerce_product_load_by_sku($sku) {
  $products = commerce_product_load_multiple(array(), array('sku' => $sku));
  return $products ? reset($products) : FALSE;
}

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

/**
 * Generate an array for rendering the given product.
 *
 * @param $product
 *   A fully loaded product object.
 * @param $view_mode
 *   The view mode for displaying the product, 'full', 'node_full',
 *     'node_teaser', 'node_rss'...
 *
 * @return
 *   An array as expected by drupal_render().
 *
 * @deprecated since 7.x-1.0-rc1
 */
function commerce_product_build_content($product, $view_mode = 'full') {
  return entity_get_controller('commerce_product')->view(array($product->product_id => $product), $view_mode);
}

/**
 * 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');
}

/**
 * Implementation of hook_query_commerce_product_access_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, ',')) {
    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, $field_name, $bundle, $string = '') {
  $field = field_info_field($field_name);
  $instance = field_info_instance($entity_type, $field_name, $bundle);

  $matches = array();

  // 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) . ', ' : '';

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

    // Loop through the products and convert them into autocomplete output.
    foreach ($products as $product_id => $data) {
      // Add a class wrapper for a few required CSS overrides.
      $matches[$prefix . $data['sku']] = '<div class="reference-autocomplete">' . $data['rendered'] . '</div>';
    }
  }

  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])) {
    $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');
  }

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

/**
 * Access callback: determines access to a product's translation tab.
 */
function commerce_product_entity_translation_tab_access($product) {
  if (!empty($product->language) && $product->language != LANGUAGE_NONE) {
    if (commerce_product_entity_translation_supported_type($product->type)) {
      return entity_translation_tab_access('commerce_product');
    }
  }

  return FALSE;
}

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