Skip to content
EntityLookup.php 11.5 KiB
Newer Older
namespace Drupal\migrate_plus\Plugin\migrate\process;

use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
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.
 * - 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.
 * - 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.
 * - bundle: (optional) The value to query for the bundle - can be a string or
 *   an array.
 * - 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
 *   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].
 * @codingStandardsIgnoreStart
 *
 * Example usage with minimal configuration:
 * @code
 * destination:
 *   plugin: 'entity:node'
 * process:
 *   type:
 *     plugin: default_value
 *     default_value: page
 *   field_tags:
 *     plugin: entity_lookup
 * In this example above, the access check is disabled.
 * 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
 * @codingStandardsIgnoreEnd
 *
 * @see \Drupal\Core\Entity\Query\QueryInterface::condition()
 *
 * @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 ?string $lookupEntityType = NULL;
  protected ?string $destinationProperty;
  protected bool $accessCheck = TRUE;
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition, MigrationInterface $migration = NULL) {
    $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;
  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
    // If the source data is an empty array, return the same.
    if (gettype($value) === 'array' && count($value) === 0) {
      return [];
    }

    // 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 {
    if (isset($this->configuration['access_check'])) {
      $this->accessCheck = (bool) $this->configuration['access_check'];
    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'];
        $fieldConfig = $this->entityFieldManager->getFieldDefinitions($this->destinationEntityType, $destinationEntityBundle)[$destinationProperty]->getConfig($destinationEntityBundle);
        switch ($fieldConfig->getType()) {
          case 'entity_reference':
            if (empty($this->lookupBundle)) {
              $handlerSettings = $fieldConfig->getSetting('handler_settings');
              $bundles = array_filter((array) $handlerSettings['target_bundles']);
              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.
            $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');
            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) {
    // 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'] : '=';
    // Apply correct operator for multiple values.
    if ($multiple && $operator === '=') {
      $operator = 'IN';
    }

    $query = $this->entityTypeManager->getStorage($this->lookupEntityType)
      ->condition($this->lookupValueKey, $value, $operator);
    // Sqlite and possibly others returns data in a non-deterministic order.
    // Make it deterministic.
    if ($multiple) {
      $query->sort($this->lookupValueKey, 'DESC');
    }
      $query->condition($this->lookupBundleKey, (array) $this->lookupBundle, 'IN');
    }
    $results = $query->execute();

    if (empty($results)) {
      return NULL;
    }

    // Do a case-sensitive comparison only for strict operators.
    if (!$ignoreCase && in_array($operator, ['=', 'IN'], TRUE)) {
      // Returns the entity's identifier.
        $entity = $this->entityTypeManager->getStorage($this->lookupEntityType)->load($identifier);
        $result_value = $entity instanceof ConfigEntityInterface ? $entity->get($this->lookupValueKey) : $entity->get($this->lookupValueKey)->value;
        if (($multiple && !in_array($result_value, $value, TRUE)) || (!$multiple && $result_value !== $value)) {
          unset($results[$k]);
    if ($multiple && !empty($this->destinationProperty)) {
      array_walk($results, function (&$value): void {
        $value = [$this->destinationProperty => $value];
      });
    }

    return $multiple ? array_values($results) : reset($results);