You are here

facets.module in Facets 8

Contains facets.module.

File

facets.module
View source
<?php

/**
 * @file
 * Contains facets.module.
 */
use Drupal\Component\Utility\Html;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\facets\Entity\Facet;
use Drupal\facets\Entity\FacetSource;
use Drupal\facets\FacetInterface;
use Drupal\views\Entity\View;
use Drupal\Core\Entity\EntityInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Drupal\Core\Logger\RfcLogLevel;

/**
 * Implements hook_help().
 */
function facets_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {

    // Main module help for the facets module.
    case 'help.page.facets':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('Facets test') . '</p>';
      return $output;
    case 'entity.facets_facet.collection':
      $output = '';
      $output .= '<p>' . t('Below is a list of facets grouped by facetsources they are associated with. A facetsource is the instance where the facet does the actual filtering, for example a View on a Search API index.') . '</p>';
      $output .= '<p>' . t('The facets weight can be changed with drag and drop within the same facet source. Although you can drag and drop a facet under any facet source, this change will not be performed on save.') . '</p>';
      return $output;
  }
}

/**
 * Implements hook_theme().
 */
function facets_theme($existing, $type, $theme, $path) {
  return [
    'facets_result_item' => [
      'variables' => [
        'facet' => NULL,
        'raw_value' => '',
        'value' => '',
        'show_count' => FALSE,
        'count' => NULL,
        'is_active' => FALSE,
      ],
    ],
    'facets_item_list' => [
      'variables' => [
        'facet' => NULL,
        'items' => [],
        'title' => '',
        'list_type' => 'ul',
        'wrapper_attributes' => [],
        'attributes' => [],
        'empty' => NULL,
        'context' => [],
      ],
    ],
  ];
}

/**
 * Implements hook_entity_presave().
 *
 * We implement this to make sure that a facet gets removed on view updates, so
 * we don't get broken facet blocks.
 */
function facets_entity_presave(EntityInterface $entity) {

  // Make sure that we only react on view entities with changed displays.
  if ($entity instanceof View && !empty($entity->original)) {
    if ($entity->original
      ->get('display') != $entity
      ->get('display')) {

      /** @var \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_plugin_manager */
      $facet_source_plugin_manager = \Drupal::getContainer()
        ->get('plugin.manager.facets.facet_source');
      $definitions = $facet_source_plugin_manager
        ->getDefinitions();

      // Setup an array of sources that are deleted.
      $sources = [];
      foreach ($entity->original
        ->get('display') as $k => $display) {

        // Check if the current display is also a facet source plugin and that
        // is removed from the view. We use the double underscore here to make
        // sure that we use core convention of "plugin:derived_plugin".
        $facets_source_plugin_id = 'search_api:views_' . $display['display_plugin'] . '__' . $entity
          ->id() . '__' . $display['id'];
        if (array_key_exists($facets_source_plugin_id, $definitions) && !array_key_exists($k, $entity
          ->get('display'))) {
          $entity_id = str_replace(':', '__', $facets_source_plugin_id);
          $source_entity = FacetSource::load($entity_id);
          $sources[] = $facets_source_plugin_id;
          if (!is_null($source_entity)) {
            $source_entity
              ->delete();
          }
        }
      }

      // Loop over all deleted sources and delete the facets that were linked to
      // that source.
      if (count($sources) > 0) {

        /** @var \Drupal\facets\FacetManager\DefaultFacetManager $fm */
        $fm = \Drupal::getContainer()
          ->get('facets.manager');
        foreach ($sources as $source) {
          $facets = $fm
            ->getFacetsByFacetSourceId($source);
          foreach ($facets as $facet) {
            $facet
              ->delete();
          }
        }
      }
      $facet_source_plugin_manager
        ->clearCachedDefinitions();
    }
  }
}

/**
 * Implements hook_preprocess_block().
 *
 * Adds a class for the widget to the facet block to allow for more specific
 * styling.
 */
function facets_preprocess_block(&$variables) {
  if ($variables['configuration']['provider'] == 'facets') {

    // Hide the block if it's empty.
    if (!empty($variables['elements']['content'][0]['#attributes']['class']) && in_array('facet-hidden', $variables['elements']['content'][0]['#attributes']['class'])) {

      // Add the Drupal class for hiding this for everyone, including screen
      // readers. See hidden.module.css in the core system module.
      $variables['attributes']['class'][] = 'hidden';
    }
    if (!empty($variables['derivative_plugin_id'])) {
      $facet = Facet::load($variables['derivative_plugin_id']);
      $variables['attributes']['class'][] = 'block-facet--' . Html::cleanCssIdentifier($facet
        ->getWidget()['type']);
    }
  }
}

