external_entities-8.x-2.x-dev/src/StorageClient/StorageClientBase.php

src/StorageClient/StorageClientBase.php
<?php

namespace Drupal\external_entities\StorageClient;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Plugin\PluginDependencyTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Utility\Token;
use Drupal\external_entities\Entity\ExternalEntityTypeInterface;
use Drupal\external_entities\Event\ExternalEntitiesEvents;
use Drupal\external_entities\Event\ExternalEntityTransliterateDrupalFiltersEvent;
use Drupal\external_entities\Event\ExternalEntityTransliterateDrupalSortsEvent;
use Drupal\external_entities\Plugin\PluginDebugTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Base class for external entity storage clients.
 */
abstract class StorageClientBase extends PluginBase implements StorageClientInterface {

  use PluginDependencyTrait;
  use PluginDebugTrait;

  /**
   * The external entity type this storage client is configured for.
   *
   * @var \Drupal\external_entities\Entity\ExternalEntityTypeInterface
   */
  protected $externalEntityType;

  /**
   * The logger channel factory.
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
   */
  protected $loggerChannelFactory;

  /**
   * The storage client plugin logger channel.
   *
   * @var \Drupal\Core\Logger\LoggerChannel
   */
  protected $logger;

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

  /**
   * The entity field manager.
   *
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
   */
  protected $entityFieldManager;

  /**
   * The token service.
   *
   * @var \Drupal\Core\Utility\Token
   */
  protected $tokenService;

