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.
*/
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 [];
}
/**
*/
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
}
// 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)));
}
}
+ /**
+ * 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}
*/
- public function synchronizeItems(array &$values, array $unchanged_items, $sync_langcode, array $translations, array $columns) {
+ public function synchronizeItems(array &$values, array $unchanged_items, $sync_langcode, array $translations, array $properties) {
$source_items = $values[$sync_langcode];
// Make sure we can detect any change in the source items.
// for each column.
for ($delta = 0; $delta < $total; $delta++) {
foreach (['old' => $unchanged_items, 'new' => $source_items] as $key => $items) {
- if ($item_id = $this->itemHash($items, $delta, $columns)) {
+ if ($item_id = $this->itemHash($items, $delta, $properties)) {
$change_map[$item_id][$key][] = $delta;
}
}
$old_delta = NULL;
$new_delta = NULL;
- if ($item_id = $this->itemHash($source_items, $delta, $columns)) {
+ if ($item_id = $this->itemHash($source_items, $delta, $properties)) {
if (!empty($change_map[$item_id]['old'])) {
$old_delta = array_shift($change_map[$item_id]['old']);
}
// 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], $properties);
}
// If the delta doesn't exist, copy from the source language.
elseif ($created) {
// 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, $properties);
}
}
}
}
}
+ /**
+ * 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.
*
* An array of field items.
* @param int $delta
* The delta identifying the item to be processed.
- * @param array $columns
+ * @param array $properties
* An array of column names to be synchronized.
*
* @returns string
* A hash code that can be used to identify the item.
*/
- protected function itemHash(array $items, $delta, array $columns) {
+ protected function itemHash(array $items, $delta, array $properties) {
$values = [];
if (isset($items[$delta])) {
- foreach ($columns as $column) {
- if (!empty($items[$delta][$column])) {
- $value = $items[$delta][$column];
+ foreach ($properties as $property) {
+ if (!empty($items[$delta][$property])) {
+ $value = $items[$delta][$property];
// String and integer values are by far the most common item values,
// thus we special-case them to improve performance.
$values[] = is_string($value) || is_int($value) ? $value : hash('sha256', serialize($value));