You are here

abstract class EntityProcessorBase in Feeds 8.3

Defines a base entity processor.

Creates entities from feed items.

Hierarchy

Expanded class hierarchy of EntityProcessorBase

2 files declare their use of EntityProcessorBase
EntityProcessorBaseTest.php in tests/src/Kernel/Feeds/Processor/EntityProcessorBaseTest.php
EntityTestProcessor.php in tests/modules/feeds_test_plugin/src/Feeds/Processor/EntityTestProcessor.php

File

src/Feeds/Processor/EntityProcessorBase.php, line 41

Namespace

Drupal\feeds\Feeds\Processor
View source
abstract class EntityProcessorBase extends ProcessorBase implements EntityProcessorInterface, ContainerFactoryPluginInterface, MappingPluginFormInterface {

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

  /**
   * The entity storage controller for the entity type being processed.
   *
   * @var \Drupal\Core\Entity\EntityStorageInterface
   */
  protected $storageController;

  /**
   * The entity info for the selected entity type.
   *
   * @var \Drupal\Core\Entity\EntityTypeInterface
   */
  protected $entityType;

  /**
   * Flag indicating that this processor is locked.
   *
   * @var bool
   */
  protected $isLocked;

  /**
   * The entity type bundle info.
   *
   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
   */
  protected $entityTypeBundleInfo;

