diff --git a/search_api.views.inc b/search_api.views.inc index 2124f79..6bf2f9a 100644 --- a/search_api.views.inc +++ b/search_api.views.inc @@ -17,6 +17,8 @@ use Drupal\search_api\IndexInterface; */ function search_api_views_data() { $data = array(); + $entity_type_manager = \Drupal::entityTypeManager(); + $logger = \Drupal::logger('search_api'); /** @var \Drupal\search_api\IndexInterface $index */ foreach (Index::loadMultiple() as $index) { @@ -58,82 +60,55 @@ function search_api_views_data() { // Add special fields. _search_api_views_data_special_fields($table, $index); - $i = 0; - foreach ($index->getDatasources() as $datasource) { - if (!$entity_type_id = $datasource->getEntityTypeId()) { + foreach ($index->getDatasources() as $datasource_id => $datasource) { + $entity_type_id = $datasource->getEntityTypeId(); + if (!$entity_type_id) { + // @todo Add our own base table for the datasource. continue; } - $entity_manager = \Drupal::entityManager(); - if (!$entity_type = $entity_manager->getDefinition($entity_type_id)) { - continue; - } - $storage = $entity_manager->getStorage($entity_type_id); - if (!$base_table = $entity_type->getBaseTable()) { + + $entity_type = $entity_type_manager->getDefinition($entity_type_id); + if (!$entity_type || !$entity_type->getBaseTable()) { + $args = array( + '%index' => $index->label(), + '%datasource' => $datasource->label(), + '%entity_type' => $entity_type_id, + ); + $logger->warning("Datasource %datasource of index %index specifies an unknown entity type %entity_type. Could not add integration for the datasource's fields to Views.", $args); continue; } - $entity_tables = array($base_table => $entity_type_id); + // Collect the different tables for this entity type. + $entity_tables = array( + 'base' => $entity_type->getBaseTable() + ); // Some entities may not have a data table. $data_table = $entity_type->getDataTable(); if ($data_table) { - $entity_tables[$data_table] = $entity_type_id; + $entity_tables['data'] = $data_table; } - // Description of the field tables. - // @todo This is the same code as views itself uses, issue about the sql - // assumptions https://www.drupal.org/node/2079019. - $table_mapping = $storage->getTableMapping(); - - foreach ($entity_tables as $base_table => $entity_type_id) { - $table[$base_table]['relationship'] = array( - 'label' => $base_table, - 'title' => $base_table, - 'id' => 'search_api_datasource_entity', - 'base' => $base_table, + // Now add relationships for all tables of this entity type. + foreach ($entity_tables as $type => $entity_table) { + $args = array( + '@datasource' => $datasource->label(), + ); + if ($type == 'base') { + $title = t('@datasource entity base fields from search results', $args); + $label = t('@datasource base fields', $args); + } + else { + $title = t('@datasource entity data from search results', $args); + $label = t('@datasource data', $args); + } + $field_alias = _search_api_views_find_field_alias($datasource_id . '_' . $type, $table); + $table[$field_alias]['relationship'] = array( + 'title' => $title, + 'label' => $label, + 'id' => 'search_api_datasource', + 'base' => $entity_table, + 'datasource' => $datasource_id, ); } } - - if (isset($table['search_api_language']['filter']['id'])) { - $table['search_api_language']['filter']['id'] = 'search_api_language'; - $table['search_api_language']['filter']['allow empty'] = FALSE; - } - - // Add handlers for special fields. - $table['search_api_id']['title'] = t('Entity ID'); - $table['search_api_id']['help'] = t("The entity's ID"); - $table['search_api_id']['field']['id'] = 'numeric'; - $table['search_api_id']['sort']['id'] = 'search_api'; - - $table['search_api_datasource']['title'] = t('Datasource'); - $table['search_api_datasource']['help'] = t("The data source ID"); - $table['search_api_datasource']['field']['id'] = 'standard'; - $table['search_api_datasource']['filter']['id'] = 'search_api_datasource'; - $table['search_api_datasource']['sort']['id'] = 'search_api'; - - $table['search_api_relevance']['group'] = t('Search'); - $table['search_api_relevance']['title'] = t('Relevance'); - $table['search_api_relevance']['help'] = t('The relevance of this search result with respect to the query'); - $table['search_api_relevance']['field']['type'] = 'decimal'; - $table['search_api_relevance']['field']['id'] = 'numeric'; - $table['search_api_relevance']['field']['click sortable'] = TRUE; - $table['search_api_relevance']['sort']['id'] = 'search_api'; - - $table['search_api_excerpt']['group'] = t('Search'); - $table['search_api_excerpt']['title'] = t('Excerpt'); - $table['search_api_excerpt']['help'] = t('The search result excerpted to show found search terms'); - $table['search_api_excerpt']['field']['id'] = 'search_api_excerpt'; - - $table['search_api_fulltext']['group'] = t('Search'); - $table['search_api_fulltext']['title'] = t('Fulltext search'); - $table['search_api_fulltext']['help'] = t('Search several or all fulltext fields at once.'); - $table['search_api_fulltext']['filter']['id'] = 'search_api_fulltext'; - $table['search_api_fulltext']['argument']['id'] = 'search_api_fulltext'; - - $table['search_api_more_like_this']['group'] = t('Search'); - $table['search_api_more_like_this']['title'] = t('More like this'); - $table['search_api_more_like_this']['help'] = t('Find similar content.'); - $table['search_api_more_like_this']['argument']['id'] = 'search_api_more_like_this'; - - // @todo Add an "All taxonomy terms" contextual filter (if applicable). } catch (\Exception $e) { watchdog_exception('search_api', $e); @@ -152,10 +127,6 @@ function search_api_views_plugins_cache_alter(array &$plugins) { /** @var \Drupal\search_api\IndexInterface $index */ foreach (Index::loadMultiple() as $index) { $bases[] = 'search_api_index_' . $index->id(); - $i = 0; - foreach ($index->getDatasources() as $datasource) { - $bases[] = 'search_api_index_entity_' . sprintf("%02d", ++$i) . '_' . $index->id(); - } } $plugins['search_api']['base'] = $bases; } @@ -169,10 +140,6 @@ function search_api_views_plugins_row_alter(array &$plugins) { /** @var \Drupal\search_api\IndexInterface $index */ foreach (Index::loadMultiple() as $index) { $bases[] = 'search_api_index_' . $index->id(); - $i = 0; - foreach ($index->getDatasources() as $datasource) { - $bases[] = 'search_api_index_entity_' . sprintf("%02d", ++$i) . '_' . $index->id(); - } } $plugins['search_api']['base'] = $bases; } @@ -256,6 +223,13 @@ function _search_api_views_get_handlers(FieldInterface $field) { * The handler definitions for the field, as a reference. */ function _search_api_views_handler_adjustments($type, FieldInterface $field, array &$definitions) { + // By default, all fields can be empty (or at least have to be treated that + // way by the Search API). + if (!isset($definitions['filter']['allow empty'])) { + $definitions['filter']['allow empty'] = TRUE; + } + + // For taxonomy term references, set the referenced vocabulary. $data_definition = $field->getDataDefinition(); if ($type == 'entity:taxonomy_term') { if (isset($data_definition->getSettings()['handler_settings']['target_bundles'])) { @@ -265,10 +239,11 @@ function _search_api_views_handler_adjustments($type, FieldInterface $field, arr } } } - // By default, all fields can be empty (or at least have to be treated that - // way by the Search API). - if (!isset($definitions['filter']['allow empty'])) { - $definitions['filter']['allow empty'] = TRUE; + + // Special case for our own language field. + if (!$field->getDatasourceId() && $field->getPropertyPath() === 'search_api_language') { + $definitions['filter']['id'] = 'search_api_language'; + $definitions['filter']['allow empty'] = FALSE; } } @@ -400,53 +375,70 @@ function _search_api_views_handler_mapping() { } /** - * Define the special fields for this index. + * Adds definitions for our special fields to a Views data table definition. * * @param array $table - * An array describing the fields. - * @param \Drupal\search_api\IndexInterface $index - * The index we're adding fields for. + * The existing Views data table definition. */ -function _search_api_views_data_special_fields(&$table, IndexInterface $index) { - if (isset($table['search_api_language']['filter']['id'])) { - $table['search_api_language']['filter']['id'] = 'search_api_language'; - $table['search_api_language']['filter']['allow empty'] = FALSE; +function _search_api_views_data_special_fields(array &$table) { + $id_field = _search_api_views_find_field_alias('search_api_id', $table); + $table[$id_field]['title'] = t('Entity ID'); + $table[$id_field]['help'] = t("The entity's ID"); + $table[$id_field]['field']['id'] = 'numeric'; + $table[$id_field]['sort']['id'] = 'search_api'; + if ($id_field != 'search_api_id') { + $table[$id_field]['real field'] = 'search_api_id'; + } + + $datasource_field = _search_api_views_find_field_alias('search_api_datasource', $table); + $table[$datasource_field]['title'] = t('Datasource'); + $table[$datasource_field]['help'] = t("The data source ID"); + $table[$datasource_field]['field']['id'] = 'standard'; + $table[$datasource_field]['filter']['id'] = 'search_api_datasource'; + $table[$datasource_field]['sort']['id'] = 'search_api'; + if ($datasource_field != 'search_api_datasource') { + $table[$datasource_field]['real field'] = 'search_api_datasource'; + } + + $relevance_field = _search_api_views_find_field_alias('search_api_relevance', $table); + $table[$relevance_field]['group'] = t('Search'); + $table[$relevance_field]['title'] = t('Relevance'); + $table[$relevance_field]['help'] = t('The relevance of this search result with respect to the query'); + $table[$relevance_field]['field']['type'] = 'decimal'; + $table[$relevance_field]['field']['id'] = 'numeric'; + $table[$relevance_field]['field']['click sortable'] = TRUE; + $table[$relevance_field]['sort']['id'] = 'search_api'; + if ($relevance_field != 'search_api_relevance') { + $table[$relevance_field]['real field'] = 'search_api_relevance'; + } + + $excerpt_field = _search_api_views_find_field_alias('search_api_excerpt', $table); + $table[$excerpt_field]['group'] = t('Search'); + $table[$excerpt_field]['title'] = t('Excerpt'); + $table[$excerpt_field]['help'] = t('The search result excerpted to show found search terms'); + $table[$excerpt_field]['field']['id'] = 'search_api_excerpt'; + if ($excerpt_field != 'search_api_excerpt') { + $table[$excerpt_field]['real field'] = 'search_api_excerpt'; + } + + $fulltext_field = _search_api_views_find_field_alias('search_api_fulltext', $table); + $table[$fulltext_field]['group'] = t('Search'); + $table[$fulltext_field]['title'] = t('Fulltext search'); + $table[$fulltext_field]['help'] = t('Search several or all fulltext fields at once.'); + $table[$fulltext_field]['filter']['id'] = 'search_api_fulltext'; + $table[$fulltext_field]['argument']['id'] = 'search_api_fulltext'; + if ($fulltext_field != 'search_api_fulltext') { + $table[$fulltext_field]['real field'] = 'search_api_fulltext'; } - $table['search_api_id']['title'] = t('Entity ID'); - $table['search_api_id']['help'] = t("The entity's ID"); - $table['search_api_id']['field']['id'] = 'numeric'; - $table['search_api_id']['sort']['id'] = 'search_api'; - - $table['search_api_datasource']['title'] = t('Datasource'); - $table['search_api_datasource']['help'] = t("The data source ID"); - $table['search_api_datasource']['field']['id'] = 'standard'; - $table['search_api_datasource']['filter']['id'] = 'search_api_datasource'; - $table['search_api_datasource']['sort']['id'] = 'search_api'; - - $table['search_api_relevance']['group'] = t('Search'); - $table['search_api_relevance']['title'] = t('Relevance'); - $table['search_api_relevance']['help'] = t('The relevance of this search result with respect to the query'); - $table['search_api_relevance']['field']['type'] = 'decimal'; - $table['search_api_relevance']['field']['id'] = 'numeric'; - $table['search_api_relevance']['field']['click sortable'] = TRUE; - $table['search_api_relevance']['sort']['id'] = 'search_api'; - - $table['search_api_excerpt']['group'] = t('Search'); - $table['search_api_excerpt']['title'] = t('Excerpt'); - $table['search_api_excerpt']['help'] = t('The search result excerpted to show found search terms'); - $table['search_api_excerpt']['field']['id'] = 'search_api_excerpt'; - - $table['search_api_fulltext']['group'] = t('Search'); - $table['search_api_fulltext']['title'] = t('Fulltext search'); - $table['search_api_fulltext']['help'] = t('Search several or all fulltext fields at once.'); - $table['search_api_fulltext']['filter']['id'] = 'search_api_fulltext'; - $table['search_api_fulltext']['argument']['id'] = 'search_api_fulltext'; - - $table['search_api_more_like_this']['group'] = t('Search'); - $table['search_api_more_like_this']['title'] = t('More like this'); - $table['search_api_more_like_this']['help'] = t('Find similar content.'); - $table['search_api_more_like_this']['argument']['id'] = 'search_api_more_like_this'; + $mlt_field = _search_api_views_find_field_alias('search_api_more_like_this', $table); + $table[$mlt_field]['group'] = t('Search'); + $table[$mlt_field]['title'] = t('More like this'); + $table[$mlt_field]['help'] = t('Find similar content.'); + $table[$mlt_field]['argument']['id'] = 'search_api_more_like_this'; + if ($mlt_field != 'search_api_more_like_this') { + $table[$mlt_field]['real field'] = 'search_api_more_like_this'; + } // @todo Add an "All taxonomy terms" contextual filter (if applicable). } diff --git a/src/Plugin/views/query/SearchApiEntityQuery.php b/src/Plugin/views/query/SearchApiEntityQuery.php deleted file mode 100644 index 08087f3..0000000 --- a/src/Plugin/views/query/SearchApiEntityQuery.php +++ /dev/null @@ -1,72 +0,0 @@ -getStorage('search_api_index')->load($index_id); - } - return Index::load($index_id); - } - return NULL; - } - - /** - * {@inheritdoc} - */ - protected function addResults(array $results, ViewExecutable $view) { - $rows = $this->retrieveItemResults($results); - $this->loadResultEntities($rows); - $view->result = array_values($rows); - } - - /** - * {@inheritdoc} - */ - protected function loadResultEntities(&$results) { - foreach ($results as $item_id => $result) { - try { - $entity = $this->index->loadItem($result->search_api_id)->getValue(); - if (!empty($this->options['entity_access'])) { - if (!$entity->access('view')) { - continue; - } - } - $results[$item_id]->_entity = $entity; - } - catch (\Exception $e) { - // No entity so no wrapper to getValue(). - } - } - } -} - - diff --git a/src/Plugin/views/query/SearchApiQuery.php b/src/Plugin/views/query/SearchApiQuery.php index 50c3028..cd88424 100644 --- a/src/Plugin/views/query/SearchApiQuery.php +++ b/src/Plugin/views/query/SearchApiQuery.php @@ -475,125 +475,16 @@ class SearchApiQuery extends QueryPluginBase { $this->loadResultEntities($rows, $view, $entity_information); } $view->result = array_values($rows); - array_walk($view->result, function (ResultRow $row, $index) { - $row->index = $index; - }); - } - - /** - * Add relationship. - * - * @todo This is a method called by standard relationship handlers. - * So it's called when a chained entity relationship is added. - * This is probably good enough to add data to load the entity - * when executed. - */ - public function addRelationship($alias, JoinPluginBase $join, $base, $link_point = NULL) { - // @todo What does this do? - if (empty($link_point)) { - $link_point = $this->view->storage->get('base_table'); - } - elseif (!array_key_exists($link_point, $this->relationships)) { - return FALSE; - } - - // Make sure $alias isn't already used; if it, start adding stuff. - $alias_base = $alias; - $count = 1; - while (!empty($this->relationships[$alias])) { - $alias = $alias_base . '_' . $count++; - } - - $this->relationships[$alias] = array( - 'link' => $link_point, - 'table' => $join->table, - 'base' => $base, - ); - - return $alias; - } - - /** - * Adds a query tag. - * - * @todo This is a method called by standard relationship handlers. - */ - public function addTag($tag) { - $this->tags[] = $tag; - } - - /** - * Load the views results as entities. - * - * @param array $results - * An array of results. - * @param \Drupal\views\ViewExecutable $view - * The view where these results are shown. - * @param array $entity_information - */ - protected function loadResultEntities(&$results, ViewExecutable $view, $entity_information) { - // List entities types to load from the relationships on the view. - $entities_to_load = []; - foreach ($entity_information as $join) { - $entities_to_load[$join['entity_type']][] = $join; - } - - foreach ($results as $item_id => $result) { - // Load the entity if it is one of the relationship entities. - list($datasource_id, $raw_id) = Utility::splitCombinedId($item_id); - if (isset($entities_to_load[$this->index->getDatasource($datasource_id)->getEntityTypeId()])) { - $this->loadResultEntity($results, $result, $view->relationship, $entities_to_load); - } - } - } - - /** - * Load the views result as an entity. - * - * @param array $results - * An array of results. - * @param $result - * One item out of the results set. - * @param $relationships - * @param $entities_to_load - */ - private function loadResultEntity(&$results, $result, $relationships, $entities_to_load) { - $item = $this->index->loadItem($result->search_api_id); - if ($item) { - $item_id = $result->search_api_id; - $entity = $item->getValue(); - // Check access to the entity if access check on. - foreach ($entities_to_load[$entity->getEntityTypeId()] as $join) { - if ($relationships[$join['base']]->options['entity_access'] && !$entity->access('view')) { - unset($results[$item_id]); - continue; - } - } - // Add entity to all relationships that use it. - // Multiple example: node and node_fields. - foreach ($entities_to_load[$entity->getEntityTypeId()] as $join) { - $results[$item_id]->_relationship_entities[$join['relationship_id']] = $entity; - } - } - else { - // No entity. If it is required remove the row. - foreach ($entities_to_load[$entity->getEntityTypeId()] as $join) { - if ($relationships[$join['base']]->options['required']) { - unset($results[$item_id]); - continue; - } - } - } } /** * Retrieves items from the results. * - * @param array $results - * An array of results + * @param \Drupal\search_api\Item\ItemInterface[] $results + * The search results. * * @return \Drupal\views\ResultRow[] - * The results as views resultRow objects. + * The results as Views result row objects. */ protected function retrieveItemResults(array $results) { /** @var \Drupal\views\ResultRow[] $rows */ @@ -601,7 +492,7 @@ class SearchApiQuery extends QueryPluginBase { // Views \Drupal\views\Plugin\views\style\StylePluginBase::renderFields() // uses a numeric results index to key the rendered results. - // The Row::index is the key then used to retrieve these. + // The ResultRow::index property is the key then used to retrieve these. $count = 0; // First off, we try to gather as many property values as possible without @@ -642,6 +533,72 @@ class SearchApiQuery extends QueryPluginBase { } /** + * Loads the views results as entities. + * + * @param \Drupal\views\ResultRow[] $results + * An array of results. + * @param \Drupal\views\ViewExecutable $view + * The view where these results are shown. + * @param array $entity_information + * An array containing information about all the available tables in this + * view that contain entity data. + */ + protected function loadResultEntities(array &$results, ViewExecutable $view, $entity_information) { + // List entities types to load from the relationships on the view. + $entities_to_load = []; + foreach ($entity_information as $join) { + $entities_to_load[$join['entity_type']][] = $join; + } + + foreach ($results as $item_id => $result) { + // Load the entity if it is one of the relationship entities. + list($datasource_id) = Utility::splitCombinedId($item_id); + if (isset($entities_to_load[$this->index->getDatasource($datasource_id)->getEntityTypeId()])) { + $this->loadResultEntity($results, $result, $view->relationship, $entities_to_load); + } + } + } + + /** + * Loads the views result as an entity. + * + * @param \Drupal\views\ResultRow[] $results + * An array of results. + * @param \Drupal\views\ResultRow $result + * One item out of the results set. + * @param \Drupal\views\Plugin\views\relationship\RelationshipPluginBase[] $relationships + * The relationships set for the current view. + * @param array $entities_to_load + * An array containing information about all the available tables in this + * view that contain entity data, keyed by entity type. + */ + private function loadResultEntity(array &$results, ResultRow $result, array $relationships, $entities_to_load) { + if (!isset($result->search_api_id)) { + return; + } + + $item_id = $result->search_api_id; + $item = $this->index->loadItem($item_id); + if (!$item) { + return; + } + + $entity = $item->getValue(); + // Check access to the entity if access check on. + foreach ($entities_to_load[$entity->getEntityTypeId()] as $join) { + if ($relationships[$join['base']]->options['entity_access'] && !$entity->access('view')) { + unset($results[$item_id]); + continue; + } + } + // Add entity to all relationships that use it. + // Multiple example: node and node_fields. + foreach ($entities_to_load[$entity->getEntityTypeId()] as $join) { + $results[$item_id]->_relationship_entities[$join['relationship_id']] = $entity; + } + } + + /** * Returns the Search API query object used by this Views query. * * @return \Drupal\search_api\Query\QueryInterface|null diff --git a/src/Plugin/views/relationship/DatasourceEntity.php b/src/Plugin/views/relationship/Datasource.php similarity index 51% rename from src/Plugin/views/relationship/DatasourceEntity.php rename to src/Plugin/views/relationship/Datasource.php index 6435483..fabb517 100644 --- a/src/Plugin/views/relationship/DatasourceEntity.php +++ b/src/Plugin/views/relationship/Datasource.php @@ -2,31 +2,40 @@ /** * @file - * Contains \Drupal\views\Plugin\views\relationship\DatasourceEntity. + * Contains \Drupal\views\Plugin\views\relationship\Datasource. */ namespace Drupal\search_api\Plugin\views\relationship; use Drupal\Core\Form\FormStateInterface; use Drupal\views\Plugin\views\relationship\RelationshipPluginBase; -use Drupal\views\ViewExecutable; -use Drupal\views\Plugin\views\display\DisplayPluginBase; /** * Views relationship plugin for datasources. * * @ingroup views_relationship_handlers * - * @ViewsRelationship("search_api_datasource_entity") + * @ViewsRelationship("search_api_datasource") */ -class DatasourceEntity extends RelationshipPluginBase { +class Datasource extends RelationshipPluginBase { + + /** + * The query plugin for the current view. + * + * @var \Drupal\search_api\Plugin\views\query\SearchApiQuery + */ + public $query = NULL; /** * {@inheritdoc} */ protected function defineOptions() { $options = parent::defineOptions(); - $options['entity_access'] = FALSE; + + $options['entity_access']= array( + 'default' => FALSE, + 'bool' => TRUE, + ); return $options; } @@ -40,16 +49,19 @@ class DatasourceEntity extends RelationshipPluginBase { $form['entity_access'] = array( '#type' => 'checkbox', '#title' => $this->t('Check view access permissions'), - '#description' => $this->t('Enable to check user has permission to view the entity. This prevents users from seeing inappropriate content when the index contains stale data, or doesn\'t provide access checks. However, result counts, paging and other things won\'t work correctly if results are eliminated in this way, so only use this as a last ressort (and in addition to other checks, if possible).'), + '#description' => $this->t("Enable to check user has permission to view the entity. This prevents users from seeing inappropriate content when the index contains stale data, or doesn't provide access checks. However, result counts, paging and other things won't work correctly if results are eliminated in this way, so only use this as a last resort (and in addition to other checks, if possible)."), '#default_value' => !empty($this->options['required']), ); } /** - * We don't need to add anything to the query, because this is SQL-specific. + * {@inheritdoc} */ public function query() { - return; + // If the relationship is required, add a filter for this datasource. + if (!empty($this->options['required'])) { + $this->query->addCondition('search_api_datasource', $this->definition['datasource']); + } } /**