Skip to content
facets.module 14.4 KiB
Newer Older
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\facets\Entity\FacetSource;
use Drupal\facets\FacetInterface;
use Drupal\facets\FacetSource\SearchApiFacetSourceInterface;
use Drupal\Core\Entity\EntityInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;

/**
 * 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>';
/**
 * Implements hook_theme().
 */
function facets_theme($existing, $type, $theme, $path) {
  return [
        'value' => '',
        'show_count' => FALSE,
        'count' => NULL,
        'is_active' => FALSE,
      ],
    ],
    'facets_item_list' => [
      'variables' => [
        'items' => [],
        'title' => '',
        'list_type' => 'ul',
        'wrapper_attributes' => [],
        'attributes' => [],
        'empty' => NULL,
        'context' => [],
      ],
    ],
    'facets_views_plugin' => [
      'variables' => [
        'content' => [],
      ],
    ],
/**
 * 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);
          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();
    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.');
        $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) {
  /** @var FacetInterface $facet */
  $facet = $variables['facet'];
  if ($facet !== NULL) {
    if ($facet->get('show_title') === TRUE) {
      $variables['title'] = $facet->label();
    }
    if (Settings::get('facets_debug_cacheable_metadata', FALSE) && $facet->getFacetSource() instanceof SearchApiFacetSourceInterface) {
      $variables['cache_hash'] = substr(base_convert(hash('sha256', uniqid(time())), 16, 36), 0, 6);
      $variables['cache_contexts'] = implode(', ', $facet->getCacheContexts());
      $variables['cache_tags'] = implode(', ', $facet->getCacheTags());
      $variables['cache_max_age'] = $facet->getCacheMaxAge() . ' seconds';
  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) {
      return (int) $a->getWeight() - $b->getWeight();
    });

    /** @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()]);
        }
    // @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] = [];
      // 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();
      }

      $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();
  }