2013-03-15 17:32:30 +01:00

565 lines
16 KiB
PHP
Executable File

<?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;
}
}