class SearchHelpSearch

Same name and namespace in other branches
  1. 11.x core/modules/search/modules/search_help/src/Plugin/Search/SearchHelpSearch.php \Drupal\search_help\Plugin\Search\SearchHelpSearch

Handles searching for help using the Search module index.

Help items are indexed if their HelpSection plugin implements \Drupal\help\HelpSearchInterface.

@internal Plugin classes are internal.

Attributes

#[Search(id: 'help_search', title: new TranslatableMarkup('Help'), use_admin_theme: TRUE)]

Hierarchy

Expanded class hierarchy of SearchHelpSearch

See also

\Drupal\help\HelpSearchInterface

\Drupal\help\HelpSectionPluginInterface

1 file declares its use of SearchHelpSearch
HelpTopicSearchTest.php in core/modules/search/modules/search_help/tests/src/Functional/HelpTopicSearchTest.php

File

core/modules/search/modules/search_help/src/Plugin/Search/SearchHelpSearch.php, line 39

Namespace

Drupal\search_help\Plugin\Search
View source
class SearchHelpSearch extends SearchPluginBase implements AccessibleInterface, SearchIndexingInterface {
  
  /**
   * The messenger.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;
  
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) : static {
    return new static($configuration, $plugin_id, $plugin_definition, $container->get('database'), $container->get('config.factory')
      ->get('search.settings'), $container->get('language_manager'), $container->get('messenger'), $container->get('current_user'), $container->get('state'), $container->get('plugin.manager.help_section'), $container->get('search.index'));
  }
  public function __construct(array $configuration, $plugin_id, $plugin_definition, protected Connection $database, protected Config $searchSettings, protected LanguageManagerInterface $languageManager, MessengerInterface $messenger, protected AccountInterface $account, protected StateInterface $state, protected HelpSectionManager $helpSectionManager, protected SearchIndexInterface $searchIndex) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->messenger = $messenger;
  }
  
  /**
   * {@inheritdoc}
   */
  public function access($operation = 'view', ?AccountInterface $account = NULL, $return_as_object = FALSE) {
    $result = AccessResult::allowedIfHasPermission($account, 'access help pages');
    return $return_as_object ? $result : $result->isAllowed();
  }
  
  /**
   * {@inheritdoc}
   */
  public function getType() : ?string {
    return $this->getPluginId();
  }
  
  /**
   * {@inheritdoc}
   */
  public function execute() : array {
    if ($this->isSearchExecutable()) {
      $results = $this->findResults();
      if ($results) {
        return $this->prepareResults($results);
      }
    }
    return [];
  }
  
