You are here

DefaultFacetManager.php in Facets 8

File

src/FacetManager/DefaultFacetManager.php
View source
<?php

namespace Drupal\facets\FacetManager;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\facets\Exception\InvalidProcessorException;
use Drupal\facets\FacetInterface;
use Drupal\facets\FacetSource\FacetSourcePluginManager;
use Drupal\facets\Processor\BuildProcessorInterface;
use Drupal\facets\Processor\PostQueryProcessorInterface;
use Drupal\facets\Processor\PreQueryProcessorInterface;
use Drupal\facets\Processor\ProcessorInterface;
use Drupal\facets\Processor\ProcessorPluginManager;
use Drupal\facets\QueryType\QueryTypePluginManager;

/**
 * The facet manager.
 *
 * The manager is responsible for interactions with the Search backend, such as
 * altering the query, it is also responsible for executing and building the
 * facet. It is also responsible for running the processors.
 */
class DefaultFacetManager {
  use StringTranslationTrait;

  /**
   * The query type plugin manager.
   *
   * @var \Drupal\facets\QueryType\QueryTypePluginManager
   *   The query type plugin manager.
   */
  protected $queryTypePluginManager;

  /**
   * The facet source plugin manager.
   *
   * @var \Drupal\facets\FacetSource\FacetSourcePluginManager
   */
  protected $facetSourcePluginManager;

  /**
   * The processor plugin manager.
   *
   * @var \Drupal\facets\Processor\ProcessorPluginManager
   */
  protected $processorPluginManager;

  /**
   * An array of facets that are being rendered.
   *
   * @var \Drupal\facets\FacetInterface[]
   *
   * @see \Drupal\facets\FacetInterface
   * @see \Drupal\facets\Entity\Facet
   */
  protected $facets = [];

  /**
   * An array of all entity ids in the active resultset which are a child.
   *
   * @var string[]
   */
  protected $childIds = [];

  /**
   * The entity storage for facets.
   *
   * @var \Drupal\Core\Entity\EntityStorageInterface|object
   */
  protected $facetStorage;

  /**
   * A static cache of already processed facets.
   *
   * @var \Drupal\facets\FacetInterface[]
   */
  protected $processedFacets;

  /**
   * Constructs a new instance of the DefaultFacetManager.
   *
   * @param \Drupal\facets\QueryType\QueryTypePluginManager $query_type_plugin_manager
   *   The query type plugin manager.
   * @param \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_manager
   *   The facet source plugin manager.
   * @param \Drupal\facets\Processor\ProcessorPluginManager $processor_plugin_manager
   *   The processor plugin manager.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type plugin manager.
   */
  public function __construct(QueryTypePluginManager $query_type_plugin_manager, FacetSourcePluginManager $facet_source_manager, ProcessorPluginManager $processor_plugin_manager, EntityTypeManagerInterface $entity_type_manager) {
    $this->queryTypePluginManager = $query_type_plugin_manager;
    $this->facetSourcePluginManager = $facet_source_manager;
    $this->processorPluginManager = $processor_plugin_manager;
    $this->facetStorage = $entity_type_manager
      ->getStorage('facets_facet');
  }

  /**
   * Allows the backend to add facet queries to its native query object.
   *
   * This method is called by the implementing module to initialize the facet
   * display process.
   *
   * @param mixed $query
   *   The backend's native query object.
   * @param string $facetsource_id
   *   The facet source ID to process.
   */
  public function alterQuery(&$query, $facetsource_id) {

    /** @var \Drupal\facets\FacetInterface[] $facets */
    foreach ($this
      ->getFacetsByFacetSourceId($facetsource_id) as $facet) {

      /** @var \Drupal\facets\QueryType\QueryTypeInterface $query_type_plugin */
      $query_type_plugin = $this->queryTypePluginManager
        ->createInstance($facet
        ->getQueryType(), [
        'query' => $query,
        'facet' => $facet,
      ]);
      $query_type_plugin
        ->execute();
    }
  }

  /**
   * Returns enabled facets for the searcher associated with this FacetManager.
   *
   * @return \Drupal\facets\FacetInterface[]
   *   An array of enabled facets.
   */
  public function getEnabledFacets() {
    return $this->facetStorage
      ->loadMultiple();
  }

