Commit f7df0f20 authored by Christophe Goffin's avatar Christophe Goffin Committed by Joseph Olstad
Browse files

Issue #3241506: Extra Search API features (submodule)

parent 436d03c0
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
name: Search API Taxonomy Machine Name
description: Search API integration for the Taxonomy Machine Name module
type: module
package: Other
core: 8.x
core_version_requirement: ^8 || ^9

dependencies:
  - taxonomy_machine_name:taxonomy_machine_name
  - search_api:search_api
+23 −0
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Contains all hook implementations for this module.
 */

use Drupal\search_api\IndexInterface;
use Drupal\search_api\SearchApiException;

/**
 * Implements hook_search_api_solr_field_mapping_alter().
 */
function search_api_taxonomy_machine_name_search_api_solr_field_mapping_alter(IndexInterface $index, array &$fields, string $language_id) {
  try {
    /** @var \Drupal\search_api_taxonomy_machine_name\Plugin\search_api\processor\AddHierarchy $processor */
    $processor = $index->getProcessor('taxonomy_machine_name_hierarchy');
    $processor->alterFieldMapping($index, $fields, $language_id);
  }
  catch (SearchApiException $exception) {
    // Processor not active for this index, so proceed. Logging isn't necessary.
  }
}
+33 −0
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Provide views data.
 */

use Drupal\search_api\Entity\Index;

/**
 * Implements hook_views_data_alter().
 */
function search_api_taxonomy_machine_name_views_data_alter(array &$data) {
  // Adjust the filter handler for all taxonomy machine name fields.
  foreach (Index::loadMultiple() as $index) {
    $key = 'search_api_index_' . $index->id();
    if (!isset($data[$key])) {
      continue;
    }

    foreach ($data[$key] as $field_alias => &$field_definition) {
      if (!isset($field_definition['field']['entity_type'], $field_definition['field']['field_name'])) {
        continue;
      }

      if ($field_definition['field']['entity_type'] !== 'taxonomy_term' || $field_definition['field']['field_name'] !== 'machine_name') {
        continue;
      }

      $field_definition['filter']['id'] = 'search_api_taxonomy_machine_name';
    }
  }
}
+206 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\search_api_taxonomy_machine_name\Plugin\search_api\processor;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Item\FieldInterface;
use Drupal\search_api\Plugin\PluginFormTrait;
use Drupal\search_api\Processor\ProcessorPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Adds all ancestors' machine names to a hierarchical field.
 *
 * @SearchApiProcessor(
 *   id = "taxonomy_machine_name_hierarchy",
 *   label = @Translation("Index machine name hierarchy"),
 *   description = @Translation("Allows the indexing of taxonomy machine names along with all their ancestors."),
 *   stages = {
 *     "preprocess_index" = -45
 *   }
 * )
 */
class AddHierarchy extends ProcessorPluginBase implements PluginFormInterface {

  use PluginFormTrait;

  protected array $indexHierarchyFields = [];

  /**
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected EntityTypeManagerInterface $entityTypeManager;

  /**
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
   */
  protected LoggerChannelFactoryInterface $logger;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $processor = parent::create($container, $configuration, $plugin_id, $plugin_definition);

    $processor->entityTypeManager = $container->get('entity_type.manager');
    $processor->logger = $container->get('logger.factory');

