Newer
Older
<?php
namespace Drupal\search_api\Plugin\views\query;
Thomas Seidl
committed
use Drupal\Component\Render\FormattableMarkup;
Mohd Rashid
committed
use Drupal\Component\Utility\Html;
Thomas Seidl
committed
use Drupal\Core\Database\Query\ConditionInterface;
use Drupal\Core\Entity\EntityInterface;
Joris Vercammen
committed
use Drupal\Core\Entity\EntityTypeManagerInterface;
Thomas Seidl
committed
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\search_api\Entity\Index;
drholera
committed
use Drupal\search_api\LoggerTrait;
use Drupal\search_api\ParseMode\ParseModeInterface;
use Drupal\search_api\Plugin\views\field\SearchApiStandard;
Thomas Seidl
committed
use Drupal\search_api\Processor\ConfigurablePropertyInterface;
Thomas Seidl
committed
use Drupal\search_api\Query\ConditionGroupInterface;
Joris Vercammen
committed
use Drupal\search_api\Query\QueryInterface;
Thomas Seidl
committed
use Drupal\search_api\SearchApiException;
Thomas Seidl
committed
use Drupal\search_api\Utility\Utility;
use Drupal\user\Entity\User;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
use Symfony\Component\DependencyInjection\ContainerInterface;
* Defines a Views query class for searching on Search API indexes.
*
* @ViewsQuery(
* id = "search_api_query",
* title = @Translation("Search API Query"),
* help = @Translation("The query will be generated and run using the Search API.")
* )
*/
class SearchApiQuery extends QueryPluginBase {
drholera
committed
use LoggerTrait;
Wim Leers
committed
/**
* Number of results to display.
*
* @var int
*/
protected $limit;
/**
* Offset of first displayed result.
*
* @var int
*/
protected $offset;
/**
* The index this view accesses.
*
Thomas Seidl
committed
* @var \Drupal\search_api\IndexInterface
*/
protected $index;
/**
* The query that will be executed.
*
* @var \Drupal\search_api\Query\QueryInterface
*/
protected $query;
/**
* 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
*/
Thomas Seidl
committed
protected $errors = [];
/**
* Whether to abort the search instead of executing it.
*
* @var bool
*/
protected $abort = FALSE;
/**
Thomas Seidl
committed
* The properties that should be retrieved from result items.
*
* The array is keyed by datasource ID (which might be NULL) and property
* path, the values are the associated combined property paths.
* @var string[][]
Thomas Seidl
committed
protected $retrievedProperties = [];
Thomas Seidl
committed
* The query's conditions representing the different Views filter groups.
*
* @var array
*/
Thomas Seidl
committed
protected $where = [];
/**
* The conjunction with which multiple filter groups are combined.
*
* @var string
*/
Joris Vercammen
committed
protected $groupOperator = 'AND';
Thomas Seidl
committed
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface|null
*/
protected $moduleHandler;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
/** @var static $plugin */
$plugin = parent::create($container, $configuration, $plugin_id, $plugin_definition);
Thomas Seidl
committed
$plugin->setModuleHandler($container->get('module_handler'));
git
committed
$plugin->setLogger($container->get('logger.channel.search_api'));
return $plugin;
}
/**
* Loads the search index belonging to the given Views base table.
*
* @param string $table
* The Views base table ID.
Joris Vercammen
committed
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* (optional) The entity type manager to use.
*
* @return \Drupal\search_api\IndexInterface|null
* The requested search index, or NULL if it could not be found and loaded.
*/
Joris Vercammen
committed
public static function getIndexFromTable($table, EntityTypeManagerInterface $entity_type_manager = NULL) {
Thomas Seidl
committed
// @todo Instead use Views::viewsData() – injected, too – to load the base
// table definition and use the "index" (or maybe rename to
// "search_api_index") field from there.
if (substr($table, 0, 17) == 'search_api_index_') {
$index_id = substr($table, 17);
Joris Vercammen
committed
if ($entity_type_manager) {
Thomas Seidl
committed
return $entity_type_manager->getStorage('search_api_index')
->load($index_id);
}
return Index::load($index_id);
}
return NULL;
}
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/**
* Retrieves the contained entity from a Views result row.
*
* @param \Drupal\views\ResultRow $row
* The Views result row.
* @param string $relationship_id
* The ID of the view relationship to use.
* @param \Drupal\views\ViewExecutable $view
* The current view object.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The entity contained in the result row, if any.
*/
public static function getEntityFromRow(ResultRow $row, $relationship_id, ViewExecutable $view) {
if ($relationship_id === 'none') {
$object = $row->_object ?: $row->_item->getOriginalObject();
$entity = $object->getValue();
if ($entity instanceof EntityInterface) {
return $entity;
}
return NULL;
}
// To avoid code duplication, just create a dummy field handler and use it
// to retrieve the entity.
$handler = new SearchApiStandard([], '', ['title' => '']);
$options = ['relationship' => $relationship_id];
$handler->init($view, $view->display_handler, $options);
return $handler->getEntity($row);
}
Thomas Seidl
committed
/**
* 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;
}
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
try {
parent::init($view, $display, $options);
$this->index = static::getIndexFromTable($view->storage->get('base_table'));
if (!$this->index) {
Thomas Seidl
committed
$this->abort(new FormattableMarkup('View %view is not based on Search API but tries to use its query plugin.', ['%view' => $view->storage->label()]));
Thomas Seidl
committed
$this->retrievedProperties = array_fill_keys($this->index->getDatasourceIds(), []);
$this->retrievedProperties[NULL] = [];
Thomas Seidl
committed
$this->query = $this->index->query();
$this->query->addTag('views');
$this->query->addTag('views_' . $view->id());
Joris Vercammen
committed
$display_type = $display->getPluginId();
if ($display_type === 'rest_export') {
$display_type = 'rest';
}
$this->query->setSearchId("views_$display_type:" . $view->id() . '__' . $view->current_display);
Thomas Seidl
committed
$this->query->setOption('search_api_view', $view);
}
catch (\Exception $e) {
$this->abort($e->getMessage());
}
}
/**
Thomas Seidl
committed
* Adds a property to be retrieved.
* Currently doesn't serve any purpose, but might be added to the search query
* in the future to help backends that support returning fields determine
* which of the fields should actually be returned.
*
Thomas Seidl
committed
* @param string $combined_property_path
* The combined property path of the property that should be retrieved.
*
* @return $this
public function addRetrievedProperty($combined_property_path) {
list($datasource_id, $property_path) = Utility::splitCombinedId($combined_property_path);
$this->retrievedProperties[$datasource_id][$property_path] = $combined_property_path;
return $this;
/**
* Adds a field to the table.
*
* This replicates the interface of Views' default SQL backend to simplify
* the Views integration of the Search API. If you are writing Search
* API-specific Views code, you should better use the addRetrievedProperty()
* method.
*
* @param string|null $table
* Ignored.
* @param string $field
* The combined property path of the property that should be retrieved.
* @param string $alias
* (optional) Ignored.
* @param array $params
* (optional) Ignored.
*
* @return string
* The name that this field can be referred to as (always $field).
*
* @see \Drupal\views\Plugin\views\query\Sql::addField()
* @see \Drupal\search_api\Plugin\views\query\SearchApiQuery::addField()
*/
Thomas Seidl
committed
public function addField($table, $field, $alias = '', array $params = []) {
$this->addRetrievedProperty($field);
return $field;
}
* {@inheritdoc}
*/
public function defineOptions() {
Thomas Seidl
committed
return parent::defineOptions() + [
'bypass_access' => [
'default' => FALSE,
Thomas Seidl
committed
],
'skip_access' => [
'default' => FALSE,
Thomas Seidl
committed
],
];
}
/**
* {@inheritdoc}
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
Thomas Seidl
committed
$form['skip_access'] = [
Thomas Seidl
committed
'#type' => 'checkbox',
'#title' => $this->t('Skip item access checks'),
'#description' => $this->t("By default, an additional access check will be executed for each item returned by the search query. However, since removing results this way will break paging and result counts, it is preferable to configure the view in a way that it will only return accessible results. If you are sure that only accessible results will be returned in the search, or if you want to show results to which the user normally wouldn't have access, you can enable this option to skip those additional access checks. This should be used with care."),
'#default_value' => $this->options['skip_access'],
'#weight' => -1,
Thomas Seidl
committed
];
Thomas Seidl
committed
Thomas Seidl
committed
$form['bypass_access'] = [
'#type' => 'checkbox',
'#title' => $this->t('Bypass access checks'),
Rajeshwari Variar
committed
'#description' => $this->t('If the underlying search index has access checks enabled (for example, through the "Content access" processor), this option allows you to disable them for this view. This will never disable any filters placed on this view.'),
'#default_value' => $this->options['bypass_access'],
Thomas Seidl
committed
];
Thomas Seidl
committed
$form['bypass_access']['#states']['visible'][':input[name="query[options][skip_access]"]']['checked'] = TRUE;
}
/**
* Checks for entity types contained in the current view's index.
*
* @param bool $return_bool
* (optional) If TRUE, returns a boolean instead of a list of datasources.
*
* @return string[]|bool
* If $return_bool is FALSE, an associative array mapping all datasources
* containing entities to their entity types. Otherwise, TRUE if there is at
* least one such datasource.
*
* @deprecated Will be removed in a future version of the module. Use
* \Drupal\search_api\IndexInterface::getEntityTypes() instead.
*/
public function getEntityTypes($return_bool = FALSE) {
$types = $this->index->getEntityTypes();
return $return_bool ? (bool) $types : $types;
}
/**
* {@inheritdoc}
*/
public function build(ViewExecutable $view) {
$this->view = $view;
// Initialize the pager and let it modify the query to add limits. This has
// to be done even for aborted queries since it might otherwise lead to a
// fatal error when Views tries to access $view->pager.
$view->initPager();
$view->pager->query();
// If the query was aborted by some plugin (or, possibly, hook), we don't
// need to do anything else here. Adding conditions or other options to an
// aborted query doesn't make sense.
Sascha Grossenbacher
committed
if ($this->shouldAbort()) {
return;
}
// 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.
Joris Vercammen
committed
if ($this->groupOperator === 'OR') {
Thomas Seidl
committed
$base = $this->query->createConditionGroup('OR');
$this->query->addConditionGroup($base);
}
else {
$base = $this->query;
}
// Add a nested filter for each filter group, with its set conjunction.
foreach ($this->where as $group_id => $group) {
Thomas Seidl
committed
if (!empty($group['conditions']) || !empty($group['condition_groups'])) {
Thomas Seidl
committed
$group += ['type' => 'AND'];
// For filters without a group, we want to always add them directly to
// the query.
Thomas Seidl
committed
$conditions = ($group_id === '') ? $this->query : $this->query->createConditionGroup($group['type']);
if (!empty($group['conditions'])) {
foreach ($group['conditions'] as $condition) {
list($field, $value, $operator) = $condition;
Thomas Seidl
committed
$conditions->addCondition($field, $value, $operator);
}
}
Thomas Seidl
committed
if (!empty($group['condition_groups'])) {
foreach ($group['condition_groups'] as $nested_conditions) {
Thomas Seidl
committed
$conditions->addConditionGroup($nested_conditions);
}
}
// If no group was given, the filters were already set on the query.
if ($group_id !== '') {
Thomas Seidl
committed
$base->addConditionGroup($conditions);
}
}
}
}
// Add the "search_api_bypass_access" option to the query, if desired.
if (!empty($this->options['bypass_access'])) {
$this->query->setOption('search_api_bypass_access', TRUE);
}
// If the View and the Panel conspire to provide an overridden path then
// pass that through as the base path.
if (($path = $this->view->getPath()) && strpos(Url::fromRoute('<current>')->toString(), $this->view->override_path) !== 0) {
$this->query->setOption('search_api_base_path', $path);
Thomas Seidl
committed
// Save query information for Views UI.
$view->build_info['query'] = (string) $this->query;
// Add the properties to be retrieved to the query, as information for the
// backend.
$this->query->setOption('search_api_retrieved_properties', $this->retrievedProperties);
}
/**
* {@inheritdoc}
*/
public function alter(ViewExecutable $view) {
Thomas Seidl
committed
$this->getModuleHandler()->invokeAll('views_query_alter', [$view, $this]);
}
/**
*/
public function execute(ViewExecutable $view) {
if ($this->shouldAbort()) {
if (error_displayable()) {
foreach ($this->errors as $msg) {
Mohd Rashid
committed
drupal_set_message(Html::escape($msg), 'error');
}
}
Thomas Seidl
committed
$view->result = [];
$view->total_rows = 0;
$view->execute_time = 0;
return;
}
// Calculate the "skip result count" option, if it wasn't already set to
// FALSE.
$skip_result_count = $this->query->getOption('skip result count', TRUE);
if ($skip_result_count) {
$skip_result_count = !$view->pager->useCountQuery() && empty($view->get_total_rows);
$this->query->setOption('skip result count', $skip_result_count);
}
try {
// Trigger pager preExecute().
$view->pager->preExecute($this->query);
// Views passes sometimes NULL and sometimes the integer 0 for "All" in a
// pager. If set to 0 items, a string "0" is passed. Therefore, we unset
// the limit if an empty value OTHER than a string "0" was passed.
if (!$this->limit && $this->limit !== '0') {
$this->limit = NULL;
}
Joris Vercammen
committed
// Set the range. We always set this, as there might be an offset even if
// all items are shown.
$this->query->range($this->offset, $this->limit);
$start = microtime(TRUE);
// Execute the search.
$results = $this->query->execute();
// Store the results.
if (!$skip_result_count) {
Marcin Grabias
committed
$view->pager->total_items = $results->getResultCount();
Thomas Seidl
committed
if (!empty($view->pager->options['offset'])) {
$view->pager->total_items -= $view->pager->options['offset'];
Marcin Grabias
committed
$view->total_rows = $view->pager->total_items;
Thomas Seidl
committed
$view->result = [];
if ($results->getResultItems()) {
$this->addResults($results->getResultItems(), $view);
}
$view->execute_time = microtime(TRUE) - $start;
// Trigger pager postExecute().
$view->pager->postExecute($view->result);
Sascha Grossenbacher
committed
$view->pager->updatePageInfo();
}
catch (\Exception $e) {
$this->abort($e->getMessage());
// Recursion to get the same error behaviour as above.
$this->execute($view);
}
}
/**
* Aborts this search query.
*
* Used by handlers to flag a fatal error which shouldn't be displayed but
* still lead to the view returning empty and the search not being executed.
*
Thomas Seidl
committed
* @param \Drupal\Component\Render\MarkupInterface|string|null $msg
* Optionally, a translated, unescaped error message to display.
*/
public function abort($msg = NULL) {
if ($msg) {
$this->errors[] = $msg;
}
$this->abort = TRUE;
Thomas Seidl
committed
if (isset($this->query)) {
$this->query->abort($msg);
}
/**
* Checks whether this query should be aborted.
*
* @return bool
* TRUE if the query should/will be aborted, FALSE otherwise.
*
* @see SearchApiQuery::abort()
*/
public function shouldAbort() {
return $this->abort || !$this->query || $this->query->wasAborted();
* Adds Search API result items to a view's result set.
*
* @param \Drupal\search_api\Item\ItemInterface[] $results
* The search results.
* @param \Drupal\views\ViewExecutable $view
* The executed view.
protected function addResults(array $results, ViewExecutable $view) {
// Views \Drupal\views\Plugin\views\style\StylePluginBase::renderFields()
// uses a numeric results index to key the rendered results.
// The ResultRow::index property is the key then used to retrieve these.
$count = 0;
// First, unless disabled, check access for all entities in the results.
Thomas Seidl
committed
if (!$this->options['skip_access']) {
$account = $this->getAccessAccount();
foreach ($results as $item_id => $result) {
Thomas Seidl
committed
if (!$result->checkAccess($account)) {
unset($results[$item_id]);
Dragos Dumitrescu
committed
}
}
}
Thomas Seidl
committed
$values = [];
$values['_item'] = $result;
$object = $result->getOriginalObject(FALSE);
if ($object) {
$values['_object'] = $object;
Thomas Seidl
committed
$values['_relationship_objects'][NULL] = [$object];
$values['search_api_datasource'] = $result->getDatasourceId();
$values['search_api_language'] = $result->getLanguage();
$values['search_api_relevance'] = $result->getScore();
$values['search_api_excerpt'] = $result->getExcerpt() ?: '';
// Gather any properties from the search results.
foreach ($result->getFields(FALSE) as $field_id => $field) {
Thomas Seidl
committed
$path = $field->getCombinedPropertyPath();
try {
$property = $field->getDataDefinition();
// For configurable processor-defined properties, our Views field
// handlers use a special property path to distinguish multiple
// fields with the same property path. Therefore, we here also set
// the values using that special property path so this will work
// correctly.
if ($property instanceof ConfigurablePropertyInterface) {
$path .= '|' . $field_id;
}
}
catch (SearchApiException $e) {
// If we're not able to retrieve the data definition at this point,
// it doesn't really matter.
}
Markus Kalkbrenner
committed
$values[$path] = $field->getValues();
$values['index'] = $count++;
$view->result[] = new ResultRow($values);
Thomas Seidl
committed
/**
* Retrieves the conditions placed on this query.
*
* @return array
* The conditions placed on this query, separated by groups, as an
* associative array with a structure like this:
* - GROUP_ID:
* - type: "AND"/"OR"
* - conditions:
* - [FILTER, VALUE, OPERATOR]
* - [FILTER, VALUE, OPERATOR]
* …
* - condition_groups:
* - ConditionGroupInterface object
* - ConditionGroupInterface object
* …
* - GROUP_ID:
* …
* Returned by reference.
*/
public function &getWhere() {
return $this->where;
}
/**
* Retrieves the account object to use for access checks for this query.
*
* @return \Drupal\Core\Session\AccountInterface|null
* The account for which to check access to returned or displayed entities.
* Or NULL to use the currently logged-in user.
*/
public function getAccessAccount() {
$account = $this->getOption('search_api_access_account');
if ($account && is_scalar($account)) {
$account = User::load($account);
return $account;
}
/**
* Returns the Search API query object used by this Views query.
* @return \Drupal\search_api\Query\QueryInterface|null
* The search query object used internally by this plugin, if any has been
* successfully created. NULL otherwise.
*/
public function getSearchApiQuery() {
return $this->query;
}
/**
* Sets the Search API query object.
*
* Usually this is done by the query plugin class itself, but in rare cases
* (such as for caching purposes) it might be necessary to set it from
* outside.
*
* @param \Drupal\search_api\Query\QueryInterface $query
* The new query.
*
* @return $this
*/
Joris Vercammen
committed
public function setSearchApiQuery(QueryInterface $query) {
$this->query = $query;
return $this;
}
* Retrieves the Search API result set returned for this query.
* @return \Drupal\search_api\Query\ResultSetInterface
* The result set of this query. Might not contain the actual results yet if
* the query hasn't been executed yet.
*/
public function getSearchApiResults() {
Thomas Seidl
committed
return $this->query->getResults();
}
Thomas Seidl
committed
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
$dependencies[$this->index->getConfigDependencyKey()][] = $this->index->getConfigDependencyName();
return $dependencies;
}
//
// Query interface methods (proxy to $this->query)
//
/**
* Retrieves the parse mode.
*
* @return \Drupal\search_api\ParseMode\ParseModeInterface|null
* The parse mode, or NULL if the query was aborted.
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
*
* @see \Drupal\search_api\Query\QueryInterface::getParseMode()
*/
public function getParseMode() {
if (!$this->shouldAbort()) {
return $this->query->getParseMode();
}
return NULL;
}
/**
* Sets the parse mode.
*
* @param \Drupal\search_api\ParseMode\ParseModeInterface $parse_mode
* The parse mode.
*
* @return $this
*
* @see \Drupal\search_api\Query\QueryInterface::setParseMode()
*/
public function setParseMode(ParseModeInterface $parse_mode) {
if (!$this->shouldAbort()) {
$this->query->setParseMode($parse_mode);
}
return $this;
}
/**
* Retrieves the languages that will be searched by this query.
*
* @return string[]|null
* The language codes of languages that will be searched by this query, or
* NULL if there shouldn't be any restriction on the language.
*
* @see \Drupal\search_api\Query\QueryInterface::getLanguages()
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
*/
public function getLanguages() {
if (!$this->shouldAbort()) {
return $this->query->getLanguages();
}
return NULL;
}
/**
* Sets the languages that should be searched by this query.
*
* @param string[]|null $languages
* The language codes to search for, or NULL to not restrict the query to
* specific languages.
*
* @return $this
*
* @see \Drupal\search_api\Query\QueryInterface::setLanguages()
*/
public function setLanguages(array $languages = NULL) {
if (!$this->shouldAbort()) {
$this->query->setLanguages($languages);
}
return $this;
}
Thomas Seidl
committed
* Creates a new condition group to use with this query object.
*
* @param string $conjunction
Thomas Seidl
committed
* The conjunction to use for the condition group – either 'AND' or 'OR'.
* @param string[] $tags
Thomas Seidl
committed
* (optional) Tags to set on the condition group.
Thomas Seidl
committed
* @return \Drupal\search_api\Query\ConditionGroupInterface
* A condition group object that is set to use the specified conjunction.
Thomas Seidl
committed
* @see \Drupal\search_api\Query\QueryInterface::createConditionGroup()
Thomas Seidl
committed
public function createConditionGroup($conjunction = 'AND', array $tags = []) {
if (!$this->shouldAbort()) {
Thomas Seidl
committed
return $this->query->createConditionGroup($conjunction, $tags);
return NULL;
/**
* Sets the keys to search for.
*
* If this method is not called on the query before execution, this will be a
* filter-only query.
*
* @param string|array|null $keys
* A string with the search keys, in one of the formats specified by
* getKeys(). A passed string will be parsed according to the set parse
* mode. Use NULL to not use any search keys.
*
* @return $this
*
* @see \Drupal\search_api\Query\QueryInterface::keys()
*/
public function keys($keys = NULL) {
if (!$this->shouldAbort()) {
$this->query->keys($keys);
}
return $this;
}
/**
* Sets the fields that will be searched for the search keys.
*
* If this is not called, all fulltext fields will be searched.
*
* @param array $fields
* An array containing fulltext fields that should be searched.
*
* @return $this
Thomas Seidl
committed
* @throws \Drupal\search_api\SearchApiException
Alka Kumari
committed
* Thrown if one of the fields isn't of type "text".
Sorin Dediu
committed
* @see \Drupal\search_api\Query\QueryInterface::setFulltextFields()
Thomas Seidl
committed
public function setFulltextFields(array $fields = NULL) {
if (!$this->shouldAbort()) {
Sorin Dediu
committed
$this->query->setFulltextFields($fields);
}
return $this;
}
/**
Thomas Seidl
committed
* Adds a nested condition group.
*
* If $group is given, the filter is added to the relevant filter group
* instead.
Thomas Seidl
committed
* @param \Drupal\search_api\Query\ConditionGroupInterface $condition_group
* A condition group that should be added.
* @param string|null $group
* (optional) The Views query filter group to add this filter to.
*
Thomas Seidl
committed
* @see \Drupal\search_api\Query\QueryInterface::addConditionGroup()
Thomas Seidl
committed
public function addConditionGroup(ConditionGroupInterface $condition_group, $group = NULL) {
if (!$this->shouldAbort()) {
Thomas Seidl
committed
// Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all
// the default group.
if (empty($group)) {
$group = 0;
}
$this->where[$group]['condition_groups'][] = $condition_group;
}
return $this;
}
/**
* Adds a new ($field $operator $value) condition filter.
* @param string $field
Rajeshwari Variar
committed
* The ID of the field to filter on, for example "status". The special
* fields "search_api_datasource" (filter on datasource ID),
* "search_api_language" (filter on language code) and "search_api_id"
* (filter on item ID) can be used in addition to all indexed fields on the
* index.
* However, for filtering on language code, using
* \Drupal\search_api\Plugin\views\query\SearchApiQuery::setLanguages is the
* preferred method, unless a complex condition containing the language code
* is required.
* @param mixed $value
Rajeshwari Variar
committed
* The value the field should have (or be related to by the operator). If
* $operator is "IN" or "NOT IN", $value has to be an array of values. If
* $operator is "BETWEEN" or "NOT BETWEEN", it has to be an array with
* exactly two values: the lower bound in key 0 and the upper bound in key 1
* (both inclusive). Otherwise, $value must be a scalar.
* @param string $operator
* The operator to use for checking the constraint. The following operators
Rajeshwari Variar
committed
* are always supported for primitive types: "=", "<>", "<", "<=", ">=",
* ">", "IN", "NOT IN", "BETWEEN", "NOT BETWEEN". They have the same
* semantics as the corresponding SQL operators. Other operators might be
* added by backend features.
* If $field is a fulltext field, $operator can only be "=" or "<>", which
* are in this case interpreted as "contains" or "doesn't contain",
* respectively.
* If $value is NULL, $operator also can only be "=" or "<>", meaning the
* field must have no or some value, respectively.
* @param string|null $group
* (optional) The Views query filter group to add this filter to.
*
* @return $this
Thomas Seidl
committed
* @see \Drupal\search_api\Query\QueryInterface::addCondition()
Thomas Seidl
committed
public function addCondition($field, $value, $operator = '=', $group = NULL) {
if (!$this->shouldAbort()) {
Thomas Seidl
committed
// Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all
// the default group.
if (empty($group)) {
$group = 0;
}
Thomas Seidl
committed
$condition = [$field, $value, $operator];
$this->where[$group]['conditions'][] = $condition;
}
return $this;
}
Thomas Seidl
committed
/**
* Adds a simple condition to the query.
*
* This replicates the interface of Views' default SQL backend to simplify
* the Views integration of the Search API. If you are writing Search
Rajeshwari Variar
committed
* API-specific Views code, you should better use the addConditionGroup() or
* addCondition() methods.
Thomas Seidl
committed
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
*
* @param int $group
* The condition group to add these to; groups are used to create AND/OR
* sections. Groups cannot be nested. Use 0 as the default group.
* If the group does not yet exist it will be created as an AND group.
* @param string|\Drupal\Core\Database\Query\ConditionInterface|\Drupal\search_api\Query\ConditionGroupInterface $field
* The ID of the field to check; or a filter object to add to the query; or,
* for compatibility purposes, a database condition object to transform into
* a search filter object and add to the query. If a field ID is passed and
* starts with a period (.), it will be stripped.
* @param mixed $value
* (optional) The value the field should have (or be related to by the
* operator). Or NULL if an object is passed as $field.
* @param string|null $operator
* (optional) The operator to use for checking the constraint. The following
* operators are supported for primitive types: "=", "<>", "<", "<=", ">=",
* ">". They have the same semantics as the corresponding SQL operators.
* If $field is a fulltext field, $operator can only be "=" or "<>", which
* are in this case interpreted as "contains" or "doesn't contain",
* respectively.
* If $value is NULL, $operator also can only be "=" or "<>", meaning the
* field must have no or some value, respectively.
* To stay compatible with Views, "!=" is supported as an alias for "<>".
* If an object is passed as $field, $operator should be NULL.
*
* @return $this
*
* @see \Drupal\views\Plugin\views\query\Sql::addWhere()
* @see \Drupal\search_api\Plugin\views\query\SearchApiQuery::filter()
* @see \Drupal\search_api\Plugin\views\query\SearchApiQuery::condition()
*/
public function addWhere($group, $field, $value = NULL, $operator = NULL) {
if ($this->shouldAbort()) {
return $this;
}
// Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all the
// default group.
if (empty($group)) {
$group = 0;
}
if (is_object($field)) {
if ($field instanceof ConditionInterface) {
$field = $this->transformDbCondition($field);
}
if ($field instanceof ConditionGroupInterface) {
$this->where[$group]['condition_groups'][] = $field;
Thomas Seidl
committed
}
elseif (!$this->shouldAbort()) {
// We only need to abort if that wasn't done by transformDbCondition()
// already.
$this->abort('Unexpected condition passed to addWhere().');
}
}
else {
Thomas Seidl
committed
$condition = [
Joris Vercammen
committed
$this->sanitizeFieldId($field),
$value,
$this->sanitizeOperator($operator),
Thomas Seidl
committed
];
$this->where[$group]['conditions'][] = $condition;
Thomas Seidl
committed
}
return $this;
}
/**
* Retrieves the conjunction with which multiple filter groups are combined.
*
* @return string
* Either "AND" or "OR".
*/
public function getGroupOperator() {
return $this->groupOperator;
}
/**
* Returns the group type of the given group.
*
* @param int $group
* The group whose type should be retrieved.
*
* @return string
* The group type – "AND" or "OR".
*/
public function getGroupType($group) {
return !empty($this->where[$group]) ? $this->where[$group] : 'AND';
}
Thomas Seidl
committed
/**
* Transforms a database condition to an equivalent search filter.
*
* @param \Drupal\Core\Database\Query\ConditionInterface $db_condition
* The condition to transform.
*
* @return \Drupal\search_api\Query\ConditionGroupInterface|null
* A search filter equivalent to $condition, or NULL if the transformation
* failed.
*/
protected function transformDbCondition(ConditionInterface $db_condition) {
$conditions = $db_condition->conditions();
$filter = $this->query->createConditionGroup($conditions['#conjunction']);
unset($conditions['#conjunction']);
foreach ($conditions as $condition) {
if ($condition['operator'] === NULL) {
$this->abort('Trying to include a raw SQL condition in a Search API query.');