/**
 * Implements hook_entity_predelete().
 *
 * We implement this hook to make sure that facet source plugins are cleared
 * when a view is deleted. It also deletes facets that are created on those
 * plugins.
 */
function facets_entity_predelete(EntityInterface $entity) {
  if ($entity instanceof View) {
    $facet_source_plugin_manager = \Drupal::getContainer()
      ->get('plugin.manager.facets.facet_source');
    $definitions = $facet_source_plugin_manager
      ->getDefinitions();
    if (!is_array($definitions)) {
      return;
    }
    foreach ($definitions as $plugin_id => $definition) {
      if (strpos($plugin_id, 'search_api:' . $entity
        ->id() . '__') !== FALSE) {
        try {
          $facetManager = \Drupal::getContainer()
            ->get('facets.manager');
        } catch (ServiceNotFoundException $e) {
          \Drupal::logger('facets')
            ->log(RfcLogLevel::DEBUG, 'Facet manager not found on trying to delete a view.');
          return;
        }
        $facets = $facetManager
          ->getFacetsByFacetSourceId($plugin_id);
        foreach ($facets as $facet) {
          $facet
            ->delete();
        }
      }
    }

    // Clear cached plugin definitions for facet source to make sure we don't
    // show stale data.
    $facet_source_plugin_manager
      ->clearCachedDefinitions();
  }
}

/**
 * Prepares variables for facets item list templates.
 *
 * Default template: facets-item-list.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - items: An array of items to be displayed in the list. Each item can be
 *     either a string or a render array. If #type, #theme, or #markup
 *     properties are not specified for child render arrays, they will be
 *     inherited from the parent list, allowing callers to specify larger
 *     nested lists without having to explicitly specify and repeat the
 *     render properties for all nested child lists.
 *   - title: A title to be prepended to the list.
 *   - list_type: The type of list to return (e.g. "ul", "ol").
 *   - wrapper_attributes: HTML attributes to be applied to the list wrapper.
 *
 * @see https://www.drupal.org/node/1842756
 */
function facets_preprocess_facets_item_list(array &$variables) {
  if ($variables['facet'] !== NULL && $variables['facet']
    ->get('show_title') === TRUE) {
    $variables['title'] = $variables['facet']
      ->label();
  }
  template_preprocess_item_list($variables);
}

/**
 * Implements hook_system_breadcrumb_alter().
 */
