toolshed-8.x-1.x-dev/modules/toolshed_search/src/Controller/SearchAutocomplete.php

modules/toolshed_search/src/Controller/SearchAutocomplete.php
<?php

namespace Drupal\toolshed_search\Controller;

use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Plugin\DataType\EntityAdapter;
use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Utility\Error;
use Drupal\search_api\ParseMode\ParseModePluginManager;
use Drupal\search_api\Query\QueryInterface;
use Drupal\search_api\SearchApiException;
use Drupal\toolshed_search\Plugin\views\filter\EntitySelectionFilter;
use Drupal\views\ViewEntityInterface;
use Drupal\views\ViewExecutableFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
 * The Toolshed search autocomplete route controller.
 *
 * Handles building autocomplete suggestions for Views filter handlers connected
 * to a Search API index. Allows for better fulltext autocomplete using a
 * Search API query instead of the default Drupal database handler.
 *
 * Because it uses the Search API index, ensure that the required information
 * for entity you wish to search using this is being indexed (for example the
 * "bundle", "label", etc...).
 */
class SearchAutocomplete implements ContainerInjectionInterface {

  use LoggerChannelTrait;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected EntityTypeManagerInterface $entityTypeManager;

  /**
   * The Views executable factory.
   *
   * @var \Drupal\views\ViewExecutableFactory
   */
  protected ViewExecutableFactory $viewExecFactory;

  /**
   * The Search API parse mode plugin manager for fulltext fields.
   *
   * @var \Drupal\search_api\ParseMode\ParseModePluginManager
   */
  protected ParseModePluginManager $parseModeManager;

  /**
   * Create a new instance of the SearchAutocomplete route controller.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\views\ViewExecutableFactory $view_executable_factory
   *   The Views executable factory.
   * @param \Drupal\search_api\ParseMode\ParseModePluginManager $parse_mode_manager
   *   The Search API parse mode plugin manager for fulltext fields.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, ViewExecutableFactory $view_executable_factory, ParseModePluginManager $parse_mode_manager) {
    $this->entityTypeManager = $entity_type_manager;
    $this->viewExecFactory = $view_executable_factory;
    $this->parseModeManager = $parse_mode_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('views.executable'),
      $container->get('plugin.manager.search_api.parse_mode')
    );
  }

  /**
   * Access check for a views display for autocomplete routes based from views.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The user account to check access for.
   * @param \Drupal\views\ViewEntityInterface $view
   *   The Views configuration entity to base the entity autocomplete values.
   * @param string $display
   *   The ID of the views display being utilized for this autocomplete route.
   *
   * @return \Drupal\Core\Access\AccessResultInterface
   *   The access results for the view display.
   */
  public function viewAccess(AccountInterface $account, ViewEntityInterface $view, string $display): AccessResultInterface {
    $viewExec = $this->viewExecFactory->get($view);

    if ($viewExec->setDisplay($display)) {
      $displayHandler = $viewExec->display_handler;
      $access = AccessResult::allowedIf($displayHandler->access($account))
        ->addCacheableDependency($displayHandler->getCacheMetadata());
    }
    else {
      $access = AccessResult::forbidden()
        ->addCacheTags($viewExec->getCacheTags());
    }

    $access->addCacheContexts(['route']);
    return $access;
  }

