View source
<?php
namespace Drupal\feeds\Feeds\Processor;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\TranslatableInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\feeds\Entity\FeedType;
use Drupal\feeds\Event\FeedsEvents;
use Drupal\feeds\Exception\EmptyFeedException;
use Drupal\feeds\Exception\EntityAccessException;
use Drupal\feeds\Exception\MissingTargetException;
use Drupal\feeds\Exception\ValidationException;
use Drupal\feeds\FeedInterface;
use Drupal\feeds\Feeds\Item\ItemInterface;
use Drupal\feeds\Feeds\State\CleanStateInterface;
use Drupal\feeds\FieldTargetDefinition;
use Drupal\feeds\Plugin\Type\MappingPluginFormInterface;
use Drupal\feeds\Plugin\Type\Processor\EntityProcessorInterface;
use Drupal\feeds\Plugin\Type\Target\TargetInterface;
use Drupal\feeds\Plugin\Type\Target\TranslatableTargetInterface;
use Drupal\feeds\StateInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\user\EntityOwnerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
abstract class EntityProcessorBase extends ProcessorBase implements EntityProcessorInterface, ContainerFactoryPluginInterface, MappingPluginFormInterface {
protected $entityTypeManager;
protected $storageController;
protected $entityType;
protected $isLocked;
protected $entityTypeBundleInfo;
protected $languageManager;
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);
}
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'));
}
public function process(FeedInterface $feed, ItemInterface $item, StateInterface $state) {
$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 ($existing_entity_id) {
$clean_state
->removeItem($existing_entity_id);
}
if ($skip_existing && $existing_entity_id || !$existing_entity_id && $skip_new) {
$state->skipped++;
return;
}
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;
if ($existing_entity_id && !$changed && !$this->configuration['skip_hash_check']) {
$state->skipped++;
return;
}
if (!$existing_entity_id && !$skip_new) {
$entity = $this
->newEntity($feed);
}
try {
$feeds_item = $entity
->get('feeds_item');
$feeds_item->target_id = $feed
->id();
$feeds_item->hash = $hash;
$this
->map($feed, $entity, $item);
$feed
->dispatchEntityEvent(FeedsEvents::PROCESS_ENTITY_PREVALIDATE, $entity, $item);
$this
->entityValidate($entity);
$feed
->dispatchEntityEvent(FeedsEvents::PROCESS_ENTITY_PRESAVE, $entity, $item);
$this
->entitySaveAccess($entity);
$entity
->get('feeds_item')->imported = \Drupal::service('datetime.time')
->getRequestTime();
$this->storageController
->save($entity);
$feed
->dispatchEntityEvent(FeedsEvents::PROCESS_ENTITY_POSTSAVE, $entity, $item);
$existing_entity_id ? $state->updated++ : $state->created++;
} catch (EmptyFeedException $e) {
$state->skipped++;
} catch (ValidationException $e) {
$state->failed++;
$state
->setMessage($e
->getFormattedMessage(), 'warning');
} catch (\Exception $e) {
$state->failed++;
$state
->setMessage($e
->getMessage(), 'warning');
}
}
protected function initCleanList(FeedInterface $feed, CleanStateInterface $state) {
$state
->setEntityTypeId($this
->entityType());
if ($this
->getConfiguration('update_non_existent') === static::KEEP_NON_EXISTENT) {
return;
}
$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);
$state->total = $state
->count();
$state
->progress($state->total, 0);
}
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) {
return;
}
switch ($update_non_existent) {
case static::KEEP_NON_EXISTENT:
return;
case static::DELETE_NON_EXISTENT:
$entity
->delete();
break;
default:
try {
\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;
}
$entity = $this->storageController
->load($entity
->id());
if (isset($entity->feeds_item)) {
$entity
->get('feeds_item')->hash = $update_non_existent;
$this->storageController
->save($entity);
}
$state->updated++;
$state
->progress($state->total, $state->updated);
}
public function clear(FeedInterface $feed, StateInterface $state) {
$query = $this->entityTypeManager
->getStorage($this
->entityType())
->getQuery()
->condition('feeds_item.target_id', $feed
->id());
if (!$state->total) {
$count_query = clone $query;
$state->total = (int) $count_query
->count()
->execute();
}
$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);
}
}
public function entityType() {
return $this->pluginDefinition['entity_type'];
}
public function bundleKey() {
return $this->entityType
->getKey('bundle');
}
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];
}
}
public function bundleLabel() {
if ($label = $this->entityType
->getBundleLabel()) {
return $label;
}
return $this
->t('Bundle');
}
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;
}
public function languageOptions() {
foreach ($this->languageManager
->getLanguages(LanguageInterface::STATE_ALL) as $language) {
$langcodes[$language
->getId()] = $language
->getName();
}
return $langcodes;
}
public function entityTypeLabel() {
return $this->entityType
->getLabel();
}
public function entityTypeLabelPlural() {
return $this->entityType
->getPluralLabel();
}
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();
}
public function getItemLabelPlural() {
if (!$this->entityType
->getKey('bundle') || !$this->entityType
->getBundleEntityType()) {
return $this
->entityTypeLabelPlural();
}
$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,
]);
}
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']);
}
}
if ($this->entityType
->hasKey('langcode')) {
$entity->{$this->entityType
->getKey('langcode')} = $this
->entityLanguage();
}
return $entity;
}
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);
}
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;
}
protected function entityValidate(EntityInterface $entity) {
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();
$invalid_value = $violation
->getInvalidValue();
if ($invalid_value instanceof FieldItemListInterface) {
$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,
];
$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);
$message_element = [
'#markup' => implode("\n", $messages),
];
$message = \Drupal::service('renderer')
->renderRoot($message_element);
throw new ValidationException($message);
}
protected function entitySaveAccess(EntityInterface $entity) {
if (!$this->configuration['authorize'] || !$entity instanceof EntityOwnerInterface) {
return;
}
$account = $entity
->getOwner();
if (!$account) {
$owner_id = $entity
->getOwnerId();
if ($owner_id == 0) {
return;
}
throw new EntityAccessException($this
->t('Invalid user with ID %uid mapped to %label.', [
'%uid' => $owner_id,
'%label' => $entity
->label(),
]));
}
if ($account
->isAnonymous()) {
return;
}
$op = $entity
->isNew() ? 'create' : 'update';
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));
}
public function entityLanguage() {
$langcodes = $this
->languageOptions();
if (isset($this->configuration['langcode']) && isset($langcodes[$this->configuration['langcode']])) {
return $this->configuration['langcode'];
}
return $this->languageManager
->getDefaultLanguage()
->getId();
}
protected function entityDeleteMultiple(array $entity_ids) {
$entities = $this->storageController
->loadMultiple($entity_ids);
$this->storageController
->delete($entities);
}
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,
];
if ($bundle_key = $this->entityType
->getKey('bundle')) {
$defaults['values'] = [
$bundle_key => NULL,
];
}
if ($langcode_key = $this->entityType
->getKey('langcode')) {
$defaults['langcode'] = $this->languageManager
->getDefaultLanguage()
->getId();
}
return $defaults;
}
public function onFeedTypeSave($update = TRUE) {
$this
->prepareFeedsItemField();
}
public function onFeedTypeDelete() {
$this
->removeFeedItemField();
}
protected function prepareFeedsItemField() {
if (\Drupal::isConfigSyncing()) {
return FALSE;
}
if (!FieldStorageConfig::loadByName($this
->entityType(), 'feeds_item')) {
FieldStorageConfig::create([
'field_name' => 'feeds_item',
'entity_type' => $this
->entityType(),
'type' => 'feeds_item',
'translatable' => FALSE,
])
->save();
}
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();
}
}
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;
}
if ($config = FieldConfig::loadByName($this
->entityType(), $this
->bundle(), 'feeds_item')) {
$config
->delete();
}
if ($storage_in_use) {
return;
}
if ($storage = FieldStorageConfig::loadByName($this
->entityType(), 'feeds_item')) {
$storage
->delete();
}
}
public function expiryTime() {
return $this->configuration['expire'];
}
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();
}
public function expireItem(FeedInterface $feed, $item_id, StateInterface $state) {
$this
->entityDeleteMultiple([
$item_id,
]);
$state->total++;
}
public function getItemCount(FeedInterface $feed) {
return $this->entityTypeManager
->getStorage($this
->entityType())
->getQuery()
->condition('feeds_item.target_id', $feed
->id())
->count()
->execute();
}
public function getImportedItemIds(FeedInterface $feed) {
return $this->entityTypeManager
->getStorage($this
->entityType())
->getQuery()
->condition('feeds_item.target_id', $feed
->id())
->execute();
}
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;
}
}
}
}
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;
}
public function mappingFormAlter(array &$form, FormStateInterface $form_state) {
$added_target = $form_state
->getValue('add_target');
if (!$added_target) {
return;
}
$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;
}
$field_definition = $target_definition
->getFieldDefinition();
if ($field_definition
->getName() != $id_key) {
return;
}
$form['mappings'][$last_delta]['unique'][$field_definition
->getMainPropertyName()]['#default_value'] = TRUE;
}
public function mappingFormValidate(array &$form, FormStateInterface $form_state) {
$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;
}
$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) {
$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) {
}
}
}
public function mappingFormSubmit(array &$form, FormStateInterface $form_state) {
}
public function isLocked() {
if ($this->isLocked === NULL) {
$this->isLocked = (bool) $this->entityTypeManager
->getStorage('feeds_feed')
->getQuery()
->condition('type', $this->feedType
->id())
->range(0, 1)
->execute();
}
return $this->isLocked;
}
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()));
}
protected function map(FeedInterface $feed, EntityInterface $entity, ItemInterface $item) {
$mappings = $this->feedType
->getMappings();
foreach ($mappings as $delta => $mapping) {
if ($mapping['target'] == 'feeds_item') {
continue;
}
$this
->clearTarget($entity, $this->feedType
->getTargetPlugin($delta), $mapping['target']);
}
$source_values = [];
foreach ($mappings as $delta => $mapping) {
$target = $mapping['target'];
foreach ($mapping['map'] as $column => $source) {
if ($source === '') {
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);
}
}
}
$field_values = [];
foreach ($source_values as $field => $field_value) {
$field_values[$field] = [];
foreach ($field_value as $column => $values) {
foreach (array_values($values) as $delta => $value) {
$field_values[$field][$delta][$column] = $value;
}
}
}
foreach ($mappings as $delta => $mapping) {
$plugin = $this->feedType
->getTargetPlugin($delta);
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;
}
protected function clearTarget(EntityInterface $entity, TargetInterface $target, $target_name) {
if (!$target
->isMutable()) {
return;
}
$entity_target = $entity;
if ($entity instanceof TranslatableInterface && $target instanceof TranslatableTargetInterface && $entity
->isTranslatable()) {
$langcode = $target
->getLangcode();
if ($langcode) {
if ($entity
->hasTranslation($langcode)) {
$entity_target = $entity
->getTranslation($langcode);
}
else {
return;
}
}
}
unset($entity_target->{$target_name});
}
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();
}
public function calculateDependencies() {
$entity_type = $this->entityTypeManager
->getDefinition($this
->entityType());
$this
->addDependency('module', $entity_type
->getProvider());
if ($this
->bundle()) {
$bundle_dependency = $entity_type
->getBundleConfigDependency($this
->bundle());
$this
->addDependency($bundle_dependency['type'], $bundle_dependency['name']);
}
switch ($this
->getConfiguration('update_non_existent')) {
case static::KEEP_NON_EXISTENT:
case static::DELETE_NON_EXISTENT:
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) {
\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;
}
}