function facets_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context) {

  /** @var \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_manager */
  $facet_source_manager = \Drupal::service('plugin.manager.facets.facet_source');

  /** @var \Drupal\facets\FacetManager\DefaultFacetManager $facet_manager */
  $facet_manager = \Drupal::service('facets.manager');

  /** @var \Drupal\Core\Entity\EntityTypeManager $entity_type_manager */
  $entity_type_manager = \Drupal::service('entity_type.manager');

  /** @var \Drupal\Core\Entity\EntityStorageInterface $facet_source_storage */
  $facet_source_storage = $entity_type_manager
    ->getStorage('facets_facet_source');
  $facet_sources_definitions = $facet_source_manager
    ->getDefinitions();
  $facets_url_generator = \Drupal::service('facets.utility.url_generator');

  // No facet sources found, so don't do anything.
  if (empty($facet_sources_definitions)) {
    return;
  }
  foreach ($facet_sources_definitions as $definition) {

    /* @var \Drupal\facets\FacetSource\FacetSourcePluginBase $facet_source_plugin */
    $facetsource_id = $definition['id'];
    $facet_source_plugin = $facet_source_manager
      ->createInstance($facetsource_id);

    // If the current facet source is not being rendered, don't do anything with
    // these facet sources.
    if (!$facet_source_plugin
      ->isRenderedInCurrentRequest()) {
      continue;
    }
    $source_id = str_replace(':', '__', $facetsource_id);

    /** @var \Drupal\facets\FacetSourceInterface $facet_source */
    $facet_source = $facet_source_storage
      ->load($source_id);

    // If the facet source is not loaded, or the facet source doesn't have
    // breadcrumbs enabled, don't do anything.
    if (!($facet_source && !empty($facet_source
      ->getBreadcrumbSettings()['active']))) {
      continue;
    }

    // Add the required cacheability metadata.
    $breadcrumb
      ->addCacheContexts([
      'url',
    ]);
    $breadcrumb
      ->addCacheableDependency($facet_source);

    // Process the facets if they are not already processed.
    $facet_manager
      ->processFacets($facetsource_id);
    $facets = $facet_manager
      ->getFacetsByFacetSourceId($facetsource_id);

    // Sort facets by weight.
    uasort($facets, function (FacetInterface $a, FacetInterface $b) {
      if ($a
        ->getWeight() == $b
        ->getWeight()) {
        return 0;
      }
      return $a
        ->getWeight() < $b
        ->getWeight() ? -1 : 1;
    });

    /** @var \Drupal\facets\UrlProcessor\UrlProcessorPluginManager $url_processor_manager */
    $url_processor_manager = \Drupal::service('plugin.manager.facets.url_processor');

    // Get active facets and results to use them at building the crumbs.
    $active_results = [];
    $active_facets = [];
    foreach ($facets as $facet) {
      if (count($facet
        ->getActiveItems()) > 0) {

        // Add the facet as a cacheable dependency.
        $breadcrumb
          ->addCacheableDependency($facet);

        /** @var \Drupal\facets\UrlProcessor\UrlProcessorInterface $url_processor */
        $url_processor = $url_processor_manager
          ->createInstance($facet_source
          ->getUrlProcessorName(), [
          'facet' => $facet,
        ]);
        $facet_manager
          ->build($facet);
        foreach ($facet
          ->getResults() as $result) {
          if ($result
            ->isActive() || $result
            ->hasActiveChildren()) {

            // Clone the result so we can mark it as inactive to be added to the
            // url parameters when calling buildUrls.
            $cloned_result = clone $result;
            $cloned_result
              ->setActiveState(FALSE);
            $active_results[$facet
              ->id()][] = $cloned_result;
          }
        }
        if (!empty($active_results[$facet
          ->getUrlAlias()])) {
          $url_processor
            ->buildUrls($facet, $active_results[$facet
            ->getUrlAlias()]);
        }
        $active_facets[$facet
          ->id()] = $facet;
      }
    }

    // TODO find a better way to construct the url for a crumb maybe url
    // processor will have a function to get params for a result
    // without all the other request parameters; with this we could implement:
    // @see https://www.drupal.org/node/2861586
    // TODO handle not grouped facets.

    /** @var \Drupal\facets\Result\ResultInterface[] $facet_results */
    foreach ($active_results as $facet_id => $facet_results) {
      $facet_used_result[$facet_id] = [];
      $facet_crumb_items = [];

      // Because we can't get the desired display value trough a url processor
      // method we iterate each result url and remove the facet params that
      // haven't been used on previous crumbs.
      foreach ($facet_results as $res) {
        $facet_used_result[$facet_id][] = $res
          ->getRawValue();
        $facet_crumb_items[] = $res
          ->getDisplayValue();
      }
      sort($facet_crumb_items);
      $facet_url = $facets_url_generator
        ->getUrl($facet_used_result, FALSE);
      if (!empty($facet_source
        ->getBreadcrumbSettings()['before'])) {
        $crumb_text = $active_facets[$facet_id]
          ->label() . ': ' . implode(', ', $facet_crumb_items);
      }
      else {
        $crumb_text = implode(', ', $facet_crumb_items);
      }
      $link = Link::fromTextAndUrl($crumb_text, $facet_url);
      $breadcrumb
        ->addLink($link);
    }
  }
}

/**
 * Implements hook_language_switch_links_alter().
 */
function facets_language_switch_links_alter(array &$links, $type, Url $url) {

  /** @var \Drupal\facets\LanguageSwitcherLinksAlterer $alterer */
  $alterer = \Drupal::service('facets.language_switcher_links_alterer');
  $alterer
    ->alter($links, $type, $url);
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function facets_form_facets_facet_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $facet_sources = [];
  foreach (\Drupal::service('plugin.manager.facets.facet_source')
    ->getDefinitions() as $facet_source_id => $definition) {
    $facet_sources[$definition['id']] = !empty($definition['label']) ? $definition['label'] : $facet_source_id;
  }
  if (count($facet_sources) == 0) {
    unset($form['actions']);
  }
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function facets_theme_suggestions_facets_result_item(array $variables) {
  $suggestions = [];
  $facet = $variables['facet'];
  if ($facet instanceof FacetInterface) {
    $suggestions[] = 'facets_result_item__' . $facet
      ->getWidget()['type'];
    $suggestions[] = 'facets_result_item__' . $facet
      ->getWidget()['type'] . '__' . $facet
      ->id();
  }
  return $suggestions;
}