You are here

search_api_et.module in Search API Entity Translation 7.2

Same filename and directory in other branches
  1. 7 search_api_et.module

Adds Entity Translation support to the Search API.

File

search_api_et.module
View source
<?php

/**
 * @file
 * Adds Entity Translation support to the Search API.
 */

/**
 * Implements hook_views_api().
 */
function search_api_et_views_api() {
  return array(
    'api' => '3',
  );
}

/**
 * Implements hook_search_api_item_type_info().
 *
 * Provides a multilingual version of all entity types.
 */
function search_api_et_search_api_item_type_info() {

  // Ensure the SearchApiEtHelper class is loaded.
  module_load_include('php', 'search_api_et', 'includes/SearchApiEtHelper');
  $types = array();
  foreach (entity_get_property_info() as $type => $property_info) {
    if (($info = entity_get_info($type)) && field_has_translation_handler($type)) {
      $types[SearchApiEtHelper::getItemType($type)] = array(
        'name' => t('Multilingual !type', array(
          '!type' => $info['label'],
        )),
        'datasource controller' => 'SearchApiEtDatasourceController',
        'entity_type' => $type,
      );
    }
  }
  return $types;
}

/**
 * Helper function: updates the stored IndexID with a new IndexID.
 *
 * @see search_api_et_search_api_index_update()
 * @see search_api_et_search_api_index_insert()
 * @see search_api_et_search_api_index_delete()
 *
 * @param integer $original_id
 *  The original IndexID
 * @param integer $id
 *  The new IndexID
 */
function _search_api_et_update_index_id($original_id, $id) {
  db_update('search_api_et_item')
    ->fields(array(
    'index_id' => $id,
  ))
    ->condition('index_id', $original_id)
    ->execute();
}

/**
 * Implements search_api_index_update().
 */
function search_api_et_search_api_index_update(SearchApiIndex $index) {

  // On feature reverts, the index's numerical ID might change. In that case,
  // we need to keep the {search_api_et_item} table up to date.
  if ($index->id != $index->original->id) {
    _search_api_et_update_index_id($index->original->id, $index->id);
  }
}

/**
 * Implements search_api_index_insert().
 */
function search_api_et_search_api_index_insert(SearchApiIndex $index) {

  // On feature reverts, the index's numerical ID might change. In that case,
  // we need to keep the {search_api_et_item} table up to date.
  // Check whether this is actually part of a revert.
  $reverts =& drupal_static('search_api_et_search_api_index_delete', array());
  if (isset($reverts[$index->machine_name])) {
    _search_api_et_update_index_id($reverts[$index->machine_name], $index->id);
    unset($reverts[$index->machine_name]);
  }
}

/**
 * Implements hook_search_api_index_delete().
 */
function search_api_et_search_api_index_delete(SearchApiIndex $index) {

  // Keep track of Reverted indexes.
  if ($index
    ->hasStatus(ENTITY_IN_CODE)) {
    $reverts =& drupal_static(__FUNCTION__, array());
    $reverts[$index->machine_name] = $index->id;
  }
  else {

    // We must delete our tracked items when the index is deleted.
    db_delete('search_api_et_item')
      ->condition('index_id', $index->id)
      ->execute();
  }
}

/**
 * Retrieve the Search API ET settings for a specific index.
 *
 * @param SearchApiIndex $index
 *   The index whose settings should be retrieved.
 *
 * @return array
 *   An associative array with the Search API ET settings for the specified
 *   index. The settings are as follows:
 *   - languages: All languages which should be included in the index.
 *   - include: Determines with which languages an item should be included in
 *     the index. Possible values are:
 *     - all: Include items in all enabled languages, even if they don't have a
 *       translation.
 *     - incomplete: Include languages which have at least one translated fields
 *       on the entity.
 *     - complete: Only include entities for which all translatable fields have
 *       been translated.
 *   - fallback language: The language to be used for fields if the field isn't
 *     available in the target language. If NULL, fields without appropriate
 *     translations are removed.
 */
function search_api_et_get_index_settings(SearchApiIndex $index) {
  $settings = isset($index->options['search_api_et']) ? $index->options['search_api_et'] : array();
  $settings += array(
    'include' => 'incomplete',
    'restrict undefined' => FALSE,
    'fallback language' => NULL,
  );
  return $settings;
}

/**
 * Implements hook_form_FORM_ID_alter() for search_api_admin_index_edit().
 *
 * @see search_api_admin_index_edit()
 */
