toolshed-8.x-1.x-dev/modules/toolshed_search/src/Plugin/views/filter/EntitySelectionFilter.php

modules/toolshed_search/src/Plugin/views/filter/EntitySelectionFilter.php
<?php

namespace Drupal\toolshed_search\Plugin\views\filter;

use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
use Drupal\search_api\Item\FieldInterface;
use Drupal\search_api\ParseMode\ParseModePluginManager;
use Drupal\search_api\Plugin\views\filter\SearchApiFilterTrait;
use Drupal\search_api\Utility\DataTypeHelperInterface;
use Drupal\views\Plugin\views\filter\FilterPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * A filter for selecting entities values of an entity type.
 *
 * @ingroup views_filter_handlers
 *
 * @ViewsFilter("toolshed_entity_selection")
 */
class EntitySelectionFilter extends FilterPluginBase implements ContainerFactoryPluginInterface {

  use SearchApiFilterTrait;

  /**
   * The entity type this filter field belongs to.
   *
   * @var string|false|null
   */
  protected string|false|null $entityTypeId;

  /**
   * Information about target entity to generate autocomplete values for.
   *
   * @var array|null
   */
  protected ?array $targetEntityInfo;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected EntityTypeManagerInterface $entityTypeManager;

  /**
   * The Search API parse mode plugin manager for fulltext word parsing.
   *
   * @var \Drupal\search_api\ParseMode\ParseModePluginManager
   */
  protected ParseModePluginManager $parseModeManager;

  /**
   * The Search API data type helper.
   *
   * @var \Drupal\search_api\Utility\DataTypeHelperInterface
   */
  protected DataTypeHelperInterface $dataTypeHelper;

  /**
   * Create a new instance of the EntitySelectionFilter View plugin handler.
   *
   * @param array $configuration
   *   The filter plugin handler.
   * @param string $plugin_id
   *   The plugin identifier.
   * @param mixed $plugin_definition
   *   The plugin definition.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\search_api\ParseMode\ParseModePluginManager $parse_mode_manager
   *   The fulltext search word parse mode manager.
   * @param \Drupal\search_api\Utility\DataTypeHelperInterface $data_type_helper
   *   The Search API data type helper utility.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ParseModePluginManager $parse_mode_manager, DataTypeHelperInterface $data_type_helper) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);

    $this->entityTypeManager = $entity_type_manager;
    $this->parseModeManager = $parse_mode_manager;
    $this->dataTypeHelper = $data_type_helper;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('entity_type.manager'),
      $container->get('plugin.manager.search_api.parse_mode'),
      $container->get('search_api.data_type_helper')
    );
  }

  /**
   * Gets the entity type ID for the entity that owns this field.
   *
   * Get the entity type of the field parent if the field is an entity field. If
   * the field is not part of an entity, then return NULL.
   *
   * @return string|null
   *   The entity type machine name of the entity that has the real field of
   *   this filter. Returns NULL if the underlying field is not an entity field.
   */
  protected function getEntityTypeId(): ?string {
    if (!isset($this->entityTypeId)) {
      $this->entityTypeId = FALSE;

      if ($field = $this->getIndex()->getField($this->realField)) {
        $dataDef = $field->getDataDefinition();

        if ($dataDef instanceof FieldItemDataDefinitionInterface) {
          $this->entityTypeId = $dataDef
            ->getFieldDefinition()
            ->getTargetEntityTypeId();
        }
      }
    }
    return $this->entityTypeId ?: NULL;
  }

