diff --git a/composer.json b/composer.json index 1a392f6d643da2540665116d6f5efcc47e43f586..639831a05dbe4c3d52239adea04de90cf9f347ac 100644 --- a/composer.json +++ b/composer.json @@ -117,7 +117,7 @@ "drupal/entity_browser": "1.4", "drupal/entity_clone": "1.0.0-beta3", "drupal/entity_embed": "1.0-beta2", - "drupal/entity_reference_revisions": "1.3", + "drupal/entity_reference_revisions": "1.8", "drupal/externalauth": "1.1", "drupal/features": "3.8", "drupal/field_group": "3.0", @@ -318,4 +318,4 @@ "php": "7.0.8" } } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index fd58f4396839813c4a9dd5201d7ab9e636712b47..4d0ada5e3eb06fa5093c59064935b98bbc259bc9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f3f9a51e6c09cc6554fa012dbf1ad64d", + "content-hash": "99fd23635d971fe3335373bc7bbfb9d2", "packages": [ { "name": "alchemy/zippy", @@ -4612,23 +4612,23 @@ }, { "name": "drupal/entity_reference_revisions", - "version": "1.3.0", + "version": "1.8.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/entity_reference_revisions.git", - "reference": "8.x-1.3" + "reference": "8.x-1.8" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.3.zip", - "reference": "8.x-1.3", - "shasum": "78aebb58efbbfcbb2faa40a1afc0830312b32631" + "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.8.zip", + "reference": "8.x-1.8", + "shasum": "c1279e6c683edc2dbccedba8de1505340c8a62b6" }, "require": { - "drupal/core": "~8.0" + "drupal/core": "^8.7.7 || ^9" }, "require-dev": { - "drupal/diff": "*" + "drupal/diff": "1.x-dev" }, "type": "drupal-module", "extra": { @@ -4636,8 +4636,8 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.3", - "datestamp": "1515143885", + "version": "8.x-1.8", + "datestamp": "1583961846", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -4646,9 +4646,13 @@ }, "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0-or-later" + "GPL-2.0" ], "authors": [ + { + "name": "Berdir", + "homepage": "https://www.drupal.org/user/214652" + }, { "name": "Frans", "homepage": "https://www.drupal.org/user/514222" @@ -4662,10 +4666,10 @@ "homepage": "https://www.drupal.org/user/227761" } ], - "description": "Adds a Entity Reference field type with revision support.", + "description": "Entity Reference Revisions", "homepage": "https://www.drupal.org/project/entity_reference_revisions", "support": { - "source": "http://cgit.drupalcode.org/entity_reference_revisions" + "source": "https://git.drupalcode.org/project/entity_reference_revisions" } }, { @@ -6064,8 +6068,7 @@ "homepage": "https://www.drupal.org/project/migrate_devel", "support": { "source": "http://cgit.drupalcode.org/migrate_devel" - }, - "time": "2017-06-25T23:46:13+00:00" + } }, { "name": "drupal/migrate_plus", diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 1dcf950ce4129566464e6f27ae1a5efed7f4661c..302b90d8cf88b39300a8d87a52bfa0c9d87a8fba 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -4748,24 +4748,24 @@ }, { "name": "drupal/entity_reference_revisions", - "version": "1.3.0", - "version_normalized": "1.3.0.0", + "version": "1.8.0", + "version_normalized": "1.8.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/entity_reference_revisions.git", - "reference": "8.x-1.3" + "reference": "8.x-1.8" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.3.zip", - "reference": "8.x-1.3", - "shasum": "78aebb58efbbfcbb2faa40a1afc0830312b32631" + "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.8.zip", + "reference": "8.x-1.8", + "shasum": "c1279e6c683edc2dbccedba8de1505340c8a62b6" }, "require": { - "drupal/core": "~8.0" + "drupal/core": "^8.7.7 || ^9" }, "require-dev": { - "drupal/diff": "*" + "drupal/diff": "1.x-dev" }, "type": "drupal-module", "extra": { @@ -4773,8 +4773,8 @@ "dev-1.x": "1.x-dev" }, "drupal": { - "version": "8.x-1.3", - "datestamp": "1515143885", + "version": "8.x-1.8", + "datestamp": "1583961846", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -4784,9 +4784,13 @@ "installation-source": "dist", "notification-url": "https://packages.drupal.org/8/downloads", "license": [ - "GPL-2.0-or-later" + "GPL-2.0" ], "authors": [ + { + "name": "Berdir", + "homepage": "https://www.drupal.org/user/214652" + }, { "name": "Frans", "homepage": "https://www.drupal.org/user/514222" @@ -4800,10 +4804,10 @@ "homepage": "https://www.drupal.org/user/227761" } ], - "description": "Adds a Entity Reference field type with revision support.", + "description": "Entity Reference Revisions", "homepage": "https://www.drupal.org/project/entity_reference_revisions", "support": { - "source": "http://cgit.drupalcode.org/entity_reference_revisions" + "source": "https://git.drupalcode.org/project/entity_reference_revisions" } }, { diff --git a/web/modules/entity_reference_revisions/composer.json b/web/modules/entity_reference_revisions/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..7fdfb2e91999c1c74d5a33474ad0cc79a209f845 --- /dev/null +++ b/web/modules/entity_reference_revisions/composer.json @@ -0,0 +1,12 @@ +{ + "name": "drupal/entity_reference_revisions", + "description": "Entity Reference Revisions", + "type": "drupal-module", + "license": "GPL-2.0", + "require": { + "drupal/core": "^8.7.7 || ^9" + }, + "require-dev": { + "drupal/diff": "1.x-dev" + } +} diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.info.yml b/web/modules/entity_reference_revisions/entity_reference_revisions.info.yml index 6c425b7f118b1ddc550a5275aa8a6ac8c8975f8e..55807d43482d557361720824c7805204112f28af 100644 --- a/web/modules/entity_reference_revisions/entity_reference_revisions.info.yml +++ b/web/modules/entity_reference_revisions/entity_reference_revisions.info.yml @@ -1,14 +1,10 @@ name: Entity Reference Revisions type: module description: Adds a Entity Reference field type with revision support. -# core: 8.x +core_version_requirement: ^8.7.7 || ^9 package: Field types -test_dependencies: - - diff:diff - -# Information added by Drupal.org packaging script on 2017-05-26 -version: '8.x-1.3' -core: '8.x' +# Information added by Drupal.org packaging script on 2020-03-11 +version: '8.x-1.8' project: 'entity_reference_revisions' -datestamp: 1495814304 +datestamp: 1583961849 diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.links.menu.yml b/web/modules/entity_reference_revisions/entity_reference_revisions.links.menu.yml new file mode 100644 index 0000000000000000000000000000000000000000..beb8161771ec246b11b1d3c12f3db09d65377197 --- /dev/null +++ b/web/modules/entity_reference_revisions/entity_reference_revisions.links.menu.yml @@ -0,0 +1,6 @@ +entity_reference_revisions.delete_orphans: + title: 'Delete orphaned composite entities' + parent: system.admin_config_system + description: 'Delete revisions of entities that are no longer used in Entity Reference Revisions fields.' + route_name: entity_reference_revisions.delete_orphans + weight: 30 diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.module b/web/modules/entity_reference_revisions/entity_reference_revisions.module index ba9ac496f96fb8eb7cf439eb0c618205f66b0aea..fa24f77eab5fe6092919d721786c36778a016370 100644 --- a/web/modules/entity_reference_revisions/entity_reference_revisions.module +++ b/web/modules/entity_reference_revisions/entity_reference_revisions.module @@ -6,9 +6,14 @@ */ use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Entity\TranslatableRevisionableStorageInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem; use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\Entity\FieldConfig; use Drupal\field\FieldStorageConfigInterface; @@ -218,3 +223,129 @@ function entity_reference_revisions_form_field_ui_field_storage_add_form_alter(a unset($form['add']['new_storage_type']['#options'][(string) t('Reference revisions')]['entity_reference_revisions']); $form['add']['new_storage_type']['#options'][(string) t('Reference revisions')]['entity_reference_revisions'] = t('Other…'); } + +/** + * Implements hook_entity_revision_create(). + */ +function entity_reference_revisions_entity_revision_create(ContentEntityInterface $new_revision, ContentEntityInterface $entity, $keep_untranslatable_fields) { + $entity_type_manager = \Drupal::entityTypeManager(); + $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId()); + foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) { + if ($field_definition->getType() == 'entity_reference_revisions' && !$field_definition->isTranslatable()) { + $target_entity_type_id = $field_definition->getSetting('target_type'); + if ($entity_type_manager->getDefinition($target_entity_type_id)->get('entity_revision_parent_id_field')) { + + // The default implementation copied the values from the current + // default revision into the field since it is not translatable. + // Take the originally referenced entity, create a new revision + // of it and set that instead on the new entity revision. + $active_langcode = $entity->language()->getId(); + $target_storage = \Drupal::entityTypeManager()->getStorage($target_entity_type_id); + if ($target_storage instanceof TranslatableRevisionableStorageInterface) { + + $items = $entity->get($field_name); + $translation_items = NULL; + if (!$new_revision->isDefaultTranslation() && $storage instanceof TranslatableRevisionableStorageInterface) { + $translation_items = $items; + $items = $storage->load($new_revision->id())->get($field_name); + } + + $values = []; + foreach ($items as $delta => $item) { + // If the target entity is missing, let's skip it. + if (empty($item->entity)) { + continue; + } + + // Use the item from the translation if it exists. + // If we have translation items, use that if one with the matching + // target id exists. + if ($translation_items) { + foreach ($translation_items as $translation_item) { + if ($item->target_id == $translation_item->target_id) { + $item = $translation_item; + break; + } + } + } + + /** @var \Drupal\Core\Entity\ContentEntityInterface $target_entity */ + $target_entity = $item->entity; + if (!$target_entity->hasTranslation($active_langcode)) { + $target_entity->addTranslation($active_langcode, $target_entity->toArray()); + } + $target_entity = $item->entity->getTranslation($active_langcode); + $revised_entity = $target_storage->createRevision($target_entity, $new_revision->isDefaultRevision(), $keep_untranslatable_fields); + + // Restore the revision ID. + $revision_key = $revised_entity->getEntityType()->getKey('revision'); + $revised_entity->set($revision_key, $revised_entity->getLoadedRevisionId()); + $values[$delta] = $revised_entity; + } + $new_revision->set($field_name, $values); + } + } + } + } +} + +/** + * Batch callback to dispatch the orphan composite batch operation to a service. + */ +function _entity_reference_revisions_orphan_purger_batch_dispatcher() { + $args = func_get_args(); + list($service, $method) = explode(':', array_shift($args)); + // The second argument (context) is passed by reference. + $values = $args[1]; + $args[1] = &$values; + call_user_func_array([\Drupal::service($service), $method], $args); +} + +/** + * Implements hook_entity_delete(). + * + * Performs garbage collection for composite entities that were not removed + * by EntityReferenceRevisionsItem. + */ +function entity_reference_revisions_entity_delete(EntityInterface $entity) { + if (!$entity instanceof FieldableEntityInterface) { + return; + } + + $entity_type_manager = \Drupal::entityTypeManager(); + /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */ + $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); + foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) { + $field_class = $field_type_manager->getPluginClass($field_definition->getType()); + if ($field_class == EntityReferenceRevisionsItem::class || is_subclass_of($field_class, EntityReferenceRevisionsItem::class)) { + $target_entity_type_id = $field_definition->getSetting('target_type'); + $target_entity_storage = $entity_type_manager->getStorage($target_entity_type_id); + $target_entity_type = $target_entity_storage->getEntityType(); + + $parent_type_field = $target_entity_type->get('entity_revision_parent_type_field'); + $parent_id_field = $target_entity_type->get('entity_revision_parent_id_field'); + $parent_name_field = $target_entity_type->get('entity_revision_parent_field_name_field'); + + if ($parent_type_field && $parent_id_field && $parent_name_field) { + $entity_ids = $target_entity_storage + ->getQuery() + ->allRevisions() + ->condition($parent_type_field, $entity->getEntityTypeId()) + ->condition($parent_id_field, $entity->id()) + ->condition($parent_name_field, $field_name) + ->execute(); + + if (empty($entity_ids)) { + continue; + } + $entity_ids = array_unique($entity_ids); + foreach ($entity_ids as $revision_id => $entity_id) { + \Drupal::queue('entity_reference_revisions_orphan_purger')->createItem([ + 'entity_id' => $entity_id, + 'entity_type_id' => $target_entity_type_id, + ]); + } + } + } + } +} diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.permissions.yml b/web/modules/entity_reference_revisions/entity_reference_revisions.permissions.yml new file mode 100644 index 0000000000000000000000000000000000000000..bcd64c16d34741ee2c09057aadbc4e5e5b8349cf --- /dev/null +++ b/web/modules/entity_reference_revisions/entity_reference_revisions.permissions.yml @@ -0,0 +1,3 @@ +delete orphan revisions: + title: 'Delete orphan revisions' + description: 'Allow to access to the Entity Reference Revisions orphan deletion form.' diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.routing.yml b/web/modules/entity_reference_revisions/entity_reference_revisions.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..cd90db817dfa6418cefe7cb5c2dbfb8f5eea3062 --- /dev/null +++ b/web/modules/entity_reference_revisions/entity_reference_revisions.routing.yml @@ -0,0 +1,7 @@ +entity_reference_revisions.delete_orphans: + path: '/admin/config/system/delete-orphans' + defaults: + _form: 'Drupal\entity_reference_revisions\Form\OrphanedCompositeEntitiesDeleteForm' + _title: 'Delete orphaned composite entities' + requirements: + _permission: 'delete orphan revisions' diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.services.yml b/web/modules/entity_reference_revisions/entity_reference_revisions.services.yml new file mode 100755 index 0000000000000000000000000000000000000000..d112df982e4c4830e49666f570e9819ffadb4056 --- /dev/null +++ b/web/modules/entity_reference_revisions/entity_reference_revisions.services.yml @@ -0,0 +1,4 @@ +services: + entity_reference_revisions.orphan_purger: + class: Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger + arguments: ['@entity_type.manager', '@date.formatter', '@datetime.time', '@database', '@messenger'] diff --git a/web/modules/entity_reference_revisions/entity_reference_revisions.views.inc b/web/modules/entity_reference_revisions/entity_reference_revisions.views.inc index 9d351b3765b5ccf7b1483aaa26d9941eba52af5d..6dd8ec66f71ade9143dc80931a407ab0ec505680 100644 --- a/web/modules/entity_reference_revisions/entity_reference_revisions.views.inc +++ b/web/modules/entity_reference_revisions/entity_reference_revisions.views.inc @@ -43,7 +43,7 @@ function entity_reference_revisions_field_views_data(FieldStorageConfigInterface // Provide a reverse relationship for the entity type that is referenced by // the field. $args['@entity'] = $entity_type->getLabel(); - $args['@label'] = $target_entity_type->getLowercaseLabel(); + $args['@label'] = $target_entity_type->getSingularLabel(); $pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_name; /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping(); diff --git a/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php b/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php index ba907277add9b397cae852212a1535035c1508e1..983e7be2697e036570f1756cf5a9795bedb3d12a 100644 --- a/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php +++ b/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php @@ -3,6 +3,8 @@ namespace Drupal\entity_reference_revisions; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Field\FieldItemListTranslationChangesInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\EntityReferenceFieldItemList; @@ -122,4 +124,28 @@ public function defaultValuesFormSubmit(array $element, array &$form, FormStateI return $default_value; } + /** + * {@inheritdoc} + */ + public function hasAffectingChanges(FieldItemListInterface $original_items, $langcode) { + // If there are fewer items, then it is a change. + if (count($this) < count($original_items)) { + return TRUE; + } + + foreach ($this as $delta => $item) { + // If this is a different entity, then it is an affecting change. + if (!$original_items->offsetExists($delta) || $item->target_id != $original_items[$delta]->target_id) { + return TRUE; + } + // If it is the same entity, only consider it as having affecting changes + // if the target entity itself has changes. + if ($item->entity && $item->entity->hasTranslation($langcode) && $item->entity->getTranslation($langcode)->hasTranslationChanges()) { + return TRUE; + } + } + + return FALSE; + } + } diff --git a/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsOrphanPurger.php b/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsOrphanPurger.php new file mode 100644 index 0000000000000000000000000000000000000000..8735817fc013be61a37c740895dfaa743b836d46 --- /dev/null +++ b/web/modules/entity_reference_revisions/src/EntityReferenceRevisionsOrphanPurger.php @@ -0,0 +1,370 @@ +<?php + +namespace Drupal\entity_reference_revisions; + +use Drupal\Component\Datetime\TimeInterface; +use Drupal\Core\Database\Connection; +use Drupal\Core\Datetime\DateFormatterInterface; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Core\Site\Settings; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * Manages orphan composite revision deletion. + */ +class EntityReferenceRevisionsOrphanPurger { + + use StringTranslationTrait; + + /** + * Parent is valid. + */ + const PARENT_VALID = 0; + + /** + * Parent is invalid and usage can not be verified. + */ + const PARENT_INVALID_SKIP = 1; + + /** + * Parent is invalid and paragraph is safe to delete. + */ + const PARENT_INVALID_DELETE = 2; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The date formatter service. + * + * @var \Drupal\Core\Datetime\DateFormatterInterface + */ + protected $dateFormatter; + + /** + * The time service. + * + * @var \Drupal\Component\Datetime\TimeInterface + */ + protected $time; + + /** + * The database service. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** + * List of already checked parents. + * + * @var bool[][] + */ + protected $validParents = []; + + /** + * Constructs a EntityReferenceRevisionsOrphanManager object. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter + * The date formatter service. + * @param \Drupal\Component\Datetime\TimeInterface $time + * The time service. + * @param \Drupal\Core\Database\Connection $database + * The database service. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger service. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, DateFormatterInterface $date_formatter, TimeInterface $time, Connection $database, MessengerInterface $messenger) { + $this->entityTypeManager = $entity_type_manager; + $this->dateFormatter = $date_formatter; + $this->time = $time; + $this->database = $database; + $this->messenger = $messenger; + } + + /** + * Deletes unused revision or an entity if there are no revisions remaining. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $composite_revision + * The composite revision. + * + * @return bool + * TRUE if an entity revision was deleted. Otherwise, FALSE. + */ + public function deleteUnusedRevision(ContentEntityInterface $composite_revision) { + // If this is the default revision of the composite entity, check if there + // are other revisions. If there are not, delete the composite entity. + $composite_storage = $this->entityTypeManager->getStorage($composite_revision->getEntityTypeId()); + + if ($composite_revision->isDefaultRevision()) { + $count = $composite_storage + ->getQuery() + ->accessCheck(FALSE) + ->allRevisions() + ->condition($composite_storage->getEntityType()->getKey('id'), $composite_revision->id()) + ->count() + ->execute(); + if ($count <= 1) { + $composite_revision->delete(); + return TRUE; + } + } + else { + // Delete the revision if this is not the default one. + $composite_storage->deleteRevision($composite_revision->getRevisionId()); + return TRUE; + } + + return FALSE; + } + + /** + * Batch operation for checking orphans for a given entity type. + * + * @param string $entity_type_id + * The entity type id, for example 'paragraph'. + * @param array $context + * The context array. + */ + public function deleteOrphansBatchOperation($entity_type_id, array &$context) { + $composite_type = $this->entityTypeManager->getDefinition($entity_type_id); + $composite_revision_key = $composite_type->getKey('revision'); + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $composite_storage */ + $composite_storage = $this->entityTypeManager->getStorage($entity_type_id); + $batch_size = Settings::get('entity_update_batch_size', 50); + + if (empty($context['sandbox']['total'])) { + $context['sandbox']['progress'] = 0; + $context['sandbox']['current_revision_id'] = -1; + $context['sandbox']['total'] = (int) $composite_storage->getQuery() + ->allRevisions() + ->accessCheck(FALSE) + ->count() + ->execute(); + } + + if (!isset($context['results'][$entity_type_id])) { + $context['results'][$entity_type_id]['entity_count'] = 0; + $context['results'][$entity_type_id]['revision_count'] = 0; + $context['results'][$entity_type_id]['start'] = $this->time->getRequestTime(); + } + + // Get the next batch of revision ids from the selected entity type. + // @todo Replace with an entity query on all revisions with a revision ID + // condition after https://www.drupal.org/project/drupal/issues/2766135. + $revision_table = $composite_type->getRevisionTable(); + $entity_revision_ids = $this->database->select($revision_table, 'r') + ->fields('r', [$composite_revision_key]) + ->range(0, $batch_size) + ->orderBy($composite_revision_key) + ->condition($composite_revision_key, $context['sandbox']['current_revision_id'], '>') + ->execute() + ->fetchCol(); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $composite_revision */ + foreach ($composite_storage->loadMultipleRevisions($entity_revision_ids) as $composite_revision) { + $context['sandbox']['progress']++; + $context['sandbox']['current_revision_id'] = $composite_revision->getRevisionId(); + + if ($this->isUsed($composite_revision)) { + continue; + } + + if ($this->deleteUnusedRevision($composite_revision)) { + $context['results'][$entity_type_id]['revision_count']++; + if ($composite_revision->isDefaultRevision()) { + $context['results'][$entity_type_id]['entity_count']++; + } + } + } + + // This entity type is completed if no new revision ids were found or the + // total is reached. + if ($entity_revision_ids && $context['sandbox']['progress'] < $context['sandbox']['total']) { + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['total']; + } + else { + $context['finished'] = 1; + $context['results'][$entity_type_id]['end'] = $this->time->getRequestTime(); + } + + $interval = $this->dateFormatter->formatInterval($this->time->getRequestTime() - $context['results'][$entity_type_id]['start']); + $context['message'] = t('Checked @entity_type revisions for orphans: @current of @total in @interval (@deletions deleted)', [ + '@entity_type' => $composite_type->getLabel(), + '@current' => $context['sandbox']['progress'], + '@total' => $context['sandbox']['total'], + '@interval' => $interval, + '@deletions' => $context['results'][$entity_type_id]['revision_count'], + ]); + } + + /** + * Batch dispatch submission finished callback. + */ + public static function batchSubmitFinished($success, $results, $operations) { + return \Drupal::service('entity_reference_revisions.orphan_purger')->doBatchSubmitFinished($success, $results, $operations); + } + + /** + * Sets a batch for executing deletion of the orphaned composite entities. + * + * @param array $composite_entity_type_ids + * An array of composite entity type IDs to remove orphaned items for. + */ + public function setBatch(array $composite_entity_type_ids) { + if (empty($composite_entity_type_ids)) { + return; + } + + $operations = []; + foreach ($composite_entity_type_ids as $entity_type_id) { + $operations[] = ['_entity_reference_revisions_orphan_purger_batch_dispatcher', + [ + 'entity_reference_revisions.orphan_purger:deleteOrphansBatchOperation', + $entity_type_id, + ], + ]; + } + + $batch = [ + 'operations' => $operations, + 'finished' => [EntityReferenceRevisionsOrphanPurger::class, 'batchSubmitFinished'], + 'title' => $this->t('Removing orphaned entities.'), + 'progress_message' => $this->t('Processed @current of @total entity types.'), + 'error_message' => $this->t('This batch encountered an error.'), + ]; + batch_set($batch); + } + + /** + * Finished callback for the batch process. + * + * @param bool $success + * Whether the batch completed successfully. + * @param array $results + * The results array. + * @param array $operations + * The operations array. + */ + public function doBatchSubmitFinished($success, $results, $operations) { + if ($success) { + foreach ($results as $entity_type_id => $result) { + if ($this->entityTypeManager->hasDefinition($entity_type_id)) { + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + $interval = $this->dateFormatter->formatInterval($result['end'] - $result['start']); + $this->messenger->addMessage($this->t('@label: Deleted @revision_count revisions (@entity_count entities) in @interval.', [ + '@label' => $entity_type->getLabel(), + '@revision_count' => $result['revision_count'], + '@entity_count' => $result['entity_count'], + '@interval' => $interval, + ])); + } + } + } + else { + // $operations contains the operations that remained unprocessed. + $error_operation = reset($operations); + $this->messenger->addError($this->t('An error occurred while processing @operation with arguments : @args', [ + '@operation' => $error_operation[0], + '@args' => print_r($error_operation[0], TRUE), + ])); + } + } + + /** + * Checks if the composite entity is used. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $composite_revision + * The composite revision. + * + * @return bool + * Whether the composite entity is used, FALSE if it is safe to delete. + */ + public function isUsed(ContentEntityInterface $composite_revision) { + $composite_type = $this->entityTypeManager->getDefinition($composite_revision->getEntityTypeId()); + + $parent_type_field = $composite_type->get('entity_revision_parent_type_field'); + $parent_type = $composite_revision->get($parent_type_field)->value; + $parent_field_name_field = $composite_type->get('entity_revision_parent_field_name_field'); + $parent_field_name = $composite_revision->get($parent_field_name_field)->value; + + $status = $this->isValidParent($parent_type, $parent_field_name); + if ($status !== static::PARENT_VALID) { + return $status == static::PARENT_INVALID_SKIP ? TRUE : FALSE; + } + + // Check if the revision is used in any revision of the parent, if that + // entity type supports revisions. + $query = $this->entityTypeManager->getStorage($parent_type) + ->getQuery() + ->condition("$parent_field_name.target_revision_id", $composite_revision->getRevisionId()) + ->range(0, 1) + ->accessCheck(FALSE); + + if ($this->entityTypeManager->getDefinition($parent_type)->isRevisionable()) { + $query = $query->allRevisions(); + } + + $revisions = $query->execute(); + // If there are parent revisions where this revision is used, skip it. + return !empty($revisions); + } + + /** + * Checks if the parent type/field is a valid combination that can be queried. + * + * @param string $parent_type + * Parent entity type ID. + * @param string $parent_field_name + * Parent field name. + * + * @return int + * static::PARENT_VALID, static::PARENT_INVALID_SKIP or + * static::PARENT_INVALID_DELETE. + */ + protected function isValidParent($parent_type, $parent_field_name) { + // There is not certainty that this revision is not used because we do not + // know what to query for if the parent fields are empty. + if ($parent_type == NULL) { + return static::PARENT_INVALID_SKIP; + } + + if (isset($this->validParents[$parent_type][$parent_field_name])) { + return $this->validParents[$parent_type][$parent_field_name]; + } + + $status = static::PARENT_VALID; + // If the parent type does not exist anymore, the composite is not used. + if (!$this->entityTypeManager->hasDefinition($parent_type)) { + $status = static::PARENT_INVALID_DELETE; + } + // Check if the parent field is valid. + elseif (!($parent_field_config = $this->entityTypeManager->getStorage('field_storage_config')->load("$parent_type.$parent_field_name"))) { + $status = static::PARENT_INVALID_DELETE; + } + // In case the parent field has no target revision ID key we can not be sure + // that this revision is not used anymore. + elseif (empty($parent_field_config->getSchema()['columns']['target_revision_id'])) { + $status = static::PARENT_INVALID_SKIP; + } + $this->validParents[$parent_type][$parent_field_name] = $status; + + return $status; + } + +} diff --git a/web/modules/entity_reference_revisions/src/Form/OrphanedCompositeEntitiesDeleteForm.php b/web/modules/entity_reference_revisions/src/Form/OrphanedCompositeEntitiesDeleteForm.php new file mode 100644 index 0000000000000000000000000000000000000000..b3da36da6bfd8fdadf272c8543b3eba134f0bdee --- /dev/null +++ b/web/modules/entity_reference_revisions/src/Form/OrphanedCompositeEntitiesDeleteForm.php @@ -0,0 +1,130 @@ +<?php + +namespace Drupal\entity_reference_revisions\Form; + +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Messenger\MessengerInterface; +use Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Class OrphanedCompositeEntitiesDeleteForm. + * + * @package Drupal\entity_reference_revisions\Form + */ +class OrphanedCompositeEntitiesDeleteForm extends FormBase { + + /** + * The Entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** + * The entity reference revisions orphan purger service. + * + * @var \Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger + */ + protected $purger; + + /** + * OrphanedCompositeEntitiesDeleteForm constructor. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager + * The EntityTypeManager service. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. + * @param \Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger $purger + * The entity reference revisions orphan purger. + */ + public function __construct(EntityTypeManagerInterface $entity_manager, MessengerInterface $messenger, EntityReferenceRevisionsOrphanPurger $purger) { + $this->entityTypeManager = $entity_manager; + $this->messenger = $messenger; + $this->purger = $purger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('messenger'), + $container->get('entity_reference_revisions.orphan_purger') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'orphaned_composite_entities_delete_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $this->messenger->addWarning($this->t('The submission of the current form can cause the deletion of entities that are still used, backup all data first.'), 'warning'); + $form['description'] = [ + '#markup' => $this->t('Delete orphaned composite entities revisions that are no longer referenced. If there are no revisions left, the entity will be deleted as long as it is not used.'), + ]; + $options = []; + foreach ($this->getCompositeEntityTypes() as $entity_type) { + $options[$entity_type->id()] = $entity_type->getLabel(); + } + $form['composite_entity_types'] = [ + '#type' => 'checkboxes', + '#required' => TRUE, + '#title' => $this->t('Select the entity types to check for orphans'), + '#options' => $options, + '#default_value' => array_keys($options), + ]; + $form['actions'] = ['#type' => 'actions']; + $form['actions']['submit'] = [ + '#type' => 'submit', + '#button_type' => 'primary', + '#value' => $this->t('Delete orphaned composite revisions'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->purger->setBatch(array_filter($form_state->getValue('composite_entity_types'))); + } + + /** + * Returns a list of composite entity types. + * + * @return \Drupal\Core\Entity\EntityTypeInterface[] + * An array of composite entity types. + */ + public function getCompositeEntityTypes() { + $composite_entity_types = []; + $entity_types = $this->entityTypeManager->getDefinitions(); + foreach ($entity_types as $entity_type) { + $has_parent_type_field = $entity_type->get('entity_revision_parent_type_field'); + $has_parent_id_field = $entity_type->get('entity_revision_parent_id_field'); + $has_parent_field_name_field = $entity_type->get('entity_revision_parent_field_name_field'); + if ($has_parent_type_field && $has_parent_id_field && $has_parent_field_name_field) { + $composite_entity_types[] = $entity_type; + } + } + return $composite_entity_types; + } + +} diff --git a/web/modules/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php b/web/modules/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php index c9e66ac6656263e0517264bd43baff8285cd3528..b24b6b10765f89cfe2c6ac420dd37f82f46721db 100644 --- a/web/modules/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php +++ b/web/modules/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php @@ -73,8 +73,13 @@ public function isTargetNew() { */ public function getTarget() { if (!isset($this->target) && isset($this->revision_id)) { - // If we have a valid reference, return the entity's TypedData adapter. - $entity = \Drupal::entityTypeManager()->getStorage($this->getTargetDefinition()->getEntityTypeId())->loadRevision($this->revision_id); + $storage = \Drupal::entityTypeManager()->getStorage($this->getTargetDefinition()->getEntityTypeId()); + // By default always load the default revision, so caches get used. + $entity = $storage->load($this->id); + if ($entity !== NULL && $entity->getRevisionId() != $this->revision_id) { + // A non-default revision is a referenced, so load this one. + $entity = $storage->loadRevision($this->revision_id); + } $this->target = isset($entity) ? $entity->getTypedData() : NULL; } return $this->target; diff --git a/web/modules/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php b/web/modules/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php index cdf2867b1b389046f0c2ea54a20fdfadda0b875e..16496dbc7e47247060fba0fa32077359b520d337 100644 --- a/web/modules/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php +++ b/web/modules/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php @@ -2,8 +2,10 @@ namespace Drupal\entity_reference_revisions\Plugin\Field\FieldType; +use Drupal\Component\Utility\Random; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\TranslatableRevisionableInterface; use Drupal\Core\Entity\TypedData\EntityDataDefinition; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; @@ -179,7 +181,7 @@ public function setValue($values, $notify = TRUE) { // If the entity has been saved and we're trying to set both the // target_id and the entity values with a non-null target ID, then the // value for target_id should match the ID of the entity value. - if (!$this->entity->isNew() && $values['target_id'] !== NULL && ($entity_id !== $values['target_id'])) { + if (!$this->entity->isNew() && $values['target_id'] !== NULL && ($entity_id != $values['target_id'])) { throw new \InvalidArgumentException('The target id and entity passed to the entity reference item do not match.'); } } @@ -253,24 +255,55 @@ public function preSave() { // If it is a new entity, parent will save it. parent::preSave(); + $is_affected = TRUE; if (!$has_new) { // Create a new revision if it is a composite entity in a host with a new // revision. $host = $this->getEntity(); $needs_save = $this->entity instanceof EntityNeedsSaveInterface && $this->entity->needsSave(); - if (!$host->isNew() && $host->isNewRevision() && $this->entity && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) { - $this->entity->setNewRevision(); - if ($host->isDefaultRevision()) { - $this->entity->isDefaultRevision(TRUE); + + // The item is considered to be affected if the field is either + // untranslatable or there are translation changes. This ensures that for + // translatable fields, a new revision of the referenced entity is only + // created for the affected translations and that the revision ID does not + // change on the unaffected translations. In turn, the host entity is not + // marked as affected for these translations. + $is_affected = !$this->getFieldDefinition()->isTranslatable() || ($host instanceof TranslatableRevisionableInterface && $host->hasTranslationChanges()); + if ($is_affected && !$host->isNew() && $this->entity && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) { + if ($host->isNewRevision()) { + $this->entity->setNewRevision(); + $needs_save = TRUE; + } + // Additionally ensure that the default revision state is kept in sync. + if ($this->entity && $host->isDefaultRevision() != $this->entity->isDefaultRevision()) { + $this->entity->isDefaultRevision($host->isDefaultRevision()); + $needs_save = TRUE; } - $needs_save = TRUE; } if ($needs_save) { + + // Because ContentEntityBase::hasTranslationChanges() does not check for + // EntityReferenceRevisionsFieldItemList::hasAffectingChanges() on field + // items that are not translatable, hidden on translation forms and not + // in the default translation, this has to be handled here by setting + // setRevisionTranslationAffected on host translations that holds a + // reference that has been changed. + if ($is_affected && $host instanceof TranslatableRevisionableInterface) { + $languages = $host->getTranslationLanguages(); + foreach ($languages as $langcode => $language) { + $translation = $host->getTranslation($langcode); + if ($this->entity->hasTranslation($langcode) && $this->entity->getTranslation($langcode)->hasTranslationChanges() && $this->target_revision_id != $this->entity->getRevisionId()) { + $translation->setRevisionTranslationAffected(TRUE); + $translation->setRevisionTranslationAffectedEnforced(TRUE); + } + } + } + $this->entity->save(); } } - if ($this->entity) { + if ($this->entity && $is_affected) { $this->target_revision_id = $this->entity->getRevisionId(); } } @@ -301,6 +334,22 @@ public function postSave($update) { } } + // Keep in sync the translation languages between the parent and the child. + // For non translatable fields we have to do this in ::preSave but for + // translatable fields we have all the information we need in ::delete. + if (isset($parent_entity->original) && !$this->getFieldDefinition()->isTranslatable()) { + $langcodes = array_keys($parent_entity->getTranslationLanguages()); + $original_langcodes = array_keys($parent_entity->original->getTranslationLanguages()); + if ($removed_langcodes = array_diff($original_langcodes, $langcodes)) { + foreach ($removed_langcodes as $removed_langcode) { + if ($entity->hasTranslation($removed_langcode) && $entity->getUntranslated()->language()->getId() != $removed_langcode) { + $entity->removeTranslation($removed_langcode); + } + } + $needs_save = TRUE; + } + } + $parent_type = $entity->getEntityType()->get('entity_revision_parent_type_field'); $parent_id = $entity->getEntityType()->get('entity_revision_parent_id_field'); @@ -328,8 +377,11 @@ public function postSave($update) { */ public function deleteRevision() { $child = $this->entity; - if ($child->isDefaultRevision()) { - // Do not delete if it is the default revision. + // Return early, and do not delete the child revision, when the child + // revision is either: + // 1: Missing. + // 2: A default revision. + if (!$child || $child->isDefaultRevision()) { return; } @@ -338,6 +390,7 @@ public function deleteRevision() { $all_revisions = \Drupal::entityQuery($host->getEntityTypeId()) ->condition($field_name, $child->getRevisionId()) ->allRevisions() + ->accessCheck(FALSE) ->execute(); if (count($all_revisions) > 1) { @@ -357,15 +410,59 @@ public function delete() { if ($this->entity && $this->entity->getEntityType()->get('entity_revision_parent_type_field') && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) { // Only delete composite entities if the host field is not translatable. if (!$this->getFieldDefinition()->isTranslatable()) { - $this->entity->delete(); + \Drupal::queue('entity_reference_revisions_orphan_purger')->createItem([ + 'entity_id' => $this->entity->id(), + 'entity_type_id' => $this->entity->getEntityTypeId(), + ]); } } } - /** - * {@inheritdoc} - */ + + /** + * {@inheritdoc} + */ public static function onDependencyRemoval(FieldDefinitionInterface $field_definition, array $dependencies) { - return FALSE; + $changed = FALSE; + $entity_type_manager = \Drupal::entityTypeManager(); + $target_entity_type = $entity_type_manager->getDefinition($field_definition->getFieldStorageDefinition() + ->getSetting('target_type')); + $handler_settings = $field_definition->getSetting('handler_settings'); + + // Update the 'target_bundles' handler setting if a bundle config dependency + // has been removed. + if (!empty($handler_settings['target_bundles'])) { + if ($bundle_entity_type_id = $target_entity_type->getBundleEntityType()) { + if ($storage = $entity_type_manager->getStorage($bundle_entity_type_id)) { + foreach ($storage->loadMultiple($handler_settings['target_bundles']) as $bundle) { + if (isset($dependencies[$bundle->getConfigDependencyKey()][$bundle->getConfigDependencyName()])) { + unset($handler_settings['target_bundles'][$bundle->id()]); + $changed = TRUE; + + // In case we deleted the only target bundle allowed by the field + // we can log a message because the behaviour of the field will + // have changed. + if ($handler_settings['target_bundles'] === []) { + \Drupal::logger('entity_reference_revisions') + ->notice('The %target_bundle bundle (entity type: %target_entity_type) was deleted. As a result, the %field_name entity reference revisions field (entity_type: %entity_type, bundle: %bundle) no longer specifies a specific target bundle. The field will now accept any bundle and may need to be adjusted.', [ + '%target_bundle' => $bundle->label(), + '%target_entity_type' => $bundle->getEntityType() + ->getBundleOf(), + '%field_name' => $field_definition->getName(), + '%entity_type' => $field_definition->getTargetEntityTypeId(), + '%bundle' => $field_definition->getTargetBundle() + ]); + } + } + } + } + } + } + + if ($changed) { + $field_definition->setSetting('handler_settings', $handler_settings); + } + + return $changed; } /** diff --git a/web/modules/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php b/web/modules/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php index faad139aba5389e3aa58ecd1e9559ce655d7f81c..c1d5ee51d96b4e2c4ad80f1c4afbec8d80dc8d04 100644 --- a/web/modules/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php +++ b/web/modules/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php @@ -2,7 +2,6 @@ namespace Drupal\entity_reference_revisions\Plugin\Field\FieldWidget; -use Drupal\Core\Entity\Entity; use Drupal\Core\Field\Plugin\Field\FieldWidget\EntityReferenceAutocompleteWidget; use Drupal\Core\Form\FormStateInterface; diff --git a/web/modules/entity_reference_revisions/src/Plugin/QueueWorker/OrphanPurger.php b/web/modules/entity_reference_revisions/src/Plugin/QueueWorker/OrphanPurger.php new file mode 100644 index 0000000000000000000000000000000000000000..a2bf051f1badda6050c5aade5d5a461fb90a8c57 --- /dev/null +++ b/web/modules/entity_reference_revisions/src/Plugin/QueueWorker/OrphanPurger.php @@ -0,0 +1,113 @@ +<?php + +namespace Drupal\entity_reference_revisions\Plugin\QueueWorker; + +use Drupal\Core\Database\Connection; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Queue\QueueWorkerBase; +use Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Removes composite revisions that are no longer used. + * + * @QueueWorker( + * id = "entity_reference_revisions_orphan_purger", + * title = @Translation("Entity Reference Revisions Orphan Purger"), + * cron = {"time" = 60} + * ) + */ +class OrphanPurger extends QueueWorkerBase implements ContainerFactoryPluginInterface { + + /** + * The entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The purger. + * + * @var \Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger + */ + protected $purger; + + /** + * The database. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * Constructs a new OrphanPurger instance. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager service. + * @param \Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger $purger + * The purger service. + * @param \Drupal\Core\Database\Connection $database + * The database service. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityReferenceRevisionsOrphanPurger $purger, Connection $database) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->entityTypeManager = $entity_type_manager; + $this->purger = $purger; + $this->database = $database; + } + + /** + * {@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_reference_revisions.orphan_purger'), + $container->get('database') + ); + } + + /** + * {@inheritdoc} + */ + public function processItem($data) { + $entity_type_id = $data['entity_type_id']; + if (!$this->entityTypeManager->hasDefinition($entity_type_id)) { + return; + } + + // Check the usage of data item and remove if not used. + $composite_storage = $this->entityTypeManager->getStorage($entity_type_id); + $composite_type = $this->entityTypeManager->getDefinition($entity_type_id); + $composite_revision_key = $composite_type->getKey('revision'); + + // Load all revisions of the composite type. + // @todo Replace with an entity query on all revisions with a revision ID + // condition after https://www.drupal.org/project/drupal/issues/2766135. + $entity_revision_ids = $this->database->select($composite_type->getRevisionTable(), 'r') + ->fields('r', [$composite_revision_key]) + ->condition($composite_type->getKey('id'), $data['entity_id']) + ->orderBy($composite_revision_key) + ->execute() + ->fetchCol(); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $composite_revision */ + foreach ($composite_storage->loadMultipleRevisions($entity_revision_ids) as $composite_revision) { + if (!$this->purger->isUsed($composite_revision)) { + $this->purger->deleteUnusedRevision($composite_revision); + } + } + } + +} diff --git a/web/modules/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php b/web/modules/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php index a50f5a76750cf719b1e5473a56b0eb7730ae65cb..d4baf31c65e7076e773e56993ba73ec2a8c0523e 100644 --- a/web/modules/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php +++ b/web/modules/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php @@ -2,6 +2,7 @@ namespace Drupal\entity_reference_revisions\Plugin\migrate\destination; +use Drupal\Component\Plugin\ConfigurableInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\TypedData\TranslatableInterface; use Drupal\migrate\MigrateException; @@ -12,12 +13,40 @@ /** * Provides entity_reference_revisions destination plugin. * + * Available configuration keys: + * - new_revisions: (optional) Flag to indicate if a new revision should be + * created instead of updating a previous default record. Only applicable when + * providing an entity id without a revision_id. + * * @MigrateDestination( * id = "entity_reference_revisions", * deriver = "Drupal\entity_reference_revisions\Plugin\Derivative\MigrateEntityReferenceRevisions" * ) */ -class EntityReferenceRevisions extends EntityRevision { +class EntityReferenceRevisions extends EntityRevision implements ConfigurableInterface { + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + $this->configuration = $configuration; + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'new_revisions' => FALSE, + ]; + } /** * {@inheritdoc} @@ -32,7 +61,7 @@ protected static function getEntityTypeId($pluginId) { /** * {@inheritdoc} */ - protected function save(ContentEntityInterface $entity, array $oldDestinationIdValues = array()) { + protected function save(ContentEntityInterface $entity, array $oldDestinationIdValues = []) { $entity->save(); return [ @@ -70,12 +99,33 @@ public function getIds() { * {@inheritdoc} */ protected function getEntity(Row $row, array $oldDestinationIdValues) { + $entity_id = $oldDestinationIdValues ? + array_shift($oldDestinationIdValues) : + $this->getEntityId($row); $revision_id = $oldDestinationIdValues ? array_pop($oldDestinationIdValues) : $row->getDestinationProperty($this->getKey('revision')); + + // If a specific revision_id is supplied and exists, assert the entity_id + // matches (if supplied), and update the revision. + /** @var \Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\EntityInterface $entity */ if (!empty($revision_id) && ($entity = $this->storage->loadRevision($revision_id))) { - $entity->setNewRevision(FALSE); + if (!empty($entity_id) && ($entity->id() != $entity_id)) { + throw new MigrateException("The revision_id exists for this entity type, but does not belong to the given entity id"); + } + $entity = $this->updateEntity($entity, $row) ?: $entity; + } + // If there is no revision_id supplied, but there is an entity_id + // supplied that exists, update it. + elseif (!empty($entity_id) && ($entity = $this->storage->load($entity_id))) { + // If so configured, create a new revision while updating. + if (!empty($this->configuration['new_revisions'])) { + $entity->setNewRevision(TRUE); + } + $entity = $this->updateEntity($entity, $row) ?: $entity; } + + // Otherwise, create a new (possibly stub) entity. else { // Attempt to ensure we always have a bundle. if ($bundle = $this->getBundle($row)) { @@ -90,7 +140,6 @@ protected function getEntity(Row $row, array $oldDestinationIdValues) { ->enforceIsNew(TRUE); $entity->setNewRevision(TRUE); } - $entity = $this->updateEntity($entity, $row) ?: $entity; $this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE; return $entity; } @@ -117,8 +166,8 @@ protected function rollbackTranslation(array $destination_identifiers) { $entity = $this->storage->loadRevision(array_pop($destination_identifiers)); if ($entity && $entity instanceof TranslatableInterface) { if ($key = $this->getKey('langcode')) { - if (isset($destination_identifier[$key])) { - $langcode = $destination_identifier[$key]; + if (isset($destination_identifiers[$key])) { + $langcode = $destination_identifiers[$key]; if ($entity->hasTranslation($langcode)) { // Make sure we don't remove the default translation. $translation = $entity->getTranslation($langcode); @@ -150,4 +199,5 @@ protected function rollbackNonTranslation(array $destination_identifiers) { } } } + } diff --git a/web/modules/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php b/web/modules/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php index 3c738564d2f4174dea61020c5770f686a7333565..65ad121f9bb692ccc0e6df8529a3857d787446a2 100644 --- a/web/modules/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php +++ b/web/modules/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php @@ -2,6 +2,7 @@ namespace Drupal\entity_reference_revisions\Plugin\views\display; +use Drupal\Core\Database\Query\Condition; use Drupal\views\Plugin\views\display\DisplayPluginBase; /** @@ -123,13 +124,13 @@ public function query() { // Restrict the autocomplete options based on what's been typed already. if (isset($options['match'])) { $style_options = $this->getOption('style'); - $value = db_like($options['match']) . '%'; + $value = \Drupal::database()->escapeLike($options['match']) . '%'; if ($options['match_operator'] != 'STARTS_WITH') { $value = '%' . $value; } // Multiple search fields are OR'd together. - $conditions = db_or(); + $conditions = new Condition('OR'); // Build the condition using the selected search fields. foreach ($style_options['options']['search_fields'] as $field_alias) { diff --git a/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml b/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml index d81f3c3672c4ee1be2daaed0b8b46c91c5f1a6d0..7b906fc29d371869a923883fddcde58671fc1faf 100644 --- a/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml +++ b/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml @@ -2,13 +2,13 @@ name: 'ERR Composite relationship test' type: module description: 'Entity with parent type and ID.' package: Testing -# core: 8.x +core_version_requirement: ^8.7.7 || ^9 dependencies: - - entity_reference_revisions - - entity_test -# Information added by Drupal.org packaging script on 2017-05-26 -version: '8.x-1.3' -core: '8.x' + - entity_reference_revisions:entity_reference_revisions + - drupal:entity_test + +# Information added by Drupal.org packaging script on 2020-03-11 +version: '8.x-1.8' project: 'entity_reference_revisions' -datestamp: 1495814304 +datestamp: 1583961849 diff --git a/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php b/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php index fb61ad15940d4ac80a6458a7b10a51b5be932f1e..9ef825c409854c2d8bedfcffd347e8c303d080d1 100644 --- a/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php +++ b/web/modules/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php @@ -18,6 +18,7 @@ * revision_table = "entity_test_composite_revision", * data_table = "entity_test_composite_field_data", * revision_data_table = "entity_test_composite_field_revision", + * content_translation_ui_skip = TRUE, * translatable = TRUE, * entity_revision_parent_type_field = "parent_type", * entity_revision_parent_id_field = "parent_id", @@ -44,15 +45,18 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields = parent::baseFieldDefinitions($entity_type); $fields['parent_id'] = BaseFieldDefinition::create('string') ->setLabel(t('Parent ID')) - ->setDescription(t('The ID of the parent entity of which this entity is referenced.')); + ->setDescription(t('The ID of the parent entity of which this entity is referenced.')) + ->setRevisionable(TRUE); $fields['parent_type'] = BaseFieldDefinition::create('string') ->setLabel(t('Parent type')) - ->setDescription(t('The entity parent type to which this entity is referenced.')); + ->setDescription(t('The entity parent type to which this entity is referenced.')) + ->setRevisionable(TRUE); $fields['parent_field_name'] = BaseFieldDefinition::create('string') ->setLabel(t('Parent field name')) - ->setDescription(t('The entity parent field name to which this entity is referenced.')); + ->setDescription(t('The entity parent field name to which this entity is referenced.')) + ->setRevisionable(TRUE); return $fields; } diff --git a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAdminTest.php similarity index 74% rename from web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php rename to web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAdminTest.php index e0a4c592740a2ce3871045364112aedc8b072deb..f2618b914f8337a868ec65d627c1d37a85e734a3 100644 --- a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAdminTest.php @@ -1,17 +1,17 @@ <?php -namespace Drupal\entity_reference_revisions\Tests; +namespace Drupal\Tests\entity_reference_revisions\Functional; -use Drupal\field_ui\Tests\FieldUiTestTrait; use Drupal\node\Entity\Node; -use Drupal\simpletest\WebTestBase; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; /** * Tests the entity_reference_revisions configuration. * * @group entity_reference_revisions */ -class EntityReferenceRevisionsAdminTest extends WebTestBase { +class EntityReferenceRevisionsAdminTest extends BrowserTestBase { use FieldUiTestTrait; @@ -28,6 +28,11 @@ class EntityReferenceRevisionsAdminTest extends WebTestBase { 'block', ); + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + /** * {@inheritdoc} */ @@ -38,12 +43,6 @@ protected function setUp() { $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); // Place the breadcrumb, tested in fieldUIAddNewField(). $this->drupalPlaceBlock('system_breadcrumb_block'); - } - - /** - * Tests the entity reference revisions configuration. - */ - public function testEntityReferenceRevisions() { $admin_user = $this->drupalCreateUser(array( 'administer site configuration', 'administer nodes', @@ -55,7 +54,12 @@ public function testEntityReferenceRevisions() { 'edit any article content', )); $this->drupalLogin($admin_user); + } + /** + * Tests the entity reference revisions configuration. + */ + public function testEntityReferenceRevisions() { // Create a test target node used as entity reference by another test node. $node_target = Node::create([ 'title' => 'Target node', @@ -90,7 +94,7 @@ public function testEntityReferenceRevisions() { 'title[0][value]' => $title, 'body[0][value]' => 'Revision 1', ); - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostForm('node/add/article', $edit, t('Save')); $this->assertText($title); $this->assertText('Revision 1'); $node = $this->drupalGetNodeByTitle($title); @@ -103,7 +107,7 @@ public function testEntityReferenceRevisions() { 'title[0][value]' => 'Entity reference revision content', 'field_entity_reference_revisions[1][target_id]' => $node->label() . ' (' . $node->id() . ')', ]; - $this->drupalPostForm(NULL, $edit, t('Save and publish')); + $this->drupalPostForm(NULL, $edit, t('Save')); $this->assertLinkByHref('node/' . $node_target->id()); $this->assertText('Entity revisions Entity reference revision content has been created.'); $this->assertText('Entity reference revision content'); @@ -115,7 +119,7 @@ public function testEntityReferenceRevisions() { 'body[0][value]' => 'Revision 2', 'revision' => TRUE, ); - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); $this->assertText($title); $this->assertText('Revision 2'); @@ -148,4 +152,37 @@ public function testEntityReferenceRevisions() { $this->assertEqual((string) $properties['entity']->getLabel(), 'Content'); } + /** + * Tests target bundle settings for an entity reference revisions field. + */ + public function testMultipleTargetBundles() { + // Create a couple of content types for the ERR field to point to. + $target_types = []; + for ($i = 0; $i < 2; $i++) { + $target_types[$i] = $this->drupalCreateContentType([ + 'type' => strtolower($this->randomMachineName()), + 'name' => 'Test type ' . $i + ]); + } + + // Create a new field that can point to either target content type. + $node_type_path = 'admin/structure/types/manage/entity_revisions'; + + // Generate a random field name, must be only lowercase characters. + $field_name = strtolower($this->randomMachineName()); + + $field_edit = []; + $storage_edit = ['settings[target_type]' => 'node', 'cardinality' => '-1']; + $field_edit['settings[handler_settings][target_bundles][' . $target_types[0]->id() . ']'] = TRUE; + $field_edit['settings[handler_settings][target_bundles][' . $target_types[1]->id() . ']'] = TRUE; + + $this->fieldUIAddNewField($node_type_path, $field_name, 'Entity reference revisions', 'entity_reference_revisions', $storage_edit, $field_edit); + + // Deleting one of these content bundles at this point should only delete + // that bundle's body field. Test that there is no second field that will + // be deleted. + $this->drupalGet('/admin/structure/types/manage/' . $target_types[0]->id() . '/delete'); + $this->assertNoFieldByXPath('(//details[@id="edit-entity-deletes"]//ul[@data-drupal-selector="edit-field-config"]/li)[2]'); + } + } diff --git a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAutocompleteTest.php similarity index 92% rename from web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php rename to web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAutocompleteTest.php index 4998a41cee51c9164182098c824f4068bd422b88..95cff09154cc2b9a6778faa3ea025dc94999182e 100644 --- a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsAutocompleteTest.php @@ -1,19 +1,19 @@ <?php -namespace Drupal\entity_reference_revisions\Tests; +namespace Drupal\Tests\entity_reference_revisions\Functional; use Drupal\block_content\Entity\BlockContent; use Drupal\Component\Utility\Html; -use Drupal\field_ui\Tests\FieldUiTestTrait; use Drupal\node\Entity\Node; -use Drupal\simpletest\WebTestBase; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; /** * Tests the entity_reference_revisions autocomplete. * * @group entity_reference_revisions */ -class EntityReferenceRevisionsAutocompleteTest extends WebTestBase { +class EntityReferenceRevisionsAutocompleteTest extends BrowserTestBase { use FieldUiTestTrait; @@ -30,6 +30,11 @@ class EntityReferenceRevisionsAutocompleteTest extends WebTestBase { 'field_ui', ); + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + /** * {@inheritdoc} */ @@ -91,7 +96,7 @@ public function testEntityReferenceRevisionsAutocompleteProcessing() { 'body[0][value]' => 'Revision 1', 'field_entity_reference_revisions[0][target_id]' => $block_label . ' (' . $block->id() . ')', ); - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostForm('node/add/article', $edit, t('Save')); $this->assertText($title); $this->assertText(Html::escape($block_content)); diff --git a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsDiffTest.php similarity index 83% rename from web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php rename to web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsDiffTest.php index e8ea4ad0c66cc56770ee8a3e9e048903d16f508d..a79d4d94dbbb8bf5029faa00458b05d10f46a2b5 100644 --- a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsDiffTest.php @@ -1,9 +1,9 @@ <?php -namespace Drupal\entity_reference_revisions\Tests; +namespace Drupal\Tests\entity_reference_revisions\Functional; -use Drupal\field_ui\Tests\FieldUiTestTrait; -use Drupal\simpletest\WebTestBase; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; /** * Tests the entity_reference_revisions diff plugin. @@ -12,9 +12,10 @@ * * @dependencies diff */ -class EntityReferenceRevisionsDiffTest extends WebTestBase { +class EntityReferenceRevisionsDiffTest extends BrowserTestBase { use FieldUiTestTrait; + /** * Modules to enable. * @@ -29,6 +30,11 @@ class EntityReferenceRevisionsDiffTest extends WebTestBase { 'diff', ]; + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + /** * {@inheritdoc} */ @@ -76,7 +82,7 @@ public function testEntityReferenceRevisionsDiff() { 'title[0][value]' => $title_node_1, 'body[0][value]' => 'body_node_1', ]; - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostForm('node/add/article', $edit, t('Save')); // Create second referenced node. $title_node_2 = 'referenced_node_2'; @@ -84,7 +90,7 @@ public function testEntityReferenceRevisionsDiff() { 'title[0][value]' => $title_node_2, 'body[0][value]' => 'body_node_2', ]; - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostForm('node/add/article', $edit, t('Save')); // Create referencing node. $title = 'referencing_node'; @@ -93,11 +99,11 @@ public function testEntityReferenceRevisionsDiff() { 'title[0][value]' => $title, 'field_err_field[0][target_id]' => $title_node_1 . ' (' . $node->id() . ')', ]; - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostForm('node/add/article', $edit, t('Save')); // Check the plugin is set. $this->drupalGet('admin/config/content/diff/fields'); - $this->drupalPostForm(NULL, ['fields[node.field_err_field][plugin][type]' => 'entity_reference_revisions_field_diff_builder'], t('Save')); + $this->drupalPostForm(NULL, ['fields[node__field_err_field][plugin][type]' => 'entity_reference_revisions_field_diff_builder'], t('Save')); // Update the referenced node of the err field and create a new revision. $node = $this->drupalGetNodeByTitle($title); @@ -106,7 +112,7 @@ public function testEntityReferenceRevisionsDiff() { 'field_err_field[0][target_id]' => $title_node_2 . ' (' . $referenced_node_new->id() . ')', 'revision' => TRUE, ]; - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); // Compare the revisions of the referencing node. $this->drupalPostForm('node/' . $node->id() . '/revisions', [], t('Compare selected revisions')); diff --git a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsNormalizerTest.php similarity index 90% rename from web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php rename to web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsNormalizerTest.php index 3faf8fb41a044234014364b2f41e2cd28f88b375..740fe8b1927ca8a430b70b0acb8a6a163cd211fb 100644 --- a/web/modules/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsNormalizerTest.php @@ -1,17 +1,17 @@ <?php -namespace Drupal\entity_reference_revisions\Tests; +namespace Drupal\Tests\entity_reference_revisions\Functional; -use Drupal\field_ui\Tests\FieldUiTestTrait; use Drupal\node\Entity\Node; -use Drupal\simpletest\WebTestBase; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; /** * Tests the entity_reference_revisions configuration. * * @group entity_reference_revisions */ -class EntityReferenceRevisionsNormalizerTest extends WebTestBase { +class EntityReferenceRevisionsNormalizerTest extends BrowserTestBase { use FieldUiTestTrait; @@ -31,6 +31,11 @@ class EntityReferenceRevisionsNormalizerTest extends WebTestBase { 'rest', ); + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + /** * {@inheritdoc} */ @@ -68,7 +73,7 @@ public function testEntityReferenceRevisions() { 'title[0][value]' => $title, 'body[0][value]' => 'Revision 1', ); - $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->drupalPostForm('node/add/article', $edit, t('Save')); $this->assertText($title); $this->assertText('Revision 1'); $node = $this->drupalGetNodeByTitle($title); @@ -79,7 +84,7 @@ public function testEntityReferenceRevisions() { 'title[0][value]' => $err_title, 'field_entity_reference_revisions[0][target_id]' => $node->label() . ' (' . $node->id() . ')', ); - $this->drupalPostForm('node/add/entity_revisions', $edit, t('Save and publish')); + $this->drupalPostForm('node/add/entity_revisions', $edit, t('Save')); $this->assertText('Entity revisions Entity reference revision content has been created.'); $err_node = $this->drupalGetNodeByTitle($err_title); @@ -92,7 +97,7 @@ public function testEntityReferenceRevisions() { 'body[0][value]' => 'Revision 2', 'revision' => TRUE, ); - $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); $serializer = $this->container->get('serializer'); $normalized = $serializer->normalize($err_node, 'hal_json'); $request = \Drupal::request(); diff --git a/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsOrphanRemovalTest.php b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsOrphanRemovalTest.php new file mode 100644 index 0000000000000000000000000000000000000000..3fa8798747c4e1aa92bbe1bfcfb685c0a48a0a90 --- /dev/null +++ b/web/modules/entity_reference_revisions/tests/src/Functional/EntityReferenceRevisionsOrphanRemovalTest.php @@ -0,0 +1,371 @@ +<?php + +namespace Drupal\Tests\entity_reference_revisions\Functional; + +use Drupal\Core\Site\Settings; +use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\node\Entity\NodeType; +use Drupal\Tests\BrowserTestBase; + +/** + * Tests orphan composite revisions are properly removed. + * + * @group entity_reference_revisions + */ +class EntityReferenceRevisionsOrphanRemovalTest extends BrowserTestBase { + + /** + * A user with administration access. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = [ + 'node', + 'field', + 'entity_reference_revisions', + 'entity_composite_relationship_test' + ]; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + $this->adminUser = $this->drupalCreateUser([ + 'delete orphan revisions', + ]); + $this->drupalLogin($this->adminUser); + $this->insertRevisionableData(); + $this->insertNonRevisionableData(); + } + + /** + * Tests that revisions that are no longer used are properly deleted. + */ + public function testNotUsedRevisionDeletion() { + $entity_test_composite_storage = \Drupal::entityTypeManager()->getStorage('entity_test_composite'); + + $composite_entity_first = $entity_test_composite_storage->loadByProperties(['name' => 'first not used, second used']); + $composite_entity_first = reset($composite_entity_first); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_first->id()); + + $composite_entity_second = $entity_test_composite_storage->loadByProperties(['name' => 'first used, second not used']); + $composite_entity_second = reset($composite_entity_second); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_second->id()); + + $composite_entity_third = $entity_test_composite_storage->loadByProperties(['name' => 'first not used, second not used']); + $composite_entity_third = reset($composite_entity_third); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_third->id()); + + $composite_entity_fourth = $entity_test_composite_storage->loadByProperties(['name' => '1st filled not, 2nd filled not']); + $composite_entity_fourth = reset($composite_entity_fourth); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_fourth->id()); + + $composite_entity_fifth = $entity_test_composite_storage->loadByProperties(['name' => '1st not, 2nd used, 3rd not, 4th']); + $composite_entity_fifth = reset($composite_entity_fifth); + $this->assertRevisionCount(4, 'entity_test_composite', $composite_entity_fifth->id()); + + $composite_entity_sixth = $entity_test_composite_storage->loadByProperties(['name' => 'wrong parent fields']); + $composite_entity_sixth = reset($composite_entity_sixth); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_entity_sixth->id()); + + // Test non revisionable parent entities. + $composite_entity_seventh = $entity_test_composite_storage->loadByProperties(['name' => 'NR first not used, second used']); + $composite_entity_seventh = reset($composite_entity_seventh); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_seventh->id()); + + $composite_entity_eighth = $entity_test_composite_storage->loadByProperties(['name' => 'NR first used, second not used']); + $composite_entity_eighth = reset($composite_entity_eighth); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_eighth->id()); + + $composite_entity_ninth = $entity_test_composite_storage->loadByProperties(['name' => 'NR 1st not, 2nd, 3rd not, 4th']); + $composite_entity_ninth = reset($composite_entity_ninth); + $this->assertRevisionCount(3, 'entity_test_composite', $composite_entity_ninth->id()); + + // Set the batch size to 1. + $settings = Settings::getInstance() ? Settings::getAll() : []; + $settings['entity_update_batch_size'] = 1; + new Settings($settings); + + // Run the delete process through the form. + $this->runDeleteForm(); + $this->assertSession()->pageTextContains('Test entity - composite relationship: Deleted 8 revisions (1 entities)'); + + $this->assertRevisionCount(1, 'entity_test_composite', $composite_entity_first->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_second->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_third->id()); + $this->assertRevisionCount(0, 'entity_test_composite', $composite_entity_fourth->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_fifth->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_entity_sixth->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_entity_seventh->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite_entity_eighth->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_entity_ninth->id()); + } + + /** + * Programmatically runs the 'Delete orphaned composite entities' form. + */ + public function runDeleteForm() { + $this->drupalGet('admin/config/system/delete-orphans'); + $this->submitForm([], t('Delete orphaned composite revisions')); + $this->checkForMetaRefresh(); + } + + /** + * Asserts the revision count of a certain entity. + * + * @param int $expected + * The expected count. + * @param string $entity_type_id + * The entity type ID, e.g. node. + * @param int $entity_id + * The entity ID. + */ + protected function assertRevisionCount($expected, $entity_type_id, $entity_id) { + $id_field = \Drupal::entityTypeManager()->getDefinition($entity_type_id)->getKey('id'); + $revision_count = \Drupal::entityQuery($entity_type_id) + ->condition($id_field, $entity_id) + ->allRevisions() + ->count() + ->execute(); + $this->assertEquals($expected, $revision_count); + } + + /** + * Inserts revisionable entities needed for testing. + */ + public function insertRevisionableData() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + NodeType::create(['type' => 'revisionable', 'new_revision' => TRUE])->save(); + // Add a translatable field and a not translatable field to both content + // types. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_composite_entity', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => [ + 'target_type' => 'entity_test_composite' + ], + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'revisionable', + 'translatable' => FALSE, + ]); + $field->save(); + + // Scenario 1: A composite with a default revision that is referenced and an + // old revision that is not. Result: Only the old revision is deleted. + $composite_entity_first = EntityTestCompositeRelationship::create([ + 'name' => 'first not used, second used', + 'parent_id' => 1000, + 'parent_type' => 'node', + 'parent_field_name' => 'field_composite_entity', + ]); + $composite_entity_first->save(); + $composite_entity_first = EntityTestCompositeRelationship::load($composite_entity_first->id()); + $composite_entity_first->setNewRevision(TRUE); + $composite_entity_first->save(); + $node = $this->drupalCreateNode([ + 'type' => 'revisionable', + 'title' => 'First composite', + 'field_composite_entity' => $composite_entity_first, + ]); + $node->save(); + + // Scenario 2: A composite with an old revision that is used and a default + // revision that is not. Result: Nothing should be deleted. + $composite_entity_second = EntityTestCompositeRelationship::create([ + 'name' => 'first used, second not used', + ]); + $composite_entity_second->save(); + $node = $this->drupalCreateNode([ + 'type' => 'revisionable', + 'title' => 'Second composite', + 'field_composite_entity' => $composite_entity_second, + ]); + $node->save(); + $node = $this->getNodeByTitle('Second composite'); + $node = $node_storage->createRevision($node); + $node->set('field_composite_entity', NULL); + $node->save(); + $composite_entity_second = EntityTestCompositeRelationship::load($composite_entity_second->id()); + $composite_entity_second->setNewRevision(TRUE); + $composite_entity_second->save(); + + // Scenario 3: A composite with an old revision and a default revision both + // that are not used with empty parent fields. Result: Nothing should be + // deleted since we do not know if it is still used. + $composite_entity_third = EntityTestCompositeRelationship::create([ + 'name' => 'first not used, second not used', + ]); + $composite_entity_third->save(); + $composite_entity_third = EntityTestCompositeRelationship::load($composite_entity_third->id()); + $composite_entity_third->setNewRevision(TRUE); + $composite_entity_third->save(); + + // Scenario 4: A composite with an old revision and a default revision both + // that are not used with filled parent fields. Result: Should first delete + // the old revision and then the default revision. Delete the entity too. + $composite_entity_fourth = EntityTestCompositeRelationship::create([ + 'name' => '1st filled not, 2nd filled not', + 'parent_id' => 1001, + 'parent_type' => 'node', + 'parent_field_name' => 'field_composite_entity', + ]); + $composite_entity_fourth->save(); + $composite_entity_fourth = EntityTestCompositeRelationship::load($composite_entity_fourth->id()); + $composite_entity_fourth->setNewRevision(TRUE); + $composite_entity_fourth->set('parent_id', 1001); + $composite_entity_fourth->save(); + + // Scenario 5: A composite with many revisions and 2 at least used. Result: + // Delete all unused revisions. + $composite_entity_fifth = EntityTestCompositeRelationship::create([ + 'name' => '1st not, 2nd used, 3rd not, 4th', + 'parent_id' => 1001, + 'parent_type' => 'node', + 'parent_field_name' => 'field_composite_entity', + ]); + $composite_entity_fifth->save(); + $composite_entity_fifth = EntityTestCompositeRelationship::load($composite_entity_fifth->id()); + $composite_entity_fifth->setNewRevision(TRUE); + $composite_entity_fifth->save(); + $node = $this->drupalCreateNode([ + 'type' => 'revisionable', + 'title' => 'Third composite', + 'field_composite_entity' => $composite_entity_fifth, + ]); + $node->save(); + $node = $this->getNodeByTitle('Third composite'); + $node = $node_storage->createRevision($node); + $node->set('field_composite_entity', NULL); + $node->save(); + $composite_entity_fifth = EntityTestCompositeRelationship::load($composite_entity_fifth->id()); + $composite_entity_fifth->setNewRevision(TRUE); + $composite_entity_fifth->save(); + $node = $this->getNodeByTitle('Third composite'); + $node = $node_storage->createRevision($node); + $node->set('field_composite_entity', $composite_entity_fifth); + $node->save(); + + // Scenario 6: A composite with wrong parent fields filled pointing to a non + // existent parent (Parent 1). However, Parent 2 references it. Result: Must + // not be deleted. + $node = $this->drupalCreateNode([ + 'type' => 'revisionable', + 'title' => 'DELETED composite', + ]); + $node->save(); + $composite_entity_sixth = EntityTestCompositeRelationship::create([ + 'name' => 'wrong parent fields', + 'parent_id' => $node->id(), + 'parent_type' => 'node', + 'parent_field_name' => 'field_composite_entity', + ]); + $composite_entity_sixth->save(); + $node->delete(); + $node = $this->drupalCreateNode([ + 'type' => 'revisionable', + 'title' => 'Fourth composite', + 'field_composite_entity' => $composite_entity_sixth, + ]); + $node->save(); + } + + /** + * Inserts non revisionable entities needed for testing. + */ + public function insertNonRevisionableData() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + NodeType::create(['type' => 'non_revisionable', 'new_revision' => FALSE])->save(); + // Add a translatable field and a not translatable field to both content + // types. + $field_storage = FieldStorageConfig::loadByName('node', 'field_composite_entity'); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'non_revisionable', + 'translatable' => FALSE, + ]); + $field->save(); + + // Scenario 1: A composite with a default revision that is referenced and an + // old revision that is not. Result: Only the old revision is deleted. + $composite_entity_first = EntityTestCompositeRelationship::create([ + 'name' => 'NR first not used, second used', + 'parent_id' => 1001, + 'parent_type' => 'node', + 'parent_field_name' => 'field_composite_entity', + ]); + $composite_entity_first->save(); + $composite_entity_first = EntityTestCompositeRelationship::load($composite_entity_first->id()); + $composite_entity_first->setNewRevision(TRUE); + $composite_entity_first->save(); + $node = $this->drupalCreateNode([ + 'type' => 'non_revisionable', + 'title' => 'First composite', + 'field_composite_entity' => $composite_entity_first, + ]); + $node->save(); + + // Scenario 2: A composite with an old revision that is used and a default + // revision that is not. Result: Nothing should be deleted. + $composite_entity_second = EntityTestCompositeRelationship::create([ + 'name' => 'NR first used, second not used', + ]); + $composite_entity_second->save(); + $node = $this->drupalCreateNode([ + 'type' => 'non_revisionable', + 'title' => 'Second composite', + 'field_composite_entity' => $composite_entity_second, + ]); + $node->save(); + $composite_entity_second = EntityTestCompositeRelationship::load($composite_entity_second->id()); + $composite_entity_second->setNewRevision(TRUE); + $composite_entity_second->save(); + + // Scenario 3: A composite with many revisions and 2 at least used. Result: + // Delete all unused revisions. + $composite_entity_third = EntityTestCompositeRelationship::create([ + 'name' => 'NR 1st not, 2nd, 3rd not, 4th', + 'parent_id' => 1001, + 'parent_type' => 'node', + 'parent_field_name' => 'field_composite_entity', + ]); + $composite_entity_third->save(); + $composite_entity_third = EntityTestCompositeRelationship::load($composite_entity_third->id()); + $composite_entity_third->setNewRevision(TRUE); + $composite_entity_third->save(); + $node = $this->drupalCreateNode([ + 'type' => 'non_revisionable', + 'title' => 'Third composite', + 'field_composite_entity' => $composite_entity_third, + ]); + $node->save(); + $node = $this->getNodeByTitle('Third composite'); + $node->set('field_composite_entity', NULL); + $node->save(); + $composite_entity_third = EntityTestCompositeRelationship::load($composite_entity_third->id()); + $composite_entity_third->setNewRevision(TRUE); + $composite_entity_third->save(); + $node = $this->getNodeByTitle('Third composite'); + $node->set('field_composite_entity', $composite_entity_third); + $node->save(); + } +} diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php index e92bfbe2b236bfb6d901c7ff333cd0f99bf890a5..ac226fa193e83e102522b04907517d2e336f683d 100644 --- a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php @@ -9,8 +9,8 @@ use Drupal\language\Entity\ConfigurableLanguage; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; -use Drupal\simpletest\ContentTypeCreationTrait; -use Drupal\simpletest\NodeCreationTrait; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; +use Drupal\Tests\node\Traits\NodeCreationTrait; /** * Tests the entity_reference_revisions composite relationship. @@ -50,6 +50,13 @@ class EntityReferenceRevisionsCompositeTest extends EntityKernelTestBase { */ protected $entityTypeManager; + /** + * The cron service. + * + * @var \Drupal\Core\Cron + */ + protected $cron; + /** * {@inheritdoc} */ @@ -79,9 +86,10 @@ protected function setUp() { )); $field->save(); - // Inject database connection and entity type manager for the tests. + // Inject database connection, entity type manager and cron for the tests. $this->database = \Drupal::database(); $this->entityTypeManager = \Drupal::entityTypeManager(); + $this->cron = \Drupal::service('cron'); } /** @@ -102,53 +110,116 @@ public function testEntityReferenceRevisionsCompositeRelationship() { $this->assertEquals(1, $composite_revisions_count); // Create a node with a reference to the test composite entity. + /** @var \Drupal\node\NodeInterface $node */ $node = Node::create(array( 'title' => $this->randomMachineName(), 'type' => 'article', - 'composite_reference' => $composite, )); $node->save(); + $node->set('composite_reference', $composite); + $this->assertTrue($node->hasTranslationChanges()); + $node->save(); // Assert that there is only 1 revision when creating a node. $node_revisions_count = \Drupal::entityQuery('node')->condition('nid', $node->id())->allRevisions()->count()->execute(); - $this->assertEqual($node_revisions_count, 1); + $this->assertEquals(1, $node_revisions_count); // Assert there is no new composite revision after creating a host entity. $composite_revisions_count = \Drupal::entityQuery('entity_test_composite')->condition('uuid', $composite->uuid())->allRevisions()->count()->execute(); $this->assertEquals(1, $composite_revisions_count); // Verify the value of parent type and id after create a node. $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); - $this->assertEqual($composite->parent_id->value, $node->id()); - $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); + $this->assertEquals($node->getEntityTypeId(), $composite->parent_type->value); + $this->assertEquals($node->id(), $composite->parent_id->value); + $this->assertEquals('composite_reference', $composite->parent_field_name->value); // Create second revision of the node. $original_composite_revision = $node->composite_reference[0]->target_revision_id; $original_node_revision = $node->getRevisionId(); $node->setTitle('2nd revision'); $node->setNewRevision(); $node->save(); - $node = node_load($node->id(), TRUE); + $node = Node::load($node->id()); // Check the revision of the node. - $this->assertEqual('2nd revision', $node->getTitle(), 'New node revision has changed data.'); - $this->assertNotEqual($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host did.'); + $this->assertEquals('2nd revision', $node->getTitle(), 'New node revision has changed data.'); + $this->assertNotEquals($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host did.'); // Make sure that there are only 2 revisions. $node_revisions_count = \Drupal::entityQuery('node')->condition('nid', $node->id())->allRevisions()->count()->execute(); - $this->assertEqual($node_revisions_count, 2); + $this->assertEquals(2,$node_revisions_count); // Revert to first revision of the node. $node = $this->entityTypeManager->getStorage('node')->loadRevision($original_node_revision); $node->setNewRevision(); $node->isDefaultRevision(TRUE); $node->save(); - $node = node_load($node->id(), TRUE); + $node = Node::load($node->id()); // Check the revision of the node. - $this->assertNotEqual('2nd revision', $node->getTitle(), 'Node did not keep changed title after reversion.'); - $this->assertNotEqual($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host reverted to an old revision.'); + $this->assertNotEquals('2nd revision', $node->getTitle(), 'Node did not keep changed title after reversion.'); + $this->assertNotEquals($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host reverted to an old revision.'); + + $node_storage = $this->entityTypeManager->getStorage('node'); + // Test that removing composite references results in translation changes. + $node->set('composite_reference', []); + $this->assertTrue($node->hasTranslationChanges()); + + // Test that changing composite reference results in translation changes. + $changed_composite_reference = $composite; + $changed_composite_reference->set('name', 'Changing composite reference'); + $this->assertTrue((bool) $changed_composite_reference->isRevisionTranslationAffected()); + + $node->set('composite_reference', $changed_composite_reference); + $node->setNewRevision(); + $this->assertTrue($node->hasTranslationChanges()); + $node->save(); + $nid = $node->id(); + $node_storage->resetCache([$nid]); + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->load($nid); + + // Check the composite has changed. + $this->assertEquals('Changing composite reference', $node->get('composite_reference')->entity->getName()); + + // Make sure the node has 4 revisions. + $node_revisions_count = $node_storage->getQuery()->condition('nid', $nid)->allRevisions()->count()->execute(); + $this->assertEqual($node_revisions_count, 4); + + // Make sure the node has no revision with revision translation affected + // flag set to NULL. + $node_revisions_count = $node_storage->getQuery()->condition('nid', $nid)->allRevisions()->condition('revision_translation_affected', NULL, 'IS NULL')->count()->execute(); + $this->assertEqual($node_revisions_count, 0, 'Node has a revision with revision translation affected set to NULL'); + + // Revert the changes to avoid interfering with the delete test. + $node->set('composite_reference', $composite); // Test that the composite entity is deleted when its parent is deleted. $node->delete(); + $this->assertNotNull(EntityTestCompositeRelationship::load($composite->id())); + + $this->cron->run(); $this->assertNull(EntityTestCompositeRelationship::load($composite->id())); + + // Test that the deleting composite entity does not break the parent entity + // when creating a new revision. + $composite = EntityTestCompositeRelationship::create([ + 'name' => $this->randomMachineName(), + ]); + $composite->save(); + // Create a node with a reference to the test composite entity. + /** @var \Drupal\node\NodeInterface $node */ + $node = Node::create([ + 'title' => $this->randomMachineName(), + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + // Delete the composite entity. + $composite->delete(); + // Re-apply the field item values to unset the computed "entity" property. + $field_item = $node->get('composite_reference')->get(0); + $field_item->setValue($field_item->getValue(), FALSE); + + $new_revision = $this->entityTypeManager->getStorage('node')->createRevision($node); + $this->assertTrue($new_revision->get('composite_reference')->isEmpty()); } /** @@ -178,23 +249,29 @@ function testCompositeRelationshipWithTranslationNonTranslatableField() { // Verify the value of parent type and id after create a node. $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); - $this->assertEqual($composite->parent_id->value, $node->id()); - $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); + $this->assertEquals($node->getEntityTypeId(), $composite->parent_type->value); + $this->assertEquals($node->id(), $composite->parent_id->value); + $this->assertEquals('composite_reference', $composite->parent_field_name->value); $this->assertTrue($composite->hasTranslation('de')); - // Test that the composite entity is not when the german translation of the - // parent is deleted. + // Test that the composite entity is not deleted when the german translation + // of the parent is deleted. $node->removeTranslation('de'); $node->save(); $composite = EntityTestCompositeRelationship::load($composite->id()); $this->assertNotNull($composite); - // @todo Support deleting translations of a composite reference. - // @see https://www.drupal.org/node/2834314. - //$this->assertFalse($composite->hasTranslation('de')); + $this->assertFalse($composite->hasTranslation('de')); + + // Change the language of the entity, ensure that doesn't try to delete + // the default translation. + $node->set('langcode', 'de'); + $node->save(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); // Test that the composite entity is deleted when its parent is deleted. $node->delete(); + $this->cron->run(); $composite = EntityTestCompositeRelationship::load($composite->id()); $this->assertNull($composite); } @@ -228,23 +305,23 @@ function testCompositeRelationshipWithTranslationTranslatableField() { // Verify the value of parent type and id after create a node. $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); - $this->assertEqual($composite->parent_id->value, $node->id()); - $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); + $this->assertEquals($node->getEntityTypeId(), $composite->parent_type->value); + $this->assertEquals($node->id(), $composite->parent_id->value); + $this->assertEquals('composite_reference', $composite->parent_field_name->value); - // Test that the composite entity is not when the german translation of the parent is deleted. + // Test that the composite entity is not deleted when the German parent + // translation is removed. $node->removeTranslation('de'); $node->save(); - //$this->entityTypeManager->getStorage('entity_test_composite')->resetCache(); + $this->cron->run(); $composite = EntityTestCompositeRelationship::load($composite->id()); $this->assertNotNull($composite); // Test that the composite entity is deleted when its parent is deleted. $node->delete(); + $this->cron->run(); $composite = EntityTestCompositeRelationship::load($composite->id()); - // @todo Support deletions for translatable fields. - // @see https://www.drupal.org/node/2834374 - // $this->assertNull($composite); + $this->assertNull($composite); } /** @@ -272,15 +349,15 @@ function testCompositeRelationshipWithRevisions() { $composite = EntityTestCompositeRelationship::load($composite->id()); $composite_original_revision_id = $composite->getRevisionId(); $node_original_revision_id = $node->getRevisionId(); - $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); - $this->assertEqual($composite->parent_id->value, $node->id()); - $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); + $this->assertEquals($node->getEntityTypeId(), $composite->parent_type->value); + $this->assertEquals($node->id(), $composite->parent_id->value); + $this->assertEquals('composite_reference', $composite->parent_field_name->value); $node->setNewRevision(TRUE); $node->save(); // Ensure that we saved a new revision ID. $composite = EntityTestCompositeRelationship::load($composite->id()); - $this->assertNotEqual($composite->getRevisionId(), $composite_original_revision_id); + $this->assertNotEquals($composite_original_revision_id, $composite->getRevisionId()); // Test that deleting the first revision does not delete the composite. $this->entityTypeManager->getStorage('node')->deleteRevision($node_original_revision_id); @@ -293,6 +370,7 @@ function testCompositeRelationshipWithRevisions() { // Test that the composite entity is deleted when its parent is deleted. $node->delete(); + $this->cron->run(); $composite = EntityTestCompositeRelationship::load($composite->id()); $this->assertNull($composite); } @@ -400,8 +478,235 @@ function testCompositeRelationshipDuplicatedRevisions() { // Test that the composite entity is deleted when its parent is deleted. $node->delete(); + $this->cron->run(); $composite = EntityTestCompositeRelationship::load($composite2->id()); $this->assertNull($composite); } + /** + * Tests the composite entity is deleted after removing its reference. + */ + public function testCompositeDeleteAfterRemovingReference() { + list($composite, $node) = $this->assignCompositeToNode(); + + // Remove reference to the composite entity from the node. + $node->set('composite_reference', NULL); + $node->save(); + + // Verify that the composite entity is not yet removed after deleting the + // parent. + $node->delete(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + + // Verify that the composite entity is removed after running cron. + $this->cron->run(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNull($composite); + } + + /** + * Tests the composite entity is deleted after removing its reference. + * + * Includes revisions on the host entity. + */ + public function testCompositeDeleteAfterRemovingReferenceWithRevisions() { + list($composite, $node) = $this->assignCompositeToNode(); + + // Remove reference to the composite entity from the node in a new revision. + $node->set('composite_reference', NULL); + $node->setNewRevision(); + $node->save(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + // Verify the composite entity is not removed on nodes with revisions. + $this->assertNotNull($composite); + + // Verify that the composite entity is not yet removed after deleting the + // parent. + $node->delete(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + + // Verify that the composite entity is removed after running cron. + $this->cron->run(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNull($composite); + } + + /** + * Tests the composite entity is not deleted when changing parents. + * + * Includes revisions on the host entity. + */ + public function testCompositeDeleteAfterChangingParent() { + list($composite, $node) = $this->assignCompositeToNode(); + // Remove reference to the composite entity from the node. + $node->set('composite_reference', NULL); + $node->setNewRevision(); + $node->save(); + + // Setting a new revision of the composite entity in the second node. + $composite = EntityTestCompositeRelationship::load($composite->id()); + $composite->setNewRevision(TRUE); + $composite->save(); + $second_node = Node::create([ + 'title' => 'Second node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $second_node->save(); + // Remove reference to the composite entity from the node. + $second_node->set('composite_reference', NULL); + $second_node->setNewRevision(TRUE); + $second_node->save(); + // Verify the composite entity is not removed on nodes with revisions. + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + // Verify the amount of revisions of each entity. + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(2, 'node', $node->id()); + $this->assertRevisionCount(2, 'node', $second_node->id()); + // Test that the composite entity is not deleted when its new parent is + // deleted, since it is still being used in a previous revision with a + // different parent. + $second_node->delete(); + $this->cron->run(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + + // Delete the parent of the previous revision. + $node->delete(); + + // Verify that the composite entity is removed after running cron. + $this->cron->run(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNull($composite); + } + + /** + * Composite entity with revisions isn't deleted when changing parents. + * + * Includes revisions on the host entity. + */ + public function testCompositeDeleteRevisionAfterChangingParent() { + list($composite, $node) = $this->assignCompositeToNode(); + // Remove reference to the composite entity from the node. + $node->set('composite_reference', NULL); + $node->setNewRevision(); + $node->save(); + + // Setting a new revision of the composite entity in the second node. + $composite = EntityTestCompositeRelationship::load($composite->id()); + $composite->setNewRevision(TRUE); + $composite->save(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $second_node = Node::create([ + 'title' => 'Second node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $second_node->save(); + // Remove reference to the composite entity from the node. + $second_node->set('composite_reference', NULL); + $second_node->setNewRevision(TRUE); + $second_node->save(); + // Verify the composite entity is not removed on nodes with revisions. + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + // Verify the amount of revisions of each entity. + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(2, 'node', $node->id()); + $this->assertRevisionCount(2, 'node', $second_node->id()); + // Test that the composite entity is not deleted when its old parent is + // deleted. + $node->delete(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + + // Verify that the composite entity is not removed after running cron but + // the previous unused revision is deleted. + $this->cron->run(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + $this->assertRevisionCount(1, 'entity_test_composite', $composite->id()); + } + + /** + * Tests the composite entity is not deleted when duplicating host entity. + * + * Includes revisions on the host entity. + */ + public function testCompositeDeleteAfterDuplicatingParent() { + list($composite, $node) = $this->assignCompositeToNode(); + $node->setNewRevision(TRUE); + $node->save(); + + // Create a duplicate of the node. + $duplicate_node = $node->createDuplicate(); + $duplicate_node->save(); + $duplicate_node->setNewRevision(TRUE); + $duplicate_node->save(); + + // Verify the amount of revisions of each entity. + $this->assertRevisionCount(3, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(2, 'node', $node->id()); + $this->assertRevisionCount(2, 'node', $duplicate_node->id()); + // Test that the composite entity is not deleted when the duplicate is + // deleted. + $duplicate_node->delete(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + + $this->cron->run(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + } + + /** + * Asserts the revision count of a certain entity. + * + * @param int $expected + * The expected count. + * @param string $entity_type_id + * The entity type ID, e.g. node. + * @param int $entity_id + * The entity ID. + */ + protected function assertRevisionCount($expected, $entity_type_id, $entity_id) { + $id_field = \Drupal::entityTypeManager() + ->getDefinition($entity_type_id) + ->getKey('id'); + $revision_count = \Drupal::entityQuery($entity_type_id) + ->condition($id_field, $entity_id) + ->allRevisions() + ->count() + ->execute(); + $this->assertEquals($expected, $revision_count); + } + + /** + * Creates and assigns the composite entity to a node. + * + * @param string $node_type + * The node type. + * + * @return array + * An array containing a composite and a node entity. + */ + protected function assignCompositeToNode($node_type = 'article') { + $composite = EntityTestCompositeRelationship::create([ + 'uuid' => $this->randomMachineName(), + 'name' => $this->randomMachineName(), + ]); + $composite->save(); + $node = Node::create([ + 'title' => $this->randomMachineName(), + 'type' => $node_type, + 'composite_reference' => $composite, + ]); + $node->save(); + + return [$composite, $node]; + } + } diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php new file mode 100644 index 0000000000000000000000000000000000000000..4e261bfcf4385ea6fc5314eb529df414f70304a1 --- /dev/null +++ b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php @@ -0,0 +1,351 @@ +<?php + +namespace Drupal\Tests\entity_reference_revisions\Kernel; + +use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\node\Entity\Node; +use Drupal\node\Entity\NodeType; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; +use Drupal\Tests\node\Traits\NodeCreationTrait; + +/** + * Tests entity_reference_revisions composites with a translatable field. + * + * @group entity_reference_revisions + */ +class EntityReferenceRevisionsCompositeTranslatableFieldTest extends EntityKernelTestBase { + + use ContentTypeCreationTrait; + use NodeCreationTrait; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array( + 'node', + 'field', + 'entity_reference_revisions', + 'entity_composite_relationship_test', + 'language', + 'content_translation' + ); + + /** + * The current database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + * + */ + protected $entityTypeManager; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + ConfigurableLanguage::createFromLangcode('de')->save(); + ConfigurableLanguage::createFromLangcode('fr')->save(); + + $this->installEntitySchema('entity_test_composite'); + $this->installSchema('node', ['node_access']); + + // Create article content type. + NodeType::create(['type' => 'article', 'name' => 'Article'])->save(); + + // Create the reference to the composite entity test. + $field_storage = FieldStorageConfig::create(array( + 'field_name' => 'composite_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => array( + 'target_type' => 'entity_test_composite' + ), + )); + $field_storage->save(); + $field = FieldConfig::create(array( + 'field_storage' => $field_storage, + 'bundle' => 'article', + 'translatable' => TRUE, + )); + $field->save(); + + // Inject database connection and entity type manager for the tests. + $this->database = \Drupal::database(); + $this->entityTypeManager = \Drupal::entityTypeManager(); + + // @todo content_translation should not be needed for a storage test, but + // \Drupal\Core\Entity\ContentEntityBase::isTranslatable() only returns + // TRUE if the bundle is explicitly translatable. + \Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE); + \Drupal::service('content_translation.manager')->setEnabled('entity_test_composite', 'entity_test_composite', TRUE); + \Drupal::service('content_translation.manager')->setBundleTranslationSettings('node', 'article', [ + 'untranslatable_fields_hide' => TRUE, + ]); + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + } + + /** + * Test the storage for handling pending revisions with translations. + */ + public function testCompositePendingRevisionTranslation() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + + // Create the test composite entity. + $composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Source Composite', + ]); + $composite->save(); + + // Create a node with a reference to the test composite entity. + $node = Node::create([ + 'langcode' => 'en', + 'title' => 'Initial Source Node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->load($node->id()); + + // Assert the revision count. + $this->assertRevisionCount(1, 'node', $node->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite->id()); + + // Create a translation as a pending revision for both the composite and the + // node. While technically, the referenced composite could be the same + // entity, for translatable fields, it makes more sense if each translation + // points to a separate entity, each only with a single language. + $composite_de = $node->get('composite_reference')->entity->createDuplicate(); + $composite_de->set('langcode', 'de'); + $composite_de->set('name', 'Pending Revision Composite #1 DE'); + /** @var \Drupal\node\NodeInterface $node_de */ + $node_de = $node->addTranslation('de', ['title' => 'Pending Revision Node #1 DE', 'composite_reference' => $composite_de] + $node->toArray()); + $node_de->setNewRevision(TRUE); + $node_de->isDefaultRevision(FALSE); + $node_de->save(); + + // Assert the revision count. + $this->assertRevisionCount(2, 'node', $node->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + + // The DE translation will now reference to a pending revision of the + // composite entity but the en translation will reference the existing, + // unchanged revision. + /** @var \Drupal\node\NodeInterface $node_revision */ + $node_revision = $node_storage->loadRevision($node_de->getRevisionId()); + $this->assertFalse($node_revision->isDefaultRevision()); + $this->assertFalse((bool) $node_revision->isRevisionTranslationAffected()); + $this->assertEquals('Initial Source Node', $node_revision->label()); + $this->assertTrue($node_revision->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Initial Source Composite', $node_revision->get('composite_reference')->entity->label()); + $this->assertFalse($node_revision->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals($node->get('composite_reference')->target_revision_id, $node_revision->get('composite_reference')->target_revision_id); + + $node_de = $node_revision->getTranslation('de'); + $this->assertTrue((bool) $node_de->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->label()); + // The composite is the default revision because it is a new entity. + $this->assertTrue($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->label()); + $this->assertNotEquals($node->get('composite_reference')->target_revision_id, $node_de->get('composite_reference')->target_revision_id); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Create a second translation revision for FR. + $composite_fr = $node->get('composite_reference')->entity->createDuplicate(); + $composite_fr->set('langcode', 'fr'); + $composite_fr->set('name', 'Pending Revision Composite #1 FR'); + $node_fr = $node->addTranslation('fr', ['title' => 'Pending Revision Node #1 FR', 'composite_reference' => $composite_fr] + $node->toArray()); + $node_fr->setNewRevision(TRUE); + $node_fr->isDefaultRevision(FALSE); + $node_fr->save(); + + // Assert the revision count. + $this->assertRevisionCount(3, 'node', $node->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // Now assert that all 3 revisions exist as expected. Two translation + // pending revisions, each has the original revision as parent without + // any existing translation. + /** @var \Drupal\node\NodeInterface $node_fr */ + $node_revision = $node_storage->loadRevision($node_fr->getRevisionId()); + $this->assertFalse($node_revision->isDefaultRevision()); + $this->assertFalse((bool) $node_revision->isRevisionTranslationAffected()); + $this->assertEquals('Initial Source Node', $node_revision->label()); + $this->assertTrue($node_revision->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Initial Source Composite', $node_revision->get('composite_reference')->entity->label()); + $this->assertFalse($node_revision->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals($node->get('composite_reference')->target_revision_id, $node_revision->get('composite_reference')->target_revision_id); + + $node_fr = $node_revision->getTranslation('fr'); + $this->assertTrue((bool) $node_fr->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->label()); + $this->assertTrue($node_fr->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->label()); + $this->assertNotEquals($node->get('composite_reference')->target_revision_id, $node_fr->get('composite_reference')->target_revision_id); + + $node_de = $node_storage->loadRevision($node_de->getRevisionId())->getTranslation('de'); + $this->assertTrue((bool) $node_de->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->label()); + $this->assertTrue($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->label()); + $this->assertNotEquals($node->get('composite_reference')->target_revision_id, $node_de->get('composite_reference')->target_revision_id); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Now make a change to the initial source revision, save as a new default + // revision. + $initial_revision_id = $node->getRevisionId(); + $node->get('composite_reference')->entity->set('name', 'Updated Source Composite'); + $node->setTitle('Updated Source Node'); + $node->setNewRevision(TRUE); + $node->save(); + + // Assert the revision count. + $this->assertRevisionCount(4, 'node', $node->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // Assert the two english revisions. + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertFalse($node->hasTranslation('fr')); + $this->assertTrue((bool) $node->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + + $node_initial = $node_storage->loadRevision($initial_revision_id); + $this->assertFalse($node_initial->isDefaultRevision()); + $this->assertFalse($node_initial->hasTranslation('de')); + $this->assertFalse($node_initial->hasTranslation('fr')); + $this->assertEquals('Initial Source Node', $node_initial->label()); + $this->assertFalse($node_initial->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node_initial->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node_initial->get('composite_reference')->entity->label()); + + // Now publish the FR pending revision. + $node_storage->createRevision($node_fr->getTranslation('fr'))->save(); + + // Assert the revision count. + $this->assertRevisionCount(5, 'node', $node->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // The new default revision should now have the updated english source and + // the french pending revision. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $node_fr = $node->getTranslation('fr'); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertTrue($node_fr->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + + // Now publish the DE pending revision as well. + $node_storage->createRevision($node_de->getTranslation('de'))->save(); + + // Assert the revision count. + $this->assertRevisionCount(6, 'node', $node->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // The new default revision should now have the updated source and both + // translations. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $node_fr = $node->getTranslation('fr'); + $node_de = $node->getTranslation('de'); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertFalse((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('de')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + + // Each translation only has the composite in its translation. + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('en')); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertFalse($node_fr->get('composite_reference')->entity->hasTranslation('en')); + $this->assertTrue($node_fr->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertFalse($node_fr->get('composite_reference')->entity->hasTranslation('de')); + $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('en')); + $this->assertTrue($node_de->get('composite_reference')->entity->hasTranslation('de')); + $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('fr')); + + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->label()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + } + + /** + * Asserts the revision count of a certain entity. + * + * @param int $expected + * The expected count. + * @param string $entity_type_id + * The entity type ID, e.g. node. + * @param int $entity_id + * The entity ID. + */ + protected function assertRevisionCount($expected, $entity_type_id, $entity_id) { + $id_field = \Drupal::entityTypeManager()->getDefinition($entity_type_id)->getKey('id'); + + $revision_count = \Drupal::entityQuery($entity_type_id) + ->condition($id_field, $entity_id) + ->allRevisions() + ->count() + ->execute(); + $this->assertEquals($expected, $revision_count); + } + +} diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2a406176ae35ef6e75ddaee7838a1ce2bf0d05b9 --- /dev/null +++ b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php @@ -0,0 +1,641 @@ +<?php + +namespace Drupal\Tests\entity_reference_revisions\Kernel; + +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\node\Entity\Node; +use Drupal\node\Entity\NodeType; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; +use Drupal\Tests\node\Traits\NodeCreationTrait; + +/** + * Tests the entity_reference_revisions composite relationship. + * + * @group entity_reference_revisions + */ +class EntityReferenceRevisionsCompositeTranslationTest extends EntityKernelTestBase { + + use ContentTypeCreationTrait; + use NodeCreationTrait; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = [ + 'node', + 'field', + 'entity_reference_revisions', + 'entity_composite_relationship_test', + 'language', + 'content_translation' + ]; + + /** + * The current database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + * + */ + protected $entityTypeManager; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + ConfigurableLanguage::createFromLangcode('de')->save(); + ConfigurableLanguage::createFromLangcode('fr')->save(); + + $this->installEntitySchema('entity_test_composite'); + $this->installSchema('node', ['node_access']); + + // Create article content type. + NodeType::create(['type' => 'article', 'name' => 'Article'])->save(); + + // Create the reference to the composite entity test. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'composite_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + 'settings' => [ + 'target_type' => 'entity_test_composite' + ], + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'article', + 'translatable' => FALSE, + ]); + $field->save(); + + // Create an untranslatable field on the composite entity. + $text_field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_untranslatable', + 'entity_type' => 'entity_test_composite', + 'type' => 'string', + ]); + $text_field_storage->save(); + $text_field = FieldConfig::create([ + 'field_storage' => $text_field_storage, + 'bundle' => 'entity_test_composite', + 'translatable' => FALSE, + ]); + $text_field->save(); + + // Add a nested composite field. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'composite_reference', + 'entity_type' => 'entity_test_composite', + 'type' => 'entity_reference_revisions', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + 'settings' => [ + 'target_type' => 'entity_test_composite' + ], + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'entity_test_composite', + 'translatable' => FALSE, + ]); + $field->save(); + + // Inject database connection and entity type manager for the tests. + $this->database = \Drupal::database(); + $this->entityTypeManager = \Drupal::entityTypeManager(); + + // @todo content_translation should not be needed for a storage test, but + // \Drupal\Core\Entity\ContentEntityBase::isTranslatable() only returns + // TRUE if the bundle is explicitly translatable. + \Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE); + \Drupal::service('content_translation.manager')->setEnabled('entity_test_composite', 'entity_test_composite', TRUE); + \Drupal::service('content_translation.manager')->setBundleTranslationSettings('node', 'article', [ + 'untranslatable_fields_hide' => TRUE, + ]); + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + } + + /** + * Test the storage for handling pending revisions with translations. + */ + public function testCompositePendingRevisionTranslation() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + + // Create a nested composite entity. + $nested_composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Nested Source Composite', + ]); + $nested_composite->save(); + + // Create a composite entity. + $composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Source Composite', + 'field_untranslatable' => 'Initial untranslatable field', + 'composite_reference' => $nested_composite, + ]); + $composite->save(); + + // Create a node with a reference to the test composite entity. + $node = Node::create([ + 'langcode' => 'en', + 'title' => 'Initial Source Node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + $initial_revision_id = $node->getRevisionId(); + + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->load($node->id()); + + // Assert that there is only 1 revision when creating a node. + $this->assertRevisionCount(1, $node); + // Assert there is no new composite revision after creating a host entity. + $this->assertRevisionCount(1, $composite); + // Assert there is no new composite revision after creating a host entity. + $this->assertRevisionCount(1, $nested_composite); + + // Create a second nested composite entity. + $second_nested_composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Nested Composite #2', + ]); + + // Add a pending revision. + $node = $node_storage->createRevision($node, FALSE); + $node->get('composite_reference')->entity->get('composite_reference')->appendItem($second_nested_composite); + $node->save(); + $pending_en_revision_id = $node->getRevisionId(); + + $this->assertRevisionCount(2, $node); + $this->assertRevisionCount(2, $composite); + $this->assertRevisionCount(2, $nested_composite); + $this->assertRevisionCount(1, $second_nested_composite); + + // Create a DE translation, start as a draft to replicate the behavior of + // the UI. + $node_de = $node->addTranslation('de', ['title' => 'New Node #1 DE'] + $node->toArray()); + $node_de = $node_storage->createRevision($node_de, FALSE); + + // Despite starting of the draft revision, creating draft of the translation + // uses the paragraphs of the default revision. + $this->assertCount(1, $node_de->get('composite_reference')->entity->get('composite_reference')); + + $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Composite #1 DE'); + $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Nested Composite #1 DE'); + $node_de->isDefaultRevision(TRUE); + $violations = $node_de->validate(); + foreach ($violations as $violation) { + $this->fail($violation->getPropertyPath() . ': ' . $violation->getMessage()); + } + $this->assertEquals(0, count($violations)); + $node_de->save(); + + $this->assertRevisionCount(3, $node); + $this->assertRevisionCount(3, $composite); + $this->assertRevisionCount(3, $nested_composite); + $this->assertRevisionCount(1, $second_nested_composite); + + // Update the translation as a pending revision for both the composite and + // the node. + $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'Pending Revision Composite #1 DE'); + $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->set('name', 'Pending Nested Composite #1 DE'); + $node_de->set('title', 'Pending Revision Node #1 DE'); + $node_de->setNewRevision(TRUE); + $node_de->isDefaultRevision(FALSE); + $violations = $node_de->validate(); + foreach ($violations as $violation) { + $this->fail($violation->getMessage()); + } + $this->assertEquals(0, count($violations)); + $node_de->save(); + + $this->assertRevisionCount(4, $node); + $this->assertRevisionCount(4, $composite); + $this->assertRevisionCount(4, $nested_composite); + $this->assertRevisionCount(1, $second_nested_composite); + + /** @var \Drupal\node\NodeInterface $node_de */ + $node_de = $node_storage->loadRevision($node_de->getRevisionId()); + $this->assertFalse($node_de->isDefaultRevision()); + $this->assertFalse((bool) $node_de->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node_de->getTranslation('de')->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->getTranslation('de')->label()); + $this->assertEquals('Initial Source Node', $node_de->label()); + $this->assertFalse($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Pending Nested Composite #1 DE', $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Initial untranslatable field', $node_de->get('composite_reference')->entity->getTranslation('de')->get('field_untranslatable')->value); + $this->assertEquals('Initial Source Composite', $node_de->get('composite_reference')->entity->label()); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Create a FR translation, start as a draft to replicate the behavior of + // the UI. + $node_fr = $node->addTranslation('fr', ['title' => 'Pending Revision Node #1 FR'] + $node->toArray()); + $node_fr = $node_storage->createRevision($node_fr, FALSE); + $node_fr->get('composite_reference')->entity->getTranslation('fr')->set('name', 'Pending Revision Composite #1 FR'); + $node_fr->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('fr')->set('name', 'Pending Nested Composite #1 FR'); + $violations = $node_fr->validate(); + $this->assertEquals(0, count($violations)); + $node_fr->save(); + + // Now assert that all 3 revisions exist as expected. Two translation + // pending revisions, each composite has the original revision as parent + // without any existing translation. + /** @var \Drupal\node\NodeInterface $node_fr */ + $node_fr = $node_storage->loadRevision($node_fr->getRevisionId()); + $this->assertFalse($node_fr->isDefaultRevision()); + $this->assertTrue($node_fr->hasTranslation('de')); + $this->assertFalse((bool) $node_fr->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node_fr->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->getTranslation('fr')->label()); + $this->assertEquals('Initial Source Node', $node_fr->label()); + $this->assertFalse($node_fr->get('composite_reference')->entity->isDefaultRevision()); + $this->assertTrue($node_fr->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Pending Nested Composite #1 FR', $node_fr->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Initial untranslatable field', $node_fr->get('composite_reference')->entity->getTranslation('fr')->get('field_untranslatable')->value); + $this->assertEquals('Initial Source Composite', $node_fr->get('composite_reference')->entity->label()); + + $node_de = $node_storage->loadRevision($node_de->getRevisionId()); + $this->assertFalse($node_de->isDefaultRevision()); + $this->assertFalse($node_de->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->getTranslation('de')->label()); + $this->assertEquals('Initial Source Node', $node_de->label()); + $this->assertFalse($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Pending Nested Composite #1 DE', $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Initial untranslatable field', $node_de->get('composite_reference')->entity->getTranslation('de')->get('field_untranslatable')->value); + $this->assertEquals('Initial Source Composite', $node_de->get('composite_reference')->entity->label()); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Create another pending EN revision and make that the default. + $node = $node_storage->loadRevision($pending_en_revision_id); + $new_revision = $node_storage->createRevision($node); + $new_revision->get('composite_reference')->entity->set('name', 'Updated Source Composite'); + $new_revision->get('composite_reference')->entity->set('field_untranslatable', 'Updated untranslatable field'); + $new_revision->setTitle('Updated Source Node'); + $new_revision->get('composite_reference')->entity->get('composite_reference')[1]->entity->set('name', 'Draft Nested Source Composite #2'); + $violations = $new_revision->validate(); + $this->assertEquals(0, count($violations)); + $new_revision->save(); + + // Assert the two english revisions. + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertFalse($node->hasTranslation('fr')); + $this->assertTrue((bool) $node->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + $this->assertEquals('Initial Nested Source Composite', $node->get('composite_reference')->entity->get('composite_reference')->entity->label()); + $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->label()); + $this->assertEquals('Updated untranslatable field', $node->get('composite_reference')->entity->get('field_untranslatable')->value); + + $node_initial = $node_storage->loadRevision($initial_revision_id); + $this->assertFalse($node_initial->isDefaultRevision()); + $this->assertFalse($node_initial->hasTranslation('de')); + $this->assertFalse($node_initial->hasTranslation('fr')); + $this->assertEquals('Initial Source Node', $node_initial->label()); + $this->assertFalse($node_initial->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node_initial->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node_initial->get('composite_reference')->entity->label()); + $this->assertEquals('Initial Nested Source Composite', $node_initial->get('composite_reference')->entity->get('composite_reference')->entity->label()); + $this->assertEquals('Initial untranslatable field', $node_initial->get('composite_reference')->entity->get('field_untranslatable')->value); + $this->assertCount(1, $node_initial->get('composite_reference')->entity->get('composite_reference')); + + // The current node_fr pending revision still has the initial value before + // "merging" it, but it will get the new value for the untranslatable field + // in the new revision. + $node_fr = $node_storage->loadRevision($node_fr->getRevisionId()); + $this->assertEquals('Initial untranslatable field', $node_fr->get('composite_reference')->entity->get('field_untranslatable')->value); + $this->assertCount(1, $node_fr->get('composite_reference')->entity->get('composite_reference')); + + // Now publish the FR pending revision and also add a translation for + // the second composite that it now has. + $new_revision = $node_storage->createRevision($node_fr->getTranslation('fr')); + $this->assertCount(2, $new_revision->get('composite_reference')->entity->get('composite_reference')); + $new_revision->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('fr')->set('name', 'FR Nested Composite #2'); + + $violations = $new_revision->validate(); + $this->assertEquals(0, count($violations)); + $new_revision->save(); + + $this->assertRevisionCount(7, $node); + $this->assertRevisionCount(7, $composite); + $this->assertRevisionCount(7, $nested_composite); + $this->assertRevisionCount(3, $second_nested_composite); + + // The new default revision should now have the updated english source, + // original german translation and the french pending revision. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 FR', $node->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Pending Nested Composite #1 FR', $node->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('New Node #1 DE', $node->getTranslation('de')->label()); + $this->assertEquals('New Composite #1 DE', $node->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('New Nested Composite #1 DE', $node->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + $this->assertEquals('Updated untranslatable field', $node->get('composite_reference')->entity->get('field_untranslatable')->value); + $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->label()); + $this->assertEquals('FR Nested Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('fr')->label()); + + // Now publish the DE pending revision as well. + $new_revision = $node_storage->createRevision($node_de->getTranslation('de')); + $violations = $new_revision->validate(); + $this->assertCount(2, $new_revision->get('composite_reference')->entity->get('composite_reference')); + $this->assertEquals(0, count($violations)); + $new_revision->save(); + + $this->assertRevisionCount(8, $node); + $this->assertRevisionCount(8, $composite); + $this->assertRevisionCount(8, $nested_composite); + $this->assertRevisionCount(4, $second_nested_composite); + + // The new default revision should now have the updated source and both + // translations. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertFalse((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('de')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 FR', $node->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Node #1 DE', $node->getTranslation('de')->label()); + $this->assertEquals('Pending Revision Composite #1 DE', $node->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Pending Nested Composite #1 DE', $node->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + $this->assertEquals('Updated untranslatable field', $node->get('composite_reference')->entity->get('field_untranslatable')->value); + $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->label()); + $this->assertEquals('FR Nested Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('fr')->label()); + + // The second nested composite of DE inherited the default values for its + // translation. + $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('de')->label()); + + // Simulate creating a new pending revision like + // \Drupal\content_moderation\EntityTypeInfo::entityPrepareForm(). + $new_revision = $node_storage->createRevision($node); + $revision_key = $new_revision->getEntityType()->getKey('revision'); + $new_revision->set($revision_key, $new_revision->getLoadedRevisionId()); + $new_revision->save(); + $this->assertEquals('Pending Nested Composite #1 DE', $new_revision->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label()); + + } + + /** + * Tests that composite translations affects the host entity's translations. + */ + public function testCompositeTranslation() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + $node_storage = $this->entityTypeManager->getStorage('node'); + + // Create a composite entity. + $composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Source Composite', + ]); + $composite->save(); + + // Create a node with a reference to the test composite entity. + $node = Node::create([ + 'langcode' => 'en', + 'title' => 'Initial Source Node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->load($node->id()); + + // Assert that there is only 1 revision when creating a node. + $this->assertRevisionCount(1, $node); + // Assert that there is only 1 affected revision when creating a node. + $this->assertAffectedRevisionCount(1, $node); + // Assert there is no new composite revision after creating a host entity. + $this->assertRevisionCount(1, $composite); + + $node_de = $node->addTranslation('de', ['title' => 'New Node #1 DE'] + $node->toArray()); + $node_de = $node_storage->createRevision($node_de, FALSE); + + $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Composite #1 DE'); + $node_de->isDefaultRevision(TRUE); + $violations = $node_de->validate(); + foreach ($violations as $violation) { + $this->fail($violation->getPropertyPath() . ': ' . $violation->getMessage()); + } + $this->assertEquals(0, count($violations)); + $node_de->save(); + $this->assertAffectedRevisionCount(1, $node_de); + $this->assertAffectedRevisionCount(1, $node); + + // Test that changing composite non default language (DE) reference results + // in translation changes for this language but not for the default + // language. + $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'Change Composite #1 DE'); + $node_de->setNewRevision(); + $node_de->save(); + + $this->assertEquals('Change Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->getName()); + + // Make sure the node DE has one more affected translation revision. + $this->assertAffectedRevisionCount(2, $node_de); + // Make sure the node EN has only one 1 affected translation revision. + $this->assertAffectedRevisionCount(1, $node); + + // Test that changing composite in default language (EN) results in + // translation changes for this language but not for the DE language. + $node = $node_storage->load($node->id()); + $node->get('composite_reference')->entity->set('name', 'Update Source #1'); + $node->setNewRevision(); + $node->save(); + + $this->assertEquals('Update Source #1', $node->get('composite_reference')->entity->getTranslation('en')->getName()); + + // The node EN now has 2 affected translation revision. + $this->assertAffectedRevisionCount(2, $node); + // The node DE still has 2 affected translation revisions. + $this->assertAffectedRevisionCount(2, $node_de); + } + + /** + * Tests that nested composite translations affects the host translations. + */ + public function testNestedCompositeTranslation() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + + // Create a nested composite entity. + $nested_composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Nested Source Composite', + ]); + $nested_composite->addTranslation('de', ['name' => 'Nested Source Composite DE'] + $nested_composite->toArray()); + $nested_composite->save(); + + // Create a composite entity. + $composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Source Composite', + 'field_untranslatable' => 'Initial untranslatable field', + 'composite_reference' => $nested_composite, + ]); + $composite->addTranslation('de', ['name' => 'Source Composite DE'] + $composite->toArray()); + $composite->save(); + + // Create a node with a reference to the test composite entity. + $node = Node::create([ + 'langcode' => 'en', + 'title' => 'Initial Source Node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->load($node->id()); + + // Assert that there is only 1 revision when creating a node. + $this->assertRevisionCount(1, $node); + // Assert that there is only 1 affected revision when creating a node. + $this->assertAffectedRevisionCount(1, $node); + // Assert there is no new composite revision after creating a host entity. + $this->assertRevisionCount(1, $composite); + // Assert there is no new nested composite revision after creating a host + // entity. + $this->assertRevisionCount(1, $nested_composite); + + $node_de = $node->addTranslation('de', ['title' => 'New Node #1 DE'] + $node->toArray()); + $node_de = $node_storage->createRevision($node_de, FALSE); + + $node_de->get('composite_reference')->entity->getTranslation('de')->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Nested Composite #1 DE'); + $node_de->isDefaultRevision(TRUE); + $node_de->save(); + $this->assertAffectedRevisionCount(1, $node_de); + $this->assertAffectedRevisionCount(1, $node); + + // Test that changing nested composite non default language (DE) reference + // results in translation changes for this language but not for the default + // language. + $node_de->get('composite_reference')->entity->getTranslation('de')->get('composite_reference')->entity->getTranslation('de')->set('name', 'Change Nested Composite #1 DE'); + $node_de->setNewRevision(); + $node_de->save(); + + $this->assertEquals('Change Nested Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->get('composite_reference')->entity->getTranslation('de')->getName()); + + // Make sure the node DE has one more affected translation revision. + $this->assertAffectedRevisionCount(2, $node_de); + // Make sure the node EN has only one 1 affected translation revision. + $this->assertAffectedRevisionCount(1, $node); + + // Test that changing nested composite in default language (EN) results in + // translation changes for this language but not for the DE language. + $node = $node_storage->load($node->id()); + $node->get('composite_reference')->entity->get('composite_reference')->entity->set('name', 'Update Nested Source #1'); + $node->setNewRevision(); + $node->save(); + + $this->assertEquals('Update Nested Source #1', $node->get('composite_reference')->entity->getTranslation('en')->get('composite_reference')->entity->getTranslation('en')->getName()); + + // The node EN now has 2 affected translation revision. + $this->assertAffectedRevisionCount(2, $node); + // The node DE still has 2 affected translation revisions. + $this->assertAffectedRevisionCount(2, $node_de); + } + + /** + * Asserts the affected revision count of a certain entity. + * + * @param int $expected + * The expected count. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + */ + protected function assertAffectedRevisionCount($expected, EntityInterface $entity) { + $entity_type = $entity->getEntityType(); + $affected_revisions_count = $this->entityTypeManager->getStorage($entity_type->id()) + ->getQuery() + ->condition($entity_type->getKey('id'), $entity->id()) + ->condition($entity_type->getKey('langcode'), $entity->language()->getId()) + ->condition($entity_type->getKey('revision_translation_affected'), 1) + ->allRevisions() + ->count() + ->execute(); + + $this->assertEquals($expected, $affected_revisions_count); + } + + /** + * Asserts the revision count of an entity. + * + * @param int $expected + * The expected amount of revisions. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + */ + protected function assertRevisionCount($expected, EntityInterface $entity) { + $node_revisions_count = \Drupal::entityQuery($entity->getEntityTypeId()) + ->condition($entity->getEntityType()->getKey('id'), $entity->id()) + ->allRevisions() + ->count() + ->execute(); + $this->assertEquals($expected, $node_revisions_count); + } + +} diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php index 430d06e6209eea49a6acda88403ae28bda24208f..f65e0745d8339bd2f355cdfd95591d95256cad86 100644 --- a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php @@ -8,7 +8,7 @@ use Drupal\KernelTests\KernelTestBase; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; -use Drupal\simpletest\UserCreationTrait; +use Drupal\Tests\user\Traits\UserCreationTrait; /** * @coversDefaultClass \Drupal\entity_reference_revisions\Plugin\Field\FieldFormatter\EntityReferenceRevisionsEntityFormatter diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php index e73a2dddc1d1eff63a71a2510ce927d177fc09b7..2b66a63058d9197152193f37a3226d8dc107ff80 100644 --- a/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php @@ -88,7 +88,8 @@ public function testNeedsSave() { 'type' => 'article', 'composite_reference' => $entity_test, ]); - // Check the name is properly set. + // Check the name is properly set and that getValue() returns the entity + // when it is marked as needs save." $values = $node->composite_reference->getValue(); $this->assertTrue(isset($values[0]['entity'])); static::assertEquals($values[0]['entity']->name->value, $text); @@ -102,20 +103,22 @@ public function testNeedsSave() { static::assertEquals($entity_test_after->name->value, $text); $new_text = 'Dummy text again'; - // Set the name again. - $entity_test->name = $new_text; - $entity_test->setNeedsSave(FALSE); + // Set another name and save the node without marking it as needs saving. + $entity_test_after->name = $new_text; + $entity_test_after->setNeedsSave(FALSE); - // Load the Node and check the composite reference field is not set. + // Load the Node and check the composite reference entity is not returned + // from getValue() if it is not marked as needs saving. $node = Node::load($node->id()); $values = $node->composite_reference->getValue(); $this->assertFalse(isset($values[0]['entity'])); - $node->composite_reference = $entity_test; + $node->composite_reference = $entity_test_after; $node->save(); // Check the name is not updated. + \Drupal::entityTypeManager()->getStorage('entity_test_composite')->resetCache(); $entity_test_after = EntityTestCompositeRelationship::load($entity_test->id()); - static::assertEquals($entity_test_after->name->value, $text); + static::assertEquals($text, $entity_test_after->name->value); // Test if after delete the referenced entity there are no problems setting // the referencing values to the parent. @@ -263,4 +266,56 @@ public function testEntityReferenceRevisionsDefaultValue() { $this->assertEquals($dependencies['config'][1], 'node.type.article'); $this->assertEquals($dependencies['module'][0], 'entity_reference_revisions'); } + + /** + * Tests FieldType\EntityReferenceRevisionsItem::deleteRevision + */ + public function testEntityReferenceRevisionsDeleteHandleDeletedChild() { + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => [ + 'target_type' => 'node', + ], + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'article', + ]); + $field->save(); + + $child = Node::create([ + 'type' => 'article', + 'title' => 'Child node', + ]); + $child->save(); + + $node = Node::create([ + 'type' => 'article', + 'title' => 'Parent node', + 'field_reference' => [ + [ + 'target_id' => $child->id(), + 'target_revision_id' => $child->getRevisionId(), + ] + ], + ]); + + // Create two revisions. + $node->save(); + $revisionId = $node->getRevisionId(); + $node->setNewRevision(TRUE); + $node->save(); + + // Force delete the child Paragraph. + // Core APIs allow this although it is an inconsistent storage situation + // for Paragraphs. + $child->delete(); + + // Previously deleting a revision with a lost child failed fatal. + \Drupal::entityTypeManager()->getStorage('node')->deleteRevision($revisionId); + } + } diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php index e68cf058caeb1693c85d9c0e83f65f784c0d2557..86c49a1ffb431f3cef56023cbf6799002124f112 100644 --- a/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php @@ -4,7 +4,6 @@ use Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions; use Drupal\KernelTests\KernelTestBase; -use Drupal\migrate\Plugin\MigrationPluginManager; use Drupal\migrate\Plugin\MigrateDestinationPluginManager; /** @@ -25,7 +24,7 @@ class EntityReferenceRevisionsDeriverTest extends KernelTestBase { */ protected function setUp() { parent::setUp(); - $this->installConfig($this->modules); + $this->installConfig(static::$modules); } /** diff --git a/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php b/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php index 02e02ec58760a932de36022c457a54506822d0d3..05f781f544756c6ac30373bc1972294fe0a34be4 100644 --- a/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php +++ b/web/modules/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php @@ -2,8 +2,6 @@ namespace Drupal\Tests\entity_reference_revisions\Kernel\Plugin\migrate\destination; -use Drupal\Core\Entity\EntityStorageBase; -use Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\KernelTests\KernelTestBase; @@ -21,9 +19,9 @@ class EntityReferenceRevisionsDestinationTest extends KernelTestBase implements MigrateMessageInterface { /** - * @var \Drupal\migrate\Plugin\MigrationPluginManager $migrationManager - * * The migration plugin manager. + * + * @var \Drupal\migrate\Plugin\MigrationPluginManager */ protected $migrationPluginManager; @@ -46,7 +44,7 @@ protected function setUp() { parent::setUp(); $this->installEntitySchema('entity_test_composite'); $this->installSchema('system', ['sequences']); - $this->installConfig($this->modules); + $this->installConfig(static::$modules); $this->migrationPluginManager = \Drupal::service('plugin.manager.migration'); } @@ -59,12 +57,12 @@ protected function setUp() { * @covers ::getEntityTypeId */ public function testGetEntityTypeId(array $definition, $expected) { - /** @var Migration $migration */ + /** @var \Drupal\migrate\Plugin\Migration $migration */ $migration = $this->migrationPluginManager->createStubMigration($definition); - /** @var EntityReferenceRevisions $destination */ + /** @var \Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions $destination */ $destination = $migration->getDestinationPlugin(); - /** @var EntityStorageBase $storage */ + /** @var \Drupal\Core\Entity\EntityStorageBase $storage */ $storage = $this->readAttribute($destination, 'storage'); $actual = $this->readAttribute($storage, 'entityTypeId'); @@ -75,13 +73,13 @@ public function testGetEntityTypeId(array $definition, $expected) { * Provides multiple migration definitions for "getEntityTypeId" test. */ public function getEntityTypeIdDataProvider() { - $datas = $this->getEntityDataProvider(); + $data = $this->getEntityDataProvider(); - foreach ($datas as &$data) { - $data['expected'] = 'entity_test_composite'; + foreach ($data as &$datum) { + $datum['expected'] = 'entity_test_composite'; } - return $datas; + return $data; } /** @@ -94,18 +92,19 @@ public function getEntityTypeIdDataProvider() { * @covers ::rollbackNonTranslation */ public function testGetEntity(array $definition, array $expected) { - /** @var Migration $migration */ + /** @var \Drupal\migrate\Plugin\Migration $migration */ $migration = $this->migrationPluginManager->createStubMigration($definition); $migrationExecutable = (new MigrateExecutable($migration, $this)); - /** @var EntityStorageBase $storage */ + /** @var \Drupal\Core\Entity\EntityStorageBase $storage */ $storage = $this->readAttribute($migration->getDestinationPlugin(), 'storage'); // Test inserting and updating by looping twice. for ($i = 0; $i < 2; $i++) { $migrationExecutable->import(); $migration->getIdMap()->prepareUpdate(); foreach ($expected as $data) { - $entity = $storage->loadRevision($data['id']); + $entity = $storage->loadRevision($data['revision_id']); $this->assertEquals($data['label'], $entity->label()); + $this->assertEquals($data['id'], $entity->id()); } } $migrationExecutable->rollback(); @@ -142,12 +141,70 @@ public function getEntityDataProvider() { ], ], 'expected' => [ - ['id' => 1, 'label' => 'content item 1a'], - ['id' => 2, 'label' => 'content item 1b'], - ['id' => 3, 'label' => 'content item 2'], + ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1a'], + ['id' => 2, 'revision_id' => 2, 'label' => 'content item 1b'], + ['id' => 3, 'revision_id' => 3, 'label' => 'content item 2'], + ], + ], + 'with ids' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + ['id' => 1, 'name' => 'content item 1a'], + ['id' => 1, 'name' => 'content item 1b'], + ['id' => 2, 'name' => 'content item 2'], + ['id' => 3, 'name' => 'content item 3'], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'text'], + ], + ], + 'process' => [ + 'name' => 'name', + 'id' => 'id', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + ], + ], + 'expected' => [ + ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1b'], + ['id' => 2, 'revision_id' => 2, 'label' => 'content item 2'], + ['id' => 3, 'revision_id' => 3, 'label' => 'content item 3'], ], ], - 'with keys' => [ + 'with ids and new revisions' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + ['id' => 1, 'name' => 'content item 1a'], + ['id' => 1, 'name' => 'content item 1b'], + ['id' => 2, 'name' => 'content item 2'], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'text'], + ], + ], + 'process' => [ + 'name' => 'name', + 'id' => 'id', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + 'new_revisions' => TRUE, + ], + ], + 'expected' => [ + ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1a'], + ['id' => 1, 'revision_id' => 2, 'label' => 'content item 1b'], + ['id' => 2, 'revision_id' => 3, 'label' => 'content item 2'], + ], + ], + 'with ids and revisions' => [ 'definition' => [ 'source' => [ 'plugin' => 'embedded_data', @@ -171,9 +228,9 @@ public function getEntityDataProvider() { ], ], 'expected' => [ - ['id' => 1, 'label' => 'content item 1'], - ['id' => 2, 'label' => 'content item 2'], - ['id' => 3, 'label' => 'content item 3'], + ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1'], + ['id' => 2, 'revision_id' => 2, 'label' => 'content item 2'], + ['id' => 3, 'revision_id' => 3, 'label' => 'content item 3'], ], ], ]; @@ -183,9 +240,8 @@ public function getEntityDataProvider() { * Tests multi-value and single-value destination field linkage. * * @dataProvider destinationFieldMappingDataProvider - * */ - public function testDestinationFieldMapping(array $datas) { + public function testDestinationFieldMapping(array $data) { $this->enableModules(['node', 'field']); $this->installEntitySchema('node'); $this->installEntitySchema('user'); @@ -202,7 +258,7 @@ public function testDestinationFieldMapping(array $datas) { 'entity_type' => 'node', 'type' => 'entity_reference_revisions', 'settings' => [ - 'target_type' => 'entity_test_composite' + 'target_type' => 'entity_test_composite', ], 'cardinality' => 1, ]); @@ -219,7 +275,7 @@ public function testDestinationFieldMapping(array $datas) { 'entity_type' => 'node', 'type' => 'entity_reference_revisions', 'settings' => [ - 'target_type' => 'entity_test_composite' + 'target_type' => 'entity_test_composite', ], 'cardinality' => -1, ]); @@ -232,9 +288,9 @@ public function testDestinationFieldMapping(array $datas) { $definitions = []; $instances = []; - foreach ($datas as $data) { - $definitions[$data['definition']['id']] = $data['definition']; - $instances[$data['definition']['id']] = $this->migrationPluginManager->createStubMigration($data['definition']); + foreach ($data as $datum) { + $definitions[$datum['definition']['id']] = $datum['definition']; + $instances[$datum['definition']['id']] = $this->migrationPluginManager->createStubMigration($datum['definition']); } // Reflection is easier than mocking. We need to use createInstance for @@ -245,13 +301,13 @@ public function testDestinationFieldMapping(array $datas) { $property->setValue($this->migrationPluginManager, $definitions); $this->container->set('plugin.manager.migration', $this->migrationPluginManager); - foreach ($datas as $data) { - $migration = $this->migrationPluginManager->createInstance($data['definition']['id']); + foreach ($data as $datum) { + $migration = $this->migrationPluginManager->createInstance($datum['definition']['id']); $migrationExecutable = (new MigrateExecutable($migration, $this)); - /** @var EntityStorageBase $storage */ + /** @var \Drupal\Core\Entity\EntityStorageBase $storage */ $storage = $this->readAttribute($migration->getDestinationPlugin(), 'storage'); $migrationExecutable->import(); - foreach ($data['expected'] as $expected) { + foreach ($datum['expected'] as $expected) { $entity = $storage->loadRevision($expected['id']); $properties = array_diff_key($expected, array_flip(['id'])); foreach ($properties as $property => $value) { @@ -262,7 +318,7 @@ public function testDestinationFieldMapping(array $datas) { } } else { - $this->assertNotEmpty($entity, 'Entity with label ' . $expected[$property] .' is empty'); + $this->assertNotEmpty($entity, 'Entity with label ' . $expected[$property] . ' is empty'); $this->assertEquals($expected[$property], $entity->label()); } } @@ -405,7 +461,7 @@ public function destinationFieldMappingDataProvider() { ], 'field_err_single/target_id' => [ [ - 'plugin' => 'migration', + 'plugin' => 'migration_lookup', 'migration' => ['single_err'], 'no_stub' => TRUE, 'source' => 'id', @@ -419,7 +475,7 @@ public function destinationFieldMappingDataProvider() { ], 'field_err_single/target_revision_id' => [ [ - 'plugin' => 'migration', + 'plugin' => 'migration_lookup', 'migration' => ['single_err'], 'no_stub' => TRUE, 'source' => 'id', @@ -433,7 +489,7 @@ public function destinationFieldMappingDataProvider() { ], 'field_err_multiple' => [ [ - 'plugin' => 'migration', + 'plugin' => 'migration_lookup', 'migration' => [ 'multiple_err_author1', 'multiple_err_author2', @@ -442,7 +498,7 @@ public function destinationFieldMappingDataProvider() { 'source' => 'author', ], [ - 'plugin' => 'iterator', + 'plugin' => 'sub_process', 'process' => [ 'target_id' => '0', 'target_revision_id' => '1',