  /**
   * Finds the search results.
   *
   * @return \Drupal\Core\Database\StatementInterface|null
   *   Results from search query execute() method, or NULL if the search
   *   failed.
   */
  protected function findResults() {
    // We need to check access for the current user to see the topics that
    // could be returned by search. Each entry in the help_search_items
    // database has an optional permission that comes from the HelpSection
    // plugin, in addition to the generic 'access help pages'
    // permission. In order to enforce these permissions so only topics that
    // the current user has permission to view are selected by the query, make
    // a list of the permission strings and pre-check those permissions.
    $this->addCacheContexts([
      'user.permissions',
    ]);
    if (!$this->account
      ->hasPermission('access help pages')) {
      return NULL;
    }
    try {
      $permissions = $this->database
        ->select('help_search_items', 'hsi')
        ->distinct()
        ->fields('hsi', [
        'permission',
      ])
        ->condition('permission', '', '<>')
        ->execute()
        ->fetchCol();
    } catch (\Exception) {
      // If the table doesn't exist yet, there are no search results.
      return [];
    }
    $denied_permissions = array_filter($permissions, function ($permission) {
      return !$this->account
        ->hasPermission($permission);
    });
    $query = $this->database
      ->select('search_index', 'i')
      ->condition('i.langcode', $this->languageManager
      ->getCurrentLanguage()
      ->getId())
      ->extend(SearchQuery::class)
      ->extend(PagerSelectExtender::class);
    $query->innerJoin('help_search_items', 'hsi', '[i].[sid] = [hsi].[sid] AND [i].[type] = :type', [
      ':type' => $this->getType(),
    ]);
    if ($denied_permissions) {
      $query->condition('hsi.permission', $denied_permissions, 'NOT IN');
    }
    $query->searchExpression($this->getKeywords(), $this->getType());
    $find = $query->fields('i', [
      'langcode',
    ])
      ->fields('hsi', [
      'section_plugin_id',
      'topic_id',
    ])
      ->groupBy('i.langcode')
      ->groupBy('hsi.section_plugin_id')
      ->groupBy('hsi.topic_id')
      ->limit(10)
      ->execute();
    // Check query status and set messages if needed.
    $status = $query->getStatus();
    if ($status & SearchQuery::EXPRESSIONS_IGNORED) {
      $this->messenger
        ->addWarning($this->t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', [
        '@count' => $this->searchSettings
          ->get('and_or_limit'),
      ]));
    }
    if ($status & SearchQuery::LOWER_CASE_OR) {
      $this->messenger
        ->addWarning($this->t('Search for either of the two terms with uppercase <strong>OR</strong>. For example, <strong>cats OR dogs</strong>.'));
    }
    if ($status & SearchQuery::NO_POSITIVE_KEYWORDS) {
      $this->messenger
        ->addWarning($this->formatPlural($this->searchSettings
        ->get('index.minimum_word_size'), 'You must include at least one keyword to match in the content, and punctuation is ignored.', 'You must include at least one keyword to match in the content. Keywords must be at least @count characters, and punctuation is ignored.'));
    }
    $unindexed = $this->state
      ->get('help_search_unindexed_count', 1);
    if ($unindexed) {
      $this->messenger()
        ->addWarning($this->t('Help search is not fully indexed. Some results may be missing or incorrect.'));
    }
    return $find;
  }
  
  /**
   * Prepares search results for display.
   *
   * @param \Drupal\Core\Database\StatementInterface $found
   *   Results found from a successful search query execute() method.
   *
   * @return array
   *   List of search result render arrays, with links, snippets, etc.
   */
  protected function prepareResults(StatementInterface $found) {
    $results = [];
    $plugins = [];
    $languages = [];
    $keys = $this->getKeywords();
    foreach ($found as $item) {
      $section_plugin_id = $item->section_plugin_id;
      if (!isset($plugins[$section_plugin_id])) {
        $plugins[$section_plugin_id] = $this->getSectionPlugin($section_plugin_id);
      }
      if ($plugins[$section_plugin_id]) {
        $langcode = $item->langcode;
        if (!isset($languages[$langcode])) {
          $languages[$langcode] = $this->languageManager
            ->getLanguage($item->langcode);
        }
        $topic = $plugins[$section_plugin_id]->renderTopicForSearch($item->topic_id, $languages[$langcode]);
        if ($topic) {
          if (isset($topic['cacheable_metadata'])) {
            $this->addCacheableDependency($topic['cacheable_metadata']);
          }
          $results[] = [
            'title' => $topic['title'],
            'link' => $topic['url']->toString(),
            'snippet' => search_excerpt($keys, $topic['title'] . ' ' . $topic['text'], $item->langcode),
            'langcode' => $item->langcode,
          ];
        }
      }
    }
    return $results;
  }
  
  /**
   * {@inheritdoc}
   */
  public function updateIndex() : void {
    // Update the list of items to be indexed.
    $this->updateTopicList();
    // Find some items that need to be updated. Start with ones that have
    // never been indexed.
    $limit = (int) $this->searchSettings
      ->get('index.cron_limit');
    $query = $this->database
      ->select('help_search_items', 'hsi');
    $query->fields('hsi', [
      'sid',
      'section_plugin_id',
      'topic_id',
    ]);
    $query->leftJoin('search_dataset', 'sd', '[sd].[sid] = [hsi].[sid] AND [sd].[type] = :type', [
      ':type' => $this->getType(),
    ]);
    $query->where('[sd].[sid] IS NULL');
    $query->groupBy('hsi.sid')
      ->groupBy('hsi.section_plugin_id')
      ->groupBy('hsi.topic_id')
      ->range(0, $limit);
    $items = $query->execute()
      ->fetchAll();
    // If there is still space in the indexing limit, index items that have
    // been indexed before, but are currently marked as needing a re-index.
    if (count($items) < $limit) {
      $query = $this->database
        ->select('help_search_items', 'hsi');
      $query->fields('hsi', [
        'sid',
        'section_plugin_id',
        'topic_id',
      ]);
      $query->leftJoin('search_dataset', 'sd', '[sd].[sid] = [hsi].[sid] AND [sd].[type] = :type', [
        ':type' => $this->getType(),
      ]);
      $query->condition('sd.reindex', 0, '<>');
      $query->groupBy('hsi.sid')
        ->groupBy('hsi.section_plugin_id')
        ->groupBy('hsi.topic_id')
        ->range(0, $limit - count($items));
      $items = $items + $query->execute()
        ->fetchAll();
    }
    // Index the items we have chosen, in all available languages.
    $language_list = $this->languageManager
      ->getLanguages(LanguageInterface::STATE_CONFIGURABLE);
    $section_plugins = [];
    $words = [];
    try {
      foreach ($items as $item) {
        $section_plugin_id = $item->section_plugin_id;
        if (!isset($section_plugins[$section_plugin_id])) {
          $section_plugins[$section_plugin_id] = $this->getSectionPlugin($section_plugin_id);
        }
        if (!$section_plugins[$section_plugin_id]) {
          $this->removeItemsFromIndex($item->sid);
          continue;
        }
        $section_plugin = $section_plugins[$section_plugin_id];
        $this->searchIndex
          ->clear($this->getType(), $item->sid);
        foreach ($language_list as $langcode => $language) {
          $topic = $section_plugin->renderTopicForSearch($item->topic_id, $language);
          if ($topic) {
            // Index the title plus body text.
            $text = '<h1>' . $topic['title'] . '</h1>' . "\n" . $topic['text'];
            $words += $this->searchIndex
              ->index($this->getType(), $item->sid, $langcode, $text, FALSE);
          }
        }
      }
    } finally {
      $this->searchIndex
        ->updateWordWeights($words);
      $this->updateIndexState();
    }
  }
  
  /**
   * {@inheritdoc}
   */
  public function indexClear() : void {
    $this->searchIndex
      ->clear($this->getType());
  }
  
  /**
   * Rebuilds the database table containing topics to be indexed.
   */
  public function updateTopicList() : void {
    try {
      // Start by fetching the existing list, so we can remove items not found
      // at the end.
      $old_list = $this->database
        ->select('help_search_items', 'hsi')
        ->fields('hsi', [
        'sid',
        'topic_id',
        'section_plugin_id',
        'permission',
      ])
        ->execute();
    } catch (\Exception $e) {
      if ($this->ensureTableExists()) {
        $old_list = $this->database
          ->select('help_search_items', 'hsi')
          ->fields('hsi', [
          'sid',
          'topic_id',
          'section_plugin_id',
          'permission',
        ])
          ->execute();
      }
      else {
        throw $e;
      }
    }
    $old_list_ordered = [];
    $sids_to_remove = [];
    foreach ($old_list as $item) {
      $old_list_ordered[$item->section_plugin_id][$item->topic_id] = $item;
      $sids_to_remove[$item->sid] = $item->sid;
    }
    $section_plugins = $this->helpSectionManager
      ->getDefinitions();
    foreach ($section_plugins as $section_plugin_id => $section_plugin_definition) {
      $plugin = $this->getSectionPlugin($section_plugin_id);
      if (!$plugin) {
        continue;
      }
      $permission = $section_plugin_definition['permission'] ?? '';
      foreach ($plugin->listSearchableTopics() as $topic_id) {
        if (isset($old_list_ordered[$section_plugin_id][$topic_id])) {
          $old_item = $old_list_ordered[$section_plugin_id][$topic_id];
          if ($old_item->permission == $permission) {
            // Record has not changed.
            unset($sids_to_remove[$old_item->sid]);
            continue;
          }
          // Permission has changed, update record.
          $this->database
            ->update('help_search_items')
            ->condition('sid', $old_item->sid)
            ->fields([
            'permission' => $permission,
          ])
            ->execute();
          unset($sids_to_remove[$old_item->sid]);
          continue;
        }
        // New record, create it.
        $this->database
          ->insert('help_search_items')
          ->fields([
          'section_plugin_id' => $section_plugin_id,
          'permission' => $permission,
          'topic_id' => $topic_id,
        ])
          ->execute();
      }
    }
    // Remove remaining items from the index.
    $this->removeItemsFromIndex($sids_to_remove);
  }
  
  /**
   * Updates the 'help_search_unindexed_count' state variable.
   *
   * The state variable is a count of help topics that have never been indexed.
   */
  public function updateIndexState() : void {
    $query = $this->database
      ->select('help_search_items', 'hsi');
    $query->addExpression('COUNT(DISTINCT([hsi].[sid]))');
    $query->leftJoin('search_dataset', 'sd', '[hsi].[sid] = [sd].[sid] AND [sd].[type] = :type', [
      ':type' => $this->getType(),
    ]);
    $query->isNull('sd.sid');
    $never_indexed = $query->execute()
      ->fetchField();
    $this->state
      ->set('help_search_unindexed_count', $never_indexed);
  }
  
  /**
   * {@inheritdoc}
   */
  public function markForReindex() : void {
    $this->updateTopicList();
    $this->searchIndex
      ->markForReindex($this->getType());
  }
  
  /**
   * {@inheritdoc}
   */
  public function indexStatus() : array {
    $this->updateTopicList();
    $total = $this->database
      ->select('help_search_items', 'hsi')
      ->countQuery()
      ->execute()
      ->fetchField();
    $query = $this->database
      ->select('help_search_items', 'hsi');
    $query->addExpression('COUNT(DISTINCT([hsi].[sid]))');
    $query->leftJoin('search_dataset', 'sd', '[hsi].[sid] = [sd].[sid] AND [sd].[type] = :type', [
      ':type' => $this->getType(),
    ]);
    $condition = $this->database
      ->condition('OR');
    $condition->condition('sd.reindex', 0, '<>')
      ->isNull('sd.sid');
    $query->condition($condition);
    $remaining = $query->execute()
      ->fetchField();
    return [
      'remaining' => $remaining,
      'total' => $total,
    ];
  }
  
  /**
   * Removes an item or items from the search index.
   *
   * @param int|int[] $sids
   *   Search ID (sid) of item or items to remove.
   */
  protected function removeItemsFromIndex($sids) : void {
    $sids = (array) $sids;
    // Remove items from our table in batches of 100, to avoid problems
    // with having too many placeholders in database queries.
    foreach (array_chunk($sids, 100) as $this_list) {
      $this->database
        ->delete('help_search_items')
        ->condition('sid', $this_list, 'IN')
        ->execute();
    }
    // Remove items from the search tables individually, as there is no bulk
    // function to delete items from the search index.
    foreach ($sids as $sid) {
      $this->searchIndex
        ->clear($this->getType(), $sid);
    }
  }
  
  /**
   * Instantiates a help section plugin and verifies it is searchable.
   *
   * @param string $section_plugin_id
   *   Type of plugin to instantiate.
   *
   * @return \Drupal\help\SearchableHelpInterface|false
   *   Plugin object, or FALSE if it is not searchable.
   */
  protected function getSectionPlugin($section_plugin_id) {
    /** @var \Drupal\help\HelpSectionPluginInterface $section_plugin */
    $section_plugin = $this->helpSectionManager
      ->createInstance($section_plugin_id);
    // Intentionally return boolean to allow caching of results.
    return $section_plugin instanceof SearchableHelpInterface ? $section_plugin : FALSE;
  }
  
  /**
   * Check if the help_search_items table exists and create it if not.
   */
  protected function ensureTableExists() : bool {
    try {
      $database_schema = $this->database
        ->schema();
      if (!$database_schema->tableExists('help_search_items')) {
        $schema_definition = $this->schemaDefinition();
        $database_schema->createTable('help_search_items', $schema_definition);
        return TRUE;
      }
    } catch (DatabaseException) {
      return TRUE;
    }
    return FALSE;
  }
  
  /**
   * Provides the schema definition for the help_search_items table.
   */
  protected function schemaDefinition() : array {
    return [
      'description' => 'Stores information about indexed help search items',
      'fields' => [
        'sid' => [
          'description' => 'Numeric index of this item in the search index',
          'type' => 'serial',
          'unsigned' => TRUE,
          'not null' => TRUE,
        ],
        'section_plugin_id' => [
          'description' => 'The help section the item comes from',
          'type' => 'varchar_ascii',
          'length' => 255,
          'not null' => TRUE,
          'default' => '',
        ],
        'permission' => [
          'description' => 'The permission needed to view this item',
          'type' => 'varchar_ascii',
          'length' => 255,
          'not null' => TRUE,
          'default' => '',
        ],
        'topic_id' => [
          'description' => 'The topic ID of the item',
          'type' => 'varchar_ascii',
          'length' => 255,
          'not null' => TRUE,
          'default' => '',
        ],
      ],
      'primary key' => [
        'sid',
      ],
      'indexes' => [
        'section_plugin_id' => [
          'section_plugin_id',
        ],
        'topic_id' => [
          'topic_id',
        ],
      ],
    ];
  }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title Overrides
