search_api_et.module in Search API Entity Translation 7.2
Same filename and directory in other branches
Adds Entity Translation support to the Search API.
File
search_api_et.moduleView 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();
}
}