function search_api_et_form_search_api_admin_index_edit_alter(&$form, &$form_state) {
  $index = $form_state['build_info']['args'][0];
  $controller = search_api_get_datasource_controller($index->item_type);
  if ($controller instanceof SearchApiEtDatasourceController) {
    $settings = search_api_et_get_index_settings($index);
    $form['options']['search_api_et'] = array(
      '#type' => 'fieldset',
      '#title' => t('Multilingual settings'),
    );
    $form['options']['search_api_et']['include'] = array(
      '#type' => 'select',
      '#title' => t('Languages to be included in the index'),
      '#description' => t('Determines for which languages an item should be created in the index for each entity:') . '<br />' . t("- <em>all site languages</em>: create items for all languages enabled on the site, even if specific entity fields don't have a translation in a given language,") . '<br />' . t('- <em>all entity languages</em>: create items for languages which have at least one translated field on the specific entity (recommended),') . '<br />' . t('- <em>completed entity languages</em>: only include languages for which all translatable fields have been translated on the specific entity.') . '<br />' . t('Changing this setting will force content for this index to be re-queued and re-indexed.'),
      '#options' => array(
        'all' => t('all site languages'),
        'incomplete' => t('all entity languages'),
        'complete' => t('completed entity languages'),
      ),
      '#default_value' => $settings['include'],
    );
    $form['options']['search_api_et']['restrict undefined'] = array(
      '#type' => 'checkbox',
      '#title' => t('Restrict undefined language'),
      '#description' => t('Removes LANGUAGE_NONE entities from being indexed. Entities that contain untranslatable fields will not be indexed, this may solve duplicate search results on partially translated content types.'),
      '#default_value' => $settings['restrict undefined'],
    );
    $form['options']['search_api_et']['fallback language'] = array(
      '#type' => 'select',
      '#title' => t('Fallback language'),
      '#description' => t("The language to be used for fields if the field isn't available in the target language. If none, fields without appropriate translations will be removed."),
      '#options' => search_api_et_languages(TRUE, FALSE),
      '#empty_option' => t('- none -'),
      '#default_value' => $settings['fallback language'],
    );

    // Extra submit function to re-queue index items if required.
    $form['#submit'][] = 'search_api_et_admin_index_edit_submit';
  }
}

/**
 * Form submission handler for search_api_admin_index_edit().
 *
 * @see search_api_admin_index_edit()
 * @see search_api_et_form_search_api_admin_index_edit_alter()
 */
function search_api_et_admin_index_edit_submit(array $form, array &$form_state) {
  $reindex = FALSE;
  $option_values = $form_state['values']['options']['search_api_et'];
  $option_default = $form['options']['search_api_et'];

  // When 'Languages to be included in the index' setting value has changed,
  // all index items need to be re-queued and re-indexed, as most probably
  // their number will change.
  $reindex = $reindex || $option_values['include'] != $option_default['include']['#default_value'];

  // When 'Restrict undefined language' setting value has changed,
  // all index items need to be re-queued and re-indexed, as most probably
  // their number will change.
  $reindex = $reindex || $option_values['restrict undefined'] != $option_default['restrict undefined']['#default_value'];
  if ($reindex) {

    /** @var SearchApiIndex $index */
    $index = $form_state['index'];
    $index
      ->queueItems();
    $index
      ->reindex();
    drupal_set_message(t('The index was successfully scheduled for re-indexing.'));
  }
}

/**
 * Returns list of languages available/enabled on the site.
 *
 * @param bool $enabled_only
 *   A boolean indicating whether to include all languages added to the site
 *   or only those enabled.
 * @param bool $include_neutral
 *   A boolean indicating whether to add the neutral language (LANGUAGE_NONE)
 *   to the language list.
 *
 * @return array
 *   An array with language codes as keys and language names as values.
 *
 * @see SearchApiAlterLanguageControl::configurationForm()
 */
function search_api_et_languages($enabled_only = FALSE, $include_neutral = TRUE) {
  $languages = array();
  if ($include_neutral) {
    $languages[LANGUAGE_NONE] = t('Language neutral');
  }
  $list = language_list();
  foreach ($list as $lang) {
    if ($enabled_only && !$lang->enabled) {
      continue;
    }
    $name = t($lang->name);
    $native = $lang->native;
    $languages[$lang->language] = $name == $native ? $name : $name . ' (' . $native . ')';
    if (!$enabled_only && !$lang->enabled) {
      $languages[$lang->language] .= ' [' . t('disabled') . ']';
    }
  }
  return $languages;
}