AutowiredInstanceTrait::createInstanceAutowired public static function Instantiates a new instance of the implementing class using autowiring.
AutowiredInstanceTrait::getAutowireArguments private static function Resolves arguments for a method using autowiring.
CacheableDependencyTrait::$cacheContexts protected property Cache contexts.
CacheableDependencyTrait::$cacheMaxAge protected property Cache max-age.
CacheableDependencyTrait::$cacheTags protected property Cache tags.
CacheableDependencyTrait::getCacheContexts public function 4
CacheableDependencyTrait::getCacheMaxAge public function 4
CacheableDependencyTrait::getCacheTags public function 4
CacheableDependencyTrait::setCacheability protected function Sets cacheability; useful for value object constructors.
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 2
DependencySerializationTrait::__wakeup public function 2
MessengerTrait::messenger public function Gets the messenger. 26
MessengerTrait::setMessenger public function Sets the messenger.
PluginBase::$configuration protected property Configuration information passed into the plugin. 1
PluginBase::$pluginDefinition protected property The plugin implementation definition. 1
PluginBase::$pluginId protected property The plugin ID.
PluginBase::DERIVATIVE_SEPARATOR constant A string which is used to separate base plugin IDs from the derivative ID.
PluginBase::getBaseId public function Overrides DerivativeInspectionInterface::getBaseId
PluginBase::getDerivativeId public function Overrides DerivativeInspectionInterface::getDerivativeId
PluginBase::getPluginDefinition public function Overrides PluginInspectionInterface::getPluginDefinition 2
PluginBase::getPluginId public function Overrides PluginInspectionInterface::getPluginId
RefinableCacheableDependencyTrait::addCacheableDependency public function 1
RefinableCacheableDependencyTrait::addCacheContexts public function
RefinableCacheableDependencyTrait::addCacheTags public function
RefinableCacheableDependencyTrait::mergeCacheMaxAge public function
SearchHelpSearch::$messenger protected property The messenger. Overrides MessengerTrait::$messenger
SearchHelpSearch::access public function Overrides AccessibleInterface::access
SearchHelpSearch::create public static function Overrides PluginBase::create
SearchHelpSearch::execute public function Overrides SearchInterface::execute
SearchHelpSearch::findResults protected function Finds the search results.
SearchHelpSearch::getSectionPlugin protected function Instantiates a help section plugin and verifies it is searchable.
SearchHelpSearch::getType public function Overrides SearchPluginBase::getType
SearchHelpSearch::indexClear public function Overrides SearchIndexingInterface::indexClear
SearchHelpSearch::indexStatus public function Overrides SearchIndexingInterface::indexStatus
SearchHelpSearch::markForReindex public function Overrides SearchIndexingInterface::markForReindex
SearchHelpSearch::prepareResults protected function Prepares search results for display.
SearchHelpSearch::removeItemsFromIndex protected function Removes an item or items from the search index.
SearchHelpSearch::updateIndex public function Overrides SearchIndexingInterface::updateIndex
SearchHelpSearch::updateIndexState public function Updates the &#039;help_search_unindexed_count&#039; state variable.
SearchHelpSearch::updateTopicList public function Rebuilds the database table containing topics to be indexed.
SearchHelpSearch::__construct public function Overrides PluginBase::__construct
SearchPluginBase::$keywords protected property The keywords to use in a search.
SearchPluginBase::$searchAttributes protected property Array of attributes - usually from the request object.
SearchPluginBase::$searchParameters protected property Array of parameters from the query string from the request.
SearchPluginBase::buildResults public function Overrides SearchInterface::buildResults 1
SearchPluginBase::buildSearchUrlQuery public function Overrides SearchInterface::buildSearchUrlQuery 1
SearchPluginBase::getAttributes public function Overrides SearchInterface::getAttributes
SearchPluginBase::getHelp public function Overrides SearchInterface::getHelp 1
SearchPluginBase::getKeywords public function Overrides SearchInterface::getKeywords
SearchPluginBase::getParameters public function Overrides SearchInterface::getParameters
SearchPluginBase::isSearchExecutable public function Overrides SearchInterface::isSearchExecutable 2
SearchPluginBase::searchFormAlter public function Overrides SearchInterface::searchFormAlter 1
SearchPluginBase::setSearch public function Overrides SearchInterface::setSearch 1
SearchPluginBase::suggestedTitle public function Overrides SearchInterface::suggestedTitle
SearchPluginBase::usesAdminTheme public function Overrides SearchInterface::usesAdminTheme
StringTranslationTrait::$stringTranslation protected property The string translation service. 3
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language. 1

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.