  /**
   * Returns currently rendered facets filtered by facetsource ID.
   *
   * @param string $facetsource_id
   *   The facetsource ID to filter by.
   *
   * @return \Drupal\facets\FacetInterface[]
   *   An array of enabled facets.
   */
  public function getFacetsByFacetSourceId($facetsource_id) {

    // Immediately initialize the facets.
    $this
      ->initFacets();
    $facets = [];
    foreach ($this->facets as $facet) {
      if ($facet
        ->getFacetSourceId() === $facetsource_id) {
        $facets[] = $facet;
      }
    }
    return $facets;
  }

  /**
   * Initializes facet builds, sets the breadcrumb trail.
   *
   * Facets are built via FacetsFacetProcessor objects. Facets only need to be
   * processed, or built, once The FacetsFacetManager::processed semaphore is
   * set when this method is called ensuring that facets are built only once
   * regardless of how many times this method is called.
   *
   * @param string|null $facetsource_id
   *   The facetsource if of the currently processed facet.
   *
   * @throws \Drupal\facets\Exception\InvalidProcessorException
   *   Thrown when one of the defined processors is invalid.
   */
  public function processFacets($facetsource_id = NULL) {
    if ($facetsource_id === NULL) {
      foreach ($this->facets as $facet) {
        $current_facetsource_id = $facet
          ->getFacetSourceId();
        $this
          ->processFacets($current_facetsource_id);
      }
    }
    $unprocessedFacets = array_filter($this->facets, function ($item) use ($facetsource_id) {

      /* @var \Drupal\facets\FacetInterface $item */
      return !isset($this->processedFacets[$facetsource_id][$item
        ->id()]);
    });

    // All facets were already processed on a previous run, so no need to do so
    // again.
    if (count($unprocessedFacets) === 0) {
      return;
    }
    $this
      ->updateResults($facetsource_id);
    foreach ($unprocessedFacets as $facet) {
      $processor_configs = $facet
        ->getProcessorConfigs();
      foreach ($facet
        ->getProcessorsByStage(ProcessorInterface::STAGE_POST_QUERY) as $processor) {
        $processor_config = $processor_configs[$processor
          ->getPluginDefinition()['id']]['settings'];
        $processor_config['facet'] = $facet;

        /** @var \Drupal\facets\processor\PostQueryProcessorInterface $post_query_processor */
        $post_query_processor = $this->processorPluginManager
          ->createInstance($processor
          ->getPluginDefinition()['id'], $processor_config);
        if (!$post_query_processor instanceof PostQueryProcessorInterface) {
          throw new InvalidProcessorException("The processor {$processor->getPluginDefinition()['id']} has a post_query definition but doesn't implement the required PostQueryProcessor interface");
        }
        $post_query_processor
          ->postQuery($facet);
      }
      $this->processedFacets[$facetsource_id][$facet
        ->id()] = $facet;
    }
  }

  /**
   * Initializes enabled facets.
   *
   * In this method all pre-query processors get called and their contents are
   * executed.
   *
   * @throws \Drupal\facets\Exception\InvalidProcessorException
   *   Thrown if one of the pre query processors is invalid.
   */
  protected function initFacets() {
    if (count($this->facets) > 0) {
      return;
    }
    $this->facets = $this
      ->getEnabledFacets();
    foreach ($this->facets as $facet) {
      foreach ($facet
        ->getProcessorsByStage(ProcessorInterface::STAGE_PRE_QUERY) as $processor) {

        /** @var \Drupal\facets\Processor\PreQueryProcessorInterface $pre_query_processor */
        $pre_query_processor = $facet
          ->getProcessors()[$processor
          ->getPluginDefinition()['id']];
        if (!$pre_query_processor instanceof PreQueryProcessorInterface) {
          throw new InvalidProcessorException("The processor {$processor->getPluginDefinition()['id']} has a pre_query definition but doesn't implement the required PreQueryProcessorInterface interface");
        }
        $pre_query_processor
          ->preQuery($facet);
      }
    }
  }