  /**
   * The language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * Constructs an EntityProcessorBase object.
   *
   * @param array $configuration
   *   The plugin configuration.
   * @param string $plugin_id
   *   The plugin id.
   * @param array $plugin_definition
   *   The plugin definition.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
   *   The entity type bundle info.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager.
   */
  public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, LanguageManagerInterface $language_manager) {
    $this->entityTypeManager = $entity_type_manager;
    $this->entityType = $entity_type_manager
      ->getDefinition($plugin_definition['entity_type']);
    $this->storageController = $entity_type_manager
      ->getStorage($plugin_definition['entity_type']);
    $this->entityTypeBundleInfo = $entity_type_bundle_info;
    $this->languageManager = $language_manager;
    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('entity_type.manager'), $container
      ->get('entity_type.bundle.info'), $container
      ->get('language_manager'));
  }

  /**
   * {@inheritdoc}
   */
  public function process(FeedInterface $feed, ItemInterface $item, StateInterface $state) {

    // Initialize clean list if needed.
    $clean_state = $feed
      ->getState(StateInterface::CLEAN);
    if (!$clean_state
      ->initiated()) {
      $this
        ->initCleanList($feed, $clean_state);
    }
    $skip_new = $this->configuration['insert_new'] == static::SKIP_NEW;
    $existing_entity_id = $this
      ->existingEntityId($feed, $item);
    $skip_existing = $this->configuration['update_existing'] == static::SKIP_EXISTING;

    // If the entity is an existing entity it must be removed from the clean
    // list.
    if ($existing_entity_id) {
      $clean_state
        ->removeItem($existing_entity_id);
    }

    // Bulk load existing entities to save on db queries.
    if ($skip_existing && $existing_entity_id || !$existing_entity_id && $skip_new) {
      $state->skipped++;
      return;
    }

    // Delay building a new entity until necessary.
    if ($existing_entity_id) {
      $entity = $this->storageController
        ->load($existing_entity_id);
    }
    $hash = $this
      ->hash($item);
    $changed = $existing_entity_id && $hash !== $entity
      ->get('feeds_item')->hash;

    // Do not proceed if the item exists, has not changed, and we're not
    // forcing the update.
    if ($existing_entity_id && !$changed && !$this->configuration['skip_hash_check']) {
      $state->skipped++;
      return;
    }

    // Build a new entity.
    if (!$existing_entity_id && !$skip_new) {
      $entity = $this
        ->newEntity($feed);
    }
    try {

      // Set feeds_item values.
      $feeds_item = $entity
        ->get('feeds_item');
      $feeds_item->target_id = $feed
        ->id();
      $feeds_item->hash = $hash;

      // Set field values.
      $this
        ->map($feed, $entity, $item);

      // Validate the entity.
      $feed
        ->dispatchEntityEvent(FeedsEvents::PROCESS_ENTITY_PREVALIDATE, $entity, $item);
      $this
        ->entityValidate($entity);

      // Dispatch presave event.
      $feed
        ->dispatchEntityEvent(FeedsEvents::PROCESS_ENTITY_PRESAVE, $entity, $item);

      // This will throw an exception on failure.
      $this
        ->entitySaveAccess($entity);

      // Set imported time.
      $entity
        ->get('feeds_item')->imported = \Drupal::service('datetime.time')
        ->getRequestTime();

      // And... Save! We made it.
      $this->storageController
        ->save($entity);

      // Dispatch postsave event.
      $feed
        ->dispatchEntityEvent(FeedsEvents::PROCESS_ENTITY_POSTSAVE, $entity, $item);

      // Track progress.
      $existing_entity_id ? $state->updated++ : $state->created++;
    } catch (EmptyFeedException $e) {

      // Not an error.
      $state->skipped++;
    } catch (ValidationException $e) {
      $state->failed++;
      $state
        ->setMessage($e
        ->getFormattedMessage(), 'warning');
    } catch (\Exception $e) {
      $state->failed++;
      $state
        ->setMessage($e
        ->getMessage(), 'warning');
    }
  }

  /**
   * Initializes the list of entities to clean.
   *
   * This populates $state->cleanList with all existing entities previously
   * imported from the source.
   *
   * @param \Drupal\feeds\FeedInterface $feed
   *   The feed to import.
   * @param \Drupal\feeds\Feeds\State\CleanStateInterface $state
   *   The state of the clean stage.
   */
  protected function initCleanList(FeedInterface $feed, CleanStateInterface $state) {
    $state
      ->setEntityTypeId($this
      ->entityType());

    // Fill the list only if needed.
    if ($this
      ->getConfiguration('update_non_existent') === static::KEEP_NON_EXISTENT) {
      return;
    }

    // Set list of entities to clean.
    $ids = $this->entityTypeManager
      ->getStorage($this
      ->entityType())
      ->getQuery()
      ->condition('feeds_item.target_id', $feed
      ->id())
      ->condition('feeds_item.hash', $this
      ->getConfiguration('update_non_existent'), '<>')
      ->execute();
    $state
      ->setList($ids);

    // And set progress.
    $state->total = $state
      ->count();
    $state
      ->progress($state->total, 0);
  }

  /**
   * {@inheritdoc}
   */
  public function clean(FeedInterface $feed, EntityInterface $entity, CleanStateInterface $state) {
    $update_non_existent = $this
      ->getConfiguration('update_non_existent');
    if ($update_non_existent === static::KEEP_NON_EXISTENT) {

      // No action to take on this entity.
      return;
    }
    switch ($update_non_existent) {
      case static::KEEP_NON_EXISTENT:

        // No action to take on this entity.
        return;
      case static::DELETE_NON_EXISTENT:
        $entity
          ->delete();
        break;
      default:
        try {

          // Apply action on entity.
          \Drupal::service('plugin.manager.action')
            ->createInstance($update_non_existent)
            ->execute($entity);
        } catch (PluginNotFoundException $e) {
          $state
            ->setMessage(t('Cleaning %entity failed because of non-existing action plugin %name.', [
            '%entity' => $entity
              ->label(),
            '%name' => $update_non_existent,
          ]), 'error');
          throw $e;
        }
        break;
    }

    // Check if the entity was deleted.
    $entity = $this->storageController
      ->load($entity
      ->id());

    // If the entity was not deleted, update hash.
    if (isset($entity->feeds_item)) {
      $entity
        ->get('feeds_item')->hash = $update_non_existent;
      $this->storageController
        ->save($entity);
    }

    // State progress.
    $state->updated++;
    $state
      ->progress($state->total, $state->updated);
  }

  /**
   * {@inheritdoc}
   */
  public function clear(FeedInterface $feed, StateInterface $state) {

    // Build base select statement.
    $query = $this->entityTypeManager
      ->getStorage($this
      ->entityType())
      ->getQuery()
      ->condition('feeds_item.target_id', $feed
      ->id());

    // If there is no total, query it.
    if (!$state->total) {
      $count_query = clone $query;
      $state->total = (int) $count_query
        ->count()
        ->execute();
    }

    // Delete a batch of entities.
    $entity_ids = $query
      ->range(0, 10)
      ->execute();
    if ($entity_ids) {
      $this
        ->entityDeleteMultiple($entity_ids);
      $state->deleted += count($entity_ids);
      $state
        ->progress($state->total, $state->deleted);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function entityType() {
    return $this->pluginDefinition['entity_type'];
  }

  /**
   * The entity's bundle key.
   *
   * @return string|null
   *   The bundle type this processor operates on, or null if it is undefined.
   */
  public function bundleKey() {
    return $this->entityType
      ->getKey('bundle');
  }

  /**
   * Bundle type this processor operates on.
   *
   * Defaults to the entity type for entities that do not define bundles.
   *
   * @return string|null
   *   The bundle type this processor operates on, or null if it is undefined.
   *
   * @todo We should be more careful about missing bundles.
   */
  public function bundle() {
    if (!($bundle_key = $this->entityType
      ->getKey('bundle'))) {
      return $this
        ->entityType();
    }
    if (isset($this->configuration['values'][$bundle_key])) {
      return $this->configuration['values'][$bundle_key];
    }
  }

  /**
   * Returns the bundle label for the entity being processed.
   *
   * @return string
   *   The bundle label.
   */
  public function bundleLabel() {
    if ($label = $this->entityType
      ->getBundleLabel()) {
      return $label;
    }
    return $this
      ->t('Bundle');
  }

  /**
   * Provides a list of bundle options for use in select lists.
   *
   * @return array
   *   A keyed array of bundle => label.
   */
  public function bundleOptions() {
    $options = [];
    foreach ($this->entityTypeBundleInfo
      ->getBundleInfo($this
      ->entityType()) as $bundle => $info) {
      if (!empty($info['label'])) {
        $options[$bundle] = $info['label'];
      }
      else {
        $options[$bundle] = $bundle;
      }
    }
    return $options;
  }

  /**
   * Provides a list of languages available on the site.
   *
   * @return array
   *   A keyed array of language_key => language_name.
   *   For example: 'en' => 'English').
   */
  public function languageOptions() {
    foreach ($this->languageManager
      ->getLanguages(LanguageInterface::STATE_ALL) as $language) {
      $langcodes[$language
        ->getId()] = $language
        ->getName();
    }
    return $langcodes;
  }

  /**
   * Returns the label of the entity type being processed.
   *
   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
   *   The label of the entity type.
   */
  public function entityTypeLabel() {
    return $this->entityType
      ->getLabel();
  }

  /**
   * Returns the plural label of the entity type being processed.
   *
   * @return string
   *   The plural label of the entity type.
   */
  public function entityTypeLabelPlural() {
    return $this->entityType
      ->getPluralLabel();
  }

  /**
   * Returns the label for items being created, updated, or deleted.
   *
   * @return string
   *   The item label.
   */
  public function getItemLabel() {
    if (!$this->entityType
      ->getKey('bundle') || !$this->entityType
      ->getBundleEntityType()) {
      return $this
        ->entityTypeLabel();
    }
    $storage = $this->entityTypeManager
      ->getStorage($this->entityType
      ->getBundleEntityType());
    return $storage
      ->load($this->configuration['values'][$this->entityType
      ->getKey('bundle')])
      ->label();
  }

  /**
   * Returns the plural label for items being created, updated, or deleted.
   *
   * @return string
   *   The plural item label.
   */
  public function getItemLabelPlural() {
    if (!$this->entityType
      ->getKey('bundle') || !$this->entityType
      ->getBundleEntityType()) {
      return $this
        ->entityTypeLabelPlural();
    }

    // Entity bundles do not support plural labels yet.
    // @todo Fix after https://www.drupal.org/project/drupal/issues/2765065.
    $storage = $this->entityTypeManager
      ->getStorage($this->entityType
      ->getBundleEntityType());
    $label = $storage
      ->load($this->configuration['values'][$this->entityType
      ->getKey('bundle')])
      ->label();
    return $this
      ->t('@label items', [
      '@label' => $label,
    ]);
  }

  /**
   * {@inheritdoc}
   */
  protected function newEntity(FeedInterface $feed) {
    $values = $this->configuration['values'];
    $entity = $this->storageController
      ->create($values);
    $entity
      ->enforceIsNew();
    if ($entity instanceof EntityOwnerInterface) {
      if ($this->configuration['owner_feed_author']) {
        $entity
          ->setOwnerId($feed
          ->getOwnerId());
      }
      else {
        $entity
          ->setOwnerId($this->configuration['owner_id']);
      }
    }

    // Set language if the entity type has a field for it.
    if ($this->entityType
      ->hasKey('langcode')) {
      $entity->{$this->entityType
        ->getKey('langcode')} = $this
        ->entityLanguage();
    }
    return $entity;
  }

  /**
   * {@inheritdoc}
   */
  public function getEntityTranslation(FeedInterface $feed, TranslatableInterface $entity, $langcode) {
    if (!$entity
      ->hasTranslation($langcode)) {
      $translation = $entity
        ->addTranslation($langcode);
      if ($translation instanceof EntityOwnerInterface) {
        if ($this->configuration['owner_feed_author']) {
          $translation
            ->setOwnerId($feed
            ->getOwnerId());
        }
        else {
          $translation
            ->setOwnerId($this->configuration['owner_id']);
        }
      }
      return $translation;
    }
    return $entity
      ->getTranslation($langcode);
  }

  /**
   * Checks if the entity exists already.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to check.
   *
   * @return bool
   *   True if the entity already exists, false otherwise.
   */
  protected function entityExists(EntityInterface $entity) {
    if ($entity
      ->id()) {
      $result = $this->storageController
        ->getQuery()
        ->condition($this->entityType
        ->getKey('id'), $entity
        ->id())
        ->execute();
      return !empty($result);
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  protected function entityValidate(EntityInterface $entity) {

    // Check if an entity with the same ID already exists if the given entity is
    // new.
    if ($entity
      ->isNew() && $this
      ->entityExists($entity)) {
      throw new ValidationException($this
        ->t('An entity with ID %id already exists.', [
        '%id' => $entity
          ->id(),
      ]));
    }
    $violations = $entity
      ->validate();
    if (!count($violations)) {
      return;
    }
    $errors = [];
    foreach ($violations as $violation) {
      $error = $violation
        ->getMessage();

      // Try to add more context to the message.
      // @todo if an exception occurred because of a different bundle, add more
      // context to the message.
      $invalid_value = $violation
        ->getInvalidValue();
      if ($invalid_value instanceof FieldItemListInterface) {

        // The invalid value is a field. Get more information about this field.
        $error = new FormattableMarkup('@name (@property_name): @error', [
          '@name' => $invalid_value
            ->getFieldDefinition()
            ->getLabel(),
          '@property_name' => $violation
            ->getPropertyPath(),
          '@error' => $error,
        ]);
      }
      else {
        $error = new FormattableMarkup('@property_name: @error', [
          '@property_name' => $violation
            ->getPropertyPath(),
          '@error' => $error,
        ]);
      }
      $errors[] = $error;
    }
    $element = [
      '#theme' => 'item_list',
      '#items' => $errors,
    ];

    // Compose error message. If available, use the entity label to indicate
    // which item failed. Fallback to the GUID value (if available) or else
    // no indication.
    $label = $entity
      ->label();
    $guid = $entity
      ->get('feeds_item')->guid;
    $messages = [];
    $args = [
      '@entity' => mb_strtolower($this
        ->entityTypeLabel()),
      '%label' => $label,
      '%guid' => $guid,
      '@errors' => \Drupal::service('renderer')
        ->renderRoot($element),
      ':url' => $this
        ->url('entity.feeds_feed_type.mapping', [
        'feeds_feed_type' => $this->feedType
          ->id(),
      ]),
    ];
    if ($label || $label === '0' || $label === 0) {
      $messages[] = $this
        ->t('The @entity %label failed to validate with the following errors: @errors', $args);
    }
    elseif ($guid || $guid === '0' || $guid === 0) {
      $messages[] = $this
        ->t('The @entity with GUID %guid failed to validate with the following errors: @errors', $args);
    }
    else {
      $messages[] = $this
        ->t('An entity of type "@entity" failed to validate with the following errors: @errors', $args);
    }
    $messages[] = $this
      ->t('Please check your <a href=":url">mappings</a>.', $args);

    // Concatenate strings as markup to mark them as safe.
    $message_element = [
      '#markup' => implode("\n", $messages),
    ];
    $message = \Drupal::service('renderer')
      ->renderRoot($message_element);
    throw new ValidationException($message);
  }

  /**
   * {@inheritdoc}
   */
  protected function entitySaveAccess(EntityInterface $entity) {

    // No need to authorize.
    if (!$this->configuration['authorize'] || !$entity instanceof EntityOwnerInterface) {
      return;
    }

    // If the uid was mapped directly, rather than by email or username, it
    // could be invalid.
    $account = $entity
      ->getOwner();
    if (!$account) {
      $owner_id = $entity
        ->getOwnerId();
      if ($owner_id == 0) {

        // We don't check access for anonymous users.
        return;
      }
      throw new EntityAccessException($this
        ->t('Invalid user with ID %uid mapped to %label.', [
        '%uid' => $owner_id,
        '%label' => $entity
          ->label(),
      ]));
    }

    // We don't check access for anonymous users.
    if ($account
      ->isAnonymous()) {
      return;
    }
    $op = $entity
      ->isNew() ? 'create' : 'update';

    // Access granted.
    if ($entity
      ->access($op, $account)) {
      return;
    }
    $args = [
      '%name' => $account
        ->getDisplayName(),
      '@op' => $op,
      '@bundle' => $this
        ->getItemLabelPlural(),
    ];
    throw new EntityAccessException($this
      ->t('User %name is not authorized to @op @bundle.', $args));
  }

  /**
   * {@inheritdoc}
   */
  public function entityLanguage() {
    $langcodes = $this
      ->languageOptions();
    if (isset($this->configuration['langcode']) && isset($langcodes[$this->configuration['langcode']])) {
      return $this->configuration['langcode'];
    }

    // Return default language.
    return $this->languageManager
      ->getDefaultLanguage()
      ->getId();
  }

  /**
   * {@inheritdoc}
   */
  protected function entityDeleteMultiple(array $entity_ids) {
    $entities = $this->storageController
      ->loadMultiple($entity_ids);
    $this->storageController
      ->delete($entities);
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    $defaults = [
      'insert_new' => static::INSERT_NEW,
      'update_existing' => static::SKIP_EXISTING,
      'update_non_existent' => static::KEEP_NON_EXISTENT,
      'skip_hash_check' => FALSE,
      'values' => [],
      'authorize' => $this->entityType
        ->entityClassImplements('Drupal\\user\\EntityOwnerInterface'),
      'expire' => static::EXPIRE_NEVER,
      'owner_id' => 0,
      'owner_feed_author' => 0,
    ];

    // Bundle.
    if ($bundle_key = $this->entityType
      ->getKey('bundle')) {
      $defaults['values'] = [
        $bundle_key => NULL,
      ];
    }

    // Language.
    if ($langcode_key = $this->entityType
      ->getKey('langcode')) {
      $defaults['langcode'] = $this->languageManager
        ->getDefaultLanguage()
        ->getId();
    }
    return $defaults;
  }

  /**
   * {@inheritdoc}
   */
  public function onFeedTypeSave($update = TRUE) {
    $this
      ->prepareFeedsItemField();
  }

  /**
   * {@inheritdoc}
   */
  public function onFeedTypeDelete() {
    $this
      ->removeFeedItemField();
  }

  /**
   * Prepares the feeds_item field.
   *
   * @todo How does ::load() behave for deleted fields?
   */
  protected function prepareFeedsItemField() {

    // Do not create field when syncing configuration.
    if (\Drupal::isConfigSyncing()) {
      return FALSE;
    }

    // Create field if it doesn't exist.
    if (!FieldStorageConfig::loadByName($this
      ->entityType(), 'feeds_item')) {
      FieldStorageConfig::create([
        'field_name' => 'feeds_item',
        'entity_type' => $this
          ->entityType(),
        'type' => 'feeds_item',
        'translatable' => FALSE,
      ])
        ->save();
    }

    // Create field instance if it doesn't exist.
    if (!FieldConfig::loadByName($this
      ->entityType(), $this
      ->bundle(), 'feeds_item')) {
      FieldConfig::create([
        'label' => 'Feeds item',
        'description' => '',
        'field_name' => 'feeds_item',
        'entity_type' => $this
          ->entityType(),
        'bundle' => $this
          ->bundle(),
      ])
        ->save();
    }
  }

  /**
   * Deletes the feeds_item field.
   */
  protected function removeFeedItemField() {
    $storage_in_use = FALSE;
    $instance_in_use = FALSE;
    foreach (FeedType::loadMultiple() as $feed_type) {
      if ($feed_type
        ->id() === $this->feedType
        ->id()) {
        continue;
      }
      $processor = $feed_type
        ->getProcessor();
      if (!$processor instanceof EntityProcessorInterface) {
        continue;
      }
      if ($processor
        ->entityType() === $this
        ->entityType()) {
        $storage_in_use = TRUE;
        if ($processor
          ->bundle() === $this
          ->bundle()) {
          $instance_in_use = TRUE;
          break;
        }
      }
    }
    if ($instance_in_use) {
      return;
    }

    // Delete the field instance.
    if ($config = FieldConfig::loadByName($this
      ->entityType(), $this
      ->bundle(), 'feeds_item')) {
      $config
        ->delete();
    }
    if ($storage_in_use) {
      return;
    }

    // Delte the field storage.
    if ($storage = FieldStorageConfig::loadByName($this
      ->entityType(), 'feeds_item')) {
      $storage
        ->delete();
    }
  }

  /**
   * {@inheritdoc}
   */
  public function expiryTime() {
    return $this->configuration['expire'];
  }

  /**
   * {@inheritdoc}
   */
  public function getExpiredIds(FeedInterface $feed, $time = NULL) {
    if ($time === NULL) {
      $time = $this
        ->expiryTime();
    }
    if ($time == static::EXPIRE_NEVER) {
      return;
    }
    $expire_time = \Drupal::service('datetime.time')
      ->getRequestTime() - $time;
    return $this->entityTypeManager
      ->getStorage($this
      ->entityType())
      ->getQuery()
      ->condition('feeds_item.target_id', $feed
      ->id())
      ->condition('feeds_item.imported', $expire_time, '<')
      ->execute();
  }

  /**
   * {@inheritdoc}
   */
  public function expireItem(FeedInterface $feed, $item_id, StateInterface $state) {
    $this
      ->entityDeleteMultiple([
      $item_id,
    ]);
    $state->total++;
  }

  /**
   * {@inheritdoc}
   */
  public function getItemCount(FeedInterface $feed) {
    return $this->entityTypeManager
      ->getStorage($this
      ->entityType())
      ->getQuery()
      ->condition('feeds_item.target_id', $feed
      ->id())
      ->count()
      ->execute();
  }

  /**
   * {@inheritdoc}
   */
  public function getImportedItemIds(FeedInterface $feed) {
    return $this->entityTypeManager
      ->getStorage($this
      ->entityType())
      ->getQuery()
      ->condition('feeds_item.target_id', $feed
      ->id())
      ->execute();
  }

  /**
   * Returns an existing entity id.
   *
   * @param \Drupal\feeds\FeedInterface $feed
   *   The feed being processed.
   * @param \Drupal\feeds\Feeds\Item\ItemInterface $item
   *   The item to find existing ids for.
   *
   * @return int|string|null
   *   The ID of the entity, or null if not found.
   */
  protected function existingEntityId(FeedInterface $feed, ItemInterface $item) {
    foreach ($this->feedType
      ->getMappings() as $delta => $mapping) {
      if (empty($mapping['unique'])) {
        continue;
      }
      foreach ($mapping['unique'] as $key => $true) {
        $plugin = $this->feedType
          ->getTargetPlugin($delta);
        $entity_id = $plugin
          ->getUniqueValue($feed, $mapping['target'], $key, $item
          ->get($mapping['map'][$key]));
        if ($entity_id) {
          return $entity_id;
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function buildAdvancedForm(array $form, FormStateInterface $form_state) {
    if ($bundle_key = $this->entityType
      ->getKey('bundle')) {
      $form['values'][$bundle_key] = [
        '#type' => 'select',
        '#options' => $this
          ->bundleOptions(),
        '#title' => $this
          ->bundleLabel(),
        '#required' => TRUE,
        '#default_value' => $this
          ->bundle() ?: key($this
          ->bundleOptions()),
        '#disabled' => $this
          ->isLocked(),
      ];
    }
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function mappingFormAlter(array &$form, FormStateInterface $form_state) {
    $added_target = $form_state
      ->getValue('add_target');
    if (!$added_target) {

      // No target was added this time around. Abort.
      return;
    }

    // When adding a mapping target to entity ID, tick 'unique' by default.
    $id_key = $this->entityType
      ->getKey('id');
    $mappings = $this->feedType
      ->getMappings();
    $last_delta = array_keys($mappings)[count($mappings) - 1];
    $mapping = end($mappings);
    if ($mapping['target'] != $added_target) {
      return;
    }
    $target_definition = $this->feedType
      ->getTargetPlugin($last_delta)
      ->getTargetDefinition();
    if (!$target_definition instanceof FieldTargetDefinition) {
      return;
    }

    /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
    $field_definition = $target_definition
      ->getFieldDefinition();
    if ($field_definition
      ->getName() != $id_key) {
      return;
    }

    // We made it! Set property as unique.
    $form['mappings'][$last_delta]['unique'][$field_definition
      ->getMainPropertyName()]['#default_value'] = TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function mappingFormValidate(array &$form, FormStateInterface $form_state) {

    // Display a warning when mapping to entity ID and having that one not set
    // as unique.
    $id_key = $this->entityType
      ->getKey('id');
    foreach ($this->feedType
      ->getMappings() as $delta => $mapping) {
      try {
        $target_definition = $this->feedType
          ->getTargetPlugin($delta)
          ->getTargetDefinition();
        if (!$target_definition instanceof FieldTargetDefinition) {
          continue;
        }

        /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
        $field_definition = $target_definition
          ->getFieldDefinition();
        if ($field_definition
          ->getName() != $id_key) {
          continue;
        }
        $is_unique = $form_state
          ->getValue([
          'mappings',
          $delta,
          'unique',
          $field_definition
            ->getMainPropertyName(),
        ]);
        if (!$is_unique) {

          // Entity ID not set as unique. Display warning.
          $this
            ->messenger()
            ->addWarning($this
            ->t('When mapping to the entity ID (@name), it is recommended to set it as unique.', [
            '@name' => $target_definition
              ->getLabel(),
          ]));
        }
      } catch (MissingTargetException $e) {

        // Ignore missing targets.
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function mappingFormSubmit(array &$form, FormStateInterface $form_state) {

    // The entity processor doesn't have to do anything when mappings are saved.
  }

  /**
   * {@inheritdoc}
   */
  public function isLocked() {
    if ($this->isLocked === NULL) {

      // Look for feeds.
      $this->isLocked = (bool) $this->entityTypeManager
        ->getStorage('feeds_feed')
        ->getQuery()
        ->condition('type', $this->feedType
        ->id())
        ->range(0, 1)
        ->execute();
    }
    return $this->isLocked;
  }

  /**
   * Creates an MD5 hash of an item.
   *
   * Includes mappings so that items will be updated if the mapping
   * configuration has changed.
   *
   * @param \Drupal\feeds\Feeds\Item\ItemInterface $item
   *   The item to hash.
   *
   * @return string
   *   An MD5 hash.
   */
  protected function hash(ItemInterface $item) {
    $sources = $this->feedType
      ->getMappedSources();
    $mapped_item = array_intersect_key($item
      ->toArray(), $sources);
    return hash('md5', serialize($mapped_item) . serialize($this->feedType
      ->getMappings()));
  }

  /**
   * Execute mapping on an item.
   *
   * This method encapsulates the central mapping functionality. When an item is
   * processed, it is passed through map() where the properties of $source_item
   * are mapped onto $target_item following the processor's mapping
   * configuration.
   */
  protected function map(FeedInterface $feed, EntityInterface $entity, ItemInterface $item) {
    $mappings = $this->feedType
      ->getMappings();

    // Mappers add to existing fields rather than replacing them. Hence we need
    // to clear target elements of each item before mapping in case we are
    // mapping on a prepopulated item such as an existing node.
    foreach ($mappings as $delta => $mapping) {
      if ($mapping['target'] == 'feeds_item') {

        // Skip feeds item as this field gets default values before mapping.
        continue;
      }

      // Clear the target.
      $this
        ->clearTarget($entity, $this->feedType
        ->getTargetPlugin($delta), $mapping['target']);
    }

    // Gather all of the values for this item.
    $source_values = [];
    foreach ($mappings as $delta => $mapping) {
      $target = $mapping['target'];
      foreach ($mapping['map'] as $column => $source) {
        if ($source === '') {

          // Skip empty sources.
          continue;
        }
        if (!isset($source_values[$delta][$column])) {
          $source_values[$delta][$column] = [];
        }
        $value = $item
          ->get($source);
        if (!is_array($value)) {
          $source_values[$delta][$column][] = $value;
        }
        else {
          $source_values[$delta][$column] = array_merge($source_values[$delta][$column], $value);
        }
      }
    }

    // Rearrange values into Drupal's field structure.
    $field_values = [];
    foreach ($source_values as $field => $field_value) {
      $field_values[$field] = [];
      foreach ($field_value as $column => $values) {

        // Use array_values() here to keep our $delta clean.
        foreach (array_values($values) as $delta => $value) {
          $field_values[$field][$delta][$column] = $value;
        }
      }
    }

    // Set target values.
    foreach ($mappings as $delta => $mapping) {
      $plugin = $this->feedType
        ->getTargetPlugin($delta);

      // Skip immutable targets for which the entity already has a value.
      if (!$plugin
        ->isMutable() && !$plugin
        ->isEmpty($feed, $entity, $mapping['target'])) {
        continue;
      }
      if (isset($field_values[$delta])) {
        $plugin
          ->setTarget($feed, $entity, $mapping['target'], $field_values[$delta]);
      }
    }
    return $entity;
  }

  /**
   * Clears the target on the entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to clear the target on.
   * @param \Drupal\feeds\Plugin\Type\Target\TargetInterface $target
   *   The target plugin.
   * @param string $target_name
   *   The property to clear on the entity.
   */
  protected function clearTarget(EntityInterface $entity, TargetInterface $target, $target_name) {
    if (!$target
      ->isMutable()) {

      // Don't clear immutable targets.
      return;
    }
    $entity_target = $entity;

    // If the target implements TranslatableTargetInterface and has a language
    // configured, empty the value for the targeted language only.
    // In all other cases, empty the target for the entity in the default
    // language or just the whole target if the entity isn't translatable.
    if ($entity instanceof TranslatableInterface && $target instanceof TranslatableTargetInterface && $entity
      ->isTranslatable()) {

      // We expect the target to return a langcode. If it doesn't return one, we
      // expect that the target for the entity in the default language must be
      // emptied.
      $langcode = $target
        ->getLangcode();
      if ($langcode) {

        // Langcode exists, check if the entity is available in that language.
        if ($entity
          ->hasTranslation($langcode)) {
          $entity_target = $entity
            ->getTranslation($langcode);
        }
        else {

          // Entity hasn't got a translation in the given langcode yet, so we
          // don't need to empty anything.
          return;
        }
      }
    }
    unset($entity_target->{$target_name});
  }

  /**
   * {@inheritdoc}
   *
   * @todo Sort this out so that we aren't calling \Drupal::database()->delete()
   * here.
   */
  public function onFeedDeleteMultiple(array $feeds) {
    $fids = [];
    foreach ($feeds as $feed) {
      $fids[] = $feed
        ->id();
    }
    $table = $this
      ->entityType() . '__feeds_item';
    \Drupal::database()
      ->delete($table)
      ->condition('feeds_item_target_id', $fids, 'IN')
      ->execute();
  }

  /**
   * {@inheritdoc}
   */
  public function calculateDependencies() {

    // Add dependency on entity type.
    $entity_type = $this->entityTypeManager
      ->getDefinition($this
      ->entityType());
    $this
      ->addDependency('module', $entity_type
      ->getProvider());

    // Add dependency on entity bundle.
    if ($this
      ->bundle()) {
      $bundle_dependency = $entity_type
        ->getBundleConfigDependency($this
        ->bundle());
      $this
        ->addDependency($bundle_dependency['type'], $bundle_dependency['name']);
    }

    // For the 'update_non_existent' setting, add dependency on selected action.
    switch ($this
      ->getConfiguration('update_non_existent')) {
      case static::KEEP_NON_EXISTENT:
      case static::DELETE_NON_EXISTENT:

        // No dependency to add.
        break;
      default:
        try {
          $definition = \Drupal::service('plugin.manager.action')
            ->getDefinition($this
            ->getConfiguration('update_non_existent'));
          if (isset($definition['provider'])) {
            $this
              ->addDependency('module', $definition['provider']);
          }
        } catch (PluginNotFoundException $e) {

          // It's possible that the selected action plugin no longer exists. Log
          // an error about it.
          \Drupal::logger('feeds')
            ->warning('The selected option for the setting "Previously imported items" in the feed type %feed_type_id no longer exists. Please edit the feed type and select a different option for that setting.', [
            '%feed_type_id' => $this->feedType
              ->id(),
          ]);
        }
        break;
    }
    return $this->dependencies;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
DependencyTrait::$dependencies protected property The object's dependencies.
DependencyTrait::addDependencies protected function Adds multiple dependencies.
DependencyTrait::addDependency protected function Adds a dependency.
EntityProcessorBase::$entityType protected property The entity info for the selected entity type.
EntityProcessorBase::$entityTypeBundleInfo protected property The entity type bundle info.
EntityProcessorBase::$entityTypeManager protected property The entity type manager.
EntityProcessorBase::$isLocked protected property Flag indicating that this processor is locked.
EntityProcessorBase::$languageManager protected property The language manager.
EntityProcessorBase::$storageController protected property The entity storage controller for the entity type being processed.
EntityProcessorBase::buildAdvancedForm public function
EntityProcessorBase::bundle public function Bundle type this processor operates on.
EntityProcessorBase::bundleKey public function The entity's bundle key.
EntityProcessorBase::bundleLabel public function Returns the bundle label for the entity being processed.
EntityProcessorBase::bundleOptions public function Provides a list of bundle options for use in select lists.
EntityProcessorBase::calculateDependencies public function Calculates dependencies for the configured plugin. Overrides PluginBase::calculateDependencies
EntityProcessorBase::clean public function Applies an action to an entity to 'clean' it. Overrides CleanableInterface::clean
EntityProcessorBase::clear public function Removes all stored results for a feed. Overrides ClearableInterface::clear
EntityProcessorBase::clearTarget protected function Clears the target on the entity.
EntityProcessorBase::create public static function Creates an instance of the plugin. Overrides ContainerFactoryPluginInterface::create
EntityProcessorBase::defaultConfiguration public function Gets default configuration for this plugin. Overrides PluginBase::defaultConfiguration
EntityProcessorBase::entityDeleteMultiple protected function
EntityProcessorBase::entityExists protected function Checks if the entity exists already.
EntityProcessorBase::entityLanguage public function Returns the current language for entities. Overrides EntityProcessorInterface::entityLanguage
EntityProcessorBase::entitySaveAccess protected function
EntityProcessorBase::entityType public function
EntityProcessorBase::entityTypeLabel public function Returns the label of the entity type being processed.
EntityProcessorBase::entityTypeLabelPlural public function Returns the plural label of the entity type being processed.
EntityProcessorBase::entityValidate protected function
EntityProcessorBase::existingEntityId protected function Returns an existing entity id.
EntityProcessorBase::expireItem public function Deletes feed items older than REQUEST_TIME - $time. Overrides ProcessorInterface::expireItem
EntityProcessorBase::expiryTime public function Returns the age of items that should be removed. Overrides ProcessorInterface::expiryTime
EntityProcessorBase::getEntityTranslation public function Returns a translation of the given entity. Overrides EntityProcessorInterface::getEntityTranslation
EntityProcessorBase::getExpiredIds public function Returns feed item ID's to expire. Overrides ProcessorInterface::getExpiredIds
EntityProcessorBase::getImportedItemIds public function Returns a list of ID's of entities that were imported. Overrides ProcessorInterface::getImportedItemIds
EntityProcessorBase::getItemCount public function Counts the number of items imported by this processor. Overrides ProcessorInterface::getItemCount
EntityProcessorBase::getItemLabel public function Returns the label for items being created, updated, or deleted. Overrides ProcessorInterface::getItemLabel
EntityProcessorBase::getItemLabelPlural public function Returns the plural label for items being created, updated, or deleted. Overrides ProcessorInterface::getItemLabelPlural
EntityProcessorBase::hash protected function Creates an MD5 hash of an item.
EntityProcessorBase::initCleanList protected function Initializes the list of entities to clean.
EntityProcessorBase::isLocked public function Returns whether or not this plugin is locked. Overrides LockableInterface::isLocked
EntityProcessorBase::languageOptions public function Provides a list of languages available on the site.
EntityProcessorBase::map protected function Execute mapping on an item.
EntityProcessorBase::mappingFormAlter public function Alter mapping form. Overrides MappingPluginFormInterface::mappingFormAlter
EntityProcessorBase::mappingFormSubmit public function Submit handler for the mapping form. Overrides MappingPluginFormInterface::mappingFormSubmit
EntityProcessorBase::mappingFormValidate public function Validate handler for the mapping form. Overrides MappingPluginFormInterface::mappingFormValidate
EntityProcessorBase::newEntity protected function
EntityProcessorBase::onFeedDeleteMultiple public function @todo Sort this out so that we aren't calling \Drupal::database()->delete() here. Overrides PluginBase::onFeedDeleteMultiple
EntityProcessorBase::onFeedTypeDelete public function The feed type is being deleted. Overrides PluginBase::onFeedTypeDelete
EntityProcessorBase::onFeedTypeSave public function The feed type is being saved. Overrides PluginBase::onFeedTypeSave
EntityProcessorBase::prepareFeedsItemField protected function Prepares the feeds_item field.
EntityProcessorBase::process public function Processes the results from a parser. Overrides ProcessorInterface::process
EntityProcessorBase::removeFeedItemField protected function Deletes the feeds_item field.
EntityProcessorBase::__construct public function Constructs an EntityProcessorBase object. Overrides PluginBase::__construct
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
PluginBase::$configuration protected property Configuration information passed into the plugin. 1
PluginBase::$feedType protected property The importer this plugin is working for.
PluginBase::$linkGenerator protected property The link generator.
PluginBase::$pluginDefinition protected property The plugin implementation definition. 1
PluginBase::$pluginId protected property The plugin_id.
PluginBase::$urlGenerator protected property The url generator.
PluginBase::container private function Returns the service container.
PluginBase::defaultFeedConfiguration public function Returns default feed configuration. Overrides FeedsPluginInterface::defaultFeedConfiguration 3
PluginBase::DERIVATIVE_SEPARATOR constant A string which is used to separate base plugin IDs from the derivative ID.
PluginBase::getBaseId public function Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface::getBaseId
PluginBase::getConfiguration public function Gets this plugin's configuration. Overrides ConfigurableInterface::getConfiguration
PluginBase::getDerivativeId public function Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface::getDerivativeId
PluginBase::getPluginDefinition public function Gets the definition of the plugin implementation. Overrides PluginInspectionInterface::getPluginDefinition 3
PluginBase::getPluginId public function Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface::getPluginId
PluginBase::isConfigurable public function Determines if the plugin is configurable.
PluginBase::l protected function Renders a link to a route given a route name and its parameters.
PluginBase::linkGenerator protected function Returns the link generator service.
PluginBase::onFeedSave public function A feed is being saved.
PluginBase::pluginType public function Returns the type of plugin. Overrides FeedsPluginInterface::pluginType
PluginBase::setConfiguration public function Sets the configuration for this plugin instance. Overrides ConfigurableInterface::setConfiguration 1
PluginBase::url protected function Generates a URL or path for a specific route based on the given parameters.
PluginBase::urlGenerator protected function Returns the URL generator service.
ProcessorBase::postClear public function
ProcessorBase::postProcess public function Called after an import is completed. Overrides ProcessorInterface::postProcess
ProcessorInterface::DELETE_NON_EXISTENT constant Delete items that no longer exist in the feed.
ProcessorInterface::EXPIRE_NEVER constant Feed items should never be expired.
ProcessorInterface::INSERT_NEW constant Create new items from Feed.
ProcessorInterface::KEEP_NON_EXISTENT constant Keep items that no longer exist in the feed.
ProcessorInterface::REPLACE_EXISTING constant Replace items that exist already.
ProcessorInterface::SKIP_EXISTING constant Skip items that exist already.
ProcessorInterface::SKIP_NEW constant Skip new items from feed.
ProcessorInterface::UPDATE_EXISTING constant Update items that exist already.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.