Issue #2940204 by plach, Wim Leers, effulgentsia, matsbla, Gábor Hojtsy, tacituseu: Translatable fields with synchronization enabled should behave as untranslatable fields with respect to pending revisions

8.6.x
Gabor Hojtsy 2018-02-02 14:27:28 +01:00
parent 5978c5f3e6
commit 7e343df734
9 changed files with 895 additions and 12 deletions

View File

@ -163,6 +163,8 @@ function content_translation_entity_type_alter(array &$entity_types) {
}
$entity_type->set('translation', $translation);
}
$entity_type->addConstraint('ContentTranslationSynchronizedFields');
}
}

View File

@ -1,7 +1,7 @@
services:
content_translation.synchronizer:
class: Drupal\content_translation\FieldTranslationSynchronizer
arguments: ['@entity.manager']
arguments: ['@entity.manager', '@plugin.manager.field.field_type']
content_translation.subscriber:
class: Drupal\content_translation\Routing\ContentTranslationRouteSubscriber

View File

@ -5,6 +5,8 @@ namespace Drupal\content_translation;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
/**
* Provides field translation synchronization capabilities.
@ -18,14 +20,57 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
*/
protected $entityManager;
/**
* The field type plugin manager.
*
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected $fieldTypeManager;
/**
* Constructs a FieldTranslationSynchronizer object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entityManager
* The entity manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type plugin manager.
*/
public function __construct(EntityManagerInterface $entityManager) {
public function __construct(EntityManagerInterface $entityManager, FieldTypePluginManagerInterface $field_type_manager) {
$this->entityManager = $entityManager;
$this->fieldTypeManager = $field_type_manager;
}
/**
* {@inheritdoc}
*/
public function getFieldSynchronizedProperties(FieldDefinitionInterface $field_definition) {
$properties = [];
$settings = $this->getFieldSynchronizationSettings($field_definition);
foreach ($settings as $group => $translatable) {
if (!$translatable) {
$field_type_definition = $this->fieldTypeManager->getDefinition($field_definition->getType());
if (!empty($field_type_definition['column_groups'][$group]['columns'])) {
$properties = array_merge($properties, $field_type_definition['column_groups'][$group]['columns']);
}
}
}
return $properties;
}
/**
* Returns the synchronization settings for the specified field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* A field definition.
*
* @return string[]
* An array of synchronized field property names.
*/
protected function getFieldSynchronizationSettings(FieldDefinitionInterface $field_definition) {
if ($field_definition instanceof ThirdPartySettingsInterface && $field_definition->isTranslatable()) {
return $field_definition->getThirdPartySetting('content_translation', 'translation_sync', []);
}
return [];
}
/**
@ -33,7 +78,6 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
*/
public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode, $original_langcode = NULL) {
$translations = $entity->getTranslationLanguages();
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
// If we have no information about what to sync to, if we are creating a new
// entity, if we have no translations for the current entity and we are not
@ -43,21 +87,55 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
}
// If the entity language is being changed there is nothing to synchronize.
$entity_type = $entity->getEntityTypeId();
$entity_unchanged = isset($entity->original) ? $entity->original : $this->entityManager->getStorage($entity_type)->loadUnchanged($entity->id());
$entity_unchanged = $this->getOriginalEntity($entity);
if ($entity->getUntranslated()->language()->getId() != $entity_unchanged->getUntranslated()->language()->getId()) {
return;
}
if ($entity->isNewRevision()) {
if ($entity->isDefaultTranslationAffectedOnly()) {
// If changes to untranslatable fields are configured to affect only the
// default translation, we need to skip synchronization in pending
// revisions, otherwise multiple translations would be affected.
if (!$entity->isDefaultRevision()) {
return;
}
// When this mode is enabled, changes to synchronized properties are
// allowed only in the default translation, thus we need to make sure this
// is always used as source for the synchronization process.
else {
$sync_langcode = $entity->getUntranslated()->language()->getId();
}
}
elseif ($entity->isDefaultRevision()) {
// If a new default revision is being saved, but a newer default
// revision was created meanwhile, use any other translation as source
// for synchronization, since that will have been merged from the
// default revision. In this case the actual language does not matter as
// synchronized properties are the same for all the translations in the
// default revision.
/** @var \Drupal\Core\Entity\ContentEntityInterface $default_revision */
$default_revision = $this->entityManager
->getStorage($entity->getEntityTypeId())
->load($entity->id());
if ($default_revision->getLoadedRevisionId() !== $entity->getLoadedRevisionId()) {
$other_langcodes = array_diff_key($default_revision->getTranslationLanguages(), [$sync_langcode => FALSE]);
if ($other_langcodes) {
$sync_langcode = key($other_langcodes);
}
}
}
}
/** @var \Drupal\Core\Field\FieldItemListInterface $items */
foreach ($entity as $field_name => $items) {
$field_definition = $items->getFieldDefinition();
$field_type_definition = $field_type_manager->getDefinition($field_definition->getType());
$field_type_definition = $this->fieldTypeManager->getDefinition($field_definition->getType());
$column_groups = $field_type_definition['column_groups'];
// Sync if the field is translatable, not empty, and the synchronization
// setting is enabled.
if ($field_definition instanceof ThirdPartySettingsInterface && $field_definition->isTranslatable() && !$items->isEmpty() && $translation_sync = $field_definition->getThirdPartySetting('content_translation', 'translation_sync')) {
if (($translation_sync = $this->getFieldSynchronizationSettings($field_definition)) && !$items->isEmpty()) {
// Retrieve all the untranslatable column groups and merge them into
// single list.
$groups = array_keys(array_diff($translation_sync, array_filter($translation_sync)));
@ -101,6 +179,26 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
}
}
/**
* Returns the original unchanged entity to be used to detect changes.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being changed.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* The unchanged entity.
*/
protected function getOriginalEntity(ContentEntityInterface $entity) {
if (!isset($entity->original)) {
$storage = $this->entityManager->getStorage($entity->getEntityTypeId());
$original = $entity->isDefaultRevision() ? $storage->loadUnchanged($entity->id()) : $storage->loadRevision($entity->getLoadedRevisionId());
}
else {
$original = $entity->original;
}
return $original;
}
/**
* {@inheritdoc}
*/
@ -174,9 +272,7 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
// items and the other columns from the existing values. This only
// works if the delta exists in the language.
elseif ($created && !empty($original_field_values[$langcode][$delta])) {
$item_columns_to_sync = array_intersect_key($source_items[$delta], array_flip($columns));
$item_columns_to_keep = array_diff_key($original_field_values[$langcode][$delta], array_flip($columns));
$values[$langcode][$delta] = $item_columns_to_sync + $item_columns_to_keep;
$values[$langcode][$delta] = $this->createMergedItem($source_items[$delta], $original_field_values[$langcode][$delta], $columns);
}
// If the delta doesn't exist, copy from the source language.
elseif ($created) {
@ -190,13 +286,37 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
// If the value has only been reordered we just move the old one in
// the new position.
$item = isset($original_field_values[$langcode][$old_delta]) ? $original_field_values[$langcode][$old_delta] : $source_items[$new_delta];
$values[$langcode][$new_delta] = $item;
// When saving a default revision starting from a pending revision,
// we may have desynchronized field values, so we make sure that
// untranslatable properties are synchronized, even if in any other
// situation this would not be necessary.
$values[$langcode][$new_delta] = $this->createMergedItem($source_items[$new_delta], $item, $columns);
}
}
}
}
}
/**
* Creates a merged item.
*
* @param array $source_item
* An item containing the untranslatable properties to be synchronized.
* @param array $target_item
* An item containing the translatable properties to be kept.
* @param string[] $properties
* An array of properties to be synchronized.
*
* @return array
* A merged item array.
*/
protected function createMergedItem(array $source_item, array $target_item, array $properties) {
$property_keys = array_flip($properties);
$item_properties_to_sync = array_intersect_key($source_item, $property_keys);
$item_properties_to_keep = array_diff_key($target_item, $property_keys);
return $item_properties_to_sync + $item_properties_to_keep;
}
/**
* Computes a hash code for the specified item.
*

View File

@ -3,6 +3,7 @@
namespace Drupal\content_translation;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
/**
* Provides field translation synchronization capabilities.
@ -54,4 +55,15 @@ interface FieldTranslationSynchronizerInterface {
*/
public function synchronizeItems(array &$field_values, array $unchanged_items, $sync_langcode, array $translations, array $columns);
/**
* Returns the synchronized properties for the specified field definition.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* A field definition.
*
* @return string[]
* An array of synchronized field property names.
*/
public function getFieldSynchronizedProperties(FieldDefinitionInterface $field_definition);
}