/**
 * Determines the languages that are available for an entity in a certain index.
 *
 * @param object $entity
 *   The entity for which languages should be determined.
 * @param string $entity_type
 *   The entity type of the entity.
 * @param SearchApiIndex $index
 *   The index whose settings should be used for determining the languages.
 *
 * @return array
 *   An array of language codes for the languages that are available.
 */
function search_api_et_item_languages($entity, $entity_type, SearchApiIndex $index) {
  module_load_include('inc', 'search_api_et');
  $settings = search_api_et_get_index_settings($index);
  switch ($settings['include']) {
    case 'all':
      $languages = search_api_et_item_languages_all();
      break;
    case 'complete':
      $languages = search_api_et_item_languages_complete($entity, $entity_type);
      break;
    case 'incomplete':
    default:
      $languages = search_api_et_item_languages_entity($entity, $entity_type);
      break;
  }

  // Removing the LANGUAGE_NONE from the available translations, if the original
  // entity is not translated, or if we are adding all the enabled languages to
  // the index.
  if (TRUE == $settings['restrict undefined']) {
    $language = entity_language($entity_type, $entity);
    if ($language != LANGUAGE_NONE || $settings['include'] == 'all') {

      // $languages is an array, flipping to easily remove the LANGUAGE_NONE item.
      $languages = array_flip($languages);
      unset($languages[LANGUAGE_NONE]);
      $languages = array_keys($languages);
    }
  }
  return $languages;
}

/**
 * Implements hook_entity_update().
 */
function search_api_et_entity_update($entity, $entity_type) {

  // We only react on entity operations for types with property information, as
  // we don't provide search integration for the others.
  if (!entity_get_property_info($entity_type)) {
    return;
  }
  list($entity_id, $revision, $bundle) = entity_extract_ids($entity_type, $entity);
  $et_entity_type = SearchApiEtHelper::getItemType($entity_type);

  // Use the translation handler to fetch the main language of the entity.
  // entity_language() together with entity_translation returns the current
  // form language of an entity. Hence if the entity is translated the form
  // language usually differs from the main language - what would lead to the
  // removal of a valid tracking language.
  if (!isset($entity->original) || !is_object($entity->original)) {
    $entity->original = entity_load_unchanged($entity_type, $entity_id);
  }
  if (entity_translation_enabled($entity_type, $entity)) {
    $translation_handler = entity_translation_get_handler($entity_type, $entity);
    $language = $translation_handler
      ->getLanguage();
    $translation_handler_original = entity_translation_get_handler($entity_type, $entity->original);
    $old_language = $translation_handler_original
      ->getLanguage();
  }
  else {
    $language = entity_language($entity_type, $entity);
    $old_language = entity_language($entity_type, $entity->original);
  }

  // If the entity language has changed, remove the old item from the index.
  if (!empty($language) && !empty($old_language) && $language !== $old_language) {
    search_api_track_item_delete($et_entity_type, array(
      SearchApiEtHelper::buildItemId($entity_id, $old_language),
    ));
    search_api_track_item_insert($et_entity_type, array(
      SearchApiEtHelper::buildItemId($entity_id, $language),
    ));
  }
  else {
    search_api_track_item_change($et_entity_type, array(
      SearchApiEtHelper::buildItemId($entity_id, $language),
    ));
  }
}

/**
 * Implements hook_entity_translation_insert().
 */
function search_api_et_entity_translation_insert($entity_type, $entity, $translation, $values = array()) {
  list($entity_id) = entity_extract_ids($entity_type, $entity);
  $item_id = SearchApiEtHelper::buildItemId($entity_id, $translation['language']);

  // Entity that has been loaded previously and cached is now stale, make sure
  // that subsequent loads will produce a fresh entity containing the inserted
  // translation.
  entity_get_controller($entity_type)
    ->resetCache(array(
    $entity_id,
  ));
  search_api_track_item_insert(SearchApiEtHelper::getItemType($entity_type), array(
    $item_id,
  ));
}

/**
 * Implements hook_entity_translation_update().
 */
function search_api_et_entity_translation_update($entity_type, $entity, $translation, $values = array()) {
  list($entity_id) = entity_extract_ids($entity_type, $entity);
  $item_id = SearchApiEtHelper::buildItemId($entity_id, $translation['language']);
  search_api_track_item_change(SearchApiEtHelper::getItemType($entity_type), array(
    $item_id,
  ));
}

/**
 * Implements hook_entity_translation_delete().
 */
