Newer
Older
declare(strict_types = 1);
namespace Drupal\migrate_plus\Plugin\migrate\process;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Query\QueryInterface;

Edouard Cunibil
committed
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* This plugin looks for existing entities.
*
* In its most simple form, this plugin needs no configuration, and determines
* the configuration automatically. This requires the migration's process to
* define a default value for the destination entity's bundle key, and the
* destination field this plugin is on to be a supported type.

Lucas Hedding
committed
* Available configuration keys:
* - entity_type: (optional) The ID of the entity type to query for.
* - value_key: (optional) The name of the entity field on which the source
* value will be queried. If omitted, defaults to one of the following
* depending on the destination field type:
* - entity_reference: The entity label key.
* - file: The uri field.
* - image: The uri field.

Ivan Doroshenko
committed
* - operator: (optional) The comparison operator supported by entity query:
* See \Drupal\Core\Entity\Query\QueryInterface::condition() for available
* values. Defaults to '=' for scalar values and 'IN' for arrays.
* - bundle_key: (optional) The name of the bundle field on the entity type
* being queried.

Ivan Doroshenko
committed
* - bundle: (optional) The value to query for the bundle - can be a string or
* an array.

Lucas Hedding
committed
* - access_check: (optional) Indicates if access to the entity for this user
* will be checked. Default is true.
* - ignore_case: (optional) Whether to ignore case in the query. Defaults to

Ivan Doroshenko
committed
* false, meaning the query is case-sensitive by default. Works only with
* strict operators: '=' and 'IN'.
* - destination_field: (optional) If specified, and if the plugin's source
* value is an array, the result array's items will be themselves arrays of
* the form [destination_field => ENTITY_ID].

Lucas Hedding
committed
*
* @codingStandardsIgnoreStart
*
* Example usage with minimal configuration:
* @code
* destination:
* plugin: 'entity:node'
* process:
* type:
* plugin: default_value
* default_value: page
* field_tags:
* plugin: entity_lookup

Lucas Hedding
committed
* access_check: false
* source: tags
* @endcode
* In this example above, the access check is disabled.

Lucas Hedding
committed
*
* Example usage with full configuration:
* @code
* field_tags:
* plugin: entity_lookup
* source: tags
* value_key: name
* bundle_key: vid
* bundle: tags
* entity_type: taxonomy_term
* ignore_case: true

Ivan Doroshenko
committed
* operator: STARTS_WITH
* @endcode

Lucas Hedding
committed
*

Ivan Doroshenko
committed
* @see \Drupal\Core\Entity\Query\QueryInterface::condition()
*