View File

@ -0,0 +1,25 @@
<?php
namespace Drupal\content_translation\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Validation constraint for the entity changed timestamp.
*
* @internal
*
* @Constraint(
* id = "ContentTranslationSynchronizedFields",
* label = @Translation("Content translation synchronized fields", context = "Validation"),
* type = {"entity"}
* )
*/
class ContentTranslationSynchronizedFieldsConstraint extends Constraint {
// In this case "elements" refers to "field properties", in fact it is what we
// are using in the UI elsewhere.
public $defaultRevisionMessage = 'Non-translatable field elements can only be changed when updating the current revision.';
public $defaultTranslationMessage = 'Non-translatable field elements can only be changed when updating the original language.';
}

View File

@ -0,0 +1,226 @@
<?php
namespace Drupal\content_translation\Plugin\Validation\Constraint;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\content_translation\FieldTranslationSynchronizerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Checks that synchronized fields are handled correctly in pending revisions.
*
* As for untranslatable fields, two modes are supported:
* - When changes to untranslatable fields are configured to affect all revision
* translations, synchronized field properties can be changed only in default
* revisions.
* - When changes to untranslatable fields affect are configured to affect only
* the revision's default translation, synchronized field properties can be
* changed only when editing the default translation. This may lead to
* temporarily desynchronized values, when saving a pending revision for the
* default translation that changes a synchronized property. These are
* actually synchronized when saving changes to the default translation as a
* new default revision.
*
* @see \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraint
* @see \Drupal\Core\Entity\Plugin\Validation\Constraint\EntityUntranslatableFieldsConstraintValidator
*
* @internal
*/
class ContentTranslationSynchronizedFieldsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The content translation manager.
*
* @var \Drupal\content_translation\ContentTranslationManagerInterface
*/
protected $contentTranslationManager;
/**
* The field translation synchronizer.
*
* @var \Drupal\content_translation\FieldTranslationSynchronizerInterface
*/
protected $synchronizer;
/**
* ContentTranslationSynchronizedFieldsConstraintValidator constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
* The content translation manager.
* @param \Drupal\content_translation\FieldTranslationSynchronizerInterface $synchronizer
* The field translation synchronizer.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, ContentTranslationManagerInterface $content_translation_manager, FieldTranslationSynchronizerInterface $synchronizer) {
$this->entityTypeManager = $entity_type_manager;
$this->contentTranslationManager = $content_translation_manager;
$this->synchronizer = $synchronizer;
}
/**
* [@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('content_translation.manager'),
$container->get('content_translation.synchronizer')
);
}
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
/** @var \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraint $constraint */
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $value;
if ($entity->isNew() || !$entity->getEntityType()->isRevisionable()) {
return;
}
// When changes to untranslatable fields are configured to affect all
// revision translations, we always allow changes in default revisions.
if ($entity->isDefaultRevision() && !$entity->isDefaultTranslationAffectedOnly()) {
return;
}
$entity_type_id = $entity->getEntityTypeId();
if (!$this->contentTranslationManager->isEnabled($entity_type_id, $entity->bundle())) {
return;
}
$synchronized_properties = $this->getSynchronizedPropertiesByField($entity->getFieldDefinitions());
if (!$synchronized_properties) {
return;
}
/** @var \Drupal\Core\Entity\ContentEntityInterface $original */
$original = $this->getOriginalEntity($entity);
$original_translation = $this->getOriginalTranslation($entity, $original);
if ($this->hasSynchronizedPropertyChanges($entity, $original_translation, $synchronized_properties)) {
if ($entity->isDefaultTranslationAffectedOnly()) {
foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) {
if ($entity->getTranslation($langcode)->hasTranslationChanges()) {
$this->context->addViolation($constraint->defaultTranslationMessage);
break;
}
}
}
else {
$this->context->addViolation($constraint->defaultRevisionMessage);
}
}
}
/**
* Checks whether any synchronized property has changes.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being validated.
* @param \Drupal\Core\Entity\ContentEntityInterface $original
* The original unchanged entity.
* @param string[][] $synchronized_properties
* An associative array of arrays of synchronized field properties keyed by
* field name.
*
* @return bool
* TRUE if changes in synchronized properties were detected, FALSE
* otherwise.
*/
protected function hasSynchronizedPropertyChanges(ContentEntityInterface $entity, ContentEntityInterface $original, array $synchronized_properties) {
foreach ($synchronized_properties as $field_name => $properties) {
foreach ($properties as $property) {
$items = $entity->get($field_name)->getValue();
$original_items = $original->get($field_name)->getValue();
if (count($items) !== count($original_items)) {
return TRUE;
}
foreach ($items as $delta => $item) {
// @todo This loose comparison is not fully reliable. Revisit this
// after https://www.drupal.org/project/drupal/issues/2941092.
if ($items[$delta][$property] != $original_items[$delta][$property]) {
return TRUE;
}
}
}
}
return FALSE;
}
/**
* Returns the original unchanged entity to be used to detect changes.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being changed.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* The unchanged entity.
*/
protected function getOriginalEntity(ContentEntityInterface $entity) {
if (!isset($entity->original)) {
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
$original = $entity->isDefaultRevision() ? $storage->loadUnchanged($entity->id()) : $storage->loadRevision($entity->getLoadedRevisionId());
}
else {
$original = $entity->original;
}
return $original;
}
/**
* Returns the original translation.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being validated.
* @param \Drupal\Core\Entity\ContentEntityInterface $original
* The original entity.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* The original entity translation object.
*/
protected function getOriginalTranslation(ContentEntityInterface $entity, ContentEntityInterface $original) {
$langcode = $entity->language()->getId();
if ($original->hasTranslation($langcode)) {
$original_langcode = $langcode;
}
else {
$metadata = $this->contentTranslationManager->getTranslationMetadata($entity);
$original_langcode = $metadata->getSource();
}
return $original->getTranslation($original_langcode);
}
/**
* Returns the synchronized properties for every specified field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions
* An array of field definitions.
*
* @return string[][]
* An associative array of arrays of field property names keyed by field
* name.
*/
public function getSynchronizedPropertiesByField(array $field_definitions) {
$synchronizer = $this->synchronizer;
$synchronized_properties = array_filter(array_map(
function (FieldDefinitionInterface $field_definition) use ($synchronizer) {
return $synchronizer->getFieldSynchronizedProperties($field_definition);
},
$field_definitions
));
return $synchronized_properties;
}
}