  /**
   * Constructs a StorageClientBase object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
   *   The string translation service.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   The logger channel factory.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   *   The entity field manager.
   * @param \Drupal\Core\Utility\Token $token_service
   *   The token service.
   */
  public function __construct(
    array $configuration,
    string $plugin_id,
    $plugin_definition,
    TranslationInterface $string_translation,
    LoggerChannelFactoryInterface $logger_factory,
    EntityTypeManagerInterface $entity_type_manager,
    EntityFieldManagerInterface $entity_field_manager,
    Token $token_service,
  ) {
    $this->setStringTranslation($string_translation);
    $this->loggerChannelFactory = $logger_factory;
    $this->entityTypeManager = $entity_type_manager;
    $this->entityFieldManager = $entity_field_manager;
    $this->tokenService = $token_service;
    $this->logger = $this->loggerChannelFactory->get('xntt_storage_client_' . $plugin_id);
    $this->debugLevel = $configuration['debug_level'] ?? NULL;

    $this->setConfiguration($configuration);
    $configuration = $this->getConfiguration();
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(
    ContainerInterface $container,
    array $configuration,
    $plugin_id,
    $plugin_definition,
  ) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('string_translation'),
      $container->get('logger.factory'),
      $container->get('entity_type.manager'),
      $container->get('entity_field.manager'),
      $container->get('token')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getLabel() :string {
    $plugin_definition = $this->getPluginDefinition();
    return $plugin_definition['label'];
  }

  /**
   * {@inheritdoc}
   */
  public function getDescription() :string {
    $plugin_definition = $this->getPluginDefinition();
    return $plugin_definition['description'] ?? '';
  }

  /**
   * {@inheritdoc}
   */
  public function getConfiguration() {
    return $this->configuration;
  }

  /**
   * {@inheritdoc}
   */
  public function setConfiguration(array $configuration) {
    $configuration = NestedArray::mergeDeep(
      $this->defaultConfiguration(),
      $configuration
    );

    if (!empty($configuration[ExternalEntityTypeInterface::XNTT_TYPE_PROP])
        && $configuration[ExternalEntityTypeInterface::XNTT_TYPE_PROP] instanceof ExternalEntityTypeInterface
    ) {
      $this->externalEntityType = $configuration[ExternalEntityTypeInterface::XNTT_TYPE_PROP];
    }
    unset($configuration[ExternalEntityTypeInterface::XNTT_TYPE_PROP]);
    $this->configuration = $configuration;
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return [];
  }

  /**
   * {@inheritdoc}
   */
  public function load(string|int $id) :array|null {
    return current($this->loadMultiple([$id])) ?: NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function countQuerySource(array $parameters = []) :int {
    // Default inefficient implementation.
    return count($this->querySource($parameters));
  }

  /**
   * {@inheritdoc}
   */
  public function query(
    array $parameters = [],
    array $sorts = [],
    ?int $start = NULL,
    ?int $length = NULL,
  ) :array {
    // @todo Add some caching (for paging performances).
    // Check for the special case of the 'IN' operator with entity identifiers:
    // if the storage client does not handle the IN operator, however we can
    // turn it into multiple loading of each identifier unless other filters are
    // provided.
    $id_list_filtering =
      (1 == count($parameters))
      && isset($parameters[0]['operator'])
      && (('IN' == $parameters[0]['operator'])
          || ('=' == $parameters[0]['operator']))
      && isset($parameters[0]['field'])
      && ('id' == $parameters[0]['field']);
    // Transliterate filters.
    $trans_parameters = $this->transliterateDrupalFilters($parameters, ['caller' => 'query']);
    if (empty($trans_parameters)) {
      // Make sure it is not just about id filtering from a list.
      if ($id_list_filtering) {
        // Put back "id IN" filter to process them below.
        $trans_parameters = [
          'drupal' => $parameters,
          'source' => [],
        ];
      }
      else {
        // Filtering not possible.
        return [];
      }
    }

    // Transliterate sorts.
    $trans_sorts = $this->transliterateDrupalSorts($sorts, ['caller' => 'query']);

    // Manage special the case of the 'IN' operator with entity identifiers.
    if (empty($trans_parameters['source']) && $id_list_filtering) {
      // We only have an IN operator with identifiers.
      $ids = (array) ($trans_parameters['drupal'][0]['value'] ?? []);
      $results = array_values($this->loadMultiple($ids));
    }
    elseif (empty($trans_parameters['drupal'])) {
      // Get filtered results from source.
      // We can use source paging and sorting.
      $results = $this->querySource(
        $trans_parameters['source'] ?? [],
        $trans_sorts['source'],
        $start,
        $length
      );
    }
    else {
      // Get all and process to a second pass.
      $first_pass_results = $this->querySource(
        $trans_parameters['source']
        ?? []
      );
      $results = $this->postFilterQuery(
        $first_pass_results,
        $trans_parameters['drupal']
      );

      // Process sorting.
      if (!empty($trans_sorts['drupal'])) {
        // We must re-sort with both Drupal and source sort parameters.
        static::sortEntities($results, $sorts);
      }

      // Process paging.
      if (isset($length)) {
        $results = array_slice($results, $start ?? 0, $length);
      }
      elseif (isset($start)) {
        $results = array_slice($results, $start);
      }
    }
    return $results;
  }

  /**
   * Sends an event to alter transliterated filters.
   *
   * This method should be called in the end of client
   * ::transliterateDrupalFilters() implementation to let other module alter
   * translitterated filters.
   *
   * @param array $trans_filters
   *   Transliterated filters obtained form the storage client after processing
   *   the parameters.
   * @param array $original_parameters
   *   Original array of parameters passed to ::transliterateDrupalFilters().
   * @param array $context
   *   A context array that can be used to pass optional informations like the
   *   calling method (eg. 'caller' => 'query', or 'caller' => 'countQuery').
   *
   * @return array
   *   The final, possibly altered, transliterated filter array.
   *
   * @see \Drupal\external_entities\StorageClient\StorageClientInterface::transliterateDrupalFilters()
   */
  protected function transliterateDrupalFiltersAlter(
    array $trans_filters,
    array $original_parameters,
    array $context = [],
  ) :array {
    // Allow other modules to alter transliteration.
    $event = new ExternalEntityTransliterateDrupalFiltersEvent(
      $trans_filters,
      $original_parameters,
      $this->externalEntityType,
      $this,
      $context
    );
    \Drupal::service('event_dispatcher')->dispatch(
      $event,
      ExternalEntitiesEvents::TRANSLITERATE_DRUPAL_FILTERS
    );

    $trans_filters = $event->getTransliteratedDrupalFilters();

    if (1 <= $this->getDebugLevel()) {
      $this->logger->debug(
        "StorageClientBase::transliterateDrupalFiltersAlter() result:\n@parameters",
        [
          '@parameters' => print_r($trans_filters, TRUE),
        ]
      );
    }

    return $trans_filters;
  }

  /**
   * Default implementation of sort parameters Transliteration.
   *
   * This method is similiar to
   * StorageClientInterface::transliterateDrupalFilters() but for sort
   * parameters.
   *
   * @param array $sorts
   *   A sort structure like the one provided to the self::query() method.
   * @param array $context
   *   A context array that can be used to pass optional informations like the
   *   calling method (eg. 'caller' => 'query', or 'caller' => 'count').
   *
   * @return array
   *   A 2 keys array with transliterated and not transliterated sort parameters
   *   array stored respectively under the keys 'source' and 'drupal'.
   */
  public function transliterateDrupalSorts(
    array $sorts,
    array $context = [],
  ) :array {
    $trans_sorts = [
      'drupal' => $sorts,
      'source' => [],
    ];
    foreach ($sorts as $sort) {
      $source_field = NULL;
      if (!empty($sort['field'])) {
        $field_prop = explode('.', $sort['field']);
        if (1 == count($field_prop) || (2 == count($field_prop))) {
          $field_prop[1] ??= NULL;
          $field_mapper = $this->externalEntityType->getFieldMapper($field_prop[0]);
          if ($field_mapper) {
            $source_field = $field_mapper->getMappedSourceFieldName($field_prop[1]);
          }
        }
      }
      if (!empty($source_field)) {
        $sort['field'] = $source_field;
        $trans_sorts['source'][] = $sort;
      }
      else {
        $trans_sorts['drupal'][] = $sort;
      }
    }

    return $this->transliterateDrupalSortsAlter(
      $trans_sorts,
      $sorts,
      $context,
    );
  }

  /**
   * Sends an event to alter transliterated sorts.
   *
   * This method should be called in the end of client
   * ::transliterateDrupalSorts() implementation to let other module alter
   * transliterated sorts.
   *
   * @param array $trans_sorts
   *   Transliterated sorts obtained form the storage client after processing
   *   the sorts.
   * @param array $original_sorts
   *   Original array of sorts passed to ::transliterateDrupalSorts().
   * @param array $context
   *   A context array that can be used to pass optional information like the
   *   calling method (e.g. 'caller' => 'query', or 'caller' => 'countQuery').
   *
   * @return array
   *   The final, possibly altered, transliterated sorts array.
   *
   * @see \Drupal\external_entities\StorageClient\StorageClientBase::transliterateDrupalSorts()
   */
  protected function transliterateDrupalSortsAlter(
    array $trans_sorts,
    array $original_sorts,
    array $context = [],
  ) :array {
    // Allow other modules to alter transliteration.
    $event = new ExternalEntityTransliterateDrupalSortsEvent(
      $trans_sorts,
      $original_sorts,
      $this->externalEntityType,
      $this,
      $context
    );
    \Drupal::service('event_dispatcher')->dispatch(
      $event,
      ExternalEntitiesEvents::TRANSLITERATE_DRUPAL_SORTS
    );

    $trans_sorts = $event->getTransliteratedDrupalSorts();

    if (1 <= $this->getDebugLevel()) {
      $this->logger->debug(
        "StorageClientBase::transliterateDrupalSortsAlter() result:\n@sorts",
        [
          '@sorts' => print_r($trans_sorts, TRUE),
        ]
      );
    }

    return $trans_sorts;
  }

  /**
   * Sorts an array of raw entities given sort parameters.
   *
   * Note: array keys/indexes are not preserved.
   *
   * @param array &$raw_entitites
   *   An array of raw entities: each entity is an array of values of a Drupal
   *   entity with complex field data structure.
   * @param array $sorts
   *   Array of sorts, each value is an array with the following
   *   key-value pairs:
   *     - field: the field to sort by
   *     - direction: the direction to sort on
   *     - langcode: optional language code.
   */
  public static function sortEntities(array &$raw_entitites, array $sorts) :void {
    // @todo Check if field values are arrays because $a[$sort['field']]
    // should be an array like [0 =>['value' => 'something'], 1=> ...] which
    // means current implementation does not work properly.
    // @todo Manage field properties field names like "field_geolocation.lat".
    usort($raw_entitites, function ($a, $b) use ($sorts) {
      foreach ($sorts as $sort) {
        if (!is_array($sort) || empty($sort['field'])) {
          // Invalid sort parameter.
          continue;
        }
        if (($sort['direction'] ?? '') == 'DESC') {
          // Descending.
          $before = 1;
          $after = -1;
        }
        else {
          // Ascending.
          $before = -1;
          $after = 1;
        }
        if (array_key_exists($sort['field'], $a)) {
          if (array_key_exists($sort['field'], $b)) {
            if ($a[$sort['field']] != $b[$sort['field']]) {
              if (is_numeric($a[$sort['field']])
                  && is_numeric($b[$sort['field']])
              ) {
                // Numeric comparison.
                return (($a[$sort['field']] - $b[$sort['field']]) < 0) ? $before : $after;
              }
              elseif (is_string($a[$sort['field']])
                && is_string($b[$sort['field']])) {
                // Text comparison.
                return (strcmp($a[$sort['field']], $b[$sort['field']]) < 0)
                  ? $before
                  : $after;
              }
            }
            // Equality, continue next sort.
          }
          else {
            // $b does not have the field.
            return $after;
          }
        }
        else {
          // $a does not have the field.
          if (array_key_exists($sort['field'], $b)) {
            // $b does.
            return $before;
          }
          // None have the field, continue next sort.
        }
      }
      // Equality.
      return 0;
    });
  }

  /**
   * {@inheritdoc}
   */
  public function countQuery(array $parameters = []) :int {
    // Translate filters.
    $trans_parameters = $this->transliterateDrupalFilters($parameters, ['caller' => 'countQuery']);
    if (empty($trans_parameters)) {
      // Filtering not possible.
      return 0;
    }
    // Get filtered results from source.
    if (empty($trans_parameters['drupal'])) {
      // We can use source count.
      return $this->countQuerySource($trans_parameters['source'] ?? []);
    }
    else {
      // We need to count post-filtered entities.
      return count($this->query($parameters));
    }
  }

  /**
   * Filter external entitiy arrays according to the given filters.
   *
   * Method used to filter the given external entities using Drupal-type filters
   * provided in the $parameters array. This method is used to filter values
   * that can not be directly filtered on the data source side (storage side).
   *
   * @param array $result_set
   *   Array of external entity arrays.
   * @param array $parameters
   *   (optional) Array of parameters, each value is an array of one of the two
   *   following structure:
   *   - type condition:
   *     - field: the Drupal field machine name the parameter applies to
   *     - value: the value of the parameter or NULL
   *     - operator: the Drupal operator of how the parameter should be applied.
   *       Should be one of '=', '<>', '>', '>=', '<', '<=', 'STARTS_WITH',
   *       'CONTAINS', 'ENDS_WITH', 'IN', 'NOT IN', 'IS NULL', 'IS NOT NULL',
   *       'BETWEEN' and 'NOT BETWEEN', but may also be a custom operator like
   *       the ones defined in \Drupal\views\Plugin\views\filter\* plugins.
   *   - type sub-condition:
   *     - conjunction: either 'or' or 'and'
   *     - conditions: an array of array of type condition described above or
   *       type sub-condition.
   *
   * @return array
   *   An array of external entities passing all the filters or an empty array
   *   if none did.
   */
  protected function postFilterQuery(
    array $result_set,
    array $parameters,
  ) :array {
    $results = [];
    // Process each entity array.
    foreach ($result_set as $result) {
      // Get mapped external entity data to match Drupal fields.
      // Note: unfortunately here, we discard any live modification made to our
      // external entity, including field mapping, data aggregation or storage
      // client changes because we pass through the entity type manager to load
      // (stored) config data. This was an issue for the group aggregator and
      // prevents it from using Drupal-side filtering as id field mapping
      // changes are not taken into account here.
      $mapped_values = $this->entityTypeManager
        ->getStorage($this->externalEntityType->getDerivedEntityTypeId())
        ->extractEntityValuesFromRawData($result);
      if ($this->testDrupalConditions(
            'and',
            $parameters,
            $mapped_values)
      ) {
        // Entity passed all filters, keep.
        $results[] = $result;
      }
    }
    return $results;
  }

  /**
   * Returns TRUE if the given conditions pass with the given conjunction.
   *
   * @param string $conjunction
   *   One of 'or' or 'and'.
   * @param array $conditions
   *   An array of conditions to test.
   *   See self::postFilterQuery() $parameters parameter for the structure.
   * @param array $mapped_values
   *   An array of mapped field values to test (mapped to a Drupal field
   *   structure).
   *
   * @return bool
   *   TRUE if the conditions pass the test.
   *
   * @see https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Entity!Query!QueryInterface.php/function/QueryInterface%3A%3Acondition/10
   */
  protected function testDrupalConditions(
    string $conjunction,
    array $conditions,
    array $mapped_values,
  ) :bool {
    switch ($conjunction) {
      case 'and':
        $pass = TRUE;
        break;

      case 'or':
        $pass = FALSE;
        break;

      default:
        $this->logger->warning('Unsupported query conjuction: ' . $conjunction);
        return FALSE;
    }
    // Process filters.
    foreach ($conditions as $filter) {
      // Check condition type.
      if (!empty($filter['conjunction']) && !empty($filter['conditions'])) {
        // Sub-condition.
        $test = $this->testDrupalConditions(
          $filter['conjunction'],
          $filter['conditions'],
          $mapped_values
        );
      }
      elseif (isset($filter['field'])) {
        // Field filter.
        // @see https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Entity!Query!QueryInterface.php/function/QueryInterface%3A%3Acondition/10
        // Get field and possible property specifications.
        $properties = explode('.', $filter['field']);
        $field = array_shift($properties);
        if (!empty($properties) && is_numeric($properties[0])) {
          // We work on a specific delta.
          $delta = array_shift($properties);
        }
        // Get mapped Drupal field field mapper.
        $field_mapper = $this
          ->externalEntityType
          ->getFieldMapper($field);
        if ($field_mapper) {
          $main_property_name = $field_mapper->getMainPropertyName() ?? '';
        }

        if (empty($mapped_values[$field])) {
          // No corresponding mapped value for the given Drupal field.
          $field_values = [];
        }
        elseif (!empty($properties)) {
          // There are mapped values for the field but there are property
          // specifications.
          // Check for special "delta" test.
          if ('%delta' == $properties[0]) {
            if (2 < count($properties)) {
              // Unsupported.
              $this->logger->warning(
                'Unsupported (ignored) query field specification: '
                . $filter['field']
              );
              continue;
            }
            else {
              // Get all values.
              $field_values = array_map(
                function ($property_set) use ($main_property_name) {
                  return $property_set[$main_property_name] ?? NULL;
                },
                $mapped_values[$field]
              );
              if (2 == count($properties)) {
                // We filter on delta and a property.
                // ->condition('tags.%delta', 0, '>'))
                // ->condition('tags.%delta.value', 'news'))
                // @todo Implement. Use previously filtered values.
                $this->logger->warning(
                  'Filtering on delta for properties is not supported yet. '
                  . print_r($filter, TRUE)
                );
              }
              else {
                // We filter on delta.
                // ->condition('tags.%delta', 0, '>'))
                // @todo Implement. $field_values = array_slice/whatever...
                $this->logger->warning(
                  'Filtering on delta is not supported yet. '
                  . print_r($filter, TRUE)
                );
              }
            }
          }
          else {
            // Work on the given property.
            [$property, $property_type] = explode(':', $properties[0]);
            $field_values = array_map(
              function ($property_set) use ($property) {
                return $property_set[$property] ?? NULL;
              },
              $mapped_values[$field]
            );
            if (isset($delta)) {
              // Only get the given delta value.
              $field_values = [$field_values[$delta]];
            }
            if (isset($property_type)) {
              // Get referenced entities and filter only the matching ones.
              // Case 'tags.entity:taxonomy_term'.
              // @todo Implement...
              $this->logger->warning(
                'Filtering on property/target entity type is not supported yet. '
                . print_r($filter, TRUE)
              );
            }
            if (1 < count($properties)) {
              // Cases 'uid.entity.name' and 'uid.entity:user.name'.
              $this->logger->warning(
                'Filtering on referenced entity properties is not supported yet. '
                . print_r($filter, TRUE)
              );
            }
          }
        }
        else {
          // There are mapped values for the field, get all available values
          // for the main property in a single array.
          $field_values = array_map(
            function ($property_set) use ($main_property_name) {
              return $property_set[$main_property_name] ?? NULL;
            },
            $mapped_values[$field]
          );
          if (isset($delta)) {
            // Only get the given delta value.
            $field_values = [$field_values[$delta]];
          }
        }
        // Drupal behavior: if any of the value matches, the filter passes,
        // but for next tests, only the field item with that matching value
        // will be tested then.
        // @todo We don't have the exact same behavior here since we keep
        // all items for next tests. We should fix that.
        $operator = $filter['operator'] ?? '=';
        $filter_value = $filter['value'] ?? NULL;
        $test = $this->testDrupalFilter(
          $field_values,
          $filter_value,
          $operator
        );
      }
      else {
        // Unsupported.
        $this->logger->warning(
          'Unsupported (ignored) query filter structure: '
          . print_r($filter, TRUE)
        );
        continue;
      }

      if ('and' == $conjunction) {
        if (!$test) {
          // We got a failure, stop.
          $pass = FALSE;
          break;
        }
      }
      else {
        // Conjuction 'or'.
        if ($test) {
          // We got one match, stop.
          $pass = TRUE;
          break;
        }
      }
    }
    return $pass;
  }

  /**
   * Returns TRUE if the given field value passes the given filter.
   *
   * @param array $field_values
   *   An array of field values to test.
   * @param mixed $filter_value
   *   The optional filter value which may be a numeric value, a string, NULL
   *   for "IS (NOT) NULL/(NOT) EXISTS" operators, or an array of 2 numeric
   *   value for "(NOT) BETWEEN" operator, or an array of values for "(NOT) IN"
   *   operator.
   * @param string $operator
   *   The operator to use which should be one of '=', '!=', '<>', '>', '>=',
   *   '<', '<=', 'STARTS_WITH', 'CONTAINS', 'ENDS_WITH', 'IN', 'NOT IN',
   *   'IS NULL', 'IS NOT NULL', 'EXISTS', 'NOT EXISTS', 'BETWEEN' and
   *   'NOT BETWEEN' but the could be other operators like the ones defined in
   *   \Drupal\views\Plugin\views\filter\* plugins.
   *   Note: '!=', 'EXISTS' and 'NOT EXISTS' are not usually used by Drupal and
   *   only supported for convenience.
   *
   * @return bool
   *   TRUE if the given field values matches the given filter value according
   *   to the given filter if supported, FALSE in any other cases.
   */
  protected function testDrupalFilter(
    array $field_values,
    $filter_value,
    string $operator,
  ) :bool {
    // @todo Support views filters defined in
    // \Drupal\views\Plugin\views\filter\* plugins.
    // @todo Add event to let plugins support other filters?
    // @todo Manage field properties field names like "field_geolocation.lat".
    // @todo Drupal default behavior is that just one field item needs to match
    //   but then, only that field item should be kept for next tests: we should
    //   stick to that behavior which is not the case here. Maybe the method
    //   should return NULL when no match and the matching field item when
    //   matching? Does Drupal default behavior filters all the matching field
    //   items or just the first one? It needs to be tested.
    $operator = strtoupper($operator);
    $pass = FALSE;
    foreach ($field_values as $field_value) {
      $not = FALSE;
      if (str_starts_with($operator, 'NOT ')) {
        $not = TRUE;
        $operator = substr($operator, 4);
      }
      switch ($operator) {
        case '=':
          $pass = ($field_value == $filter_value);
          break;

        case '<>':
        case '!=':
          $pass = ($field_value != $filter_value);
          break;

        case '>':
          $pass = ($field_value > $filter_value);
          break;

        case '>=':
          $pass = ($field_value >= $filter_value);
          break;

        case '<':
          $pass = ($field_value < $filter_value);
          break;

        case '<=':
          $pass = ($field_value <= $filter_value);
          break;

        case 'STARTS':
        case 'STARTS_WITH':
          if (is_string($field_value)) {
            $pass =
              (substr($field_value, 0, strlen($filter_value)) == $filter_value);
          }
          break;

        case 'CONTAINS':
          if (is_string($field_value)) {
            $pass = (FALSE !== strpos($field_value, $filter_value));
          }
          break;

        case 'ENDS':
        case 'ENDS_WITH':
          if (is_string($field_value)) {
            $pass =
              (substr($field_value, -1 * strlen($filter_value)) == $filter_value);
          }
          break;

        case 'IN':
          if (is_array($filter_value)) {
            $pass = in_array($field_value, $filter_value);
          }
          break;

        case 'IS NULL':
          $pass = !isset($field_value);
          break;

        case 'EXISTS':
        case 'IS NOT NULL':
          $pass = isset($field_value);
          break;

        case 'BETWEEN':
          if (is_numeric($field_value)
              && is_array($filter_value)
              && (2 == count($filter_value))
              && is_numeric($filter_value[0])
              && is_numeric($filter_value[1])
          ) {
            $pass = ($filter_value[0] <= $field_value)
              && ($field_value <= $filter_value[1]
            );
          }
          break;

        default:
          // For unsupported operators, consider the test did not pass.
          $not = FALSE;
          break;
      }
      if ($not) {
        $pass = !$pass;
      }
      // Stop if we got a matching value.
      if ($pass) {
        break;
      }
    }

    return $pass;
  }

  /**
   * Returns the list of field definition this storage client external entity.
   *
   * @return \Drupal\Core\Field\FieldDefinitionInterface[]
   *   An array of field definitions.
   */
  protected function getFieldDefinitions() :array {
    if (empty($this->externalEntityType)) {
      return [];
    }

    // Get field definitions.
    $xntt_type_id = $this->externalEntityType->getDerivedEntityTypeId();
    $field_defs = $this
      ->entityFieldManager
      ->getFieldDefinitions($xntt_type_id, $xntt_type_id);
    return $field_defs;
  }

  /**
   * Returns the source field name mapped to Drupal external entity identifier.
   *
   * Note: sometimes, the source field used for the Drupal external entity
   * identifier needs to be processed. Using this method to directly access to
   * a source data array value for the identifier field may not provide the real
   * identifier used by Drupal. To get it, consider using self::getProcessedId()
   * method instead.
   *
   * @return string|null
   *   The source field name used to store identifier or NULL if not available.
   */
  public function getSourceIdFieldName() :?string {
    if (empty($this->externalEntityType)
        || $this->externalEntityType->isNew()
        || empty($this->externalEntityType->getFieldMapper('id'))
    ) {
      return NULL;
    }
    return $this
      ->externalEntityType
      ->getFieldMapper('id')
      ->getMappedSourceFieldName('value');
  }

  /**
   * Returns the processed value for Drupal external entity identifier.
   *
   * Sometimes, the source field used to map Drupal external entity identifier
   * needs some processing (data processor). This method returns the processed
   * identifier (or the raw value of the mapped id field if no external entity
   * type is set). If the identifier field is not available, NULL is returned.
   *
   * @return string|null
   *   The identifier value used by Drupal or NULL if not available.
   */
  public function getProcessedId(array $raw_entity) :?string {
    if (!empty($this->externalEntityType)) {
      $storage = \Drupal::entityTypeManager()->getStorage(
        $this->externalEntityType->getDerivedEntityTypeId()
      );
      $id_values = $storage->extractEntityValuesFromRawData($raw_entity, ['id']);
      return $id_values['id'][0]['value'] ?? NULL;
    }
    else {
      return $raw_entity[$this->getSourceIdFieldName()] ?? NULL;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getRequestedDrupalFields() :array {
    // No request by default.
    return [];
  }

  /**
   * {@inheritdoc}
   */
  public function getRequestedMapping(string $field_name, string $field_type) :array {
    // No request by default.
    return [];
  }

}

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

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