  /**
   * Get the entity type info of the target entity.
   *
   * The get the entity type and bundles to fetch autocomplete suggestions and
   * filter against for this field's query.
   *
   * Contains the "entity_type" and "bundles" keys with the target entity info.
   *
   * @param \Drupal\search_api\Item\FieldInterface $field
   *   The field to determine the target entity info from.
   *
   * @return array
   *   The entity info for the entity type to target for the selection type.
   *
   * @throws \InvalidArgumentException
   *   When the filter is unable to determine a valid entity target.
   */
  protected function getTargetEntityInfo(FieldInterface $field): array {
    /** @var \Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface $dataDef */
    $dataDef = $field->getDataDefinition();

    if (is_a($dataDef->getClass(), EntityReferenceItem::class, TRUE)) {
      $propDef = $dataDef->getPropertyDefinition('entity');

      if ($propDef instanceof DataReferenceDefinitionInterface && $targetDef = $propDef->getTargetDefinition()) {
        /** @var \Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface $targetDef */
        return [
          'entity_type' => $targetDef->getEntityTypeId(),
          'bundles' => $targetDef->getBundles() ?? $dataDef->getSetting('handler_settings')['target_bundles'] ?? [],
        ];
      }
    }

    $error = sprintf('EntitySelectionFilter target entity cannot be determined for "%s" field.', $this->options['id']);
    throw new \InvalidArgumentException($error);
  }

  /**
   * Get the settings configured for this filter for autocomplete suggestions.
   *
   * @return array|null
   *   The settings for the autocomplete controller to setup the results.
   */
  public function getAutocompleteSettings(): ?array {
    $field = $this->getIndex()->getField($this->realField);

    if ($settings = $this->getTargetEntityInfo($field)) {
      $settings += [
        'fulltext_op' => $this->options['expose']['fulltext_op'] ?? 'AND',
        'fulltext_fields' => $this->options['expose']['autocomplete_fields'] ?: NULL,
        'parse_mode' => $this->options['expose']['autocomplete_parse_mode'] ?? NULL,
        'limit' => $this->options['expose']['autocomplete_limit'] ?? 12,
      ];
    }
    return $settings;
  }

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

    $options['type'] = ['default' => 'search'];
    $options['operator'] = ['default' => 'IN'];

    // Exposed settings specifically for autocomplete settings.
    $options['expose']['contains'] += [
      'placeholder' => ['default' => NULL],
      'allow_fulltext_search' => ['default' => TRUE],
      'fulltext_fields' => ['default' => []],
      'fulltext_op' => ['default' => 'AND'],
      'autocomplete_fields' => ['default' => []],
      'autocomplete_parse_mode' => ['default' => 'phrase'],
      'autocomplete_limit' => ['default' => 12],
      'autocomplete_min_length' => ['default' => 3],
      'autocomplete_delay' => ['default' => 200],
    ];