View File

@ -5,7 +5,10 @@
* Helper module for the Content Translation tests.
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Implements hook_entity_bundle_info_alter().
@ -23,6 +26,19 @@ function content_translation_test_entity_bundle_info_alter(&$bundles) {
}
}
/**
* Implements hook_entity_access().
*/
function content_translation_test_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
$access = \Drupal::state()->get('content_translation.entity_access.' . $entity->getEntityTypeId());
if (!empty($access[$operation])) {
return AccessResult::allowed();
}
else {
return AccessResult::neutral();
}
}
/**
* Implements hook_form_BASE_FORM_ID_alter().
*

View File

@ -0,0 +1,482 @@
<?php
namespace Drupal\Tests\content_translation\Kernel;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\entity_test\Entity\EntityTestMulRev;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\file\Entity\File;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\TestFileCreationTrait;
use Drupal\user\Entity\User;
/**
* Tests the field synchronization logic when revisions are involved.
*
* @group content_translation
*/
class ContentTranslationFieldSyncRevisionTest extends EntityKernelTestBase {
use TestFileCreationTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['file', 'image', 'language', 'content_translation', 'simpletest', 'content_translation_test'];
/**
* The synchronized field name.
*
* @var string
*/
protected $fieldName = 'sync_field';
/**
* The content translation manager.
*
* @var \Drupal\content_translation\ContentTranslationManagerInterface|\Drupal\content_translation\BundleTranslationSettingsInterface
*/
protected $contentTranslationManager;
/**
* The test entity storage.
*
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
*/
protected $storage;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$entity_type_id = 'entity_test_mulrev';
$this->installEntitySchema($entity_type_id);
$this->installEntitySchema('file');
$this->installSchema('file', ['file_usage']);
ConfigurableLanguage::createFromLangcode('it')->save();
ConfigurableLanguage::createFromLangcode('fr')->save();
/** @var \Drupal\field\Entity\FieldStorageConfig $field_storage */
$field_storage_config = FieldStorageConfig::create([
'field_name' => $this->fieldName,
'type' => 'image',
'entity_type' => $entity_type_id,
'cardinality' => 1,
'translatable' => 1,
]);
$field_storage_config->save();
$field_config = FieldConfig::create([
'entity_type' => $entity_type_id,
'field_name' => $this->fieldName,
'bundle' => $entity_type_id,
'label' => 'Synchronized field',
'translatable' => 1,
]);
$field_config->save();
$property_settings = [
'alt' => 'alt',
'title' => 'title',
'file' => 0,
];
$field_config->setThirdPartySetting('content_translation', 'translation_sync', $property_settings);
$field_config->save();
$this->entityManager->clearCachedDefinitions();
$this->contentTranslationManager = $this->container->get('content_translation.manager');
$this->contentTranslationManager->setEnabled($entity_type_id, $entity_type_id, TRUE);
$this->storage = $this->entityManager->getStorage($entity_type_id);
foreach ($this->getTestFiles('image') as $file) {
$entity = File::create((array) $file + ['status' => 1]);
$entity->save();
}
$this->state->set('content_translation.entity_access.file', ['view' => TRUE]);
$account = User::create([
'name' => $this->randomMachineName(),
'status' => 1,
]);
$account->save();
}
/**
* Checks that field synchronization works as expected with revisions.
*
* @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::create
* @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::validate
* @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::hasSynchronizedPropertyChanges
* @covers \Drupal\content_translation\FieldTranslationSynchronizer::getFieldSynchronizedProperties
* @covers \Drupal\content_translation\FieldTranslationSynchronizer::synchronizeFields
* @covers \Drupal\content_translation\FieldTranslationSynchronizer::synchronizeItems
*/
public function testFieldSynchronizationAndValidation() {
// Test that when untranslatable field widgets are displayed, synchronized
// field properties can be changed only in default revisions.
$this->setUntranslatableFieldWidgetsDisplay(TRUE);
$entity = $this->saveNewEntity();
$entity_id = $entity->id();
$this->assertLatestRevisionFieldValues($entity_id, [1, 1, 1, 'Alt 1 EN']);
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
$en_revision = $this->createRevision($entity, FALSE);
$en_revision->get($this->fieldName)->target_id = 2;
$violations = $en_revision->validate();
$this->assertViolations($violations);
$it_translation = $entity->addTranslation('it', $entity->toArray());
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
$it_revision = $this->createRevision($it_translation, FALSE);
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
$metadata->setSource('en');
$it_revision->get($this->fieldName)->target_id = 2;
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
$violations = $it_revision->validate();
$this->assertViolations($violations);
$it_revision->isDefaultRevision(TRUE);
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [2, 2, 2, 'Alt 1 EN', 'Alt 2 IT']);
$en_revision = $this->createRevision($en_revision, FALSE);
$en_revision->get($this->fieldName)->alt = 'Alt 3 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [3, 2, 2, 'Alt 3 EN', 'Alt 2 IT']);
$it_revision = $this->createRevision($it_revision, FALSE);
$it_revision->get($this->fieldName)->alt = 'Alt 4 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [4, 2, 2, 'Alt 1 EN', 'Alt 4 IT']);
$en_revision = $this->createRevision($en_revision);
$en_revision->get($this->fieldName)->alt = 'Alt 5 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [5, 2, 2, 'Alt 5 EN', 'Alt 2 IT']);
$en_revision = $this->createRevision($en_revision);
$en_revision->get($this->fieldName)->target_id = 6;
$en_revision->get($this->fieldName)->alt = 'Alt 6 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [6, 6, 6, 'Alt 6 EN', 'Alt 2 IT']);
$it_revision = $this->createRevision($it_revision);
$it_revision->get($this->fieldName)->alt = 'Alt 7 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [7, 6, 6, 'Alt 6 EN', 'Alt 7 IT']);
// Test that when untranslatable field widgets are hidden, synchronized
// field properties can be changed only when editing the default
// translation. This may lead to temporarily desynchronized values, when
// saving a pending revision for the default translation that changes a
// synchronized property (see revision 11).
$this->setUntranslatableFieldWidgetsDisplay(FALSE);
$entity = $this->saveNewEntity();
$entity_id = $entity->id();
$this->assertLatestRevisionFieldValues($entity_id, [8, 1, 1, 'Alt 1 EN']);
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
$en_revision = $this->createRevision($entity, FALSE);
$en_revision->get($this->fieldName)->target_id = 2;
$en_revision->get($this->fieldName)->alt = 'Alt 2 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [9, 2, 2, 'Alt 2 EN']);
$it_translation = $entity->addTranslation('it', $entity->toArray());
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
$it_revision = $this->createRevision($it_translation, FALSE);
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
$metadata->setSource('en');
$it_revision->get($this->fieldName)->target_id = 3;
$violations = $it_revision->validate();
$this->assertViolations($violations);
$it_revision->isDefaultRevision(TRUE);
$violations = $it_revision->validate();
$this->assertViolations($violations);
$it_revision = $this->createRevision($it_translation);
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
$metadata->setSource('en');
$it_revision->get($this->fieldName)->alt = 'Alt 3 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [10, 1, 1, 'Alt 1 EN', 'Alt 3 IT']);
$en_revision = $this->createRevision($en_revision, FALSE);
$en_revision->get($this->fieldName)->alt = 'Alt 4 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [11, 2, 1, 'Alt 4 EN', 'Alt 3 IT']);
$it_revision = $this->createRevision($it_revision, FALSE);
$it_revision->get($this->fieldName)->alt = 'Alt 5 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [12, 1, 1, 'Alt 1 EN', 'Alt 5 IT']);
$en_revision = $this->createRevision($en_revision);
$en_revision->get($this->fieldName)->target_id = 6;
$en_revision->get($this->fieldName)->alt = 'Alt 6 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [13, 6, 6, 'Alt 6 EN', 'Alt 3 IT']);
$it_revision = $this->createRevision($it_revision);
$it_revision->get($this->fieldName)->target_id = 7;
$violations = $it_revision->validate();
$this->assertViolations($violations);
$it_revision = $this->createRevision($it_revision);
$it_revision->get($this->fieldName)->alt = 'Alt 7 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [14, 6, 6, 'Alt 6 EN', 'Alt 7 IT']);
// Test that creating a default revision starting from a pending revision
// having changes to synchronized properties, without introducing new
// changes works properly.
$this->setUntranslatableFieldWidgetsDisplay(FALSE);
$entity = $this->saveNewEntity();
$entity_id = $entity->id();
$this->assertLatestRevisionFieldValues($entity_id, [15, 1, 1, 'Alt 1 EN']);
$it_translation = $entity->addTranslation('it', $entity->toArray());
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
$it_revision = $this->createRevision($it_translation);
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
$metadata->setSource('en');
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [16, 1, 1, 'Alt 1 EN', 'Alt 2 IT']);
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
$en_revision = $this->createRevision($entity);
$en_revision->get($this->fieldName)->target_id = 3;
$en_revision->get($this->fieldName)->alt = 'Alt 3 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [17, 3, 3, 'Alt 3 EN', 'Alt 2 IT']);
$en_revision = $this->createRevision($entity, FALSE);
$en_revision->get($this->fieldName)->target_id = 4;
$en_revision->get($this->fieldName)->alt = 'Alt 4 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [18, 4, 3, 'Alt 4 EN', 'Alt 2 IT']);
$en_revision = $this->createRevision($entity);
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [19, 4, 4, 'Alt 4 EN', 'Alt 2 IT']);
$it_revision = $this->createRevision($it_revision);
$it_revision->get($this->fieldName)->alt = 'Alt 6 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [20, 4, 4, 'Alt 4 EN', 'Alt 6 IT']);
// Check that we are not allowed to perform changes to multiple translations
// in pending revisions when synchronized properties are involved.
$this->setUntranslatableFieldWidgetsDisplay(FALSE);
$entity = $this->saveNewEntity();
$entity_id = $entity->id();
$this->assertLatestRevisionFieldValues($entity_id, [21, 1, 1, 'Alt 1 EN']);
$it_translation = $entity->addTranslation('it', $entity->toArray());
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
$it_revision = $this->createRevision($it_translation);
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
$metadata->setSource('en');
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [22, 1, 1, 'Alt 1 EN', 'Alt 2 IT']);
$en_revision = $this->createRevision($entity, FALSE);
$en_revision->get($this->fieldName)->target_id = 2;
$en_revision->getTranslation('it')->get($this->fieldName)->alt = 'Alt 3 IT';
$violations = $en_revision->validate();
$this->assertViolations($violations);
// Test that when saving a new default revision starting from a pending
// revision, outdated synchronized properties do not override more recent
// ones.
$this->setUntranslatableFieldWidgetsDisplay(TRUE);
$entity = $this->saveNewEntity();
$entity_id = $entity->id();
$this->assertLatestRevisionFieldValues($entity_id, [23, 1, 1, 'Alt 1 EN']);
$it_translation = $entity->addTranslation('it', $entity->toArray());
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
$it_revision = $this->createRevision($it_translation, FALSE);
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
$metadata->setSource('en');
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [24, 1, 1, 'Alt 1 EN', 'Alt 2 IT']);
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
$en_revision = $this->createRevision($entity);
$en_revision->get($this->fieldName)->target_id = 3;
$en_revision->get($this->fieldName)->alt = 'Alt 3 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [25, 3, 3, 'Alt 3 EN', 'Alt 2 IT']);
$it_revision = $this->createRevision($it_revision);
$it_revision->get($this->fieldName)->alt = 'Alt 4 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [26, 3, 3, 'Alt 3 EN', 'Alt 4 IT']);
}
/**
* Sets untranslatable field widgets' display status.
*
* @param bool $display
* Whether untranslatable field widgets should be displayed.
*/
protected function setUntranslatableFieldWidgetsDisplay($display) {
$entity_type_id = $this->storage->getEntityTypeId();
$settings = ['untranslatable_fields_hide' => !$display];
$this->contentTranslationManager->setBundleTranslationSettings($entity_type_id, $entity_type_id, $settings);
/** @var \Drupal\Core\Entity\EntityTypeBundleInfo $bundle_info */
$bundle_info = $this->container->get('entity_type.bundle.info');
$bundle_info->clearCachedBundles();
}
/**
* @return \Drupal\Core\Entity\ContentEntityInterface
*/
protected function saveNewEntity() {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = EntityTestMulRev::create([
'uid' => 1,
'langcode' => 'en',
$this->fieldName => [
'target_id' => 1,
'alt' => 'Alt 1 EN',
],
]);
$metadata = $this->contentTranslationManager->getTranslationMetadata($entity);
$metadata->setSource(LanguageInterface::LANGCODE_NOT_SPECIFIED);
$violations = $entity->validate();
$this->assertEmpty($violations);
$this->storage->save($entity);
return $entity;
}
/**
* Creates a new revision starting from the latest translation-affecting one.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $translation
* The translation to be revisioned.
* @param bool $default
* (optional) Whether the new revision should be marked as default. Defaults
* to TRUE.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* An entity revision object.
*/
protected function createRevision(ContentEntityInterface $translation, $default = TRUE) {
if (!$translation->isNewTranslation()) {
$langcode = $translation->language()->getId();
$revision_id = $this->storage->getLatestTranslationAffectedRevisionId($translation->id(), $langcode);
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
$revision = $this->storage->loadRevision($revision_id);
$translation = $revision->getTranslation($langcode);
}
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
$revision = $this->storage->createRevision($translation, $default);
return $revision;
}
/**
* Asserts that the expected violations were found.
*
* @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations
* A list of violations.
*/
protected function assertViolations(EntityConstraintViolationListInterface $violations) {
$entity_type_id = $this->storage->getEntityTypeId();
$settings = $this->contentTranslationManager->getBundleTranslationSettings($entity_type_id, $entity_type_id);
$message = !empty($settings['untranslatable_fields_hide']) ?
'Non-translatable field elements can only be changed when updating the original language.' :
'Non-translatable field elements can only be changed when updating the current revision.';
$list = [];
foreach ($violations as $violation) {
if ((string) $violation->getMessage() === $message) {
$list[] = $violation;
}
}
$this->assertCount(1, $list);
}
/**
* Asserts that the latest revision has the expected field values.
*
* @param $entity_id
* The entity ID.
* @param array $expected_values
* An array of expected values in the following order:
* - revision ID
* - target ID (en)
* - target ID (it)
* - alt (en)
* - alt (it)
*/
protected function assertLatestRevisionFieldValues($entity_id, array $expected_values) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $this->storage->loadRevision($this->storage->getLatestRevisionId($entity_id));
@list($revision_id, $target_id_en, $target_id_it, $alt_en, $alt_it) = $expected_values;
$this->assertEquals($revision_id, $entity->getRevisionId());
$this->assertEquals($target_id_en, $entity->get($this->fieldName)->target_id);
$this->assertEquals($alt_en, $entity->get($this->fieldName)->alt);
if ($entity->hasTranslation('it')) {
$it_translation = $entity->getTranslation('it');
$this->assertEquals($target_id_it, $it_translation->get($this->fieldName)->target_id);
$this->assertEquals($alt_it, $it_translation->get($this->fieldName)->alt);
}
}
}

View File

@ -59,7 +59,7 @@ class ContentTranslationSyncUnitTest extends KernelTestBase {
protected function setUp() {
parent::setUp();
$this->synchronizer = new FieldTranslationSynchronizer($this->container->get('entity.manager'));
$this->synchronizer = new FieldTranslationSynchronizer($this->container->get('entity.manager'), $this->container->get('plugin.manager.field.field_type'));
$this->synchronized = ['sync1', 'sync2'];
$this->columns = array_merge($this->synchronized, ['var1', 'var2']);
$this->langcodes = ['en', 'it', 'fr', 'de', 'es'];