Skip to content
SearchApiQuery.php 44.1 KiB
Newer Older
<?php

namespace Drupal\search_api\Plugin\views\query;

use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Database\Query\ConditionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\search_api\Entity\Index;
use Drupal\search_api\ParseMode\ParseModeInterface;
use Drupal\search_api\Plugin\views\field\SearchApiStandard;
use Drupal\search_api\Processor\ConfigurablePropertyInterface;
use Drupal\search_api\Query\ConditionGroupInterface;
use Drupal\search_api\SearchApiException;
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 {

  /**
   * Number of results to display.
   *
   * @var int
   */
  protected $limit;

  /**
   * Offset of first displayed result.
   *
   * @var int
   */
  protected $offset;

  /**
   * The index this view accesses.
   *
   */
  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
   */

  /**
   * Whether to abort the search instead of executing it.
   *
   * @var bool
   */
  protected $abort = FALSE;

  /**
   * 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.
   * The query's conditions representing the different Views filter groups.

  /**
   * The conjunction with which multiple filter groups are combined.
   *
   * @var string
   */
  /**
   * 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);

    $plugin->setModuleHandler($container->get('module_handler'));
    $plugin->setLogger($container->get('logger.channel.search_api'));
  /**
   * Loads the search index belonging to the given Views base table.
   *
   * @param string $table
   *   The Views base table ID.
   * @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.
   */
  public static function getIndexFromTable($table, EntityTypeManagerInterface $entity_type_manager = NULL) {
    // @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);
        return $entity_type_manager->getStorage('search_api_index')
          ->load($index_id);
  /**
   * 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);
  }

  /**
   * 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;
  }

   */
  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'));
        $this->abort(new FormattableMarkup('View %view is not based on Search API but tries to use its query plugin.', ['%view' => $view->storage->label()]));
      $this->retrievedProperties = array_fill_keys($this->index->getDatasourceIds(), []);
      $this->retrievedProperties[NULL] = [];
      $this->query = $this->index->query();
      $this->query->addTag('views');
      $this->query->addTag('views_' . $view->id());
      $display_type = $display->getPluginId();
      if ($display_type === 'rest_export') {
        $display_type = 'rest';
      }
      $this->query->setSearchId("views_$display_type:" . $view->id() . '__' . $view->current_display);
      $this->query->setOption('search_api_view', $view);
      $this->abort($e->getMessage());
   * 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.
   *
   * @param string $combined_property_path
   *   The combined property path of the property that should be retrieved.
  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;
  /**
   * 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()
   */
  public function addField($table, $field, $alias = '', array $params = []) {
    $this->addRetrievedProperty($field);
    return $field;
  }

   */
  public function defineOptions() {
    return parent::defineOptions() + [
      'bypass_access' => [
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
    parent::buildOptionsForm($form, $form_state);

      '#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,
      '#title' => $this->t('Bypass access checks'),
      '#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'],
    $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;
   */
  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.
    // Setup the nested filter structure for this query.
      // 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.
        $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) {
        if (!empty($group['conditions']) || !empty($group['condition_groups'])) {
          // For filters without a group, we want to always add them directly to
          // the query.
          $conditions = ($group_id === '') ? $this->query : $this->query->createConditionGroup($group['type']);
          if (!empty($group['conditions'])) {
            foreach ($group['conditions'] as $condition) {
              list($field, $value, $operator) = $condition;
              $conditions->addCondition($field, $value, $operator);
          if (!empty($group['condition_groups'])) {
            foreach ($group['condition_groups'] as $nested_conditions) {
              $conditions->addConditionGroup($nested_conditions);
            }
          }
          // If no group was given, the filters were already set on the query.
          if ($group_id !== '') {
          }
        }
      }
    }

    // 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);

    // 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) {
    $this->getModuleHandler()->invokeAll('views_query_alter', [$view, $this]);
Thomas Seidl's avatar
Thomas Seidl committed
   * {@inheritdoc}
   */
  public function execute(ViewExecutable $view) {
      if (error_displayable()) {
        foreach ($this->errors as $msg) {
      $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;
      }
      // 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) {
        $view->pager->total_items = $results->getResultCount();
        if (!empty($view->pager->options['offset'])) {
          $view->pager->total_items -= $view->pager->options['offset'];
        $view->total_rows = $view->pager->total_items;
Thomas Seidl's avatar
Thomas Seidl committed
      if ($results->getResultItems()) {
        $this->addResults($results->getResultItems(), $view);
      }
      $view->execute_time = microtime(TRUE) - $start;

      // Trigger pager postExecute().
      $view->pager->postExecute($view->result);
      $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.
   *
   * @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;
    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();
Thomas Seidl's avatar
Thomas Seidl committed
   * 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.
Thomas Seidl's avatar
Thomas Seidl committed
  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.
      $account = $this->getAccessAccount();
      foreach ($results as $item_id => $result) {
        if (!$result->checkAccess($account)) {
          unset($results[$item_id]);
Thomas Seidl's avatar
Thomas Seidl committed
    foreach ($results as $item_id => $result) {
      $values['_item'] = $result;
      $object = $result->getOriginalObject(FALSE);
      if ($object) {
        $values['_object'] = $object;
        $values['_relationship_objects'][NULL] = [$object];
Thomas Seidl's avatar
Thomas Seidl committed
      $values['search_api_id'] = $item_id;
      $values['search_api_datasource'] = $result->getDatasourceId();
      $values['search_api_language'] = $result->getLanguage();
Thomas Seidl's avatar
Thomas Seidl committed
      $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's avatar
Thomas Seidl committed
        if ($field->getValues()) {
          $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.
          }
Thomas Seidl's avatar
Thomas Seidl committed
        }
      $view->result[] = new ResultRow($values);
Thomas Seidl's avatar
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);
   * 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
   */
  public function setSearchApiQuery(QueryInterface $query) {
   * 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() {
  /**
   * {@inheritdoc}
   */
  public function calculateDependencies() {
    $dependencies = parent::calculateDependencies();
    $dependencies[$this->index->getConfigDependencyKey()][] = $this->index->getConfigDependencyName();
    return $dependencies;
  }

  //
  // Query interface methods (proxy to $this->query)
  //

   * @return \Drupal\search_api\ParseMode\ParseModeInterface|null
   *   The parse mode, or NULL if the query was aborted.
   *
   * @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()
   */
  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;
  }

   * Creates a new condition group to use with this query object.
   *   The conjunction to use for the condition group – either 'AND' or 'OR'.
   *   (optional) Tags to set on the condition group.
   * @return \Drupal\search_api\Query\ConditionGroupInterface
   *   A condition group object that is set to use the specified conjunction.
   * @see \Drupal\search_api\Query\QueryInterface::createConditionGroup()
  public function createConditionGroup($conjunction = 'AND', array $tags = []) {
    if (!$this->shouldAbort()) {
      return $this->query->createConditionGroup($conjunction, $tags);
  /**
   * 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.
   *
   *
   * @see \Drupal\search_api\Query\QueryInterface::keys()
   */
  public function keys($keys = NULL) {
    if (!$this->shouldAbort()) {
  /**
   * 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.
   *
   * @throws \Drupal\search_api\SearchApiException
   *   Thrown if one of the fields isn't of type "text".
   * @see \Drupal\search_api\Query\QueryInterface::setFulltextFields()
  public function setFulltextFields(array $fields = NULL) {
    if (!$this->shouldAbort()) {
   *
   * If $group is given, the filter is added to the relevant filter group
   * instead.
   * @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's avatar
Thomas Seidl committed
   * @return $this
   * @see \Drupal\search_api\Query\QueryInterface::addConditionGroup()
  public function addConditionGroup(ConditionGroupInterface $condition_group, $group = NULL) {
    if (!$this->shouldAbort()) {
      // 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;
   * Adds a new ($field $operator $value) condition filter.
   *   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.
   *   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
   *   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.
   *
   * @see \Drupal\search_api\Query\QueryInterface::addCondition()
  public function addCondition($field, $value, $operator = '=', $group = NULL) {
    if (!$this->shouldAbort()) {
      // 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]['conditions'][] = $condition;
  /**
   * 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
   * API-specific Views code, you should better use the addConditionGroup() or
   * addCondition() methods.
   *
   * @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;
      }
      elseif (!$this->shouldAbort()) {
        // We only need to abort  if that wasn't done by transformDbCondition()
        // already.
        $this->abort('Unexpected condition passed to addWhere().');
      }
    }
    else {
        $this->sanitizeFieldId($field),
        $value,
        $this->sanitizeOperator($operator),
      $this->where[$group]['conditions'][] = $condition;
  /**
   * 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';
  }

  /**
   * 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.');