Lucas Hedding
committed
* @MigrateProcessPlugin(
* id = "entity_lookup",
* handle_multiples = TRUE
* )
*/
class EntityLookup extends ProcessPluginBase implements ContainerFactoryPluginInterface {
protected ?EntityTypeManagerInterface $entityTypeManager;
protected EntityFieldManagerInterface $entityFieldManager;
protected MigrationInterface $migration;
protected SelectionPluginManagerInterface $selectionPluginManager;
protected ?string $destinationEntityType;
protected ?string $destinationBundleKey = NULL;
protected ?string $lookupValueKey = NULL;
protected ?string $lookupBundleKey = NULL;
protected $lookupBundle = NULL;
protected ?string $lookupEntityType = NULL;
protected ?string $destinationProperty;
protected bool $accessCheck = TRUE;

Lucas Hedding
committed
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition, MigrationInterface $migration = NULL) {

Lucas Hedding
committed
$instance = new static(
$configuration,
$pluginId,

Lucas Hedding
committed
$pluginDefinition

Lucas Hedding
committed
$instance->migration = $migration;
$instance->entityTypeManager = $container->get('entity_type.manager');
$instance->entityFieldManager = $container->get('entity_field.manager');
$instance->selectionPluginManager = $container->get('plugin.manager.entity_reference_selection');
$pluginIdParts = explode(':', $instance->migration->getDestinationPlugin()->getPluginId());
$instance->destinationEntityType = empty($pluginIdParts[1]) ? NULL : $pluginIdParts[1];
$instance->destinationBundleKey = $instance->destinationEntityType ? $instance->entityTypeManager->getDefinition($instance->destinationEntityType)->getKey('bundle') : NULL;
return $instance;
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {

Edouard Cunibil
committed
// If the source data is an empty array, return the same.
if (gettype($value) === 'array' && count($value) === 0) {
return [];
}

Mike Ryan
committed
// In case of subfields ('field_reference/target_id'), extract the field
// name only.
$parts = explode('/', $destination_property);
$destination_property = reset($parts);
$this->determineLookupProperties($destination_property);
$this->destinationProperty = $this->configuration['destination_field'] ?? NULL;
return $this->query($value);
}
/**
* Determine the lookup properties from config or target field configuration.
*
* @param string $destinationProperty
* The destination property currently worked on. This is only used together
* with the $row above.
*/
protected function determineLookupProperties(string $destinationProperty): void {

Lucas Hedding
committed
if (isset($this->configuration['access_check'])) {
$this->accessCheck = (bool) $this->configuration['access_check'];

Lucas Hedding
committed
}
if (!empty($this->configuration['value_key'])) {
$this->lookupValueKey = $this->configuration['value_key'];
}
if (!empty($this->configuration['bundle_key'])) {
$this->lookupBundleKey = $this->configuration['bundle_key'];
}
if (!empty($this->configuration['bundle'])) {
$this->lookupBundle = $this->configuration['bundle'];
}
if (!empty($this->configuration['entity_type'])) {
$this->lookupEntityType = $this->configuration['entity_type'];
}
if (empty($this->lookupValueKey) || empty($this->lookupBundleKey) || empty($this->lookupBundle) || empty($this->lookupEntityType)) {
// See if we can introspect the lookup properties from destination field.
if (!empty($this->migration->getProcess()[$this->destinationBundleKey][0]['default_value'])) {
$destinationEntityBundle = $this->migration->getProcess()[$this->destinationBundleKey][0]['default_value'];

Lucas Hedding
committed
$fieldConfig = $this->entityFieldManager->getFieldDefinitions($this->destinationEntityType, $destinationEntityBundle)[$destinationProperty]->getConfig($destinationEntityBundle);

Mike Ryan
committed
switch ($fieldConfig->getType()) {
case 'entity_reference':
if (empty($this->lookupBundle)) {
$handlerSettings = $fieldConfig->getSetting('handler_settings');

Lucas Hedding
committed
$bundles = array_filter((array) ($handlerSettings['target_bundles'] ?? []));

Mike Ryan
committed
if (count($bundles) == 1) {
$this->lookupBundle = reset($bundles);
}
// This was added in 8.1.x is not supported in 8.0.x.
elseif (!empty($handlerSettings['auto_create']) && !empty($handlerSettings['auto_create_bundle'])) {
$this->lookupBundle = reset($handlerSettings['auto_create_bundle']);
}
}
// Make an assumption that if the selection handler can target more
// than one type of entity that we will use the first entity type.

Lucas Hedding
committed
$fieldHandler = $fieldConfig->getSetting('handler');
$selection = $this->selectionPluginManager->createInstance($fieldHandler);
$this->lookupEntityType = $this->lookupEntityType ?: reset($selection->getPluginDefinition()['entity_types']);
$this->lookupValueKey = $this->lookupValueKey ?: $this->entityTypeManager->getDefinition($this->lookupEntityType)->getKey('label');
$this->lookupBundleKey = $this->lookupBundleKey ?: $this->entityTypeManager->getDefinition($this->lookupEntityType)->getKey('bundle');

Mike Ryan
committed
break;
case 'file':
case 'image':
$this->lookupEntityType = 'file';
$this->lookupValueKey = $this->lookupValueKey ?: 'uri';
break;
default:
throw new MigrateException(sprintf('Destination field type %s is not a recognized reference type.', $fieldConfig->getType()));
}
}
}
// If there aren't enough lookup properties available by now, then bail.
if (empty($this->lookupValueKey)) {
throw new MigrateException('The entity_lookup plugin requires a value_key, none located.');
}
if (!empty($this->lookupBundleKey) && empty($this->lookupBundle)) {
throw new MigrateException('The entity_lookup plugin found no bundle but destination entity requires one.');
}
if (empty($this->lookupEntityType)) {
throw new MigrateException('The entity_lookup plugin requires a entity_type, none located.');
}
}
/**
* Checks for the existence of some value.
*
* @param mixed $value
* The value to query.
*
* @return mixed|null
* Entity id if the queried entity exists. Otherwise NULL.
*/
protected function query($value) {
$query = $this->doGetQuery($value);
return $this->processResults($query->execute(), $value);
}
private function doGetQuery($value): QueryInterface {

Ivan Doroshenko
committed
$operator = !empty($this->configuration['operator']) ? $this->configuration['operator'] : '=';
$multiple = is_array($value);

Ivan Doroshenko
committed
// Apply correct operator for multiple values.
if ($multiple && $operator === '=') {
$operator = 'IN';
}

Lucas Hedding
committed
$query = $this->entityTypeManager->getStorage($this->lookupEntityType)
->getQuery()

Lucas Hedding
committed
->accessCheck($this->accessCheck)

Ivan Doroshenko
committed
->condition($this->lookupValueKey, $value, $operator);

Lucas Hedding
committed
// Sqlite and possibly others returns data in a non-deterministic order.
// Make it deterministic.
if ($multiple) {
$query->sort($this->lookupValueKey, 'DESC');
}
if ($this->lookupBundleKey) {

Ivan Doroshenko
committed
$query->condition($this->lookupBundleKey, (array) $this->lookupBundle, 'IN');
return $query;
}
private function processResults($results, $original_value) {
if (empty($results)) {
return NULL;
}
// Entity queries typically are case-insensitive. Therefore, we need to
// handle case-sensitive filtering as a post-query step. By default, it
// filters case-insensitive. Change to true if that is not the desired
// outcome.
$ignoreCase = !empty($this->configuration['ignore_case']) ?: FALSE;
$operator = !empty($this->configuration['operator']) ? $this->configuration['operator'] : '=';
$multiple = is_array($original_value);

Ivan Doroshenko
committed
// Do a case-sensitive comparison only for strict operators.
if (!$ignoreCase && in_array($operator, ['=', 'IN'], TRUE)) {
// Returns the entity's identifier.

Bec
committed
foreach ($results as $k => $identifier) {

Lucas Hedding
committed
$entity = $this->entityTypeManager->getStorage($this->lookupEntityType)->load($identifier);

Edouard Cunibil
committed
$result_value = $entity->get($this->lookupValueKey);
// If the value is a non-empty field, extract its first value's main
// property (most of the time "value" but sometimes "target_id" or
// anything declared by the field item).
if ($result_value instanceof FieldItemList && !$result_value->isEmpty()) {
$property = $result_value->first()->mainPropertyName();
$result_value = $result_value->{$property};
}
if (($multiple && !in_array($result_value, $original_value, TRUE)) || (!$multiple && $result_value !== $original_value)) {

Bec
committed
unset($results[$k]);
}
}
}

Bec
committed
if ($multiple && !empty($this->destinationProperty)) {
array_walk($results, function (&$value): void {

Bec
committed
$value = [$this->destinationProperty => $value];
});
}
return $multiple ? array_values($results) : reset($results);