123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 |
- <?php
- /**
- * Views query class using a Search API index as the data source.
- */
- class SearchApiViewsQuery extends views_plugin_query {
- /**
- * Number of results to display.
- *
- * @var int
- */
- protected $limit;
- /**
- * Offset of first displayed result.
- *
- * @var int
- */
- protected $offset;
- /**
- * The index this view accesses.
- *
- * @var SearchApiIndex
- */
- protected $index;
- /**
- * The query that will be executed.
- *
- * @var SearchApiQueryInterface
- */
- protected $query;
- /**
- * The results returned by the query, after it was executed.
- *
- * @var array
- */
- protected $search_api_results = array();
- /**
- * Array of all encountered errors.
- *
- * Each of these is fatal, meaning that a non-empty $errors property will
- * result in an empty result being returned.
- *
- * @var array
- */
- protected $errors;
- /**
- * The names of all fields whose value is required by a handler.
- *
- * The format follows the same as Search API field identifiers (parent:child).
- *
- * @var array
- */
- protected $fields;
- /**
- * The query's sub-filters representing the different Views filter groups.
- *
- * @var array
- */
- protected $filters = array();
- /**
- * The conjunction with which multiple filter groups are combined.
- *
- * @var string
- */
- public $group_operator = 'AND';
- /**
- * Create the basic query object and fill with default values.
- */
- public function init($base_table, $base_field, $options) {
- try {
- $this->errors = array();
- parent::init($base_table, $base_field, $options);
- $this->fields = array();
- if (substr($base_table, 0, 17) == 'search_api_index_') {
- $id = substr($base_table, 17);
- $this->index = search_api_index_load($id);
- $this->query = $this->index->query(array(
- 'parse mode' => 'terms',
- ));
- }
- }
- catch (Exception $e) {
- $this->errors[] = $e->getMessage();
- }
- }
- /**
- * Add a field that should be retrieved from the results by this view.
- *
- * @param $field
- * The field's identifier, as used by the Search API. E.g., "title" for a
- * node's title, "author:name" for a node's author's name.
- *
- * @return SearchApiViewsQuery
- * The called object.
- */
- public function addField($field) {
- $this->fields[$field] = TRUE;
- return $field;
- }
- /**
- * Add a sort to the query.
- *
- * @param $selector
- * The field to sort on. All indexed fields of the index are valid values.
- * In addition, the special fields 'search_api_relevance' (sort by
- * relevance) and 'search_api_id' (sort by item id) may be used.
- * @param $order
- * The order to sort items in - either 'ASC' or 'DESC'. Defaults to 'ASC'.
- */
- public function add_selector_orderby($selector, $order = 'ASC') {
- $this->query->sort($selector, $order);
- }
- /**
- * Defines the options used by this query plugin.
- *
- * Adds an option to bypass access checks.
- */
- public function option_definition() {
- return parent::option_definition() + array(
- 'search_api_bypass_access' => array(
- 'default' => FALSE,
- ),
- );
- }
- /**
- * Add settings for the UI.
- *
- * Adds an option for bypassing access checks.
- */
- public function options_form(&$form, &$form_state) {
- parent::options_form($form, $form_state);
- $form['search_api_bypass_access'] = array(
- '#type' => 'checkbox',
- '#title' => t('Bypass access checks'),
- '#description' => t('If the underlying search index has access checks enabled, this option allows to disable them for this view.'),
- '#default_value' => $this->options['search_api_bypass_access'],
- );
- }
- /**
- * Builds the necessary info to execute the query.
- */
- public function build(&$view) {
- $this->view = $view;
- // Setup the nested filter structure for this query.
- if (!empty($this->where)) {
- // If the different groups are combined with the OR operator, we have to
- // add a new OR filter to the query to which the filters for the groups
- // will be added.
- if ($this->group_operator === 'OR') {
- $base = $this->query->createFilter('OR');
- $this->query->filter($base);
- }
- else {
- $base = $this->query;
- }
- // Add a nested filter for each filter group, with its set conjunction.
- foreach ($this->where as $group_id => $group) {
- if (!empty($group['conditions']) || !empty($group['filters'])) {
- // For filters without a group, we want to always add them directly to
- // the query.
- $filter = ($group_id === '') ? $this->query : $this->query->createFilter($group['type']);
- if (!empty($group['conditions'])) {
- foreach ($group['conditions'] as $condition) {
- list($field, $value, $operator) = $condition;
- $filter->condition($field, $value, $operator);
- }
- }
- if (!empty($group['filters'])) {
- foreach ($group['filters'] as $nested_filter) {
- $filter->filter($nested_filter);
- }
- }
- // If no group was given, the filters were already set on the query.
- if ($group_id !== '') {
- $base->filter($filter);
- }
- }
- }
- }
- // Initialize the pager and let it modify the query to add limits.
- $view->init_pager();
- $this->pager->query();
- // Add the "search_api_bypass_access" option to the query, if desired.
- if (!empty($this->options['search_api_bypass_access'])) {
- $this->query->setOption('search_api_bypass_access', TRUE);
- }
- }
- /**
- * Executes the query and fills the associated view object with according
- * values.
- *
- * Values to set: $view->result, $view->total_rows, $view->execute_time,
- * $view->pager['current_page'].
- */
- public function execute(&$view) {
- if ($this->errors) {
- if (error_displayable()) {
- foreach ($this->errors as $msg) {
- drupal_set_message(check_plain($msg), 'error');
- }
- }
- $view->result = array();
- $view->total_rows = 0;
- $view->execute_time = 0;
- return;
- }
- try {
- $start = microtime(TRUE);
- // Add range and search ID (if it wasn't already set).
- $this->query->range($this->offset, $this->limit);
- if ($this->query->getOption('search id') == get_class($this->query)) {
- $this->query->setOption('search id', 'search_api_views:' . $view->name . ':' . $view->current_display);
- }
- // Execute the search.
- $results = $this->query->execute();
- $this->search_api_results = $results;
- // Store the results.
- $this->pager->total_items = $view->total_rows = $results['result count'];
- if (!empty($this->pager->options['offset'])) {
- $this->pager->total_items -= $this->pager->options['offset'];
- }
- $this->pager->update_page_info();
- $view->result = array();
- if (!empty($results['results'])) {
- $this->addResults($results['results'], $view);
- }
- // We shouldn't use $results['performance']['complete'] here, since
- // extracting the results probably takes considerable time as well.
- $view->execute_time = microtime(TRUE) - $start;
- }
- catch (Exception $e) {
- $this->errors[] = $e->getMessage();
- // Recursion to get the same error behaviour as above.
- return $this->execute($view);
- }
- }
- /**
- * Helper function for adding results to a view in the format expected by the
- * view.
- */
- protected function addResults(array $results, $view) {
- $rows = array();
- $missing = array();
- $items = array();
- // First off, we try to gather as much field values as possible without
- // loading any items.
- foreach ($results as $id => $result) {
- $row = array();
- // Include the loaded item for this result row, if present, or the item
- // ID.
- if (!empty($result['entity'])) {
- $row['entity'] = $result['entity'];
- }
- else {
- $row['entity'] = $id;
- }
- $row['_entity_properties']['search_api_relevance'] = $result['score'];
- $row['_entity_properties']['search_api_excerpt'] = empty($result['excerpt']) ? '' : $result['excerpt'];
- // Gather any fields from the search results.
- if (!empty($result['fields'])) {
- $row['_entity_properties'] += $result['fields'];
- }
- // Check whether we need to extract any properties from the result item.
- $missing_fields = array_diff_key($this->fields, $row);
- if ($missing_fields) {
- $missing[$id] = $missing_fields;
- if (is_object($row['entity'])) {
- $items[$id] = $row['entity'];
- }
- else {
- $ids[] = $id;
- }
- }
- // Save the row values for adding them to the Views result afterwards.
- $rows[$id] = (object) $row;
- }
- // Load items of those rows which haven't got all field values, yet.
- if (!empty($ids)) {
- $items += $this->index->loadItems($ids);
- // $items now includes loaded items, and those already passed in the
- // search results.
- foreach ($items as $id => $item) {
- // Extract item properties.
- $wrapper = $this->index->entityWrapper($item, FALSE);
- $rows[$id]->_entity_properties += $this->extractFields($wrapper, $missing[$id]);
- $rows[$id]->entity = $item;
- }
- }
- // Finally, add all rows to the Views result set.
- $view->result = array_values($rows);
- }
- /**
- * Helper function for extracting all necessary fields from a result item.
- *
- * Usually, this method isn't needed anymore as the properties are now
- * extracted by the field handlers themselves.
- */
- protected function extractFields(EntityMetadataWrapper $wrapper, array $all_fields) {
- $fields = array();
- foreach ($all_fields as $key => $true) {
- $fields[$key]['type'] = 'string';
- }
- $fields = search_api_extract_fields($wrapper, $fields, array('sanitized' => TRUE));
- $ret = array();
- foreach ($all_fields as $key => $true) {
- $ret[$key] = isset($fields[$key]['value']) ? $fields[$key]['value'] : '';
- }
- return $ret;
- }
- /**
- * Returns the according entity objects for the given query results.
- *
- * This is necessary to support generic entity handlers and plugins with this
- * query backend.
- *
- * If the current query isn't based on an entity type, the method will return
- * an empty array.
- */
- public function get_result_entities($results, $relationship = NULL, $field = NULL) {
- list($type, $wrappers) = $this->get_result_wrappers($results, $relationship, $field);
- $return = array();
- foreach ($wrappers as $id => $wrapper) {
- try {
- $return[$id] = $wrapper->value();
- }
- catch (EntityMetadataWrapperException $e) {
- // Ignore.
- }
- }
- return array($type, $return);
- }
- /**
- * Returns the according metadata wrappers for the given query results.
- *
- * This is necessary to support generic entity handlers and plugins with this
- * query backend.
- */
- public function get_result_wrappers($results, $relationship = NULL, $field = NULL) {
- $is_entity = (boolean) entity_get_info($this->index->item_type);
- $wrappers = array();
- $load_entities = array();
- foreach ($results as $row_index => $row) {
- if ($is_entity && isset($row->entity)) {
- // If this entity isn't load, register it for pre-loading.
- if (!is_object($row->entity)) {
- $load_entities[$row->entity] = $row_index;
- }
- $wrappers[$row_index] = $this->index->entityWrapper($row->entity);
- }
- }
- // If the results are entities, we pre-load them to make use of a multiple
- // load. (Otherwise, each result would be loaded individually.)
- if (!empty($load_entities)) {
- $entities = entity_load($this->index->item_type, array_keys($load_entities));
- foreach ($entities as $entity_id => $entity) {
- $wrappers[$load_entities[$entity_id]] = $this->index->entityWrapper($entity);
- }
- }
- // Apply the relationship, if necessary.
- $type = $this->index->item_type;
- $selector_suffix = '';
- if ($field && ($pos = strrpos($field, ':'))) {
- $selector_suffix = substr($field, 0, $pos);
- }
- if ($selector_suffix || ($relationship && !empty($this->view->relationship[$relationship]))) {
- // Use EntityFieldHandlerHelper to compute the correct data selector for
- // the relationship.
- $handler = (object) array(
- 'view' => $this->view,
- 'relationship' => $relationship,
- 'real_field' => '',
- );
- $selector = EntityFieldHandlerHelper::construct_property_selector($handler);
- $selector .= ($selector ? ':' : '') . $selector_suffix;
- list($type, $wrappers) = EntityFieldHandlerHelper::extract_property_multiple($wrappers, $selector);
- }
- return array($type, $wrappers);
- }
- /**
- * API function for accessing the raw Search API query object.
- *
- * @return SearchApiQueryInterface
- * The search query object used internally by this handler.
- */
- public function getSearchApiQuery() {
- return $this->query;
- }
- /**
- * API function for accessing the raw Search API results.
- *
- * @return array
- * An associative array containing the search results, as specified by
- * SearchApiQueryInterface::execute().
- */
- public function getSearchApiResults() {
- return $this->search_api_results;
- }
- //
- // Query interface methods (proxy to $this->query)
- //
- public function createFilter($conjunction = 'AND') {
- if (!$this->errors) {
- return $this->query->createFilter($conjunction);
- }
- }
- public function keys($keys = NULL) {
- if (!$this->errors) {
- $this->query->keys($keys);
- }
- return $this;
- }
- public function fields(array $fields) {
- if (!$this->errors) {
- $this->query->fields($fields);
- }
- return $this;
- }
- /**
- * Adds a nested filter to the search query object.
- *
- * If $group is given, the filter is added to the relevant filter group
- * instead.
- */
- public function filter(SearchApiQueryFilterInterface $filter, $group = NULL) {
- if (!$this->errors) {
- $this->where[$group]['filters'][] = $filter;
- }
- return $this;
- }
- /**
- * Set a condition on the search query object.
- *
- * If $group is given, the condition is added to the relevant filter group
- * instead.
- */
- public function condition($field, $value, $operator = '=', $group = NULL) {
- if (!$this->errors) {
- $this->where[$group]['conditions'][] = array($field, $value, $operator);
- }
- return $this;
- }
- public function sort($field, $order = 'ASC') {
- if (!$this->errors) {
- $this->query->sort($field, $order);
- }
- return $this;
- }
- public function range($offset = NULL, $limit = NULL) {
- if (!$this->errors) {
- $this->query->range($offset, $limit);
- }
- return $this;
- }
- public function getIndex() {
- return $this->index;
- }
- public function &getKeys() {
- if (!$this->errors) {
- return $this->query->getKeys();
- }
- $ret = NULL;
- return $ret;
- }
- public function getOriginalKeys() {
- if (!$this->errors) {
- return $this->query->getOriginalKeys();
- }
- }
- public function &getFields() {
- if (!$this->errors) {
- return $this->query->getFields();
- }
- $ret = NULL;
- return $ret;
- }
- public function getFilter() {
- if (!$this->errors) {
- return $this->query->getFilter();
- }
- }
- public function &getSort() {
- if (!$this->errors) {
- return $this->query->getSort();
- }
- $ret = NULL;
- return $ret;
- }
- public function getOption($name) {
- if (!$this->errors) {
- return $this->query->getOption($name);
- }
- }
- public function setOption($name, $value) {
- if (!$this->errors) {
- return $this->query->setOption($name, $value);
- }
- }
- public function &getOptions() {
- if (!$this->errors) {
- return $this->query->getOptions();
- }
- $ret = NULL;
- return $ret;
- }
- }
|