SearchApiEtDatasourceController.php in Search API Entity Translation 7.2
Contains the SearchApiEtDatasourceController class.
File
includes/SearchApiEtDatasourceController.phpView source
<?php
/**
* @file
* Contains the SearchApiEtDatasourceController class.
*/
/**
* Provides multilingual versions of all entity types.
*/
class SearchApiEtDatasourceController extends SearchApiEntityDataSourceController {
/**
* Overrides SearchApiEntityDataSourceController::$table.
*
* Needed because we have a string ID, instead of a numerical one.
*
* @var string
*/
protected $table = 'search_api_et_item';
/**
* {@inheritdoc}
*/
public function getIdFieldInfo() {
return array(
'key' => 'search_api_et_id',
'type' => 'string',
);
}
/**
* {@inheritdoc}
*/
public function loadItems(array $ids) {
$item_languages = array();
foreach ($ids as $id) {
// This method might receive two different types of item IDs depending on
// where it is being called from. For example, when called from
// search_api_index_specific_items(), it will receive multilingual IDs
// (with language prefix, like "2/en"). On the other hand, when called from
// a processor (for example from SearchApiHighlight::getFulltextFields()),
// the IDs won't be multilingual (no language prefix), just standard
// entity IDs instead. Therefore we need to account for both cases here.
// Case 1 - language is in item ID.
if (SearchApiEtHelper::isValidItemId($id)) {
$entity_id = SearchApiEtHelper::splitItemId($id, SearchApiEtHelper::ITEM_ID_ENTITY_ID);
$item_languages[$entity_id][] = SearchApiEtHelper::splitItemId($id, SearchApiEtHelper::ITEM_ID_LANGUAGE);
}
else {
$item_languages[$id][] = NULL;
}
}
$entities = entity_load($this->entityType, array_keys($item_languages));
// If some items could not be loaded, remove them from tracking.
if (count($entities) != count($item_languages)) {
$unknown = array_keys(array_diff_key($item_languages, $entities));
if ($unknown) {
$deleted = array();
foreach ($unknown as $entity_id) {
foreach ($item_languages[$entity_id] as $language) {
$deleted[] = SearchApiEtHelper::buildItemId($entity_id, $language);
}
}
search_api_track_item_delete($this->type, $deleted);
}
}
// Now arrange them according to our IDs again, with language.
$items = array();
foreach ($item_languages as $entity_id => $languages) {
if (!empty($entities[$entity_id])) {
foreach ($languages as $language) {
// Following on the two cases described above, we should return
// the same item IDs (with or without language prefix) as received.
$entity = clone $entities[$entity_id];
$id = !empty($language) ? SearchApiEtHelper::buildItemId($entity_id, $language) : $entity_id;
$entity->search_api_et_id = $id;
// Keep current entity language if language is NULL.
$entity->language = !is_null($language) ? $language : $entity->language;
$items[$id] = $entity;
}
}
}
return $items;
}
/**
* {@inheritdoc}
*/
public function getMetadataWrapper($item = NULL, array $info = array()) {
// Since this is usually called with a "property info alter" callback
// already in place (and only one value is allowed), we have to call
// the existing callback from within our own callback to make it work.
$property_info_alter = isset($info['property info alter']) ? $info['property info alter'] : NULL;
$callback = new SearchApiEtPropertyInfoAlter($property_info_alter);
$info['property info alter'] = array(
$callback,
'propertyInfoAlter',
);
// If the item isn't the object and a multilingual id is provided
// extract the entity id to load and wrap the entity.
if (SearchApiEtHelper::isValidItemId($item)) {
$item = SearchApiEtHelper::splitItemId($item, SearchApiEtHelper::ITEM_ID_ENTITY_ID);
}
$wrapper = entity_metadata_wrapper($this->entityType, $item, $info);
// If the item's language is set, let's set it on all wrapper fields,
// so that their translated values get indexed.
if (!empty($item->search_api_language)) {
// Set language on the wrapper as a whole.
$wrapper
->language($item->search_api_language);
// Also try to set language on all wrapper fields, recursively.
if (!empty($item->search_api_index)) {
$this
->setLanguage($wrapper, $item->search_api_index->options['fields'], $item->search_api_language);
}
}
return $wrapper;
}
/**
* Sets language of specific fields on an EntityMetadataWrapper object.
*
* This is essentially a copy of search_api_extract_fields(), just slightly
* adapted to set language on the wrapper fields instead of extracting them.
*
* @param EntityMetadataWrapper $wrapper
* The wrapper on which fields to set language on.
* @param array $fields
* The fields to set language on, as stored in an index. I.e., the array
* keys are field names, the values are arrays with at least a "type" key
* present.
* @param array $langcode
* A code of the language to set to wrapper fields.
*
* @return array
* The $fields array with additional "value" and "original_type" keys set.
*
* @see SearchApiEtDatasourceController::getMetadataWrapper()
* @see SearchApiEtDatasourceController::setLanguage()
*/
protected function setLanguage($wrapper, $fields, $langcode) {
// If $wrapper is a list of entities, we have to aggregate their field values.
$wrapper_info = $wrapper
->info();
if (search_api_is_list_type($wrapper_info['type'])) {
foreach ($fields as &$info) {
$info['value'] = array();
$info['original_type'] = $info['type'];
}
unset($info);
try {
foreach ($wrapper as $w) {
$nested_fields = $this
->setLanguage($w, $fields, $langcode);
foreach ($nested_fields as $field => $info) {
if (isset($info['value'])) {
$fields[$field]['value'][] = $info['value'];
}
if (isset($info['original_type'])) {
$fields[$field]['original_type'] = $info['original_type'];
}
}
}
} catch (EntityMetadataWrapperException $e) {
// Catch exceptions caused by not set list values.
}
return $fields;
}
$nested = array();
foreach ($fields as $field => $info) {
$pos = strpos($field, ':');
if ($pos === FALSE) {
if (isset($wrapper->{$field}) && method_exists($wrapper->{$field}, 'language')) {
$wrapper->{$field}
->language($langcode);
}
}
else {
list($prefix, $key) = explode(':', $field, 2);
$nested[$prefix][$key] = $info;
}
}
foreach ($nested as $prefix => $nested_fields) {
if (isset($wrapper->{$prefix})) {
$nested_fields = $this
->setLanguage($wrapper->{$prefix}, $nested_fields, $langcode);
foreach ($nested_fields as $field => $info) {
$fields["{$prefix}:{$field}"] = $info;
}
}
else {
foreach ($nested_fields as &$info) {
$info['value'] = NULL;
$info['original_type'] = $info['type'];
}
}
}
return $fields;
}
/**
* {@inheritdoc}
*/
public function getItemId($item) {
$entity_id = parent::getItemId($item);
$translation_handler = entity_translation_get_handler($this->entityType, $item);
$language = $translation_handler
->getLanguage();
return $language ? SearchApiEtHelper::buildItemId($entity_id, $language) : $entity_id;
}
/**
* Overrides SearchApiEntityDataSourceController::startTracking().
*
* Reverts the behavior to always use getAllItemIds(), instead of taking a
* shortcut via "base table".
*
* This method will also be called when the multilingual configuration of an
* index changes, to take care of new and/or out-dated IDs.
*/
public function startTracking(array $indexes) {
// In case an index is inserted during a site install, a batch is active
// so we skip this.
if (!$this->table || batch_get()) {
return;
}
// We first clear the tracking table for all indexes, so we can just insert
// all items again without any key conflicts.
$this
->stopTracking($indexes);
$operations = array();
// Find out number of all entities to be processed.
foreach ($indexes as $index) {
$entity_ids = $this
->getTrackableEntityIds($index);
$steps = ceil(count($entity_ids) / $index->options['cron_limit']);
for ($step = 0; $step < $steps; $step++) {
$operations[] = array(
'search_api_et_batch_queue_entities',
array(
$index,
$entity_ids,
$step,
),
);
}
}
if (empty($operations)) {
// There is nothing to track yet: abort.
return;
}
// This might be called both from web interface as well as from drush.
$t = drupal_is_cli() ? 'dt' : 't';
$batch = array(
'title' => $t('Adding items to the index queue'),
'operations' => $operations,
'finished' => 'search_api_et_batch_queue_entities_finished',
'progress_message' => $t('Completed about @percentage% of the queueing operation.'),
'file' => drupal_get_path('module', 'search_api_et') . '/search_api_et.batch.inc',
);
batch_set($batch);
if (drupal_is_cli()) {
// Calling drush_backend_batch_process() to start batch execution directly
// from here doesn't work for some unknown reason, so we need to call it
// from a shutdown function instead.
drupal_register_shutdown_function('search_api_et_shutdown_batch_process');
}
else {
batch_process();
}
}
/**
* {@inheritdoc}
*/
public function trackItemInsert(array $item_ids, array $indexes) {
$ret = array();
foreach ($indexes as $index_id => $index) {
// Sometimes we get item_ids not meant to be tracked, just filter them out.
$ids = $this
->filterTrackableIds($index, $item_ids);
if ($ids) {
// Some times the item could be already in the index, let try to remove
// them before inserting.
parent::trackItemDelete($ids, array(
$index,
));
// Actually add the items to the index.
parent::trackItemInsert($ids, array(
$index,
));
$ret[$index_id] = $index;
}
}
return $ret;
}
/**
* {@inheritdoc}
* @param $item_ids array|string
* @param $indexes SearchApiIndex[]
* @param $dequeue bool
*/
public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE) {
// If this method was called from _search_api_index_reindex(), $item_ids
// will be set to FALSE, which means we need to reindex all items, so no
// need for any other processing below.
if ($item_ids === FALSE) {
parent::trackItemChange($item_ids, $indexes, $dequeue);
return NULL;
}
$ret = array();
foreach ($indexes as $index_id => $index) {
// The $item_ids can contain a single EntityID if we get invoked from the
// hook: search_api_et_entity_update(). In this case we need to, for each
// Index, identify the set of ItemIDs that need to be marked as changed.
// Check if we get Entity IDs or Item IDs.
$ids = $this
->filterTrackableIds($index, $item_ids);
if (!empty($ids)) {
parent::trackItemChange($ids, array(
$index,
), $dequeue);
$ret[$index_id] = $index;
}
}
return $ret;
}
/**
* Retrieves all Item IDs from the given index, filtered by the Entity IDs.
*
* Is used instead of SearchApiAbstractDataSourceController::getAllItemIds(),
* since available items depend on the index configuration.
*
* @param SearchApiIndex $index
* The index for which item IDs should be retrieved.
*
* @param array $entity_ids
* The Entity IDs to get the ItemIDs for.
*
* @return array
* An array with all item IDs for a given index, with keys and values both
* being the IDs.
*/
public function getTrackableItemIds(SearchApiIndex $index, $entity_ids = NULL) {
$entity_ids = $this
->getTrackableEntityIds($index, $entity_ids);
if (empty($entity_ids)) {
return array();
}
$ids = array();
$entity_type = $index
->getEntityType();
$entity_controller = entity_get_controller($entity_type);
$entity_controller
->resetCache($entity_ids);
$entities = entity_load($entity_type, $entity_ids);
foreach ($entities as $entity_id => $entity) {
foreach (search_api_et_item_languages($entity, $entity_type, $index) as $lang) {
$item_id = SearchApiEtHelper::buildItemId($entity_id, $lang);
$ids[$item_id] = $item_id;
}
}
return $ids;
}
/**
* {@inheritdoc}
*/
public function trackItemDelete(array $item_ids, array $indexes) {
$ret = array();
$search_api_et_ids = array();
$ids_to_expand = array();
foreach ($item_ids as $item_id) {
// If this is a valid Search API ET ID just use it as is.
if (SearchApiEtHelper::isValidItemId($item_id)) {
$search_api_et_ids[] = $item_id;
}
else {
// The $item_ids can contain also single entity ID if we get invoked
// from search_api_entity_delete(). In this case we need to, for each
// Index, identify the set of Item IDs that need to be marked as
// deleted. This has to be done index specific - so we collect the IDs
// to process and handle them in the index loop.
$ids_to_expand[] = $item_id;
}
}
foreach ($indexes as $index_id => $index) {
// Collect all the IDs to delete, expand non search-api-et IDs according
// to the index.
$ids = $search_api_et_ids;
if (!empty($ids_to_expand)) {
$ids = array_merge($ids, $this
->getTrackableItemIdsFromMixedSource($index, $ids_to_expand));
}
if ($ids) {
parent::trackItemDelete($ids, array(
$index,
));
$ret[$index_id] = $index;
}
}
return $ret;
}
/**
* Helper function to return the list of ItemIDs, fiven
* @param \SearchApiIndex $index
* @param $mixed_ids
* @return array
*/
protected function getTrackableItemIdsFromMixedSource(SearchApiIndex $index, $mixed_ids) {
// Check if we get Entity IDs or Item IDs.
$first_item_id = reset($mixed_ids);
$is_valid_item_id = SearchApiEtHelper::isValidItemId($first_item_id);
if (!$is_valid_item_id) {
$entity_id = $first_item_id;
$ids = $this
->getTrackableItemIds($index, $entity_id);
}
else {
// Filter the item_ids that need to be tracked by this index.
$ids = $this
->filterTrackableIds($index, $mixed_ids);
}
return $ids;
}
/**
* @param SearchApiIndex $index
* The index for which item IDs should be retrieved.
* @param array $entity_ids
* The entity ids to get the trackable entity ids for.
*
* @return array
* An array with all trackable Entity IDs for a given index.
*/
public function getTrackableEntityIds(SearchApiIndex $index, $entity_ids = NULL) {
$entity_type = $index
->getEntityType();
if (!empty($this->entityInfo['base table']) && $this->idKey) {
// Assumes that all entities use the "base table" property and the
// "entity keys[id]" in the same way as the default controller.
$table = $this->entityInfo['base table'];
// Select all entity ids.
$query = db_select($table, 't');
$query
->addField('t', $this->idKey);
if ($bundles = $this
->getIndexBundles($index)) {
$query
->condition($this->bundleKey, $bundles);
}
if ($entity_ids) {
$query
->condition($this->idKey, $entity_ids);
}
$ids = $query
->execute()
->fetchCol();
}
else {
// In the absence of a 'base table', load the entities.
$query = new EntityFieldQuery();
$query
->entityCondition('entity_type', $entity_type);
if ($bundles = $this
->getIndexBundles($index)) {
$query
->entityCondition('bundle', $bundles);
}
if ($entity_ids) {
$query
->entityCondition('entity_id', $entity_ids);
}
$entities = $query
->execute();
$ids = array_keys($entities[$entity_type]);
}
return $ids;
}
/**
* Filters the given Item IDs to include only the ones handled by the Index.
*
* @param SearchApiIndex $index
* The SearchAPI index to use
* @param array $item_ids
* A list of trackable ItemID (in the form "{id}/{language}) to filter
* @return array
* The filtered list of trackable ItemID
*/
protected function filterTrackableIds(SearchApiIndex $index, $item_ids) {
if (empty($item_ids)) {
return array();
}
// Group the given ItemIds by their EntityId.
$grouped_item_ids = SearchApiEtHelper::getGroupedItemsIdsByEntity($item_ids);
if (empty($grouped_item_ids)) {
return array();
}
// Generate the list of candidate ItemIDs from the current EntityIDs
$trackable_item_ids = $this
->getTrackableItemIds($index, array_keys($grouped_item_ids));
// The $trackable_item_ids will contain all ItemIDs that should be indexed.
// Additional translations, other than the one provided in $item_ids, will
// be taken into account, to cover the case when a non-translatable field is
// changed on one translation and such change must be reflected to all other
// indexed translations.
return $trackable_item_ids;
}
}
Classes
Name | Description |
---|---|
SearchApiEtDatasourceController | Provides multilingual versions of all entity types. |