Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 110 |
FieldTranslationSynchronizer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 4 |
2162 | |
0.00% |
0 / 110 |
__construct | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
synchronizeFields | |
0.00% |
0 / 1 |
420 | |
0.00% |
0 / 44 |
|||
synchronizeItems | |
0.00% |
0 / 1 |
380 | |
0.00% |
0 / 50 |
|||
itemHash | |
0.00% |
0 / 1 |
42 | |
0.00% |
0 / 14 |
<?php | |
/** | |
* @file | |
* Contains \Drupal\content_translation\FieldTranslationSynchronizer. | |
*/ | |
namespace Drupal\content_translation; | |
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface; | |
use Drupal\Core\Entity\ContentEntityInterface; | |
use Drupal\Core\Entity\EntityManagerInterface; | |
/** | |
* Provides field translation synchronization capabilities. | |
*/ | |
class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterface { | |
/** | |
* The entity manager to use to load unchanged entities. | |
* | |
* @var \Drupal\Core\Entity\EntityManagerInterface | |
*/ | |
protected $entityManager; | |
/** | |
* Constructs a FieldTranslationSynchronizer object. | |
* | |
* @param \Drupal\Core\Entity\EntityManagerInterface $entityManager | |
* The entity manager. | |
*/ | |
public function __construct(EntityManagerInterface $entityManager) { | |
$this->entityManager = $entityManager; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
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 | |
// creating one, then there is nothing to synchronize. | |
if (empty($sync_langcode) || $entity->isNew() || count($translations) < 2) { | |
return; | |
} | |
// 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()); | |
if ($entity->getUntranslated()->language()->getId() != $entity_unchanged->getUntranslated()->language()->getId()) { | |
return; | |
} | |
/** @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()); | |
$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')) { | |
// Retrieve all the untranslatable column groups and merge them into | |
// single list. | |
$groups = array_keys(array_diff($translation_sync, array_filter($translation_sync))); | |
// If a group was selected has the require_all_groups_for_translation | |
// flag set, there are no untranslatable columns. This is done because | |
// the UI adds Javascript that disables the other checkboxes, so their | |
// values are not saved. | |
foreach (array_filter($translation_sync) as $group) { | |
if (!empty($column_groups[$group]['require_all_groups_for_translation'])) { | |
$groups = []; | |
break; | |
} | |
} | |
if (!empty($groups)) { | |
$columns = array(); | |
foreach ($groups as $group) { | |
$info = $column_groups[$group]; | |
// A missing 'columns' key indicates we have a single-column group. | |
$columns = array_merge($columns, isset($info['columns']) ? $info['columns'] : array($group)); | |
} | |
if (!empty($columns)) { | |
$values = array(); | |
foreach ($translations as $langcode => $language) { | |
$values[$langcode] = $entity->getTranslation($langcode)->get($field_name)->getValue(); | |
} | |
// If a translation is being created, the original values should be | |
// used as the unchanged items. In fact there are no unchanged items | |
// to check against. | |
$langcode = $original_langcode ?: $sync_langcode; | |
$unchanged_items = $entity_unchanged->getTranslation($langcode)->get($field_name)->getValue(); | |
$this->synchronizeItems($values, $unchanged_items, $sync_langcode, array_keys($translations), $columns); | |
foreach ($translations as $langcode => $language) { | |
$entity->getTranslation($langcode)->get($field_name)->setValue($values[$langcode]); | |
} | |
} | |
} | |
} | |
} | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function synchronizeItems(array &$values, array $unchanged_items, $sync_langcode, array $translations, array $columns) { | |
$source_items = $values[$sync_langcode]; | |
// Make sure we can detect any change in the source items. | |
$change_map = array(); | |
// By picking the maximum size between updated and unchanged items, we make | |
// sure to process also removed items. | |
$total = max(array(count($source_items), count($unchanged_items))); | |
// As a first step we build a map of the deltas corresponding to the column | |
// values to be synchronized. Recording both the old values and the new | |
// values will allow us to detect any change in the order of the new items | |
// for each column. | |
for ($delta = 0; $delta < $total; $delta++) { | |
foreach (array('old' => $unchanged_items, 'new' => $source_items) as $key => $items) { | |
if ($item_id = $this->itemHash($items, $delta, $columns)) { | |
$change_map[$item_id][$key][] = $delta; | |
} | |
} | |
} | |
// Backup field values and the change map. | |
$original_field_values = $values; | |
$original_change_map = $change_map; | |
// Reset field values so that no spurious one is stored. Source values must | |
// be preserved in any case. | |
$values = array($sync_langcode => $source_items); | |
// Update field translations. | |
foreach ($translations as $langcode) { | |
// We need to synchronize only values different from the source ones. | |
if ($langcode != $sync_langcode) { | |
// Reinitialize the change map as it is emptied while processing each | |
// language. | |
$change_map = $original_change_map; | |
// By using the maximum cardinality we ensure to process removed items. | |
for ($delta = 0; $delta < $total; $delta++) { | |
// By inspecting the map we built before we can tell whether a value | |
// has been created or removed. A changed value will be interpreted as | |
// a new value, in fact it did not exist before. | |
$created = TRUE; | |
$removed = TRUE; | |
$old_delta = NULL; | |
$new_delta = NULL; | |
if ($item_id = $this->itemHash($source_items, $delta, $columns)) { | |
if (!empty($change_map[$item_id]['old'])) { | |
$old_delta = array_shift($change_map[$item_id]['old']); | |
} | |
if (!empty($change_map[$item_id]['new'])) { | |
$new_delta = array_shift($change_map[$item_id]['new']); | |
} | |
$created = $created && !isset($old_delta); | |
$removed = $removed && !isset($new_delta); | |
} | |
// If an item has been removed we do not store its translations. | |
if ($removed) { | |
continue; | |
} | |
// If a synchronized column has changed or has been created from | |
// scratch we need to replace the values for this language as a | |
// combination of the values that need to be synced from the source | |
// 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; | |
} | |
// If the delta doesn't exist, copy from the source language. | |
elseif ($created) { | |
$values[$langcode][$delta] = $source_items[$delta]; | |
} | |
// Otherwise the current item might have been reordered. | |
elseif (isset($old_delta) && isset($new_delta)) { | |
// If for any reason the old value is not defined for the current | |
// language we fall back to the new source value, this way we ensure | |
// the new values are at least propagated to all the translations. | |
// 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; | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Computes a hash code for the specified item. | |
* | |
* @param array $items | |
* An array of field items. | |
* @param int $delta | |
* The delta identifying the item to be processed. | |
* @param array $columns | |
* 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) { | |
$values = array(); | |
if (isset($items[$delta])) { | |
foreach ($columns as $column) { | |
if (!empty($items[$delta][$column])) { | |
$value = $items[$delta][$column]; | |
// 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)); | |
} | |
else { | |
// Explicitly track also empty values. | |
$values[] = ''; | |
} | |
} | |
} | |
return implode('.', $values); | |
} | |
} |