elasticsearch_search_api-1.0.x-dev/src/Controller/SearchController.php

src/Controller/SearchController.php
<?php

namespace Drupal\elasticsearch_search_api\Controller;

use Drupal\elasticsearch_search_api\Search\ElasticSearchParamsBuilder;
use Drupal\elasticsearch_search_api\Search\ElasticSearchResultParser;
use Drupal\elasticsearch_search_api\Search\FacetedKeywordSearchAction;
use Drupal\elasticsearch_search_api\Search\FacetedSearchActionInterface;
use Drupal\elasticsearch_search_api\Search\SearchActionFactory;
use Drupal\elasticsearch_search_api\Search\SearchRepository;
use Drupal\elasticsearch_search_api\Search\SearchResult;
use Drupal\elasticsearch_search_api\Search\Suggest\SuggesterInterface;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AppendCommand;
use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Ajax\RemoveCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\CurrentRouteMatch;
use Drupal\Core\Url;
use Elasticsearch\Common\Exceptions\ElasticsearchException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

/**
 * Controller to handle the search.
 */
class SearchController extends ControllerBase {

  /**
   * The renderer.
   *
   * @var \Drupal\Core\Render\\RendererInterface
   */
  protected $renderer;

  /**
   * Route match.
   *
   * @var \Drupal\Core\Routing\RouteMatch
   */
  protected $routeMatch;

  /**
   * The Elasticsearch client.
   *
   * @var \nodespark\DESConnector\ClientInterface
   */
  protected $client;

  /**
   * The search action factory.
   *
   * @var \Drupal\elasticsearch_search_api\Search\SearchActionFactory
   */
  protected $searchActionFactory;

  /**
   * The search parameters builder.
   *
   * @var \Drupal\elasticsearch_search_api\Search\ElasticSearchParamsBuilder
   */
  protected $searchParamsBuilder;

  /**
   * The result parser.
   *
   * @var \Drupal\elasticsearch_search_api\Search\ElasticSearchResultParser
   */
  protected $resultParser;

  /**
   * The suggester.
   *
   * @var \Drupal\elasticsearch_search_api\Search\Suggest\SuggesterInterface
   */
  protected $suggester;

  /**
   * Facets.
   *
   * @var array
   */
  protected $facets;

  /**
   * Breadcrumb manager.
   *
   * @var \Drupal\Core\Breadcrumb\BreadcrumbManager
   */
  protected $breadCrumbManager;

  /**
   * SearchRepository.
   *
   * @var \Drupal\elasticsearch_search_api\Search\SearchRepository
   */
  protected $searchRepository;

  /**
   * FacetedSearchActiveFiltersBuilder service.
   *
   * @var \Drupal\elasticsearch_search_api\Search\FacetedSearchActiveFiltersBuilder
   */
  protected $activeFiltersBuilder;

  /**
   * The current path.
   *
   * @var \Drupal\Core\Path\CurrentPathStack
   */
  protected $currentPath;

  /**
   * NodeViewBuilder.
   *
   * @var \Drupal\node\NodeViewBuilder
   */
  protected $nodeViewBuilder;

