comparison core/modules/content_translation/src/FieldTranslationSynchronizer.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 1fec387a4317
comparison
equal deleted inserted replaced
-1:000000000000 0:4c8ae668cc8c
1 <?php
2
3 namespace Drupal\content_translation;
4
5 use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
6 use Drupal\Core\Entity\ContentEntityInterface;
7 use Drupal\Core\Entity\EntityManagerInterface;
8
9 /**
10 * Provides field translation synchronization capabilities.
11 */
12 class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterface {
13
14 /**
15 * The entity manager to use to load unchanged entities.
16 *
17 * @var \Drupal\Core\Entity\EntityManagerInterface
18 */
19 protected $entityManager;
20
21 /**
22 * Constructs a FieldTranslationSynchronizer object.
23 *
24 * @param \Drupal\Core\Entity\EntityManagerInterface $entityManager
25 * The entity manager.
26 */
27 public function __construct(EntityManagerInterface $entityManager) {
28 $this->entityManager = $entityManager;
29 }
30
31 /**
32 * {@inheritdoc}
33 */
34 public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode, $original_langcode = NULL) {
35 $translations = $entity->getTranslationLanguages();
36 $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
37
38 // If we have no information about what to sync to, if we are creating a new
39 // entity, if we have no translations for the current entity and we are not
40 // creating one, then there is nothing to synchronize.
41 if (empty($sync_langcode) || $entity->isNew() || count($translations) < 2) {
42 return;
43 }
44
45 // If the entity language is being changed there is nothing to synchronize.
46 $entity_type = $entity->getEntityTypeId();
47 $entity_unchanged = isset($entity->original) ? $entity->original : $this->entityManager->getStorage($entity_type)->loadUnchanged($entity->id());
48 if ($entity->getUntranslated()->language()->getId() != $entity_unchanged->getUntranslated()->language()->getId()) {
49 return;
50 }
51
52 /** @var \Drupal\Core\Field\FieldItemListInterface $items */
53 foreach ($entity as $field_name => $items) {
54 $field_definition = $items->getFieldDefinition();
55 $field_type_definition = $field_type_manager->getDefinition($field_definition->getType());
56 $column_groups = $field_type_definition['column_groups'];
57
58 // Sync if the field is translatable, not empty, and the synchronization
59 // setting is enabled.
60 if ($field_definition instanceof ThirdPartySettingsInterface && $field_definition->isTranslatable() && !$items->isEmpty() && $translation_sync = $field_definition->getThirdPartySetting('content_translation', 'translation_sync')) {
61 // Retrieve all the untranslatable column groups and merge them into
62 // single list.
63 $groups = array_keys(array_diff($translation_sync, array_filter($translation_sync)));
64
65 // If a group was selected has the require_all_groups_for_translation
66 // flag set, there are no untranslatable columns. This is done because
67 // the UI adds Javascript that disables the other checkboxes, so their
68 // values are not saved.
69 foreach (array_filter($translation_sync) as $group) {
70 if (!empty($column_groups[$group]['require_all_groups_for_translation'])) {
71 $groups = [];
72 break;
73 }
74 }
75 if (!empty($groups)) {
76 $columns = [];
77 foreach ($groups as $group) {
78 $info = $column_groups[$group];
79 // A missing 'columns' key indicates we have a single-column group.
80 $columns = array_merge($columns, isset($info['columns']) ? $info['columns'] : [$group]);
81 }
82 if (!empty($columns)) {
83 $values = [];
84 foreach ($translations as $langcode => $language) {
85 $values[$langcode] = $entity->getTranslation($langcode)->get($field_name)->getValue();
86 }
87
88 // If a translation is being created, the original values should be
89 // used as the unchanged items. In fact there are no unchanged items
90 // to check against.
91 $langcode = $original_langcode ?: $sync_langcode;
92 $unchanged_items = $entity_unchanged->getTranslation($langcode)->get($field_name)->getValue();
93 $this->synchronizeItems($values, $unchanged_items, $sync_langcode, array_keys($translations), $columns);
94
95 foreach ($translations as $langcode => $language) {
96 $entity->getTranslation($langcode)->get($field_name)->setValue($values[$langcode]);
97 }
98 }
99 }
100 }
101 }
102 }
103
104 /**
105 * {@inheritdoc}
106 */
107 public function synchronizeItems(array &$values, array $unchanged_items, $sync_langcode, array $translations, array $columns) {
108 $source_items = $values[$sync_langcode];
109
110 // Make sure we can detect any change in the source items.
111 $change_map = [];
112
113 // By picking the maximum size between updated and unchanged items, we make
114 // sure to process also removed items.
115 $total = max([count($source_items), count($unchanged_items)]);
116
117 // As a first step we build a map of the deltas corresponding to the column
118 // values to be synchronized. Recording both the old values and the new
119 // values will allow us to detect any change in the order of the new items
120 // for each column.
121 for ($delta = 0; $delta < $total; $delta++) {
122 foreach (['old' => $unchanged_items, 'new' => $source_items] as $key => $items) {
123 if ($item_id = $this->itemHash($items, $delta, $columns)) {
124 $change_map[$item_id][$key][] = $delta;
125 }
126 }
127 }
128
129 // Backup field values and the change map.
130 $original_field_values = $values;
131 $original_change_map = $change_map;
132
133 // Reset field values so that no spurious one is stored. Source values must
134 // be preserved in any case.
135 $values = [$sync_langcode => $source_items];
136
137 // Update field translations.
138 foreach ($translations as $langcode) {
139
140 // We need to synchronize only values different from the source ones.
141 if ($langcode != $sync_langcode) {
142 // Reinitialize the change map as it is emptied while processing each
143 // language.
144 $change_map = $original_change_map;
145
146 // By using the maximum cardinality we ensure to process removed items.
147 for ($delta = 0; $delta < $total; $delta++) {
148 // By inspecting the map we built before we can tell whether a value
149 // has been created or removed. A changed value will be interpreted as
150 // a new value, in fact it did not exist before.
151 $created = TRUE;
152 $removed = TRUE;
153 $old_delta = NULL;
154 $new_delta = NULL;
155
156 if ($item_id = $this->itemHash($source_items, $delta, $columns)) {
157 if (!empty($change_map[$item_id]['old'])) {
158 $old_delta = array_shift($change_map[$item_id]['old']);
159 }
160 if (!empty($change_map[$item_id]['new'])) {
161 $new_delta = array_shift($change_map[$item_id]['new']);
162 }
163 $created = $created && !isset($old_delta);
164 $removed = $removed && !isset($new_delta);
165 }
166
167 // If an item has been removed we do not store its translations.
168 if ($removed) {
169 continue;
170 }
171 // If a synchronized column has changed or has been created from
172 // scratch we need to replace the values for this language as a
173 // combination of the values that need to be synced from the source
174 // items and the other columns from the existing values. This only
175 // works if the delta exists in the language.
176 elseif ($created && !empty($original_field_values[$langcode][$delta])) {
177 $item_columns_to_sync = array_intersect_key($source_items[$delta], array_flip($columns));
178 $item_columns_to_keep = array_diff_key($original_field_values[$langcode][$delta], array_flip($columns));
179 $values[$langcode][$delta] = $item_columns_to_sync + $item_columns_to_keep;
180 }
181 // If the delta doesn't exist, copy from the source language.
182 elseif ($created) {
183 $values[$langcode][$delta] = $source_items[$delta];
184 }
185 // Otherwise the current item might have been reordered.
186 elseif (isset($old_delta) && isset($new_delta)) {
187 // If for any reason the old value is not defined for the current
188 // language we fall back to the new source value, this way we ensure
189 // the new values are at least propagated to all the translations.
190 // If the value has only been reordered we just move the old one in
191 // the new position.
192 $item = isset($original_field_values[$langcode][$old_delta]) ? $original_field_values[$langcode][$old_delta] : $source_items[$new_delta];
193 $values[$langcode][$new_delta] = $item;
194 }
195 }
196 }
197 }
198 }
199
200 /**
201 * Computes a hash code for the specified item.
202 *
203 * @param array $items
204 * An array of field items.
205 * @param int $delta
206 * The delta identifying the item to be processed.
207 * @param array $columns
208 * An array of column names to be synchronized.
209 *
210 * @returns string
211 * A hash code that can be used to identify the item.
212 */
213 protected function itemHash(array $items, $delta, array $columns) {
214 $values = [];
215
216 if (isset($items[$delta])) {
217 foreach ($columns as $column) {
218 if (!empty($items[$delta][$column])) {
219 $value = $items[$delta][$column];
220 // String and integer values are by far the most common item values,
221 // thus we special-case them to improve performance.
222 $values[] = is_string($value) || is_int($value) ? $value : hash('sha256', serialize($value));
223 }
224 else {
225 // Explicitly track also empty values.
226 $values[] = '';
227 }
228 }
229 }
230
231 return implode('.', $values);
232 }
233
234 }