  /**
   * Builds a facet and returns it as a renderable array.
   *
   * This method delegates to the relevant plugins to render a facet, it calls
   * out to a widget plugin to do the actual rendering when results are found.
   * When no results are found it calls out to the correct empty result plugin
   * to build a render array.
   *
   * Before doing any rendering, the processors that implement the
   * BuildProcessorInterface enabled on this facet will run.
   *
   * @param \Drupal\facets\FacetInterface $facet
   *   The facet we should build.
   *
   * @return array
   *   Facet render arrays.
   *
   * @throws \Drupal\facets\Exception\InvalidProcessorException
   *   Throws an exception when an invalid processor is linked to the facet.
   */
  public function build(FacetInterface $facet) {

    // Immediately initialize the facets if they are not initiated yet.
    $this
      ->initFacets();

    // It might be that the facet received here, is not the same as the already
    // loaded facets in the FacetManager.
    // For that reason, get the facet from the already loaded facets in the
    // static cache.
    $facet = $this->facets[$facet
      ->id()];
    if ($facet
      ->getOnlyVisibleWhenFacetSourceIsVisible()) {

      // Block rendering and processing should be stopped when the facet source
      // is not available on the page. Returning an empty array here is enough
      // to halt all further processing.
      $facet_source = $facet
        ->getFacetSource();
      if (is_null($facet_source) || !$facet_source
        ->isRenderedInCurrentRequest()) {
        return [];
      }
    }

    // For clarity, process facets is called each build.
    // The first facet therefor will trigger the processing. Note that
    // processing is done only once, so repeatedly calling this method will not
    // trigger the processing more than once.
    $this
      ->processFacets($facet
      ->getFacetSourceId());

    // Get the current results from the facets and let all processors that
    // trigger on the build step do their build processing.
    // @see \Drupal\facets\Processor\BuildProcessorInterface.
    // @see \Drupal\facets\Processor\SortProcessorInterface.
    $results = $facet
      ->getResults();
    foreach ($facet
      ->getProcessorsByStage(ProcessorInterface::STAGE_BUILD) as $processor) {
      if (!$processor instanceof BuildProcessorInterface) {
        throw new InvalidProcessorException("The processor {$processor->getPluginDefinition()['id']} has a build definition but doesn't implement the required BuildProcessorInterface interface");
      }
      $results = $processor
        ->build($facet, $results);
    }

    // Handle hierarchy.
    if ($results && $facet
      ->getUseHierarchy()) {
      $keyed_results = [];
      foreach ($results as $result) {
        $keyed_results[$result
          ->getRawValue()] = $result;
      }
      $parent_groups = $facet
        ->getHierarchyInstance()
        ->getChildIds(array_keys($keyed_results));
      $keyed_results = $this
        ->buildHierarchicalTree($keyed_results, $parent_groups);

      // Remove children from primary level.
      foreach (array_unique($this->childIds) as $child_id) {
        unset($keyed_results[$child_id]);
      }
      $results = array_values($keyed_results);
    }

    // Trigger sort stage.
    $active_sort_processors = [];
    foreach ($facet
      ->getProcessorsByStage(ProcessorInterface::STAGE_SORT) as $processor) {
      $active_sort_processors[] = $processor;
    }

    // Sort the actual results if we have enabled sort processors.
    if (!empty($active_sort_processors)) {
      $results = $this
        ->sortFacetResults($active_sort_processors, $results);
    }
    $facet
      ->setResults($results);

    // We include this build even if empty, it may contain attached libraries.

    /** @var \Drupal\facets\Widget\WidgetPluginInterface $widget */
    $widget = $facet
      ->getWidgetInstance();
    $build = $widget
      ->build($facet);

    // No results behavior handling. Return a custom text or false depending on
    // settings.
    if (empty($facet
      ->getResults())) {
      $empty_behavior = $facet
        ->getEmptyBehavior();
      if ($empty_behavior && $empty_behavior['behavior'] === 'text') {
        return [
          [
            $build,
            '#type' => 'container',
            '#attributes' => [
              'data-drupal-facet-id' => $facet
                ->id(),
              'class' => [
                'facet-empty',
              ],
            ],
            'empty_text' => [
              // @codingStandardsIgnoreStart
              '#markup' => $this
                ->t($empty_behavior['text']),
            ],
          ],
        ];
      }
      else {

        // If the facet has no results, but it is being rendered trough ajax we
        // should render a container (that is empty). This is because the
        // javascript needs to be able to find a div to replace with the new
        // content.
        return [
          [
            $build,
            '#type' => 'container',
            '#attributes' => [
              'data-drupal-facet-id' => $facet
                ->id(),
              'class' => [
                'facet-empty',
                'facet-hidden',
              ],
            ],
          ],
        ];
      }
    }
    return [
      $build,
    ];
  }