    return $options;
  }

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

    $index = $this->getIndex();
    $field = $index->getField($this->realField);
    $textfields = $this->getFulltextFields();

    if ($targetInfo = $this->getTargetEntityInfo($field)) {
      $targetAcFields = $textfields['entity:' . $targetInfo['entity_type']] ?? [];
      $targetEntityType = $this->entityTypeManager->getDefinition($targetInfo['entity_type']);

      $form['expose']['autocomplete_fields'] = [
        '#type' => 'select',
        '#title' => $this->t('@entity_type fulltext fields', [
          '@entity_type' => $targetEntityType->getLabel(),
        ]),
        '#options' => $textfields['_global'] + $targetAcFields,
        '#multiple' => TRUE,
        '#required' => TRUE,
        '#default_value' => $this->options['expose']['autocomplete_fields'],
        '#description' => $this->t('Fulltext fields for matching the target entity in the autocomplete.'),
      ];
      $form['expose']['autocomplete_min_length'] = [
        '#type' => 'number',
        '#title' => $this->t('Minimum keyword length'),
        '#step' => 1,
        '#min' => 1,
        '#default_value' => $this->options['expose']['autocomplete_min_length'],
      ];
      $form['expose']['autocomplete_delay'] = [
        '#type' => 'number',
        '#title' => $this->t('Time in milliseconds to delay after keypresses before retrieving autocomplete suggestions'),
        '#step' => 25,
        '#min' => 25,
        '#field_suffix' => 'ms',
        '#default_value' => $this->options['expose']['autocomplete_delay'],
      ];
    }

    $form['expose']['placeholder'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Placeholder text'),
      '#description' => $this->t('Placeholder text for the search text field'),
      '#default_value' => $this->options['expose']['placeholder'],
    ];

    $form['expose']['fulltext_op'] = [
      '#type' => 'select',
      '#title' => $this->t('Search matching operator'),
      '#options' => [
        'AND' => $this->t('Contains all these words'),
        'OR' => $this->t('Contains any of these words'),
        'NOT' => $this->t('Contains none of these words'),
      ],
      '#required' => TRUE,
      '#default_value' => $this->options['expose']['fulltext_op'],
    ];

    $form['expose']['autocomplete_parse_mode'] = [
      '#type' => 'select',
      '#title' => $this->t('Text parse mode'),
      '#options' => $this->getParseModes(),
      '#default_value' => $this->options['expose']['autocomplete_parse_mode'],
    ];

    if ($entityTypeId = $this->getEntityTypeId()) {
      $form['expose']['allow_fulltext_search'] = [
        '#type' => 'checkbox',
        '#title' => $this->t('Allow text search when entity is not selected'),
        '#required' => empty($targetInfo),
        '#default_value' => $this->options['expose']['allow_fulltext_search'],
        '#description' => $this->t('Allow search to a fulltext field fallback when an entity suggestion is not selected.'),
      ];

      $entityTextfields = $textfields['entity:' . $entityTypeId] ?? [];
      $form['expose']['fulltext_fields'] = [
        '#type' => 'select',
        '#title' => $this->t('Fallback search fulltext fields'),
        '#multiple' => TRUE,
        '#options' => $textfields['_global'] + $entityTextfields,
        '#default_value' => $this->options['expose']['fulltext_fields'],
        '#states' => [
          'visible' => [
            ':input[name="options[expose][allow_fulltext_search]"]' => [
              'checked' => TRUE,
            ],
          ],
          'required' => [
            ':input[name="options[expose][allow_fulltext_search]"]' => [
              'checked' => TRUE,
            ],
          ],
        ],
      ];
    }
  }

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

    $field = $this->getIndex()->getField($this->realField);
    $targetInfo = $this->getTargetEntityInfo($field);

    if ($form_state->get('exposed')) {
      $form['value'] = [
        '#type' => 'toolshed_autocomplete',
        '#title' => $this->t('Value'),
        '#autocomplete_route_name' => 'toolshed_search.entity_autocomplete',
        '#autocomplete_route_parameters' => [
          'view' => $this->view->id(),
          'display' => $this->view->current_display,
          'filter_id' => $this->options['id'],
        ],
        '#autocomplete_settings' => [
          'separateValue' => TRUE,
          'requireSelect' => empty($this->options['expose']['allow_fulltext_search']),
          'minLength' => $this->options['expose']['autocomplete_min_length'],
          'delay' => $this->options['expose']['autocomplete_delay'],
        ],
      ];

      if (!empty($this->options['expose']['placeholder'])) {
        $form['value']['#placeholder'] = $this->options['expose']['placeholder'];
      }

      $values = $this->extractExposeValue($this->view->getExposedInput());
      if (is_array($values)) {
        $entity = $this->entityTypeManager
          ->getStorage($targetInfo['entity_type'])
          ->load(reset($values));

        if ($entity) {
          $form['value']['#default_value'] = 'id:' . implode(',', $values);
          $form['value']['#attributes']['data-text'] = $entity->label();
        }
      }
      else {
        $form['value']['#default_value'] = $values;
      }
    }
    else {
      if ($this->value) {
        $value = is_array($this->value) ? reset($this->value) : $this->value;
        $entity = $this->entityTypeManager
          ->getStorage($targetInfo['entity_type'])
          ->load($value);
      }

      $form['value'] = [
        '#type' => 'entity_autocomplete',
        '#title' => $this->t('Value'),
        '#target_type' => $targetInfo['entity_type'],
        '#default_value' => $entity ?? NULL,
      ];

      if ($targetInfo['bundles']) {
        $form['value']['#selection_settings']['target_bundles'] = $targetInfo['bundles'];
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function operatorOptions($which = 'title'): array {
    $options = [];
    foreach ($this->operators() as $id => $info) {
      $options[$id] = $info[$which];
    }

    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function acceptExposedInput($input): bool {
    if ($values = $this->extractExposeValue($input)) {
      $this->value = $values;
      return TRUE;
    }

    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function query(): void {
    if (!isset($this->value) || '' === $this->value) {
      return;
    }

    $this->ensureMyTable();
    $query = $this->getQuery();
    $settings = $this->options['expose'];

    if (is_array($this->value)) {
      $query->addCondition($this->realField, $this->value, $this->options['operator']);
    }
    elseif ($settings['allow_fulltext_search'] && $settings['fulltext_fields']) {
      $keys = $this->value;

      try {
        if ($parseModeId = $settings['autocomplete_parse_mode']) {
          $parseConjunction = 'AND' == $settings['fulltext_op'] ? 'AND' : 'OR';
          /** @var \Drupal\search_api\ParseMode\ParseModeInterface $parseMode */
          $parseMode = $this->parseModeManager->createInstance($parseModeId);
          $parseMode->setConjunction($parseConjunction);
          $keys = $parseMode->parseInput($keys);

          if ('NOT' === $settings['fulltext_op'] && is_array($keys)) {
            $keys['#negation'] = TRUE;
          }
        }
      }
      catch (PluginNotFoundException $e) {
        // No parse mode changes, because parse mode plugin is missing.
      }

      $op = 'NOT' === $settings['fulltext_op'] ? '<>' : '=';

      if (count($settings['fulltext_fields']) > 1) {
        $orCond = $query->createAndAddConditionGroup('OR');
        foreach ($settings['fulltext_fields'] as $fulltextField) {
          $orCond->addCondition($fulltextField, $keys, $op);
        }
      }
      else {
        $query->addCondition(reset($settings['fulltext_fields']), $keys, $op);
      }
    }
  }

  /**
   * Get the available parse mode labels keyed by the plugin ID.
   *
   * @return \Drupal\Core\StringTranslation\TranslatableMarkup[]|string[]
   *   All non-hidden Search API parse mode plugin labels, keyed by the
   *   plugin ID. Array is compatible with the select form element "#options".
   */
  protected function getParseModes(): array {
    $parseModes = [];
    foreach ($this->parseModeManager->getDefinitions() as $parseModeDef) {
      if (empty($parseModeDef['no_ui'])) {
        $parseModes[$parseModeDef['id']] = $parseModeDef['label'];
      }
    }

    return $parseModes;
  }

  /**
   * Get all the fulltext fields from the search index grouped by datasource.
   *
   * @return array
   *   An array of fulltext fields from the search index, grouped by their
   *   datasources.
   */
  protected function getFulltextFields(): array {
    $index = $this->getIndex();
    $textfields = ['_global' => []];

    foreach ($index->getFields() as $name => $field) {
      if ($this->dataTypeHelper->isTextType($field->getType())) {
        $sourceId = $field->getDatasourceId() ?? '_global';
        $textfields[$sourceId][$name] = $field->getLabel();
      }
    }

    return $textfields;
  }

  /**
   * Returns information about the available operators for this filter.
   *
   * @return array[]
   *   An associative array mapping operator identifiers to their information.
   *   The operator information itself is an associative array with the
   *   following keys:
   *   - title: The translated title for the operator.
   *   - short: The translated short title for the operator.
   *   - values: The number of values the operator requires as input.
   */
  public function operators(): array {
    return [
      'IN' => [
        'title' => $this->t('Is one of'),
        'short' => $this->t('in'),
        'values' => 1,
      ],
      'NOT IN' => [
        'title' => $this->t('Is none of'),
        'short' => $this->t('not in'),
        'values' => 1,
      ],
    ];
  }

  /**
   * Extract the filter value from the exposed input values.
   *
   * @param array $input
   *   The exposed input to extract the filter values from.
   *
   * @return array|string|null
   *   An array of entity IDs if values are entities, and a string if it
   *   just a text search. NULL if no valid value is available.
   */
  protected function extractExposeValue(array $input = []): array|string|null {
    if (!$this->options['exposed']) {
      return NULL;
    }

    $id = $this->options['expose']['identifier'] ?? $this->options['id'];

    if (!empty($input[$id])) {
      if (preg_match('/^id:(\d+(?:,\d+)*)$/', $input[$id], $matches)) {
        return array_map('intval', explode(',', $matches[1]));
      }
      elseif ($this->options['expose']['allow_fulltext_search']) {
        return $input[$id];
      }
    }
    return NULL;
  }

}

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

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