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 [];
}
}
