search_api-8.x-1.15/src/Query/Query.php
src/Query/Query.php
<?php namespace Drupal\search_api\Query; use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher; use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\search_api\Display\DisplayPluginManagerInterface; use Drupal\search_api\Event\QueryPreExecuteEvent; use Drupal\search_api\Event\ProcessingResultsEvent; use Drupal\search_api\Event\SearchApiEvents; use Drupal\search_api\IndexInterface; use Drupal\search_api\ParseMode\ParseModeInterface; use Drupal\search_api\ParseMode\ParseModePluginManager; use Drupal\search_api\SearchApiException; use Drupal\search_api\Utility\QueryHelperInterface; /** * Provides a standard implementation for a Search API query. */ class Query implements QueryInterface { use StringTranslationTrait; use DependencySerializationTrait { __sleep as traitSleep; __wakeup as traitWakeup; } /** * The index on which the query will be executed. * * @var \Drupal\search_api\IndexInterface */ protected $index; /** * The index's ID. * * Used when serializing, to avoid serializing the index, too. * * @var string|null */ protected $indexId; /** * The search results. * * @var \Drupal\search_api\Query\ResultSetInterface */ protected $results; /** * The search ID set for this query. * * @var string */ protected $searchId; /** * The parse mode to use for fulltext search keys. * * @var \Drupal\search_api\ParseMode\ParseModeInterface|null */ protected $parseMode; /** * The processing level for this search query. * * One of the \Drupal\search_api\Query\QueryInterface::PROCESSING_* constants. * * @var int */ protected $processingLevel = self::PROCESSING_FULL; /** * The language codes which should be searched by this query. * * @var string[]|null */ protected $languages; /** * The search keys. * * If NULL, this will be a filter-only search. * * @var mixed */ protected $keys; /** * The unprocessed search keys, as passed to the keys() method. * * @var mixed */ protected $origKeys; /** * The fulltext fields that will be searched for the keys. * * @var array */ protected $fields; /** * The root condition group associated with this query. * * @var \Drupal\search_api\Query\ConditionGroupInterface */ protected $conditionGroup; /** * The sorts associated with this query. * * @var array */ protected $sorts = []; /** * Information about whether the query has been aborted or not. * * @var \Drupal\Component\Render\MarkupInterface|string|true|null */ protected $aborted; /** * Options configuring this query. * * @var array */ protected $options; /** * The tags set on this query. * * @var string[] */ protected $tags = []; /** * Flag for whether preExecute() was already called for this query. * * @var bool */ protected $preExecuteRan = FALSE; /** * Flag for whether execute() was already called for this query. * * @var bool */ protected $executed = FALSE; /** * The module handler. * * @var \Drupal\Core\Extension\ModuleHandlerInterface|null */ protected $moduleHandler; /** * The event dispatcher. * * @var \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher|null */ protected $eventDispatcher; /** * The parse mode manager. * * @var \Drupal\search_api\ParseMode\ParseModePluginManager|null */ protected $parseModeManager; /** * The display plugin manager. * * @var \Drupal\search_api\Display\DisplayPluginManagerInterface|null */ protected $displayPluginManager; /** * The result cache service. * * @var \Drupal\search_api\Utility\QueryHelperInterface|null */ protected $queryHelper; /** * The original query before preprocessing. * * @var static|null */ protected $originalQuery; /** * Constructs a Query object. * * @param \Drupal\search_api\IndexInterface $index * The index the query should be executed on. * @param array $options * (optional) Associative array of options configuring this query. See * \Drupal\search_api\Query\QueryInterface::setOption() for a list of * options that are recognized by default. * * @throws \Drupal\search_api\SearchApiException * Thrown if a search on that index (or with those options) won't be * possible. */ public function __construct(IndexInterface $index, array $options = []) { if (!$index->status()) { $index_label = $index->label(); throw new SearchApiException("Can't search on index '$index_label' which is disabled."); } $this->index = $index; $this->results = new ResultSet($this); $this->options = $options; $this->conditionGroup = $this->createConditionGroup('AND'); } /** * {@inheritdoc} */ public static function create(IndexInterface $index, array $options = []) { return new static($index, $options); } /** * Retrieves the module handler. * * @return \Drupal\Core\Extension\ModuleHandlerInterface * The module handler. */ public function getModuleHandler() { return $this->moduleHandler ?: \Drupal::moduleHandler(); } /** * Sets the module handler. * * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The new module handler. * * @return $this */ public function setModuleHandler(ModuleHandlerInterface $module_handler) { $this->moduleHandler = $module_handler; return $this; } /** * Retrieves the event dispatcher. * * @return \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher * The event dispatcher. */ public function getEventDispatcher() { return $this->eventDispatcher ?: \Drupal::service('event_dispatcher'); } /** * Sets the event dispatcher. * * @param \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher $event_dispatcher * The new event dispatcher. * * @return $this */ public function setEventDispatcher(ContainerAwareEventDispatcher $event_dispatcher) { $this->eventDispatcher = $event_dispatcher; return $this; } /** * Retrieves the parse mode manager. * * @return \Drupal\search_api\ParseMode\ParseModePluginManager * The parse mode manager. */ public function getParseModeManager() { return $this->parseModeManager ?: \Drupal::service('plugin.manager.search_api.parse_mode'); } /** * Sets the parse mode manager. * * @param \Drupal\search_api\ParseMode\ParseModePluginManager $parse_mode_manager * The new parse mode manager. * * @return $this */ public function setParseModeManager(ParseModePluginManager $parse_mode_manager) { $this->parseModeManager = $parse_mode_manager; return $this; } /** * Retrieves the display plugin manager. * * @return \Drupal\search_api\Display\DisplayPluginManagerInterface * The display plugin manager. */ public function getDisplayPluginManager() { return $this->displayPluginManager ?: \Drupal::service('plugin.manager.search_api.display'); } /** * Sets the display plugin manager. * * @param \Drupal\search_api\Display\DisplayPluginManagerInterface $display_plugin_manager * The new display plugin manager. * * @return $this */ public function setDisplayPluginManager(DisplayPluginManagerInterface $display_plugin_manager) { $this->displayPluginManager = $display_plugin_manager; return $this; } /** * Retrieves the query helper. * * @return \Drupal\search_api\Utility\QueryHelperInterface * The query helper. */ public function getQueryHelper() { return $this->queryHelper ?: \Drupal::service('search_api.query_helper'); } /** * Sets the query helper. * * @param \Drupal\search_api\Utility\QueryHelperInterface $query_helper * The new query helper. * * @return $this */ public function setQueryHelper(QueryHelperInterface $query_helper) { $this->queryHelper = $query_helper; return $this; } /** * {@inheritdoc} */ public function getSearchId($generate = TRUE) { if ($generate && !isset($this->searchId)) { static $num = 0; $this->searchId = 'search_' . ++$num; } return $this->searchId; } /** * {@inheritdoc} */ public function setSearchId($search_id) { $this->searchId = $search_id; return $this; } /** * {@inheritdoc} */ public function getDisplayPlugin() { $display_manager = $this->getDisplayPluginManager(); if (isset($this->searchId) && $display_manager->hasDefinition($this->searchId)) { return $display_manager->createInstance($this->searchId); } return NULL; } /** * {@inheritdoc} */ public function getParseMode() { if (!$this->parseMode) { $this->parseMode = $this->getParseModeManager()->createInstance('terms'); } return $this->parseMode; } /** * {@inheritdoc} */ public function setParseMode(ParseModeInterface $parse_mode) { $this->parseMode = $parse_mode; if (is_scalar($this->origKeys)) { $this->keys = $parse_mode->parseInput($this->origKeys); } return $this; } /** * {@inheritdoc} */ public function getLanguages() { return $this->languages; } /** * {@inheritdoc} */ public function setLanguages(array $languages = NULL) { $this->languages = isset($languages) ? array_values($languages) : NULL; return $this; } /** * {@inheritdoc} */ public function createConditionGroup($conjunction = 'AND', array $tags = []) { return new ConditionGroup($conjunction, $tags); } /** * {@inheritdoc} */ public function keys($keys = NULL) { $this->origKeys = $keys; if (is_scalar($keys)) { $this->keys = $this->getParseMode()->parseInput("$keys"); } else { $this->keys = $keys; } return $this; } /** * {@inheritdoc} */ public function setFulltextFields(array $fields = NULL) { $this->fields = $fields; return $this; } /** * {@inheritdoc} */ public function addConditionGroup(ConditionGroupInterface $condition_group) { $this->conditionGroup->addConditionGroup($condition_group); return $this; } /** * {@inheritdoc} */ public function addCondition($field, $value, $operator = '=') { $this->conditionGroup->addCondition($field, $value, $operator); return $this; } /** * {@inheritdoc} */ public function sort($field, $order = self::SORT_ASC) { $order = strtoupper(trim($order)); $order = $order == self::SORT_DESC ? self::SORT_DESC : self::SORT_ASC; if (!isset($this->sorts[$field])) { $this->sorts[$field] = $order; } return $this; } /** * {@inheritdoc} */ public function range($offset = NULL, $limit = NULL) { $this->options['offset'] = $offset; $this->options['limit'] = $limit; return $this; } /** * {@inheritdoc} */ public function getProcessingLevel() { return $this->processingLevel; } /** * {@inheritdoc} */ public function setProcessingLevel($level) { $this->processingLevel = $level; return $this; } /** * {@inheritdoc} */ public function abort($error_message = NULL) { $this->aborted = isset($error_message) ? $error_message : TRUE; } /** * {@inheritdoc} */ public function wasAborted() { return $this->aborted !== NULL; } /** * {@inheritdoc} */ public function getAbortMessage() { return !is_bool($this->aborted) ? $this->aborted : NULL; } /** * {@inheritdoc} */ public function execute() { if ($this->hasExecuted()) { return $this->results; } $this->executed = TRUE; // Check for aborted status both before and after calling preExecute(). if ($this->shouldAbort()) { return $this->results; } // Prepare the query for execution by the server. $this->preExecute(); if ($this->shouldAbort()) { return $this->results; } // Execute query. $this->index->getServerInstance()->search($this); // Postprocess the search results. $this->postExecute(); return $this->results; } /** * Determines whether the query should be aborted. * * Also prepares the result set if the query should be aborted. * * @return bool * TRUE if the query should be aborted, FALSE otherwise. */ protected function shouldAbort() { if (!$this->wasAborted() && $this->languages !== []) { return FALSE; } if (!$this->originalQuery) { $this->originalQuery = clone $this; } $this->postExecute(); return TRUE; } /** * {@inheritdoc} */ public function preExecute() { // Make sure to only execute this once per query, and not for queries with // the "none" processing level. if (!$this->preExecuteRan) { $this->originalQuery = clone $this; $this->originalQuery->executed = FALSE; $this->preExecuteRan = TRUE; if ($this->processingLevel == self::PROCESSING_NONE) { return; } // Preprocess query. $this->index->preprocessSearchQuery($this); // Let modules alter the query. $event_base_name = SearchApiEvents::QUERY_PRE_EXECUTE; $event = new QueryPreExecuteEvent($this); $this->getEventDispatcher()->dispatch($event_base_name, $event); $hooks = ['search_api_query']; foreach ($this->tags as $tag) { $hooks[] = "search_api_query_$tag"; $event_name = "$event_base_name.$tag"; $event = new QueryPreExecuteEvent($this); $this->getEventDispatcher()->dispatch($event_name, $event); } $description = 'This hook is deprecated in search_api 8.x-1.14 and will be removed in 9.x-1.0. Please use the "search_api.query_pre_execute" event instead. See https://www.drupal.org/node/3059866'; $this->getModuleHandler()->alterDeprecated($description, $hooks, $this); } } /** * {@inheritdoc} */ public function postExecute() { if ($this->processingLevel == self::PROCESSING_NONE) { return; } // Postprocess results. $this->index->postprocessSearchResults($this->results); // Let modules alter the results. $event_base_name = SearchApiEvents::PROCESSING_RESULTS; $event = new ProcessingResultsEvent($this->results); $this->results = $event->getResults(); $this->getEventDispatcher()->dispatch($event_base_name, $event); $hooks = ['search_api_results']; foreach ($this->tags as $tag) { $hooks[] = "search_api_results_$tag"; $event = new ProcessingResultsEvent($this->results); $this->getEventDispatcher()->dispatch("$event_base_name.$tag", $event); $this->results = $event->getResults(); } $description = 'This hook is deprecated in search_api 8.x-1.14 and will be removed in 9.x-1.0. Please use the "search_api.processing_results" event instead. See https://www.drupal.org/node/3059866'; $this->getModuleHandler()->alterDeprecated($description, $hooks, $this->results); // Store the results in the static cache. $this->getQueryHelper()->addResults($this->results); } /** * {@inheritdoc} */ public function hasExecuted() { return $this->executed; } /** * {@inheritdoc} */ public function getResults() { return $this->results; } /** * {@inheritdoc} */ public function getIndex() { return $this->index; } /** * {@inheritdoc} */ public function &getKeys() { return $this->keys; } /** * {@inheritdoc} */ public function getOriginalKeys() { return $this->origKeys; } /** * {@inheritdoc} */ public function &getFulltextFields() { return $this->fields; } /** * {@inheritdoc} */ public function getConditionGroup() { return $this->conditionGroup; } /** * {@inheritdoc} */ public function &getSorts() { return $this->sorts; } /** * {@inheritdoc} */ public function getOption($name, $default = NULL) { return array_key_exists($name, $this->options) ? $this->options[$name] : $default; } /** * {@inheritdoc} */ public function setOption($name, $value) { $old = $this->getOption($name); $this->options[$name] = $value; return $old; } /** * {@inheritdoc} */ public function &getOptions() { return $this->options; } /** * {@inheritdoc} */ public function addTag($tag) { $this->tags[$tag] = $tag; return $this; } /** * {@inheritdoc} */ public function hasTag($tag) { return isset($this->tags[$tag]); } /** * {@inheritdoc} */ public function hasAllTags() { return !array_diff_key(array_flip(func_get_args()), $this->tags); } /** * {@inheritdoc} */ public function hasAnyTag() { return (bool) array_intersect_key(array_flip(func_get_args()), $this->tags); } /** * {@inheritdoc} */ public function &getTags() { return $this->tags; } /** * {@inheritdoc} */ public function getOriginalQuery() { return $this->originalQuery ?: clone $this; } /** * {@inheritdoc} */ public function __clone() { $this->results = $this->getResults()->getCloneForQuery($this); $this->conditionGroup = clone $this->conditionGroup; if ($this->originalQuery) { $this->originalQuery = clone $this->originalQuery; } if ($this->parseMode) { $this->parseMode = clone $this->parseMode; } } /** * Implements the magic __sleep() method to avoid serializing the index. */ public function __sleep() { $this->indexId = $this->index->id(); $keys = $this->traitSleep(); return array_diff($keys, ['index']); } /** * Implements the magic __wakeup() method to reload the query's index. */ public function __wakeup() { if (!isset($this->index) && !empty($this->indexId) && \Drupal::hasContainer() && \Drupal::getContainer()->has('entity_type.manager')) { $this->index = \Drupal::entityTypeManager() ->getStorage('search_api_index') ->load($this->indexId); $this->indexId = NULL; } $this->traitWakeup(); } /** * Implements the magic __toString() method to simplify debugging. */ public function __toString() { $ret = 'Index: ' . $this->index->id() . "\n"; $ret .= 'Keys: ' . str_replace("\n", "\n ", var_export($this->origKeys, TRUE)) . "\n"; if (isset($this->keys)) { $ret .= 'Parsed keys: ' . str_replace("\n", "\n ", var_export($this->keys, TRUE)) . "\n"; $ret .= 'Searched fields: ' . (isset($this->fields) ? implode(', ', $this->fields) : '[ALL]') . "\n"; } if (isset($this->languages)) { $ret .= 'Searched languages: ' . implode(', ', $this->languages) . "\n"; } if ($conditions = (string) $this->conditionGroup) { $conditions = str_replace("\n", "\n ", $conditions); $ret .= "Conditions:\n $conditions\n"; } if ($this->sorts) { $sorts = []; foreach ($this->sorts as $field => $order) { $sorts[] = "$field $order"; } $ret .= 'Sorting: ' . implode(', ', $sorts) . "\n"; } $options = $this->sanitizeOptions($this->options); $options = str_replace("\n", "\n ", var_export($options, TRUE)); $ret .= 'Options: ' . $options . "\n"; return $ret; } /** * Sanitizes an array of options in a way that plays nice with var_export(). * * @param array $options * An array of options. * * @return array * The sanitized options. */ protected function sanitizeOptions(array $options) { foreach ($options as $key => $value) { if (is_object($value)) { $options[$key] = 'object (' . get_class($value) . ')'; } elseif (is_array($value)) { $options[$key] = $this->sanitizeOptions($value); } } return $options; } }