diff --git a/search_api.views.inc b/search_api.views.inc index c242642..6560043 100644 --- a/search_api.views.inc +++ b/search_api.views.inc @@ -6,70 +6,61 @@ */ use Drupal\search_api\Entity\Index; +use Drupal\search_api\IndexInterface; /** * Implements hook_views_data(). */ function search_api_views_data() { $data = array(); - try { - foreach (Index::loadMultiple() as $index) { - // Fill in base data. - $key = 'search_api_index_' . $index->id(); - $table = &$data[$key]; - $table['table']['group'] = t('Index @name', array('@name' => $index->label())); - $table['table']['base'] = array( - 'field' => 'search_api_id', - 'index' => $index->id(), - 'title' => t('Index @name', array('@name' => $index->label())), - 'help' => t('Use the %name search index for filtering and retrieving data.', array('%name' => $index->label())), - 'query_id' => 'search_api_query', - ); - - // @todo Add field, filter, … handlers for all fields. - - // 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_sort'; - - $table['search_api_datasource']['title'] = t('Datasource'); - $table['search_api_datasource']['help'] = t("The data source ID"); - $table['search_api_datasource']['field']['id'] = 'standard'; - // @todo Enable filtering on datasource. - $table['search_api_datasource']['sort']['id'] = 'search_api_sort'; - $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_sort'; - - $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'; + foreach (Index::loadMultiple() as $index) { + $key = 'search_api_index_' . $index->id(); + $table = &$data[$key]; + $table['table']['group'] = t('Index @name', array('@name' => $index->label())); + $table['table']['base'] = array( + 'field' => 'search_api_id', + 'index' => $index->id(), + 'title' => t('Index @name', array('@name' => $index->label())), + 'help' => t('Use the @name search index for filtering and retrieving data.', array('@name' => $index->label())), + 'query_id' => 'search_api_query', + ); + _search_api_views_data_special_fields($table, $index); + _search_api_views_data_searchapi_fields($table, $index); - $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'; + foreach ($index->getDatasources() as $datasource) { + if (!$entity_type_id = $datasource->getEntityTypeId()) { + 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()) { + continue; + } + $entity_tables = array($base_table => $entity_type_id); + // Some entities may not have a data table. + $data_table = $entity_type->getDataTable(); + if ($data_table) { + $entity_tables[$data_table] = $entity_type_id; + } + // 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(); - // @todo Add an "All taxonomy terms" contextual filter (if applicable). + 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, + ); + } } } - catch (Exception $e) { - watchdog_exception('search_api', $e); - } return $data; } @@ -81,6 +72,10 @@ function search_api_views_plugins_cache_alter(array &$plugins) { $bases = array(); 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; } @@ -93,6 +88,86 @@ function search_api_views_plugins_row_alter(array &$plugins) { $bases = array(); 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; } + +/** + * Define the special fields for this index. + * + * @param array $table + * An array describing the fields. + * @param \Drupal\search_api\IndexInterface $index + * The index we're adding fields for. + */ +function _search_api_views_data_special_fields(&$table, IndexInterface $index) { + // Add handlers for special fields. + $table['search_api_id']['title'] = t('Search API ID'); + $table['search_api_id']['help'] = t("The entity's encoded Search API ID"); + $table['search_api_id']['field']['id'] = 'standard'; + $table['search_api_id']['sort']['id'] = 'search_api_sort'; + + $table['search_api_datasource']['title'] = t('Datasource'); + $table['search_api_datasource']['help'] = t("The data source ID"); + $table['search_api_datasource']['field']['id'] = 'standard'; + // @todo Enable filtering on datasource. + // $table['search_api_datasource']['sort']['id'] = 'search_api_sort'; + + $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_sort'; + + $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'; +} + +/** + * Add the fields for this index as views fields. + * + * @param $table + * An array describing the fields. + * @param \Drupal\search_api\IndexInterface $index + * The index we're adding fields for. + */ +function _search_api_views_data_searchapi_fields(&$table, IndexInterface $index) { + // Add fields per DataSource. + foreach ($index->getDatasources() as $datasource) { + if (!$entity_type_id = $datasource->getEntityTypeId()) { + continue; + } + // @TODO For the moment only Base Fields. + $fields = \Drupal::service('entity.manager')->getBaseFieldDefinitions($entity_type_id); + foreach($fields as $key => $field) { + $table["{$entity_type_id}_{$key}"] = array( + 'title' => $field->getLabel(), + 'help' => $field->getDescription(), + 'field' => array( + 'id' => 'field', + 'field_name' => $key, + 'entity_type' => $entity_type_id, + ), + ); + } + } +} diff --git a/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml b/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml index 252e71b..193f69c 100644 --- a/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml +++ b/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml @@ -33,7 +33,6 @@ display: type: search_api_query options: search_api_bypass_access: false - entity_access: false parse_mode: terms exposed_form: type: basic diff --git a/src/Plugin/views/query/SearchApiQuery.php b/src/Plugin/views/query/SearchApiQuery.php index 637aaf8..a08f939 100644 --- a/src/Plugin/views/query/SearchApiQuery.php +++ b/src/Plugin/views/query/SearchApiQuery.php @@ -18,6 +18,7 @@ use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\Plugin\views\query\QueryPluginBase; use Drupal\views\ResultRow; use Drupal\views\ViewExecutable; +use Drupal\search_api\Utility; /** * Defines a Views query class for searching on Search API indexes. @@ -135,7 +136,7 @@ class SearchApiQuery extends QueryPluginBase { try { parent::init($view, $display, $options); $this->fields = array(); - $this->index = self::getIndexFromTable($view->storage->get('base_table')); + $this->index = static::getIndexFromTable($view->storage->get('base_table')); if (!$this->index) { $this->abort(new FormattableMarkup('View %view is not based on Search API but tries to use its query plugin.', array('%view' => $view->storage->label()))); } @@ -159,9 +160,14 @@ class SearchApiQuery extends QueryPluginBase { * * @return $this */ - public function addField($field) { + // @todo This is _just_ a function added by \Drupal\views\Plugin\views\query\Sql ? + // It's assumed by fields. Most important is that it indexes. + // Is is used elsewhere? + public function addField($table, $field, $alias = '', $params = array()) { + // Is there some reason to be called with an alias, would then need to store + // it? $this->fields[$field] = TRUE; - return $this; + return $alias ? $alias : $field; } /** @@ -433,6 +439,7 @@ class SearchApiQuery extends QueryPluginBase { * The executed view. */ protected function addResults(array $results, ViewExecutable $view) { + /** @var \Drupal\views\ResultRow[] $rows */ $rows = array(); $missing = array(); @@ -457,11 +464,13 @@ class SearchApiQuery extends QueryPluginBase { $values['search_api_id'] = $item_id; $values['search_api_datasource'] = $datasource_id; + // The entity is used to render fields. + $values['_entity'] = $result->getOriginalObject()->getValue(); // Include the loaded item for this result row, if present, or the item - // ID. - $values['_item'] = $result->getOriginalObject(FALSE); - if (!$values['_item']) { - $values['_item'] = $item_id; + // ID. Needed to render the complete entity. + $values['_item'] = $result->getOriginalObject(); + if (!$values['_entity']) { + $values['_entity'] = $item_id; } $values['search_api_relevance'] = $result->getScore(); @@ -478,7 +487,7 @@ class SearchApiQuery extends QueryPluginBase { $missing_fields = array_diff_key($this->fields, $values); if ($missing_fields) { $missing[$item_id] = $missing_fields; - if (!is_object($values['_item'])) { + if (!is_object($values['_entity'])) { $item_ids[] = $item_id; } } @@ -508,7 +517,6 @@ class SearchApiQuery extends QueryPluginBase { } } - // Finally, add all rows to the Views result set. $view->result = array_values($rows); array_walk($view->result, function (ResultRow $row, $index) { $row->index = $index; @@ -516,6 +524,167 @@ class SearchApiQuery extends QueryPluginBase { } /** + * 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 + * + * @return \Drupal\views\ResultRow[] + * The results as views resultRow objects. + */ + protected function retrieveItemResults(array $results) { + /** @var \Drupal\views\ResultRow[] $rows */ + $rows = array(); + + // 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. + $count = 0; + + foreach ($results as $item_id => $result) { + $datasource_id = $result->getDatasourceId(); + + // @todo Find a more elegant way of passing metadata here. + $values['search_api_id'] = $item_id; + $values['search_api_datasource'] = $datasource_id; + + // Include the loaded item for this result row, if present, or the item + // ID. + $values['_entity'] = $result->getOriginalObject(FALSE); + if (!$values['_entity']) { + $values['_entity'] = $item_id; + } + + $values['search_api_relevance'] = $result->getScore(); + $values['search_api_excerpt'] = $result->getExcerpt() ?: ''; + + // Gather any fields from the search results. + foreach ($result->getFields() as $field_id => $field) { + if ($field->getValues()) { + // @todo so results from fields here are always arrays; even when single. + // What are the different handling in views when something _can_ be multiple, + // and when it should be single? + $values[$field_id] = $field->getValues()[0]; + } + } + + $values['index'] = $count++; + + // @todo Use a custom sub-class here to also pass the result item object, + // or other information? + $rows[$item_id] = new ResultRow($values); + } + + return $rows; + } + + /** * Returns the Search API query object used by this Views query. * * @return \Drupal\search_api\Query\QueryInterface|null @@ -956,6 +1125,7 @@ class SearchApiQuery extends QueryPluginBase { * concept of "tables", this method implementation does nothing. If you are * writing Search API-specific Views code, there is therefore no reason at all * to call this method. + * See https://www.drupal.org/node/2484565 for more information. * * @return string * An empty string. diff --git a/src/Plugin/views/row/SearchApiRow.php b/src/Plugin/views/row/SearchApiRow.php index 73b63cd..7d614d7 100644 --- a/src/Plugin/views/row/SearchApiRow.php +++ b/src/Plugin/views/row/SearchApiRow.php @@ -24,7 +24,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * * @ViewsRow( * id = "search_api", - * title = @Translation("Rendered Search API item"), + * title = @Translation("Rendered entity"), * help = @Translation("Displays entity of the matching search API item"), * ) */ @@ -118,7 +118,6 @@ class SearchApiRow extends RowPluginBase { */ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) { parent::init($view, $display, $options); - $base_table = $view->storage->get('base_table'); $this->index = SearchApiQuery::getIndexFromTable($base_table, $this->getEntityTypeManager()); if (!$this->index) { @@ -247,7 +246,7 @@ class SearchApiRow extends RowPluginBase { public function query() { parent::query(); // @todo Find a better way to ensure that the item is loaded. - $this->view->query->addField('_magic'); + $this->view->query->addField(null, '_magic'); } } diff --git a/src/Tests/ViewsTest.php b/src/Tests/ViewsTest.php index e84e258..6b6e758 100644 --- a/src/Tests/ViewsTest.php +++ b/src/Tests/ViewsTest.php @@ -23,7 +23,7 @@ class ViewsTest extends WebTestBase { * * @var string[] */ - public static $modules = array('search_api_test_views'); + public static $modules = array('search_api_test_views', 'views_ui'); /** * A search index ID. @@ -72,4 +72,19 @@ class ViewsTest extends WebTestBase { } } + /** + * Test views admin. + */ + public function testViewsAdmin() { + $admin_user = $this->drupalCreateUser(array( + 'administer search_api', + 'access administration pages', + 'administer views', + )); + $this->drupalLogin($admin_user); + $this->insertExampleContent(); + + $this->drupalGet('admin/structure/views/view/search_api_test_views_fulltext'); + } + }