  /**
   * Updates all facets of a given facet source with the results.
   *
   * @param string $facetsource_id
   *   The facet source ID of the currently processed facet.
   */
  public function updateResults($facetsource_id) {
    $facets = $this
      ->getFacetsByFacetSourceId($facetsource_id);
    if ($facets) {

      /** @var \drupal\facets\FacetSource\FacetSourcePluginInterface $facet_source_plugin */
      $facet_source_plugin = $this->facetSourcePluginManager
        ->createInstance($facetsource_id);
      $facet_source_plugin
        ->fillFacetsWithResults($facets);
    }
  }

  /**
   * Returns one of the processed facets.
   *
   * Returns one of the processed facets, this is a facet with filled results.
   * Keep in mind that if you want to have the facet's build processor executed,
   * there needs to be an extra call to the FacetManager::build with the facet
   * returned here as argument.
   *
   * @param \Drupal\facets\FacetInterface $facet
   *   The facet to process.
   *
   * @return \Drupal\facets\FacetInterface|null
   *   The updated facet if it exists, NULL otherwise.
   */
  public function returnProcessedFacet(FacetInterface $facet) {
    $this
      ->processFacets($facet
      ->getFacetSourceId());
    return !empty($this->facets[$facet
      ->id()]) ? $this->facets[$facet
      ->id()] : NULL;
  }

  /**
   * Builds an hierarchical structure for results.
   *
   * When given an array of results and an array which defines the hierarchical
   * structure, this will build the results structure and set all childs.
   *
   * @param \Drupal\facets\Result\ResultInterface[] $keyed_results
   *   An array of results keyed by id.
   * @param array $parent_groups
   *   An array of 'child id arrays' keyed by their parent id.
   *
   * @return \Drupal\facets\Result\ResultInterface[]
   *   An array of results structured hierarchically.
   */
  protected function buildHierarchicalTree(array $keyed_results, array $parent_groups) {
    foreach ($keyed_results as &$result) {
      $current_id = $result
        ->getRawValue();
      if (isset($parent_groups[$current_id]) && $parent_groups[$current_id]) {
        $child_ids = $parent_groups[$current_id];
        $child_keyed_results = [];
        foreach ($child_ids as $child_id) {
          if (isset($keyed_results[$child_id])) {
            $child_keyed_results[$child_id] = $keyed_results[$child_id];
          }
          else {

            // Children could already be built by Facets Summary manager, if
            // they are, just loading them will suffice.
            $children = $keyed_results[$current_id]
              ->getChildren();
            if (!empty($children[$child_id])) {
              $child_keyed_results[$child_id] = $children[$child_id];
            }
          }
        }
        $result
          ->setChildren($child_keyed_results);
        $this->childIds = array_merge($this->childIds, $child_ids);
      }
    }
    return $keyed_results;
  }

  /**
   * Sort the facet results, and recurse to children to do the same.
   *
   * @param \Drupal\facets\Processor\SortProcessorInterface[] $active_sort_processors
   *   An array of sort processors.
   * @param \Drupal\facets\Result\ResultInterface[] $results
   *   An array of results.
   *
   * @return \Drupal\facets\Result\ResultInterface[]
   *   A sorted array of results.
   */
  protected function sortFacetResults(array $active_sort_processors, array $results) {
    uasort($results, function ($a, $b) use ($active_sort_processors) {
      $return = 0;
      foreach ($active_sort_processors as $sort_processor) {
        if ($return = $sort_processor
          ->sortResults($a, $b)) {
          if ($sort_processor
            ->getConfiguration()['sort'] == 'DESC') {
            $return *= -1;
          }
          break;
        }
      }
      return $return;
    });

    // Loop over the results and see if they have any children, if they do, fire
    // a request to this same method again with the children.
    foreach ($results as &$result) {
      if (!empty($result
        ->getChildren())) {
        $children = $this
          ->sortFacetResults($active_sort_processors, $result
          ->getChildren());
        $result
          ->setChildren($children);
      }
    }
    return $results;
  }

}

Classes

Namesort descending Description
DefaultFacetManager The facet manager.