FieldTranslationSynchronizer.php 7.94 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
<?php

/**
 * @file
 * Contains \Drupal\translation_entity\FieldTranslationSynchronizer.
 */

namespace Drupal\translation_entity;

use Drupal\Core\Entity\EntityInterface;

/**
 * Provides field translation synchronization capabilities.
 */
class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterface {

  /**
   * Implements \Drupal\translation_entity\FieldTranslationSynchronizerInterface::synchronizeFields().
   */
  public function synchronizeFields(EntityInterface $entity, $sync_langcode, $original_langcode = NULL) {
    $translations = $entity->getTranslationLanguages();

    // 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 && !$original_langcode)) {
      return;
    }

    // If the entity language is being changed there is nothing to synchronize.
    $entity_type = $entity->entityType();
    // @todo Use the entity storage controller directly to avoid accessing the
    //   global scope.
    $entity_unchanged = isset($entity->original) ? $entity->original : entity_load_unchanged($entity_type, $entity->id());
    if ($entity->language()->langcode != $entity_unchanged->language()->langcode) {
      return;
    }

    // Enable compatibility mode for NG entities.
    $entity_unchanged = $entity_unchanged->getBCEntity();

    // @todo Use Entity Field API to retrieve field definitions.
    $instances = field_info_instances($entity_type, $entity->bundle());
    foreach ($instances as $field_name => $instance) {
      $field = field_info_field($field_name);

      // Sync when the field is not empty, when the synchronization translations
      // setting is set, and the field is translatable.
      if (!empty($entity->{$field_name}) && !empty($instance['settings']['translation_sync']) && field_is_translatable($entity_type, $field)) {
        // Retrieve all the untranslatable column groups and merge them into
        // single list.
        $groups = array_keys(array_diff($instance['settings']['translation_sync'], array_filter($instance['settings']['translation_sync'])));
        if (!empty($groups)) {
          $columns = array();
          foreach ($groups as $group) {
            $info = $field['settings']['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)) {
            // 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 = !empty($entity_unchanged->{$field_name}[$langcode]) ? $entity_unchanged->{$field_name}[$langcode] : array();
            $this->synchronizeItems($entity->{$field_name}, $unchanged_items, $sync_langcode, array_keys($translations), $columns);
          }
        }
      }
    }
  }

  /**
   * Implements \Drupal\translation_entity\FieldTranslationSynchronizerInterface::synchronizeItems().
   */
  public function synchronizeItems(array &$field_values, array $unchanged_items, $sync_langcode, array $translations, array $columns) {
    $source_items = $field_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 = $field_values;
    $original_change_map = $change_map;

    // Reset field values so that no spurious one is stored. Source values must
    // be preserved in any case.
    $field_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 override the full items array for all languages.
          elseif ($created) {
            $field_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];
            $field_values[$langcode][$new_delta] = $item;
          }
        }
      }
    }
  }

  /**
   * Computes a hash code for the specified item.
   *
   * @param array $items
   *   An array of field items.
   * @param integer $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);
  }

}