function search_api_et_entity_translation_delete($entity_type, $entity, $langcode) {
  list($entity_id) = entity_extract_ids($entity_type, $entity);
  $item_id = SearchApiEtHelper::buildItemId($entity_id, $langcode);
  search_api_track_item_delete(SearchApiEtHelper::getItemType($entity_type), array(
    $item_id,
  ));
}

/**
 * Implements hook_field_update_field().
 *
 * Re-queue all "completed entity languages" indexes on field instance creation
 * (which actually is a 3-step process, and translation can be enabled only on
 * the last step, which actually then becomes field update) and update - needed
 * only when field translation is being enabled or disabled.
 */
function search_api_et_field_update_field($field, $prior_field, $has_data) {
  if ($field['translatable'] != $prior_field['translatable'] && isset($field['bundles']) && is_array($field['bundles'])) {
    $entity_types = array_keys($field['bundles']);

    // Mark for re-indexing the matched entity types.
    if (!empty($entity_types)) {
      drupal_register_shutdown_function('search_api_et_shutdown_requeue_indexes', $entity_types);
    }
  }
}

/**
 * @todo: fix the redirect to Batch index processing
 * Implements hook_field_delete_instance().
 *
 * Re-queue all "completed entity languages" indexes on field instance deletion.
 *
 * Note that instance creation and updates are handled by
 * hook_field_update_field() implementation.
 */
function search_api_et_field_delete_instance($instance) {
  $field_info = field_info_field($instance['field_name']);
  if (field_is_translatable($instance['entity_type'], $field_info)) {
    drupal_register_shutdown_function('search_api_et_shutdown_requeue_indexes', $instance['entity_type']);
  }
}

/**
 * @todo: fix the redirect to Batch index processing
 * Implements hook_multilingual_settings_changed().
 *
 * Re-queue all "completed entity languages" indexes when a language has been
 * added, removed, enabled or disabled.
 */
function search_api_et_multilingual_settings_changed() {
  drupal_register_shutdown_function('search_api_et_shutdown_requeue_indexes');
}

/**
 * Shutdown function to re-queue "completed entity languages" indexes.
 *
 * If there are any multilingual indexes configured to use "completed entity
 * languages" only (for which all translatable fields have to be translated
 * for a translation to be included in an index), after each translation
 * update we need to re-check if such translation should be included in an
 * index. (For example, for a completed translation already existing in the
 * index, if a translation of a single field was removed, then the whole
 * translation needs to be removed from the index, as it is not "complete"
 * anymore.)
 *
 * The re-queueing is called from the shutdown function because some of the
 * update hooks are invoked before the relevant change is really saved to the
 * database (for example hook_entity_translation_update() implementation is
 * called before the translation is really saved), which means that we can't
 * force index re-queueing from such hook, as it would work with incorrect
 * (old) data. Instead, we need to wait for the new data to be saved first,
 * therefore use a shutdown function to force re-queueing.
 *
 * @param string $entity_types
 *   The entity type for which indexes need to be re-queued.
 *
 * @see search_api_et_entity_translation_insert()
 * @see search_api_et_entity_translation_update()
 * @see search_api_et_field_update_field()
 * @see search_api_et_field_delete_instance()
 * @see search_api_et_multilingual_settings_changed()
 *
 * @todo: fix the redirect to Batch index processing
 */
function search_api_et_shutdown_requeue_indexes($entity_types = NULL, $entity_ids = array()) {
  $indexes = _search_api_et_get_indexes($entity_types);
  foreach ($indexes as $index) {

    // Re-queue only those indexes for which "Languages to be included in the
    // index" option is set to "completed entity languages".
    if (!empty($index->options['search_api_et']['include']) && $index->options['search_api_et']['include'] == 'complete') {
      $index
        ->queueItems();
      $index
        ->reindex();
    }
  }
}

/**
 * Helper function to return indexes related to the given entity_types.
 *
 * @param array $entity_types
 *    The entity types to filter from.
 * @param array $bundle
 *    Filter the retrieved indexed by a bundle name.
 *
 * @return SearchApiIndex[]
 */
function _search_api_et_get_indexes($entity_types = NULL) {
  $conditions = array(
    'enabled' => 1,
    'read_only' => 0,
  );
  if (!empty($entity_types)) {
    if (!is_array($entity_types)) {
      $entity_types = array(
        $entity_types,
      );
    }
    $entity_types = array_map(array(
      'SearchApiEtHelper',
      'getItemType',
    ), $entity_types);
    $conditions['item_type'] = $entity_types;
  }

  /** @var SearchApiIndex[] $indexes */
  $res = search_api_index_load_multiple(FALSE, $conditions);
  $indexes = $res ? $res : array();
  return $indexes;
}