    return $processor;
  }

  /**
   * {@inheritdoc}
   */
  public static function supportsIndex(IndexInterface $index): bool {
    $processor = new static(['#index' => $index], 'taxonomy_machine_name_hierarchy', []);

    return (bool) $processor->getHierarchyFields();
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration(): array {
    return [
      'fields' => [],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
    $form['#description'] = $this->t('Select the fields to which hierarchical data should be added.');

    foreach ($this->getHierarchyFields() as $field_id => $options) {
      $enabled = !empty($this->configuration['fields'][$field_id]['status']);
      $form['fields'][$field_id]['status'] = [
        '#type' => 'checkbox',
        '#title' => $this->index->getField($field_id)->getLabel(),
        '#default_value' => $enabled,
      ];
    }

    return $form;
  }

  /**
   * Find all taxonomy term machine name fields.
   *
   * @return string[][]
   */
  protected function getHierarchyFields(): array {
    if (!isset($this->indexHierarchyFields[$this->index->id()])) {
      $field_options = [];

      foreach ($this->index->getFields() as $field_id => $field) {
        $dependencies = $field->getDependencies();
        if (!isset($dependencies['module']) || !in_array('taxonomy_machine_name', $dependencies['module'])) {
          continue;
        }

        $field_options[$field_id] = [
          'taxonomy_term-machine_name-parent' => 'Taxonomy Term » Machine Name » Parent',
        ];
      }

      $this->indexHierarchyFields[$this->index->id()] = $field_options;
    }

    return $this->indexHierarchyFields[$this->index->id()];
  }

  /**
   * Add parent taxonomy term machine names to the field.
   *
   * @param \Drupal\Core\Entity\EntityInterface $term
   * @param \Drupal\search_api\Item\FieldInterface $field
   */
  protected function addHierarchyValues(EntityInterface $term, FieldInterface $field): void {
    try {
      foreach ($this->entityTypeManager->getStorage('taxonomy_term')->loadAllParents($term->id()) as $taxonomy_term) {
        $machine_name = $taxonomy_term->get('machine_name')->value;
        if (in_array($machine_name, $field->getValues(), TRUE)) {
          continue;
        }

        $field->addValue($machine_name);
      }
    }
    catch (\Exception $exception) {
      $this->logger->get('search_api_taxonomy_machine_name')->error('An error occurred: !message', [
        '!message' => $exception->getMessage(),
      ]);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function preprocessIndexItems(array $items): void {
    foreach ($items as $item) {
      foreach ($this->configuration['fields'] as $field_id => $property_specifier) {
        if (!$property_specifier['status']) {
          continue;
        }

        $field = $item->getField($field_id);
        if (!$field) {
          continue;
        }

        [$field_name, , ] = explode(':', $field->getPropertyPath());

        try {
          /** @var \Drupal\Core\Entity\ContentEntityBase $entity */
          $entity = $item->getOriginalObject()->getValue();
          if (!$entity->hasField($field_name)) {
            continue;
          }

          foreach ($entity->get($field_name) as $value) {
            $term = $value->get('entity')->getValue();
            if ($term === NULL) {
              continue;
            }

            $this->addHierarchyValues($term, $field);
          }
        }
        catch (\Exception $exception) {
          $this->logger->get('search_api_taxonomy_machine_name')->error('An error occurred: !message', [
            '!message' => $exception->getMessage(),
          ]);
        }
      }
    }
  }

  /**
   * When hierarchy is enabled, make the configured fields multi-value fields.
   *
   * @param \Drupal\search_api\IndexInterface $index
   * @param array $fields
   * @param string $language_id
   */
  public function alterFieldMapping(IndexInterface $index, array &$fields, string $language_id): void {
    $configuration = $this->getConfiguration();
    if (!isset($configuration['fields'])) {
      return;
    }

    foreach ($configuration['fields'] as $field_name => $property) {
      if (!isset($property['status']) || !$property['status']) {
        continue;
      }

      $fields[$field_name] = 'sm_' . $field_name;
    }
  }

}
+207 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\search_api_taxonomy_machine_name\Plugin\views\filter;

use Drupal\Core\Entity\Element\EntityAutocomplete;
use Drupal\Core\Form\FormStateInterface;
use Drupal\search_api\Plugin\views\filter\SearchApiFilterTrait;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy_machine_name\Plugin\views\filter\TaxonomyIndexMachineName;

/**
 * Filtering by taxonomy machine name.
 *
 * @ingroup views_filter_handlers
 *
 * @ViewsFilter("search_api_taxonomy_machine_name")
 */
class SearchApiTaxonomyMachineName extends TaxonomyIndexMachineName {

  use SearchApiFilterTrait;

  /**
   * {@inheritdoc}
   */
  protected function defineOptions(): array {
    $options = parent::defineOptions();

    $options['hierarchy_parent'] = ['default' => 0];
    $options['hierarchy_max_depth'] = ['default' => NULL];

    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function buildExtraOptionsForm(&$form, FormStateInterface $form_state): void {
    parent::buildExtraOptionsForm($form, $form_state);

    $form['hierarchy_parent'] = [
      '#type' => 'number',
      '#title' => $this->t('Start at level'),
      '#default_value' => $this->options['hierarchy_parent'] ?? 0,
      '#min' => 0,
      '#states' => [
        'visible' => [
          ':input[name="options[hierarchy]"]' => ['checked' => TRUE],
        ],
      ],
    ];

    $form['hierarchy_max_depth'] = [
      '#type' => 'number',
      '#title' => $this->t('Max depth'),
      '#default_value' => $this->options['hierarchy_max_depth'] ?? NULL,
      '#min' => 0,
      '#states' => [
        'visible' => [
          ':input[name="options[hierarchy]"]' => ['checked' => TRUE],
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function validateExtraOptionsForm($form, FormStateInterface $form_state): void {
    $options = $form_state->getValue('options');
    if (isset($options['hierarchy_max_depth']) && $options['hierarchy_max_depth'] === '') {
      $options['hierarchy_max_depth'] = NULL;
      $form_state->setValue('options', $options);
    }
  }

  /**
   * Override of the method in the extended TaxonomyIndexMachineName class to
   * apply the new hierarchy settings.
   *
   * @param array $form
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *
   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
   */
  protected function valueForm(&$form, FormStateInterface $form_state) {
    $vocabulary = $this->vocabularyStorage->load($this->options['vid']);
    if ($vocabulary === NULL && $this->options['limit']) {
      $form['markup'] = [
        '#markup' => '<div class="js-form-item form-item">' . $this->t('An invalid vocabulary is selected. Please change it in the options.') . '</div>',
      ];
      return;
    }

    if ($this->options['type'] === 'textfield') {
      $terms = $this->value ? Term::loadMultiple(($this->value)) : [];
      $form['value'] = [
        '#title' => $this->options['limit'] ? $this->t('Select terms from vocabulary @voc', ['@voc' => $vocabulary->label()]) : $this->t('Select terms'),
        '#type' => 'textfield',
        '#default_value' => EntityAutocomplete::getEntityLabels($terms),
      ];

      if ($this->options['limit']) {
        $form['value']['#type'] = 'entity_autocomplete';
        $form['value']['#target_type'] = 'taxonomy_term';
        $form['value']['#selection_settings']['target_bundles'] = [$vocabulary->id()];
        $form['value']['#tags'] = TRUE;
        $form['value']['#process_default_value'] = FALSE;
      }
    }
    else {
      if (!empty($this->options['hierarchy']) && $this->options['limit']) {
        // This is the only change in comparison with the overridden method.
        $tree = $this->termStorage->loadTree($vocabulary->id(), $this->options['hierarchy_parent'], $this->options['hierarchy_max_depth'], TRUE);
        $options = [];
        $options_attributes = [];

        if ($tree) {
          foreach ($tree as $term) {
            $options[$term->get('machine_name')
              ->get(0)->value] = \Drupal::service('entity.repository')
                ->getTranslationFromContext($term)
                ->label();

            $options_attributes[$term->get('machine_name')
              ->get(0)->value] = ['class' => ['level-' . $term->depth]];
          }
        }
      }
      else {
        $options = [];
        $query = \Drupal::entityQuery('taxonomy_term')
          // @todo Sorting on vocabulary properties -
          //   https://www.drupal.org/node/1821274.
          ->sort('weight')
          ->sort('name')
          ->addTag('taxonomy_term_access');
        if ($this->options['limit']) {
          $query->condition('vid', $vocabulary->id());
        }
        $terms = Term::loadMultiple($query->execute());
        foreach ($terms as $term) {
          $options[$term->get('machine_name')
            ->get(0)->value] = \Drupal::service('entity.repository')
            ->getTranslationFromContext($term)
            ->label();
        }
      }

      $default_value = (array) $this->value;

      if ($exposed = $form_state->get('exposed')) {
        $identifier = $this->options['expose']['identifier'];

        if (!empty($this->options['expose']['reduce'])) {
          $options = $this->reduceValueOptions($options);

          if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) {
            $default_value = [];
          }
        }

        if (empty($this->options['expose']['multiple'])) {
          if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) {
            $default_value = 'All';
          }
          elseif (empty($default_value)) {
            $keys = array_keys($options);
            $default_value = array_shift($keys);
          }
          // Due to #1464174 there is a chance that array('')
          // was saved in the admin ui. Let's choose a safe default value.
          elseif ($default_value == ['']) {
            $default_value = 'All';
          }
          else {
            $copy = $default_value;
            $default_value = array_shift($copy);
          }
        }
      }
      $form['value'] = [
        '#type' => 'select',
        '#title' => $this->options['limit'] ? $this->t('Select terms from vocabulary @voc', ['@voc' => $vocabulary->label()]) : $this->t('Select terms'),
        '#multiple' => TRUE,
        '#options' => $options,
        '#options_attributes' => $options_attributes ?? [],
        '#size' => min(9, count($options)),
        '#default_value' => $default_value,
      ];

      $user_input = $form_state->getUserInput();
      if ($exposed && isset($identifier) && !isset($user_input[$identifier])) {
        $user_input[$identifier] = $default_value;
        $form_state->setUserInput($user_input);
      }
    }

    if (!$form_state->get('exposed')) {
      // Retain the helper option.
      $this->helper->buildOptionsForm($form, $form_state);

      // Show help text if not exposed to end users.
      $form['value']['#description'] = t('Leave blank for all. Otherwise, the first selected term will be the default instead of "Any".');
    }
  }

}