  /**
   * SearchController constructor.
   */
  public function __construct(
    RendererInterface $renderer,
    CurrentRouteMatch $routeMatch,
    SearchActionFactory $searchActionFactory,
    ElasticSearchParamsBuilder $searchParamsBuilder,
    ElasticSearchResultParser $resultParser,
    SuggesterInterface $suggester,
    SearchRepository $searchRepository,
    EntityTypeManagerInterface $entityTypeManager
  ) {
    $this->renderer = $renderer;
    $this->routeMatch = $routeMatch;
    $this->searchActionFactory = $searchActionFactory;
    $this->searchParamsBuilder = $searchParamsBuilder;
    $this->resultParser = $resultParser;
    $this->suggester = $suggester;
    $this->searchRepository = $searchRepository;
    $this->nodeViewBuilder = $entityTypeManager->getViewBuilder('node');

    $this->facets = $this->getFacets();
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('renderer'),
      $container->get('current_route_match'),
      $container->get('elasticsearch_search_api.search_action_factory'),
      $container->get('elasticsearch_search_api.elasticsearch_params_builder'),
      $container->get('elasticsearch_search_api.elasticsearch_result_parser'),
      $container->get('elasticsearch_search_api.suggest.title_suggester'),
      $container->get('elasticsearch_search_api.search_repository'),
      $container->get('entity_type.manager')
    );
  }

  /**
   * Search for content.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The HTTP request.
   *
   * @return array
   *   A render array.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
   *   When the HTTP request does not seem to be correct.
   */
  public function search(Request $request) {
    $query = $request->query;

    try {
      $searchAction = $this->searchActionFactory->searchActionFromQuery($query, $this->facets, $request->isXmlHttpRequest());
    }
    catch (\Exception $e) {
      throw new AccessDeniedHttpException();
    }

    $drupalSettings = [
      'elasticsearch_search_api' => [
        'ajaxify' => [
          'filter_url' => $this->getFilterUrl()->toString(),
          'facets' => $this->facets,
        ],
        'retainFilter' => $this->shouldRetainFilter(),
      ],
    ];

    $render_base = [
      '#theme' => 'elasticsearch_search_api_search',
      '#header' => $this->getSearchHeader(),
      '#facets' => [],
      '#results' => [],
      '#result_count' => NULL,
      '#did_you_mean' => NULL,
      '#cache' => [
        'tags' => [
          'esa.search',
        ],
      ],
      '#attached' => [
        'library' => [
          'elasticsearch_search_api/ajaxify',
          'elasticsearch_search_api/loading-overlay',
          'elasticsearch_search_api/styles',
        ],
        'drupalSettings' => $drupalSettings,
      ],
    ];

    try {
      $result = $this->parsedResult($searchAction);
    }
    catch (ElasticsearchException $e) {
      $render_base['#results'] = $this->getErrorMessage();
      return $render_base;
    }

    $hits = $this->renderHits($searchAction, $result, $query);

    // Requested another page in the result set.
    if ($request->isXmlHttpRequest() && !((bool) $request->get('ajax_form'))) {
      return $hits + ['#type' => 'container'];
    }

    $render = [
      '#header' => $this->getSearchHeader(),
      '#facets' => $this->renderFacets($searchAction, $result),
      '#results' => $hits,
      '#result_count' => $this->getResultCount($result),
      '#did_you_mean' => $this->getSuggestions($query),
    ];

    return array_merge($render_base, $render);
  }

  /**
   * Autocomplete callback for search suggestions.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The HTTP request.
   *
   * @return \Symfony\Component\HttpFoundation\Response
   *   A HTTP response.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
   *   When the HTTP request does not seem to be correct.
   */
  public function handleAutocomplete(Request $request) {
    $searchQuery = $request->get('q');
    $results = [];

    $params = [
      'body' => [
        '_source' => 'title',
        'suggest' => [
          'search-suggest' => [
            'prefix' => $searchQuery,
            'completion' => [
              'field' => 'search_suggest',
              'size' => 10,
            ],
          ],
        ],
      ],
    ];

    $response = $this->searchRepository->query($params);

    $data = [];
    foreach ($response->getRawResponse()['suggest']['search-suggest'][0]['options'] as $suggestion) {
      $value = $suggestion['_source']['title'][0];
      $data[$value] = $value;
    }

    foreach ($data as $value => $label) {
      $results[] = [
        'value' => $value,
        'label' => $this->highlight($searchQuery, $label),
      ];
    }

    $build = [
      '#theme' => 'elasticsearch_search_api_autocomplete',
      '#results' => $results,
    ];

    if ($request->get('t')) {
      $build['#layout_wide'] = FALSE;
    }

    return new Response($this->renderer->render($build));
  }

  /**
   * Highlight the search term in the target string.
   *
   * @param string $term
   *   The term that needs to be replaced.
   * @param string $target
   *   The target.
   *
   * @return string
   *   The replaced search term.
   */
  protected function highlight(string $term, string $target) {
    return preg_replace('/(' . preg_quote($term) . ')/i', "<strong>$1</strong>", $target);
  }

  /**
   * Ajax callback to update search results, facets and active filters.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The HTTP request.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The ajax response, containing commands to update elements on the page.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
   *   When the HTTP request does not seem to be correct.
   */
  public function filter(Request $request) {
    $query = $request->request;

    try {
      $searchAction = $this->searchActionFactory->searchActionFromQuery($query, $this->facets, TRUE);
    }
    catch (\Exception $e) {
      throw new AccessDeniedHttpException();
    }

    $response = new AjaxResponse();
    try {
      $searchResult = $this->parsedResult($searchAction);
    }
    catch (ElasticsearchException $e) {
      $error = $this->getErrorMessage();

      // Replace search hits with error.
      $response->addCommand(new RemoveCommand('.esa-results-wrapper .results-wrapper > *'));
      $response->addCommand(new AppendCommand('.esa-results-wrapper .results-wrapper', $this->renderer->render($error)));

      return $response;
    }

    $facets = $this->renderFacets($searchAction, $searchResult);
    $hits = $this->renderHits($searchAction, $searchResult, $query);

    $facets = [
      '#type' => 'container',
      '#attributes' => ['class' => ['facets']],
      'facets' => $facets,
      '#attached' => [
        'drupalSettings' => [
          'elasticsearch_search_api' => [
            'ajaxify' => [
              'facets' => $this->facets,
            ],
          ],
        ],
      ],
    ];

    // Replace suggestions.
    $response->addCommand(new RemoveCommand('.esa-results-wrapper .did-you-mean'));
    $suggestions = $this->getSuggestions($query);
    if (!empty($suggestions)) {
      $response->addCommand(new PrependCommand('.esa-results-wrapper .suggestion-wrapper', $this->renderer->render($suggestions)));
    }

    // Replace results count.
    $response->addCommand(new RemoveCommand('.esa-results-wrapper .result-count'));
    $result_count = $this->getResultCount($searchResult);
    if (!empty($result_count)) {
      $response->addCommand(new PrependCommand('.esa-results-wrapper .results-count-wrapper', $this->renderer->render($result_count)));
    }

    // Replace facets.
    $response->addCommand(new ReplaceCommand('.facets', $this->renderer->render($facets)));

    // Replace search hits.
    $response->addCommand(new RemoveCommand('.esa-results-wrapper .results-wrapper > *'));
    $response->addCommand(new AppendCommand('.esa-results-wrapper .results-wrapper', $this->renderer->render($hits)));

    return $response;
  }

  /**
   * Create a render array from facets.
   *
   * @param \Drupal\elasticsearch_search_api\Search\FacetedSearchActionInterface $searchAction
   *   The current search action.
   * @param \Drupal\elasticsearch_search_api\Search\SearchResult $result
   *   SearchResult object parsed from the SearchAction.
   *
   * @return array
   *   Render array of facets.
   */
  protected function renderFacets(FacetedSearchActionInterface $searchAction, SearchResult $result) {
    // Facets as lists of checkboxes.
    $facets = array_map(
      function ($facet) use ($searchAction, $result) {
        // This \Drupal call is hard to avoid as facets are added
        // semi-dynamically.
        // @codingStandardsIgnoreLine
        $renderedFacet = \Drupal::service('elasticsearch_search_api.facet_control.' . $facet)->build($facet, $searchAction, $result);
        if (!empty($renderedFacet)) {
          return $renderedFacet;
        }

        return FALSE;
      },
      $searchAction->getAvailableFacets()
    );

    return array_filter($facets);
  }

  /**
   * Render search results.
   *
   * @param \Drupal\elasticsearch_search_api\Search\FacetedSearchActionInterface $searchAction
   *   The current search action.
   * @param \Drupal\elasticsearch_search_api\Search\SearchResult $result
   *   SearchResult.
   * @param \Symfony\Component\HttpFoundation\ParameterBag $query
   *   Query parameters.
   * @param string $view_mode
   *   The view mode to render search results in. Defaults to 'search_index'.
   *
   * @return array
   *   Render array of search results.
   */
  protected function renderHits(FacetedSearchActionInterface $searchAction, SearchResult $result, ParameterBag $query, $view_mode = 'search_index') {
    $hits = $this->searchRepository->getItemValueFromHits($result->getHits());
    $hits = $this->nodeViewBuilder->viewMultiple($hits, $view_mode);

    $start = $searchAction->getFrom() + 1;
    $end = $searchAction->getSize() + $searchAction->getFrom();
    $total = $result->getTotal();

    if ($end > $total) {
      $end = $total;
    }

    if ($total > $searchAction->getSize()) {
      $hits['summary'] = [
        '#type' => 'markup',
        '#prefix' => '<div class="pager-summary">',
        '#markup' => $this->t('<strong>@start - @end</strong> of @total results', [
          '@start' => $start,
          '@end' => $end,
          '@total' => $total,
        ]),
        '#suffix' => '</div>',
      ];

      $hits['more'] = $this->renderPager($query, $total, $searchAction->getSize());
    }

    return $hits;
  }

  /**
   * Get a render array representing the pager.
   *
   * @param \Symfony\Component\HttpFoundation\ParameterBag $query
   *   Parameter bag.
   * @param int $total
   *   Total amount of results.
   * @param int $size
   *   Size of the result set.
   *
   * @return array
   *   Pager render array.
   */
  protected function renderPager(ParameterBag $query, int $total, int $size) {
    // Tell drupal about a pager being rendered. This
    // is necessary to make pager links work correctly.
    \Drupal::service('pager.manager')->createPager($total, $size);

    return [
      '#theme' => 'esa_pager',
      '#tags' => [
        1 => 'Previous',
        3 => 'Next',
      ],
      '#element' => 0,
      '#parameters' => $query->all(),
      '#total_items' => $total,
      '#items_per_page' => $size,
      '#route_name' => $this->getSearchUrl()->getRouteName(),
      '#route_params' => $this->getSearchUrl()->getRouteParameters(),
    ];
  }

  /**
   * Parses a SearchAction into a SearchResult object.
   *
   * @param \Drupal\elasticsearch_search_api\Search\FacetedKeywordSearchAction $searchAction
   *   The current search action.
   *
   * @return \Drupal\elasticsearch_search_api\Search\SearchResult
   *   SearchResult object parsed from the SearchAction
   */
  protected function parsedResult(FacetedKeywordSearchAction $searchAction) {
    $params = $this->searchParamsBuilder->build($searchAction);
    $response = $this->searchRepository->query($params);

    return $this->resultParser->parse($searchAction, $response->getRawResponse());
  }

  /**
   * Get a list of suggestions.
   *
   * @param \Symfony\Component\HttpFoundation\ParameterBag $query
   *   Query.
   *
   * @return array
   *   List of suggestions
   */
  protected function getSuggestions(ParameterBag $query) {
    $did_you_mean = [];
    $keyword = $query->get('keyword');
    if (isset($keyword)) {
      if (empty(trim($keyword))) {
        return [];
      }

      try {
        $suggestions = $this->suggester->suggest($keyword);
      } catch (ElasticsearchException $e) {
        return [];
      }

      if (empty($suggestions)) {
        return [];
      }

      foreach ($suggestions as $suggestion) {
        $search_url = $this->getSearchUrl();
        $did_you_mean[] = [
          '#type' => 'link',
          '#url' => $search_url->setOption('query', ['keyword' => $suggestion]),
          '#title' => $suggestion,
        ];
      }
    }

    return [
      '#theme' => 'elasticsearch_search_api_suggestions',
      '#did_you_mean_label' => $this->t('Did you mean'),
      '#suggestions' => $did_you_mean,
    ];
  }

  /**
   * Get the total count of a search result.
   *
   * @return array
   *   Theming array of total result count.
   */
  public function getResultCount(SearchResult $searchResult) {
    return [
      '#theme' => 'elasticsearch_search_api_result_count',
      '#result_count' => $searchResult->getTotal(),
    ];
  }

  /**
   * Get a message to show to the client in case of an exception.
   *
   * @return array
   *   Render array to print the error message.
   */
  protected function getErrorMessage() {
    return [
      '#theme' => 'elasticsearch_search_api_error',
      '#error_message' => $this->t("Search results could not be retrieved."),
    ];
  }

  /**
   * Get a header to show above the search.
   *
   * @return array|null
   *   Render array to print the header, or NULL if no header.
   */
  protected function getSearchHeader() {
    return NULL;
  }

  /**
   * Define whether the facets should reset after searching for a new keyword.
   *
   * @return bool
   *   FALSE if the filter should be retained, TRUE if not.
   */
  protected function shouldRetainFilter() {
    return FALSE;
  }

  /**
   * Get facet machine names.
   *
   * This function must be used to assign facets
   * in all classes that inherit this base class.
   */
  protected function getFacets() {
    return [];
  }

  /**
   * Get the search url.
   *
   * This function must be used to assign route info in all classes that inherit
   * this base class, since the route name / params
   * will differ for each search instance.
   *
   * @return \Drupal\Core\Url
   *   The url object.
   */
  protected function getSearchUrl(): Url {
    return Url::fromRoute('elasticsearch_search_api.search');
  }

  /**
   * Get the ajax filter search url.
   *
   * This function must be used to assign route info in all classes
   * that inherit this base class, since the route name / params
   * will differ for each search instance.
   *
   * @return \Drupal\Core\Url
   *   The url object.
   */
  protected function getFilterUrl(): Url {
    return Url::fromRoute('elasticsearch_search_api.filter');
  }

}

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

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