/**
 * Shutdown function to start batch job for queueing items for indexes being
 * enabled.
 *
 * @see SearchApiEtDatasourceController::startTracking()
 */
function search_api_et_shutdown_batch_process() {
  drush_backend_batch_process();
}

/**
 * Implements hook_features_export_alter().
 *
 * Adds dependency information for relevant exported indexes.
 */
function search_api_et_features_export_alter(&$export, $module_name) {
  if (isset($export['features']['search_api_index'])) {

    // Check all of the exported index definitions.
    foreach ($export['features']['search_api_index'] as $index_name) {
      $indexes = search_api_index_load_multiple(FALSE, array(
        'machine_name' => $index_name,
      ));
      $index = reset($indexes);
      $controller = search_api_get_datasource_controller($index->item_type);
      if ($controller instanceof SearchApiEtDatasourceController) {
        if (!isset($export['dependencies']['search_api_et'])) {
          $export['dependencies']['search_api_et'] = 'search_api_et';
        }
      }
    }

    // Ensure the dependencies list is still sorted alphabetically.
    ksort($export['dependencies']);
  }
}

/**
 * Implements hook_search_api_index_items_alter().
 *
 * SearchApiEtDatasourceController::getMetadataWrapper() needs to know which
 * index it is adding items to, so that it can loop over all indexed fields
 * first and return their translated values before indexing them.
 *
 * @see SearchApiEtDatasourceController::getMetadataWrapper()
 * @see SearchApiEtDatasourceController::setLanguage()
 */
function search_api_et_search_api_index_items_alter(array &$items, SearchApiIndex $index) {
  $controller = search_api_get_datasource_controller($index->item_type);
  if ($controller instanceof SearchApiEtDatasourceController) {
    foreach (element_children($items) as $item_id) {
      $items[$item_id]->search_api_index = $index;
    }
  }
}

/**
 * Implements hook_search_api_index_reindex().
 */
function search_api_et_search_api_index_reindex(SearchApiIndex $index, $clear = FALSE) {

  // Search api Entity translation use a custom table for indexation. This table
  // should be cleared when reindex is processed.
  // Otherwise, some residual content or wrong content can persist.
  if ($clear) {

    // Index has to be cleared.
    db_delete('search_api_et_item')
      ->condition('index_id', $index->id)
      ->execute();

    // Add every items to queue so they will be indexed again.
    $index
      ->queueItems();
  }
}

Functions

Namesort descending Description
search_api_et_admin_index_edit_submit Form submission handler for search_api_admin_index_edit().
search_api_et_entity_translation_delete Implements hook_entity_translation_delete().
search_api_et_entity_translation_insert Implements hook_entity_translation_insert().
search_api_et_entity_translation_update Implements hook_entity_translation_update().
search_api_et_entity_update Implements hook_entity_update().
search_api_et_features_export_alter Implements hook_features_export_alter().
search_api_et_field_delete_instance @todo: fix the redirect to Batch index processing Implements hook_field_delete_instance().
search_api_et_field_update_field Implements hook_field_update_field().
search_api_et_form_search_api_admin_index_edit_alter Implements hook_form_FORM_ID_alter() for search_api_admin_index_edit().
search_api_et_get_index_settings Retrieve the Search API ET settings for a specific index.
search_api_et_item_languages Determines the languages that are available for an entity in a certain index.
search_api_et_languages Returns list of languages available/enabled on the site.
search_api_et_multilingual_settings_changed @todo: fix the redirect to Batch index processing Implements hook_multilingual_settings_changed().
search_api_et_search_api_index_delete Implements hook_search_api_index_delete().
search_api_et_search_api_index_insert Implements search_api_index_insert().
search_api_et_search_api_index_items_alter Implements hook_search_api_index_items_alter().
search_api_et_search_api_index_reindex Implements hook_search_api_index_reindex().
search_api_et_search_api_index_update Implements search_api_index_update().
search_api_et_search_api_item_type_info Implements hook_search_api_item_type_info().
search_api_et_shutdown_batch_process Shutdown function to start batch job for queueing items for indexes being enabled.
search_api_et_shutdown_requeue_indexes Shutdown function to re-queue "completed entity languages" indexes.
search_api_et_views_api Implements hook_views_api().
_search_api_et_get_indexes Helper function to return indexes related to the given entity_types.
_search_api_et_update_index_id Helper function: updates the stored IndexID with a new IndexID.