  /**
   * The route controller for building a View autocomplete entity filter.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current HTTP request.
   * @param \Drupal\views\ViewEntityInterface $view
   *   The View configuration entity that owns the filter the suggestions are
   *   being create for.
   * @param string $display
   *   The View display the contains the filter the suggestions are for.
   * @param string $filter_id
   *   The view filter the suggestions are being built for.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response which is compatible for the Toolshed autocomplete widget
   *   and can be used for the autocomplete suggestion values.
   */
  public function entityAutocomplete(Request $request, ViewEntityInterface $view, string $display, string $filter_id): JsonResponse {
    try {
      // Setup response caching.
      $cacheMeta = new CacheableMetadata();
      $cacheMeta->addCacheContexts([
        'route',
        'url.query_args:q',
      ]);

      $suggestions = [];
      $keys = $request->query->get('q');
      $viewExec = $this->viewExecFactory->get($view);

      if ($keys && $viewExec->setDisplay($display)) {
        $displayHandler = $viewExec->display_handler;
        $filter = $displayHandler->getHandler('filter', $filter_id);

        if (!$filter instanceof EntitySelectionFilter || !($settings = $filter->getAutocompleteSettings())) {
          $errorMsg = sprintf('Invalid filter %s for View: "%s" is missing or not a valid entity selection handler.', $filter_id, $view->id());
          throw new \InvalidArgumentException($errorMsg);
        }

        $fulltextOp = $settings['fulltext_op'] ?? 'AND';
        $query = $this->createQuery($view)
          ->setFulltextFields($settings['fulltext_fields'])
          ->range(0, $settings['limit']);

        // If entity results should be limited to a set of allowed bundles.
        $query->addCondition('search_api_datasource', "entity:{$settings['entity_type']}");
        $this->applyBundleFilter($query, $settings['entity_type'], $settings['bundles']);

        try {
          if ($settings['parse_mode']) {
            $parseMode = $this->parseModeManager->createInstance($settings['parse_mode']);
            $parseMode->setConjunction('AND' === $fulltextOp ? 'AND' : 'OR');
            $query->setParseMode($parseMode);
          }
        }
        catch (PluginNotFoundException $e) {
          // Parse mode is not available, let Search API use the default.
        }

        // Set keys after the parse mode is set.
        $query->keys($keys);
        if ('not' === $fulltextOp) {
          $tkeys = &$query->getKeys();
          if (is_array($tkeys)) {
            $tkeys['#negation'] = TRUE;
          }
        }

        $results = [];
        // Group multiple value with the same label together.
        // @todo make this an option?
        foreach ($query->execute()->getResultItems() as $item) {
          $entity = $item->getOriginalObject();

          if ($entity instanceof EntityAdapter) {
            $entity = $entity->getEntity();
          }

          if ($entity instanceof ContentEntityInterface) {
            $key = strtolower($entity->label());
            $results[$key]['label'] = $entity->label();
            $results[$key]['id'][] = $entity->id();
          }
        }

        $suggestions = [];
        foreach ($results as $info) {
          $suggestions[] = [
            'text' => $info['label'],
            'value' => 'id:' . implode(',', $info['id']),
          ];
        }

        // Cache these results no longer than 15 minutes so we don't accumulate
        // large numbers of cached queries that linger too long.
        $cacheMeta
          ->setCacheMaxAge(900)
          ->addCacheableDependency($query->getIndex())
          ->addCacheableDependency($displayHandler->getCacheMetadata());
      }
      else {
        // Rebuild response in case view is updated and the required display
        // handler gets added or altered.
        $cacheMeta->addCacheTags($view->getCacheTags());
      }

      $response = new CacheableJsonResponse(['list' => $suggestions]);
      $response->addCacheableDependency($cacheMeta);
      return $response;
    }
    catch (\InvalidArgumentException $e) {
      throw new NotFoundHttpException($e->getMessage(), $e);
    }
    catch (SearchApiException $e) {
      Error::logException($this->getLogger('toolshed_search'), $e);
      throw new BadRequestHttpException('Unable to query required services.', $e);
    }
  }

  /**
   * Create a Search API query from the Views information.
   *
   * @param \Drupal\views\ViewEntityInterface $view
   *   The view configuration entity to build the search query from.
   * @param array $filters
   *   Additional filter criteria to apply.
   *
   * @return \Drupal\search_api\Query\QueryInterface
   *   The query compatible with this Search API index from the View.
   *
   * @throws \Drupal\search_api\SearchApiException
   *   When a query instance can't be created for the view.
   */
  protected function createQuery(ViewEntityInterface $view, array $filters = []): QueryInterface {
    $baseTable = $view->get('base_table') ?: '';

    if (preg_match('/^search_api_index_([a-zA-Z_]+)$/', $baseTable, $matches)) {
      /** @var \Drupal\search_api\IndexInterface $index */
      $index = $this->entityTypeManager
        ->getStorage('search_api_index')
        ->load($matches[1]);

      return $index->query()
        ->addTag('entity_autocomplete')
        ->sort('search_api_relevance', QueryInterface::SORT_DESC);
    }

    $viewLabel = $view->label() ?: $view->id();
    throw new SearchApiException('Unable to create search query for view: ' . $viewLabel);
  }

  /**
   * Apply search filters to ensure results from specific entity bundles.
   *
   * @param \Drupal\search_api\Query\QueryInterface $query
   *   The query to add the bundle filters to.
   * @param string $entity_type_id
   *   The target entity type ID that the bundle filter is for.
   * @param array $bundles
   *   The bundle machine name IDs to filter to.
   *
   * @throws \Drupal\search_api\SearchApiException
   *   If an entity bundle filter field is not available.
   */
  protected function applyBundleFilter(QueryInterface $query, string $entity_type_id, array $bundles): void {
    if (empty($bundles)) {
      return;
    }

    $index = $query->getIndex();
    $datasourceId = 'entity:' . $entity_type_id;
    $bundleKey = $this->entityTypeManager
      ->getDefinition($entity_type_id)
      ->getKey('bundle');

    foreach ($index->getFields() as $field) {
      if (NULL === $field->getDatasourceId() && 'entity_bundle' === $field->getPropertyPath()) {
        // Callback function to prepend the entity type to the bundles.
        // Makes entity bundle values compatible with this type of index field.
        array_walk($bundles, function (&$bundle, $key, $prefix) {
          $bundle = $prefix . ':' . $bundle;
        }, $entity_type_id);

        $query->addCondition($field->getFieldIdentifier(), $bundles, 'IN');
        return;
      }
      elseif ($datasourceId === $field->getDatasourceId() && $bundleKey === $field->getPropertyPath()) {
        $query->addCondition($field->getFieldIdentifier(), $bundles, 'IN');
        return;
      }
    }

    $this
      ->getLogger('toolshed_search.entity_autocomplete')
      ->warning('Unable to find entity bundle field to filter @entity_type in the @index index.', [
        '@entity_type' => $entity_type_id,
        '@index' => $index->label(),
      ]);
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc