diff --git a/core/includes/entity.api.php b/core/includes/entity.api.php index 726d1b1..78099ea 100644 --- a/core/includes/entity.api.php +++ b/core/includes/entity.api.php @@ -241,6 +241,35 @@ function hook_entity_update(Drupal\Core\Entity\EntityInterface $entity) { } /** + * Acts after storing a new entity translation. + * + * @param \Drupal\Core\Entity\EntityInterface $translation + * The entity object of the translation just stored. + */ +function hook_entity_translation_insert(\Drupal\Core\Entity\EntityInterface $translation) { + $variables = array( + '@language' => $translation->language()->name, + '@label' => $translation->getOriginal()->label(), + ); + watchdog('example', 'The @language translation of @label has just been stored.', $variables); +} + +/** + * Acts after deleting an entity translation from the storage. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The original entity object. + */ +function hook_entity_translation_delete(\Drupal\Core\Entity\EntityInterface $translation) { + $languages = language_list(); + $variables = array( + '@language' => $languages[$langcode]->name, + '@label' => $entity->label(), + ); + watchdog('example', 'The @language translation of @label has just been deleted.', $variables); +} + +/** * Act before entity deletion. * * This hook runs after the entity type-specific predelete hook. diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php index fe11758..66e7424 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php @@ -14,7 +14,6 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\DatabaseStorageController; use Drupal\Core\Entity\EntityStorageException; -use Drupal\Core\TypedData\ComplexDataInterface; use Drupal\Component\Uuid\Uuid; use Drupal\Core\Database\Connection; @@ -289,6 +288,7 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) { $data = $query->execute(); $field_definition = \Drupal::entityManager()->getFieldDefinitions($this->entityType); + $translations = array(); if ($this->revisionTable) { $data_fields = array_flip(array_diff(drupal_schema_fields_sql($this->entityInfo['revision_table']), drupal_schema_fields_sql($this->entityInfo['base_table']))); } @@ -302,6 +302,7 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) { // Field values in default language are stored with // Language::LANGCODE_DEFAULT as key. $langcode = empty($values['default_langcode']) ? $values['langcode'] : Language::LANGCODE_DEFAULT; + $translations[$id][$langcode] = TRUE; foreach ($field_definition as $name => $definition) { // Set only translatable properties, unless we are dealing with a @@ -317,7 +318,7 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) { foreach ($entities as $id => $values) { $bundle = $this->bundleKey ? $values[$this->bundleKey][Language::LANGCODE_DEFAULT] : FALSE; // Turn the record into an entity class. - $entities[$id] = new $this->entityClass($values, $this->entityType, $bundle); + $entities[$id] = new $this->entityClass($values, $this->entityType, $bundle, array_keys($translations[$id])); } } } @@ -367,6 +368,9 @@ public function save(EntityInterface $entity) { $entity->postSave($this, TRUE); $this->invokeFieldMethod('update', $entity); $this->invokeHook('update', $entity); + if ($this->dataTable) { + $this->notifyTranslationChanges($entity); + } } else { $return = drupal_write_record($this->entityInfo['base_table'], $record); @@ -412,7 +416,7 @@ public function save(EntityInterface $entity) { */ protected function saveRevision(EntityInterface $entity) { $return = $entity->id(); - $default_langcode = $entity->language()->langcode; + $default_langcode = $entity->getOriginal()->language()->langcode; if (!$entity->isNewRevision()) { // Delete to handle removed values. @@ -422,9 +426,9 @@ protected function saveRevision(EntityInterface $entity) { ->execute(); } - $languages = $this->dataTable ? $entity->getTranslationLanguages(TRUE) : array($default_langcode => $entity->language()); + $languages = $this->dataTable ? $entity->getTranslationLanguages() : array($default_langcode => $entity->language()); foreach ($languages as $langcode => $language) { - $translation = $entity->getTranslation($langcode, FALSE); + $translation = $entity->getTranslation($langcode); $record = $this->mapToRevisionStorageRecord($translation); $record->langcode = $langcode; $record->default_langcode = $langcode == $default_langcode; @@ -486,6 +490,28 @@ protected function savePropertyData(EntityInterface $entity) { } /** + * Checks translation statuses and invoke the related hooks if needed. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being saved. + */ + function notifyTranslationChanges(EntityInterface $entity) { + $translations = $entity->getTranslationLanguages(FALSE); + $original_translations = $entity->original->getTranslationLanguages(FALSE); + $all_translations = array_keys($translations + $original_translations); + + // Notify modules of translation insertion/deletion. + foreach ($all_translations as $langcode) { + if (isset($translations[$langcode]) && !isset($original_translations[$langcode])) { + $this->invokeHook('translation_insert', $entity->getTranslation($langcode)); + } + elseif (!isset($translations[$langcode]) && isset($original_translations[$langcode])) { + $this->invokeHook('translation_delete', $entity->getTranslation($langcode)); + } + } + } + + /** * Maps from an entity object to the storage record of the base table. * * @param \Drupal\Core\Entity\EntityInterface $entity @@ -511,7 +537,7 @@ protected function mapToStorageRecord(EntityInterface $entity) { * @return \stdClass * The record to store. */ - protected function mapToRevisionStorageRecord(ComplexDataInterface $entity) { + protected function mapToRevisionStorageRecord(EntityInterface $entity) { $record = new \stdClass(); $definitions = $entity->getPropertyDefinitions(); foreach (drupal_schema_fields_sql($this->entityInfo['revision_table']) as $name) { @@ -534,10 +560,10 @@ protected function mapToRevisionStorageRecord(ComplexDataInterface $entity) { * The record to store. */ protected function mapToDataStorageRecord(EntityInterface $entity, $langcode) { - $default_langcode = $entity->language()->langcode; + $default_langcode = $entity->getOriginal()->language()->langcode; // Don't use strict mode, this way there's no need to do checks here, as // non-translatable properties are replicated for each language. - $translation = $entity->getTranslation($langcode, FALSE); + $translation = $entity->getTranslation($langcode); $definitions = $translation->getPropertyDefinitions(); $schema = drupal_get_schema($this->entityInfo['data_table']); diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index 8af2040..5fad856 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -9,6 +9,7 @@ use Drupal\Component\Uuid\Uuid; use Drupal\Core\Language\Language; +use Drupal\Core\TypedData\TranslatableInterface; use Drupal\Core\TypedData\TypedDataInterface; use Drupal\user\UserInterface; use IteratorAggregate; @@ -294,10 +295,13 @@ public function language() { /** * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslation(). + * + * @return \Drupal\Core\Entity\EntityInterface */ - public function getTranslation($langcode, $strict = TRUE) { + public function getTranslation($langcode) { // @todo: Replace by EntityNG implementation once all entity types have been // converted to use the entity field API. + return $this; } /** @@ -588,4 +592,34 @@ public static function postLoad(EntityStorageControllerInterface $storage_contro public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) { } + /** + * {@inheritdoc} + */ + public function getOriginal() { + return $this->getTranslation(Language::LANGCODE_DEFAULT); + } + + /** + * {@inheritdoc} + */ + public function hasTranslation($langcode) { + $translations = $this->getTranslationLanguages(); + return isset($translations[$langcode]); + } + + /** + * {@inheritdoc} + */ + public function addTranslation($langcode, array $values = array()) {} + + /** + * {@inheritdoc} + */ + public function removeTranslation($langcode) {} + + /** + * {@inheritdoc} + */ + public function initTranslation($langcode) {} + } diff --git a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php index 448d434..b84634a 100644 --- a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php +++ b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php @@ -138,7 +138,7 @@ public function &__get($name) { // Language::LANGCODE_DEFAULT. This is necessary as EntityNG always keys // default language values with Language::LANGCODE_DEFAULT while field API // expects them to be keyed by langcode. - $langcode = $this->decorated->language()->langcode; + $langcode = $this->decorated->getOriginal()->language()->langcode; if ($langcode != Language::LANGCODE_DEFAULT && isset($this->decorated->values[$name]) && is_array($this->decorated->values[$name])) { if (isset($this->decorated->values[$name][Language::LANGCODE_DEFAULT]) && !isset($this->decorated->values[$name][$langcode])) { $this->decorated->values[$name][$langcode] = &$this->decorated->values[$name][Language::LANGCODE_DEFAULT]; @@ -424,8 +424,8 @@ public function getTranslationLanguages($include_default = TRUE) { /** * Forwards the call to the decorated entity. */ - public function getTranslation($langcode, $strict = TRUE) { - return $this->decorated->getTranslation($langcode, $strict); + public function getTranslation($langcode) { + return $this->decorated->getTranslation($langcode); } /** @@ -526,12 +526,6 @@ public function onChange($property_name) { $this->decorated->onChange($property_name); } - /** - * Forwards the call to the decorated entity. - */ - public function isTranslatable() { - return $this->decorated->isTranslatable(); - } /** * Forwards the call to the decorated entity. @@ -588,4 +582,47 @@ public static function postDelete(EntityStorageControllerInterface $storage_cont */ public static function postLoad(EntityStorageControllerInterface $storage_controller, array $entities) { } + + /** + * Forwards the call to the decorated entity. + */ + public function isTranslatable() { + return $this->decorated->isTranslatable(); + } + + /** + * Forwards the call to the decorated entity. + */ + public function getOriginal() { + return $this->decorated->getOriginal(); + } + + /** + * Forwards the call to the decorated entity. + */ + public function hasTranslation($langcode) { + return $this->decorated->hasTranslation($langcode); + } + + /** + * Forwards the call to the decorated entity. + */ + public function addTranslation($langcode, array $values = array()) { + return $this->decorated->addTranslation($langcode, $values); + } + + /** + * Forwards the call to the decorated entity. + */ + public function removeTranslation($langcode) { + $this->decorated->removeTranslation($langcode); + } + + /** + * Forwards the call to the decorated entity. + */ + public function initTranslation($langcode) { + $this->decorated->initTranslation($langcode); + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityFormController.php b/core/lib/Drupal/Core/Entity/EntityFormController.php index d7107c5..a0aadfc 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormController.php +++ b/core/lib/Drupal/Core/Entity/EntityFormController.php @@ -119,6 +119,13 @@ protected function init(array &$form_state) { // Add the controller to the form state so it can be easily accessed by // module-provided form handlers there. $form_state['controller'] = $this; + + // Ensure we act on the translation object corresponding to the current form + // language. + $translation = $this->entity->getTranslation($this->getFormLangcode($form_state)); + $this->entity = $this->entity instanceof EntityBCDecorator ? $translation->getBCEntity() : $translation; + + // Prepare the entity to be presented in the entity form. $this->prepareEntity(); // @todo Allow the usage of different form modes by exposing a hook and the @@ -163,7 +170,7 @@ public function form(array $form, array &$form_state) { // new entities. $form['langcode'] = array( '#type' => 'value', - '#value' => !$entity->isNew() ? $entity->langcode : language_default()->langcode, + '#value' => !$entity->isNew() ? $entity->getOriginal()->language()->langcode : language_default()->langcode, ); } return $form; @@ -344,7 +351,6 @@ public function delete(array $form, array &$form_state) { */ public function getFormLangcode(array $form_state) { $entity = $this->entity; - $translations = $entity->getTranslationLanguages(); if (!empty($form_state['langcode'])) { $langcode = $form_state['langcode']; @@ -353,6 +359,7 @@ public function getFormLangcode(array $form_state) { // If no form langcode was provided we default to the current content // language and inspect existing translations to find a valid fallback, // if any. + $translations = $entity->getTranslationLanguages(); $langcode = language(Language::TYPE_CONTENT)->langcode; $fallback = language_multilingual() ? language_fallback_get_candidates() : array(); while (!empty($langcode) && !isset($translations[$langcode])) { @@ -362,14 +369,14 @@ public function getFormLangcode(array $form_state) { // If the site is not multilingual or no translation for the given form // language is available, fall back to the entity language. - return !empty($langcode) ? $langcode : $entity->language()->langcode; + return !empty($langcode) ? $langcode : $entity->getOriginal()->language()->langcode; } /** * Implements \Drupal\Core\Entity\EntityFormControllerInterface::isDefaultFormLangcode(). */ public function isDefaultFormLangcode(array $form_state) { - return $this->getFormLangcode($form_state) == $this->entity->language()->langcode; + return $this->getFormLangcode($form_state) == $this->entity->getOriginal()->language()->langcode; } /** @@ -398,13 +405,11 @@ protected function submitEntityLanguage(array $form, array &$form_state) { $entity_type = $entity->entityType(); if (field_has_translation_handler($entity_type)) { - $form_langcode = $this->getFormLangcode($form_state); - // If we are editing the default language values, we use the submitted // entity language as the new language for fields to handle any language // change. Otherwise the current form language is the proper value, since // in this case it is not supposed to change. - $current_langcode = $entity->language()->langcode == $form_langcode ? $form_state['values']['langcode'] : $form_langcode; + $current_langcode = $this->isDefaultFormLangcode($form_state) ? $form_state['values']['langcode'] : $this->getFormLangcode($form_state); foreach (field_info_instances($entity_type, $entity->bundle()) as $instance) { $field_name = $instance['field_name']; diff --git a/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php index f44b417..df3667a 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php +++ b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php @@ -65,11 +65,10 @@ public function buildEntity(array $form, array &$form_state) { // edited by this form. Values of fields handled by field API are copied // by field_attach_extract_form_values() below. $values_excluding_fields = $info['fieldable'] ? array_diff_key($form_state['values'], field_info_instances($entity_type, $entity->bundle())) : $form_state['values']; - $translation = $entity->getTranslation($this->getFormLangcode($form_state), FALSE); - $definitions = $translation->getPropertyDefinitions(); + $definitions = $entity->getPropertyDefinitions(); foreach ($values_excluding_fields as $key => $value) { if (isset($definitions[$key])) { - $translation->$key = $value; + $entity->$key = $value; } } diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php index a45c55a..bbfb77a 100644 --- a/core/lib/Drupal/Core/Entity/EntityNG.php +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Entity; use Drupal\Core\Language\Language; +use Drupal\Core\Session\AccountInterface; use Drupal\Core\TypedData\TypedDataInterface; use ArrayIterator; use InvalidArgumentException; @@ -26,6 +27,21 @@ class EntityNG extends Entity { /** + * Status code indentifying a removed translation. + */ + protected static $TRANSLATION_REMOVED = 0; + + /** + * Status code indentifying an existing translation. + */ + protected static $TRANSLATION_EXISTING = 1; + + /** + * Status code indentifying a newly created translation. + */ + protected static $TRANSLATION_CREATED = 2; + + /** * Local cache holding the value of the bundle field. * * @var string @@ -83,12 +99,43 @@ class EntityNG extends Entity { */ protected $uriPlaceholderReplacements; + + + /** + * Language code identifying the entity active language. + * + * This is the language field accessors will use to determine which field + * values manipulate. + * + * @var string + */ + protected $activeLangcode = Language::LANGCODE_DEFAULT; + + /** + * An array of entity translation metadata. + * + * An associative array keyed by translation language code. Every value is an + * array containg the translation status and the translation object, if it has + * already been instantiated. + * + * @var array + */ + protected $translations = array(); + + /** + * A flag indicating whether a translation object is being initialized. + * + * @var bool + */ + protected $translationInitialize = FALSE; + /** * Overrides Entity::__construct(). */ - public function __construct(array $values, $entity_type, $bundle = FALSE) { + public function __construct(array $values, $entity_type, $bundle = FALSE, $translations = array()) { $this->entityType = $entity_type; $this->bundle = $bundle ? $bundle : $this->entityType; + foreach ($values as $key => $value) { // If the key matches an existing property set the value to the property // to ensure non converted properties have the correct value. @@ -97,6 +144,20 @@ public function __construct(array $values, $entity_type, $bundle = FALSE) { } $this->values[$key] = $value; } + + // Initialize translations. Ensure we have at least an entry for the entity + // original language. + $data = array('status' => self::$TRANSLATION_EXISTING); + $this->translations[Language::LANGCODE_DEFAULT] = $data; + if ($translations) { + $default_langcode = $this->language()->langcode; + foreach ($translations as $langcode) { + if ($langcode != $default_langcode && $langcode != Language::LANGCODE_DEFAULT) { + $this->translations[$langcode] = $data; + } + } + } + $this->init(); } @@ -118,10 +179,23 @@ protected function init() { } /** + * Clear entity translation object cache to ensure we do not have stale references. + */ + protected function clearTranslationCache() { + foreach ($this->translations as &$translation) { + unset($translation['entity']); + } + } + + /** * Magic __wakeup() implementation. */ public function __wakeup() { $this->init(); + // @todo This should be done before serializing the entity, but we would + // need to provide the full list of data to be serialized. See the + // dedicated issue at https://drupal.org/node/2027795. + $this->clearTranslationCache(); } /** @@ -204,12 +278,10 @@ protected function uriPlaceholderReplacements() { * Implements \Drupal\Core\TypedData\ComplexDataInterface::get(). */ public function get($property_name) { - // Values in default language are always stored using the - // Language::LANGCODE_DEFAULT constant. - if (!isset($this->fields[$property_name][Language::LANGCODE_DEFAULT])) { - return $this->getTranslatedField($property_name, Language::LANGCODE_DEFAULT); + if (!isset($this->fields[$property_name][$this->activeLangcode])) { + return $this->getTranslatedField($property_name, $this->activeLangcode); } - return $this->fields[$property_name][Language::LANGCODE_DEFAULT]; + return $this->fields[$property_name][$this->activeLangcode]; } /** @@ -218,6 +290,7 @@ public function get($property_name) { * @return \Drupal\Core\Entity\Field\FieldInterface */ protected function getTranslatedField($property_name, $langcode) { + $this->checkTranslationStatus(); // Populate $this->fields to speed-up further look-ups and to keep track of // fields objects, possibly holding changes to field values. if (!isset($this->fields[$property_name][$langcode])) { @@ -236,9 +309,9 @@ protected function getTranslatedField($property_name, $langcode) { $value = $this->values[$property_name][$langcode]; } // @todo Remove this once the BC decorator is gone. - elseif ($property_name != 'langcode') { + elseif ($property_name != 'langcode' && $langcode == Language::LANGCODE_DEFAULT) { $default_langcode = $this->language()->langcode; - if ($langcode == Language::LANGCODE_DEFAULT && isset($this->values[$property_name][$default_langcode])) { + if (isset($this->values[$property_name][$default_langcode])) { $value = $this->values[$property_name][$default_langcode]; } } @@ -337,15 +410,40 @@ public function isEmpty() { } /** - * Implements \Drupal\Core\TypedData\TranslatableInterface::language(). + * {@inheritdoc} + */ + public function access($operation = 'view', AccountInterface $account = NULL) { + return \Drupal::entityManager() + ->getAccessController($this->entityType) + ->access($this, $operation, $this->activeLangcode, $account); + } + + /** + * {@inheritdoc} */ public function language() { + if ($this->activeLangcode != Language::LANGCODE_DEFAULT) { + $languages = language_list(Language::STATE_ALL); + if (isset($languages[$this->activeLangcode])) { + return $languages[$this->activeLangcode]; + } + } + return $this->getDefaultLanguage(); + } + + /** + * Returns the entity original language. + * + * @return \Drupal\Core\Language\Language + * The entity language object. + */ + protected function getDefaultLanguage() { // Keep a local cache of the language object and clear it if the langcode // gets changed, see EntityNG::onChange(). if (!isset($this->language)) { // Get the language code if the property exists. - if ($this->getPropertyDefinition('langcode')) { - $this->language = $this->get('langcode')->language; + if ($this->getPropertyDefinition('langcode') && ($item = $this->get('langcode')) && isset($item->language)) { + $this->language = $item->language; } if (empty($this->language)) { // Make sure we return a proper language object. @@ -369,77 +467,169 @@ public function onChange($property_name) { /** * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslation(). * - * @return \Drupal\Core\Entity\Field\Type\EntityTranslation + * @return \Drupal\Core\Entity\EntityInterface */ - public function getTranslation($langcode, $strict = TRUE) { - // If the default language is Language::LANGCODE_NOT_SPECIFIED, the entity is not - // translatable, so we use Language::LANGCODE_DEFAULT. - if ($langcode == Language::LANGCODE_DEFAULT || in_array($this->language()->langcode, array(Language::LANGCODE_NOT_SPECIFIED, $langcode))) { - // No translation needed, return the entity. - return $this; + public function getTranslation($langcode) { + // Ensure we always use the default language code when dealing with the + // original entity language. + if ($langcode != Language::LANGCODE_DEFAULT && $langcode == $this->getDefaultLanguage()->langcode) { + $langcode = Language::LANGCODE_DEFAULT; } - // Check whether the language code is valid, thus is of an available - // language. - $languages = language_list(Language::STATE_ALL); - if (!isset($languages[$langcode])) { - throw new InvalidArgumentException("Unable to get translation for the invalid language '$langcode'."); + + // Populate entity translation object cache so it will be available for all + // translation objects. + if ($langcode == $this->activeLangcode) { + $this->translations[$langcode]['entity'] = $this; } - $fields = array(); - foreach ($this->getPropertyDefinitions() as $name => $definition) { - // Load only translatable properties in strict mode. - if (!empty($definition['translatable']) || !$strict) { - $fields[$name] = $this->getTranslatedField($name, $langcode); + + // If we already have a translation object for the specified language we can + // just return it. + if (isset($this->translations[$langcode]['entity'])) { + $translation = $this->translations[$langcode]['entity']; + } + else { + // If the requested translation is valid, we instantiate a new translation + // object being a clone of the current one but with the specified language + // as active language. Before cloning we specify we are initializing a + // translation object to perform a shallow clone, in fact all the field + // data structures need to be shared among the translation objects to + // ensure all of them deal with fresh data. + if (isset($this->translations[$langcode])) { + $this->translationInitialize = TRUE; + $translation = clone $this; + $translation->activeLangcode = $langcode; + // Ensure that changes to fields, values and translations are propagated + // to all the translation objects. + // @todo Consider converting these to ArrayObject. + $translation->values = &$this->values; + $translation->fields = &$this->fields; + $translation->translations = &$this->translations; + $translation->translationInitialize = FALSE; + $this->translations[$langcode]['entity'] = $translation; + $this->translationInitialize = FALSE; + } + else { + // If we were given a valid language and there is no translation for it, + // we return a new one. + $languages = language_list(Language::STATE_ALL); + if (isset($languages[$langcode])) { + // If the entity or the requested language is not a configured + // language, we fall back to the entity itself, since in this case it + // cannot have translations. + $translation = empty($this->getDefaultLanguage()->locked) && empty($languages[$langcode]->locked) ? $this->addTranslation($langcode) : $this; + } } } - // @todo: Add a way to get the definition of a translation to the - // TranslatableInterface and leverage TypeDataManager::getPropertyInstance - // also. - $translation_definition = array( - 'type' => 'entity_translation', - 'constraints' => array( - 'entity type' => $this->entityType(), - 'bundle' => $this->bundle(), - ), - ); - $translation = \Drupal::typedData()->create($translation_definition, $fields); - $translation->setStrictMode($strict); - $translation->setContext('@' . $langcode, $this); + + if (empty($translation)) { + $message = 'Invalid translation language (@langcode) specified.'; + throw new \InvalidArgumentException(format_string($message, array('@langcode' => $langcode))); + } + return $translation; } /** - * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages(). + * {@inheritdoc} */ - public function getTranslationLanguages($include_default = TRUE) { - $translations = array(); - $definitions = $this->getPropertyDefinitions(); - // Build an array with the translation langcodes set as keys. Empty - // translations should not be included and must be skipped. - foreach ($definitions as $name => $definition) { - if (isset($this->fields[$name])) { - foreach ($this->fields[$name] as $langcode => $field) { - if (!$field->isEmpty()) { - $translations[$langcode] = TRUE; - } - } + public function hasTranslation($langcode) { + return !empty($this->translations[$langcode]['status']); + } + + /** + * {@inheritdoc} + */ + public function addTranslation($langcode, array $values = array()) { + $languages = language_list(Language::STATE_ALL); + if (!isset($languages[$langcode]) || $this->hasTranslation($langcode)) { + throw new InvalidArgumentException("Invalid translation language '$langcode'"); + } + + // Instantiate a new empty entity so default values will be populated in the + // specified language. + $info = $this->entityInfo(); + $default_values = array($info['entity_keys']['bundle'] => $this->bundle, 'langcode' => $langcode); + $entity = \Drupal::entityManager() + ->getStorageController($this->entityType()) + ->create($default_values); + + foreach ($entity as $name => $field) { + if (!isset($values[$name]) && !$field->isEmpty()) { + $values[$name] = $field->value; } - if (isset($this->values[$name])) { - foreach ($this->values[$name] as $langcode => $values) { - // If a value is there but the field object is empty, it has been - // unset, so we need to skip the field also. - if ($values && !empty($definition['translatable']) && !(isset($this->fields[$name][$langcode]) && $this->fields[$name][$langcode]->isEmpty())) { - $translations[$langcode] = TRUE; - } + } + + $this->translations[$langcode]['status'] = self::$TRANSLATION_CREATED; + $translation = $this->getTranslation($langcode); + $definitions = $translation->getPropertyDefinitions(); + + foreach ($values as $name => $value) { + if (isset($definitions[$name]) && !empty($definitions[$name]['translatable'])) { + $translation->$name = $value; + } + } + + return $translation; + } + + /** + * {@inheritdoc} + */ + public function removeTranslation($langcode) { + if (isset($this->translations[$langcode]) && $langcode != Language::LANGCODE_DEFAULT && $langcode != $this->getDefaultLanguage()->langcode) { + foreach ($this->getPropertyDefinitions() as $name => $definition) { + if (!empty($definition['translatable'])) { + unset($this->values[$langcode]); + unset($this->fields[$langcode]); } } + $this->translations[$langcode]['status'] = self::$TRANSLATION_REMOVED; } - // We include the default language code instead of the - // Language::LANGCODE_DEFAULT constant. + else { + $message = 'The specified translation (@langcode) cannot be removed.'; + throw new \InvalidArgumentException(format_string($message, array('@langcode' => $langcode))); + } + } + + /** + * {@inheritdoc} + */ + public function initTranslation($langcode) { + if ($langcode != Language::LANGCODE_DEFAULT && $langcode != $this->getDefaultLanguage()->langcode) { + $this->translations[$langcode]['status'] = self::$TRANSLATION_EXISTING; + } + } + + /** + * Checks wether the current translation object has a valid status. + * + * To avoid manipulating stale data, we invalidate a translation object as + * soon as the related translation has been removed. Any attempt to access + * invalid data causes an exception to be thrown. + * + * @return bool + * TRUE if the translation object has a valid status. + */ + protected function checkTranslationStatus() { + if ($this->translations[$this->activeLangcode]['status'] == self::$TRANSLATION_REMOVED) { + $message = 'The entity object refers to a removed translation (@langcode) and cannot be manipulated.'; + throw new \InvalidArgumentException(format_string($message, array('@langcode' => $this->activeLangcode))); + } + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getTranslationLanguages($include_default = TRUE) { + $translations = array_filter($this->translations, function($translation) { return $translation['status']; }); unset($translations[Language::LANGCODE_DEFAULT]); if ($include_default) { - $translations[$this->language()->langcode] = TRUE; + $langcode = $this->getDefaultLanguage()->langcode; + $translations[$langcode] = TRUE; } + // Now load language objects based upon translation langcodes. return array_intersect_key(language_list(Language::STATE_ALL), $translations); } @@ -491,15 +681,15 @@ public function updateOriginalValues() { public function &__get($name) { // If this is an entity field, handle it accordingly. We first check whether // a field object has been already created. If not, we create one. - if (isset($this->fields[$name][Language::LANGCODE_DEFAULT])) { - return $this->fields[$name][Language::LANGCODE_DEFAULT]; + if (isset($this->fields[$name][$this->activeLangcode])) { + return $this->fields[$name][$this->activeLangcode]; } // Inline getPropertyDefinition() to speed up things. if (!isset($this->fieldDefinitions)) { $this->getPropertyDefinitions(); } if (isset($this->fieldDefinitions[$name])) { - $return = $this->getTranslatedField($name, Language::LANGCODE_DEFAULT); + $return = $this->getTranslatedField($name, $this->activeLangcode); return $return; } // Allow the EntityBCDecorator to directly access the values and fields. @@ -527,11 +717,11 @@ public function __set($name, $value) { } // If this is an entity field, handle it accordingly. We first check whether // a field object has been already created. If not, we create one. - if (isset($this->fields[$name][Language::LANGCODE_DEFAULT])) { - $this->fields[$name][Language::LANGCODE_DEFAULT]->setValue($value); + if (isset($this->fields[$name][$this->activeLangcode])) { + $this->fields[$name][$this->activeLangcode]->setValue($value); } elseif ($this->getPropertyDefinition($name)) { - $this->getTranslatedField($name, Language::LANGCODE_DEFAULT)->setValue($value); + $this->getTranslatedField($name, $this->activeLangcode)->setValue($value); } // Else directly read/write plain values. That way, fields not yet converted // to the entity field API can always be directly accessed. @@ -568,6 +758,8 @@ public function __unset($name) { * Overrides Entity::createDuplicate(). */ public function createDuplicate() { + $this->checkTranslationStatus(); + $duplicate = clone $this; $entity_info = $this->entityInfo(); $duplicate->{$entity_info['entity_keys']['id']}->value = NULL; @@ -589,14 +781,18 @@ public function createDuplicate() { * Magic method: Implements a deep clone. */ public function __clone() { - $this->bcEntity = NULL; - - foreach ($this->fields as $name => $properties) { - foreach ($properties as $langcode => $property) { - $this->fields[$name][$langcode] = clone $property; - $this->fields[$name][$langcode]->setContext($name, $this); + // Avoid deep-cloning when we are initializing a translation object, since + // it will represent the same entity, only with a different active language. + if (!$this->translationInitialize) { + foreach ($this->fields as $name => $properties) { + foreach ($properties as $langcode => $property) { + $this->fields[$name][$langcode] = clone $property; + $this->fields[$name][$langcode]->setContext($name, $this); + } } + $this->clearTranslationCache(); } + $this->bcEntity = NULL; } /** @@ -605,6 +801,9 @@ public function __clone() { public function label($langcode = NULL) { $label = NULL; $entity_info = $this->entityInfo(); + if (!isset($langcode)) { + $langcode = $this->activeLangcode; + } if (isset($entity_info['label_callback']) && function_exists($entity_info['label_callback'])) { $label = $entity_info['label_callback']($this->entityType, $this, $langcode); } @@ -621,4 +820,5 @@ public function validate() { // @todo: Add the typed data manager as proper dependency. return \Drupal::typedData()->getValidator()->validate($this); } + } diff --git a/core/lib/Drupal/Core/Entity/Field/Type/EntityTranslation.php b/core/lib/Drupal/Core/Entity/Field/Type/EntityTranslation.php deleted file mode 100644 index 859fa9a..0000000 --- a/core/lib/Drupal/Core/Entity/Field/Type/EntityTranslation.php +++ /dev/null @@ -1,224 +0,0 @@ -strict; - } - - /** - * Sets whether the entity translation acts in strict mode. - * - * @param boolean $strict - * Whether the entity translation acts in strict mode. - * - * @see \Drupal\Core\TypedData\TranslatableInterface::getTranslation() - */ - public function setStrictMode($strict = TRUE) { - $this->strict = $strict; - } - - /** - * Overrides \Drupal\Core\TypedData\TypedData::getValue(). - */ - public function getValue() { - // The plain value of the translation is the array of translated field - // objects. - return $this->fields; - } - - /** - * Overrides \Drupal\Core\TypedData\TypedData::setValue(). - */ - public function setValue($values, $notify = TRUE) { - // Notify the parent of any changes to be made. - if ($notify && isset($this->parent)) { - $this->parent->onChange($this->name); - } - $this->fields = $values; - } - - /** - * Overrides \Drupal\Core\TypedData\TypedData::getString(). - */ - public function getString() { - $strings = array(); - foreach ($this->getProperties() as $property) { - $strings[] = $property->getString(); - } - return implode(', ', array_filter($strings)); - } - - /** - * Implements \Drupal\Core\TypedData\ComplexDataInterface::get(). - */ - public function get($property_name) { - $definitions = $this->getPropertyDefinitions(); - if (!isset($definitions[$property_name])) { - throw new InvalidArgumentException(format_string('Field @name is unknown or not translatable.', array('@name' => $property_name))); - } - return $this->fields[$property_name]; - } - - /** - * Implements \Drupal\Core\TypedData\ComplexDataInterface::set(). - */ - public function set($property_name, $value, $notify = TRUE) { - $this->get($property_name)->setValue($value, FALSE); - } - - /** - * Implements \Drupal\Core\TypedData\ComplexDataInterface::getProperties(). - */ - public function getProperties($include_computed = FALSE) { - $properties = array(); - foreach ($this->getPropertyDefinitions() as $name => $definition) { - if ($include_computed || empty($definition['computed'])) { - $properties[$name] = $this->get($name); - } - } - return $properties; - } - - /** - * Magic method: Gets a translated field. - */ - public function __get($name) { - return $this->get($name); - } - - /** - * Magic method: Sets a translated field. - */ - public function __set($name, $value) { - $this->get($name)->setValue($value); - } - - /** - * Implements \IteratorAggregate::getIterator(). - */ - public function getIterator() { - return new ArrayIterator($this->getProperties()); - } - - /** - * Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinition(). - */ - public function getPropertyDefinition($name) { - $definitions = $this->getPropertyDefinitions(); - if (isset($definitions[$name])) { - return $definitions[$name]; - } - else { - return FALSE; - } - } - - /** - * Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinitions(). - */ - public function getPropertyDefinitions() { - $definitions = array(); - foreach ($this->parent->getPropertyDefinitions() as $name => $definition) { - if (!empty($definition['translatable']) || !$this->strict) { - $definitions[$name] = $definition; - } - } - return $definitions; - } - - /** - * Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyValues(). - */ - public function getPropertyValues() { - return $this->getValue(); - } - - /** - * Implements \Drupal\Core\TypedData\ComplexDataInterface::setPropertyValues(). - */ - public function setPropertyValues($values) { - foreach ($values as $name => $value) { - $this->get($name)->setValue($value); - } - } - - /** - * Implements \Drupal\Core\TypedData\ComplexDataInterface::isEmpty(). - */ - public function isEmpty() { - foreach ($this->getProperties() as $property) { - if ($property->getValue() !== NULL) { - return FALSE; - } - } - return TRUE; - } - - /** - * Implements \Drupal\Core\TypedData\ComplexDataInterface::onChange(). - */ - public function onChange($property_name) { - // Notify the parent of changes. - if (isset($this->parent)) { - $this->parent->onChange($this->name); - } - } - - /** - * Implements \Drupal\Core\TypedData\AccessibleInterface::access(). - */ - public function access($operation = 'view', AccountInterface $account = NULL) { - // Determine the language code of this translation by cutting of the - // leading "@" from the property name to get the langcode. - // @todo Add a way to set and get the langcode so that's more obvious what - // we're doing here. - $langcode = substr($this->getName(), 1); - return \Drupal::entityManager() - ->getAccessController($this->parent->entityType()) - ->access($this->parent, $operation, $langcode, $account); - } -} diff --git a/core/lib/Drupal/Core/TypedData/TranslatableInterface.php b/core/lib/Drupal/Core/TypedData/TranslatableInterface.php index f86068a..5104271 100644 --- a/core/lib/Drupal/Core/TypedData/TranslatableInterface.php +++ b/core/lib/Drupal/Core/TypedData/TranslatableInterface.php @@ -15,7 +15,7 @@ /** * Returns the default language. * - * @return + * @return \Drupal\Core\Language\Language * The language object. */ public function language(); @@ -24,7 +24,8 @@ public function language(); * Returns the languages the data is translated to. * * @param bool $include_default - * Whether the default language should be included. + * (optional) Whether the default language should be included. Defaults to + * TRUE. * * @return * An array of language objects, keyed by language codes. @@ -42,15 +43,61 @@ public function getTranslationLanguages($include_default = TRUE); * @param $langcode * The language code of the translation to get or Language::LANGCODE_DEFAULT * to get the data in default language. - * @param $strict - * (optional) If the data is complex, whether the translation should include - * only translatable properties. If set to FALSE, untranslatable properties - * are included (in default language) as well as translatable properties in - * the specified language. Defaults to TRUE. * * @return \Drupal\Core\TypedData\TypedDataInterface * A typed data object for the translated data. */ - public function getTranslation($langcode, $strict = TRUE); + public function getTranslation($langcode); + + + /** + * Returns the entity object referring to the original language. + * + * @return \Drupal\Core\TypedData\TranslatableInterface + */ + public function getOriginal(); + + /** + * Returns TRUE if the entity has a translation for the given language code. + * + * @param string $langcode + * The language code identifiying the translation. + * + * @return bool + * TRUE if the translation exists, FALSE otherwise. + */ + public function hasTranslation($langcode); + + /** + * Adds a new translation to the entity object. + * + * @param string $langcode + * The language code identifying the translation. + * @param array $values + * (optional) An array of initial values to be assigned to the translatable + * field. Defaults to none. + * + * @return \Drupal\Core\TypedData\TranslatableInterface + */ + public function addTranslation($langcode, array $values = array()); + + /** + * Removes the translation identified by the given language code. + * + * @param string $langcode + * The language code identifying the translation to be removed. + */ + public function removeTranslation($langcode); + + /** + * Marks the translation identified by the given language code as existing. + * + * @todo Remove this as soon as translation metadata have been converted to + * regular fields. + * + * @param string $langcode + * The language code identifying the translation to be initialized. + */ + public function initTranslation($langcode); } diff --git a/core/modules/comment/lib/Drupal/comment/CommentFormController.php b/core/modules/comment/lib/Drupal/comment/CommentFormController.php index e2040d5..c4632ea 100644 --- a/core/modules/comment/lib/Drupal/comment/CommentFormController.php +++ b/core/modules/comment/lib/Drupal/comment/CommentFormController.php @@ -158,9 +158,10 @@ public function form(array $form, array &$form_state) { } // Add internal comment properties. + $original = $comment->getOriginal(); foreach (array('cid', 'pid', 'nid', 'uid', 'node_type', 'langcode') as $key) { $key_name = key($comment->$key->offsetGet(0)->getPropertyDefinitions()); - $form[$key] = array('#type' => 'value', '#value' => $comment->$key->{$key_name}); + $form[$key] = array('#type' => 'value', '#value' => $original->$key->{$key_name}); } return parent::form($form, $form_state, $comment); diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index c0c3535..2c0078a 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -279,7 +279,7 @@ function _content_translation_menu_strip_loaders($path) { */ function content_translation_translate_access(EntityInterface $entity) { $entity_type = $entity->entityType(); - return empty($entity->language()->locked) && language_multilingual() && $entity->isTranslatable() && + return empty($entity->getOriginal()->language()->locked) && language_multilingual() && $entity->isTranslatable() && (user_access('create content translations') || user_access('update content translations') || user_access('delete content translations')); } @@ -332,7 +332,7 @@ function content_translation_edit_access(EntityInterface $entity, Language $lang $language = !empty($language) ? $language : language(Language::TYPE_CONTENT); $translations = $entity->getTranslationLanguages(); $languages = language_list(); - return isset($languages[$language->langcode]) && $language->langcode != $entity->language()->langcode && isset($translations[$language->langcode]) && content_translation_access($entity, 'update'); + return isset($languages[$language->langcode]) && $language->langcode != $entity->getOriginal()->language()->langcode && isset($translations[$language->langcode]) && content_translation_access($entity, 'update'); } /** @@ -348,7 +348,7 @@ function content_translation_delete_access(EntityInterface $entity, Language $la $language = !empty($language) ? $language : language(Language::TYPE_CONTENT); $translations = $entity->getTranslationLanguages(); $languages = language_list(); - return isset($languages[$language->langcode]) && $language->langcode != $entity->language()->langcode && isset($translations[$language->langcode]) && content_translation_access($entity, 'delete'); + return isset($languages[$language->langcode]) && $language->langcode != $entity->getOriginal()->language()->langcode && isset($translations[$language->langcode]) && content_translation_access($entity, 'delete'); } /** @@ -645,7 +645,7 @@ function content_translation_field_language_alter(&$display_language, $context) $instances = field_info_instances($entity_type, $entity->bundle()); // Avoid altering the real entity. $entity = clone($entity); - $entity_langcode = $entity->language()->langcode; + $entity_langcode = $entity->getOriginal()->language()->langcode; foreach ($entity->translation as $langcode => $translation) { if ($langcode == $context['langcode'] || !content_translation_view_access($entity, $langcode)) { @@ -705,7 +705,11 @@ function content_translation_load_translation_metadata(array $entities, $entity_ // @todo Declare these as entity (translation?) properties. foreach ($record as $field_name => $value) { if (!in_array($field_name, $exclude)) { - $entity->translation[$record->langcode][$field_name] = $value; + $langcode = $record->langcode; + $entity->translation[$langcode][$field_name] = $value; + if (!$entity->hasTranslation($langcode)) { + $entity->initTranslation($langcode); + } } } } @@ -865,10 +869,11 @@ function content_translation_field_info_alter(&$info) { * Implements hook_entity_presave(). */ function content_translation_entity_presave(EntityInterface $entity) { - $entity_info = $entity->entityInfo(); - if ($entity->isTranslatable() && !empty($entity_info['fieldable'])) { - $attributes = drupal_container()->get('request')->attributes; - Drupal::service('content_translation.synchronizer')->synchronizeFields($entity, $attributes->get('working_langcode'), $attributes->get('source_langcode')); + if ($entity->isTranslatable()) { + // @todo Avoid using request attributes once translation metadata become + // regular fields. + $attributes = Drupal::request()->attributes; + Drupal::service('content_translation.synchronizer')->synchronizeFields($entity, $entity->language()->langcode, $attributes->get('source_langcode')); } } diff --git a/core/modules/content_translation/content_translation.pages.inc b/core/modules/content_translation/content_translation.pages.inc index 2940585..55c181b 100644 --- a/core/modules/content_translation/content_translation.pages.inc +++ b/core/modules/content_translation/content_translation.pages.inc @@ -19,7 +19,7 @@ function content_translation_overview(EntityInterface $entity) { $controller = content_translation_controller($entity->entityType()); $entity_manager = Drupal::entityManager(); $languages = language_list(); - $original = $entity->language()->langcode; + $original = $entity->getOriginal()->language()->langcode; $translations = $entity->getTranslationLanguages(); $field_ui = module_exists('field_ui') && user_access('administer ' . $entity->entityType() . ' fields'); @@ -238,19 +238,13 @@ function content_translation_edit_page(EntityInterface $entity, Language $langua */ function content_translation_prepare_translation(EntityInterface $entity, Language $source, Language $target) { // @todo Unify field and property handling. - $instances = field_info_instances($entity->entityType(), $entity->bundle()); $entity = $entity->getNGEntity(); if ($entity instanceof EntityNG) { $source_translation = $entity->getTranslation($source->langcode); - $target_translation = $entity->getTranslation($target->langcode); - foreach ($target_translation->getPropertyDefinitions() as $property_name => $definition) { - // @todo The "key" part should not be needed. Remove it as soon as things - // do not break. - $key = key($entity->{$property_name}[0]->getProperties()); - $target_translation->$property_name->{$key} = $source_translation->$property_name->{$key}; - } + $entity->addTranslation($target->langcode, $source_translation->getPropertyValues()); } else { + $instances = field_info_instances($entity->entityType(), $entity->bundle()); foreach ($instances as $field_name => $instance) { $field = field_info_field($field_name); if (!empty($field['translatable'])) { diff --git a/core/modules/content_translation/lib/Drupal/content_translation/ContentTranslationController.php b/core/modules/content_translation/lib/Drupal/content_translation/ContentTranslationController.php index 944ec69..53b5e59 100644 --- a/core/modules/content_translation/lib/Drupal/content_translation/ContentTranslationController.php +++ b/core/modules/content_translation/lib/Drupal/content_translation/ContentTranslationController.php @@ -126,7 +126,7 @@ public function getSourceLangcode(array $form_state) { public function entityFormAlter(array &$form, array &$form_state, EntityInterface $entity) { $form_controller = content_translation_form_controller($form_state); $form_langcode = $form_controller->getFormLangcode($form_state); - $entity_langcode = $entity->language()->langcode; + $entity_langcode = $entity->getOriginal()->language()->langcode; $source_langcode = $this->getSourceLangcode($form_state); $new_translation = !empty($source_langcode); @@ -144,7 +144,7 @@ public function entityFormAlter(array &$form, array &$form_state, EntityInterfac if (isset($languages[$form_langcode]) && ($has_translations || $new_translation)) { $title = $this->entityFormTitle($entity); // When editing the original values display just the entity label. - if ($form_langcode != $entity->language()->langcode) { + if ($form_langcode != $entity_langcode) { $t_args = array('%language' => $languages[$form_langcode]->name, '%title' => $entity->label()); $title = empty($source_langcode) ? $title . ' [' . t('%language translation', $t_args) . ']' : t('Create %language translation of %title', $t_args); } @@ -433,10 +433,9 @@ public function entityFormEntityBuild($entity_type, EntityInterface $entity, arr } // Set contextual information that can be reused during the storage phase. - // @todo Remove this once we have an EntityLanguageDecorator to deal with - // the active language. - $attributes = drupal_container()->get('request')->attributes; - $attributes->set('working_langcode', $form_langcode); + // @todo Remove this once translation metadata is converted to regular + // fields. + $attributes = \Drupal::request()->attributes; $attributes->set('source_langcode', $source_langcode); } diff --git a/core/modules/content_translation/lib/Drupal/content_translation/ContentTranslationControllerNG.php b/core/modules/content_translation/lib/Drupal/content_translation/ContentTranslationControllerNG.php index 6c27d49..73e72ef 100644 --- a/core/modules/content_translation/lib/Drupal/content_translation/ContentTranslationControllerNG.php +++ b/core/modules/content_translation/lib/Drupal/content_translation/ContentTranslationControllerNG.php @@ -25,10 +25,7 @@ public function getAccess(EntityInterface $entity, $op) { * Overrides \Drupal\content_translation\ContentTranslationControllerInterface::removeTranslation(). */ public function removeTranslation(EntityInterface $entity, $langcode) { - $translation = $entity->getTranslation($langcode); - foreach ($translation->getPropertyDefinitions() as $property_name => $langcode) { - $translation->$property_name = array(); - } + $entity->removeTranslation($langcode); } } diff --git a/core/modules/content_translation/lib/Drupal/content_translation/FieldTranslationSynchronizer.php b/core/modules/content_translation/lib/Drupal/content_translation/FieldTranslationSynchronizer.php index 45a6c91..441f11a 100644 --- a/core/modules/content_translation/lib/Drupal/content_translation/FieldTranslationSynchronizer.php +++ b/core/modules/content_translation/lib/Drupal/content_translation/FieldTranslationSynchronizer.php @@ -48,14 +48,14 @@ public function synchronizeFields(EntityInterface $entity, $sync_langcode, $orig // 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)) { + 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->entityType(); $entity_unchanged = isset($entity->original) ? $entity->original : $this->entityManager->getStorageController($entity_type)->loadUnchanged($entity->id()); - if ($entity->language()->langcode != $entity_unchanged->language()->langcode) { + if ($entity->getOriginal()->language()->langcode != $entity_unchanged->getOriginal()->language()->langcode) { return; } diff --git a/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSyncImageTest.php b/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSyncImageTest.php index 7bfa73f..d7ec242 100644 --- a/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSyncImageTest.php +++ b/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSyncImageTest.php @@ -87,7 +87,6 @@ function testImageFieldSync() { // Populate the required contextual values. $attributes = $this->container->get('request')->attributes; - $attributes->set('working_langcode', $langcode); $attributes->set('source_langcode', $default_langcode); // Populate the test entity with some random initial values. @@ -123,7 +122,7 @@ function testImageFieldSync() { 'alt' => $default_langcode . '_' . $fid . '_' . $this->randomName(), 'title' => $default_langcode . '_' . $fid . '_' . $this->randomName(), ); - $entity->{$this->fieldName}->offsetGet($delta)->setValue($item); + $entity->get($this->fieldName)->offsetGet($delta)->setValue($item); // Store the generated values keying them by fid for easier lookup. $values[$default_langcode][$fid] = $item; @@ -134,6 +133,7 @@ function testImageFieldSync() { // items will be one less than the original values to check that only the // translated ones will be preserved. In fact we want the same fids and // items order for both languages. + $translation = $entity->getTranslation($langcode); for ($delta = 0; $delta < $this->cardinality - 1; $delta++) { // Simulate a field reordering: items are shifted of one position ahead. // The modulo operator ensures we start from the beginning after reaching @@ -148,26 +148,27 @@ function testImageFieldSync() { 'alt' => $langcode . '_' . $fid . '_' . $this->randomName(), 'title' => $langcode . '_' . $fid . '_' . $this->randomName(), ); - $entity->getTranslation($langcode)->{$this->fieldName}->offsetGet($delta)->setValue($item); + $translation->get($this->fieldName)->offsetGet($delta)->setValue($item); // Again store the generated values keying them by fid for easier lookup. $values[$langcode][$fid] = $item; } // Perform synchronization: the translation language is used as source, - // while the default langauge is used as target. - $entity = $this->saveEntity($entity); + // while the default language is used as target. + $entity = $this->saveEntity($translation); + $translation = $entity->getTranslation($langcode); // Check that one value has been dropped from the original values. - $assert = count($entity->{$this->fieldName}) == 2; + $assert = count($entity->get($this->fieldName)) == 2; $this->assertTrue($assert, 'One item correctly removed from the synchronized field values.'); // Check that fids have been synchronized and translatable column values // have been retained. $fids = array(); - foreach ($entity->{$this->fieldName} as $delta => $item) { + foreach ($entity->get($this->fieldName) as $delta => $item) { $value = $values[$default_langcode][$item->fid]; - $source_item = $entity->getTranslation($langcode)->{$this->fieldName}->offsetGet($delta); + $source_item = $translation->get($this->fieldName)->offsetGet($delta); $assert = $item->fid == $source_item->fid && $item->alt == $value['alt'] && $item->title == $value['title']; $this->assertTrue($assert, format_string('Field item @fid has been successfully synchronized.', array('@fid' => $item->fid))); $fids[$item->fid] = TRUE; @@ -183,22 +184,23 @@ function testImageFieldSync() { 'alt' => $langcode . '_' . $removed_fid . '_' . $this->randomName(), 'title' => $langcode . '_' . $removed_fid . '_' . $this->randomName(), ); - $entity->getTranslation($langcode)->{$this->fieldName}->setValue(array_values($values[$langcode])); + $translation->get($this->fieldName)->setValue(array_values($values[$langcode])); // When updating an entity we do not have a source language defined. $attributes->remove('source_langcode'); - $entity = $this->saveEntity($entity); + $entity = $this->saveEntity($translation); + $translation = $entity->getTranslation($langcode); // Check that the value has been added to the default language. - $assert = count($entity->{$this->fieldName}->getValue()) == 3; + $assert = count($entity->get($this->fieldName)->getValue()) == 3; $this->assertTrue($assert, 'One item correctly added to the synchronized field values.'); - foreach ($entity->{$this->fieldName} as $delta => $item) { + foreach ($entity->get($this->fieldName) as $delta => $item) { // When adding an item its value is copied over all the target languages, // thus in this case the source language needs to be used to check the // values instead of the target one. $fid_langcode = $item->fid != $removed_fid ? $default_langcode : $langcode; $value = $values[$fid_langcode][$item->fid]; - $source_item = $entity->getTranslation($langcode)->{$this->fieldName}->offsetGet($delta); + $source_item = $translation->get($this->fieldName)->offsetGet($delta); $assert = $item->fid == $source_item->fid && $item->alt == $value['alt'] && $item->title == $value['title']; $this->assertTrue($assert, format_string('Field item @fid has been successfully synchronized.', array('@fid' => $item->fid))); } diff --git a/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationUITest.php b/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationUITest.php index 6ffad4f..94006ea 100644 --- a/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationUITest.php +++ b/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationUITest.php @@ -10,7 +10,6 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityNG; use Drupal\Core\Language\Language; -use Drupal\Core\TypedData\ComplexDataInterface; /** * Tests the Content Translation UI. @@ -261,13 +260,13 @@ protected function getFormSubmitAction(EntityInterface $entity) { protected function getTranslation(EntityInterface $entity, $langcode) { // @todo remove once EntityBCDecorator is gone. $entity = $entity->getNGEntity(); - return $entity instanceof EntityNG ? $entity->getTranslation($langcode, FALSE) : $entity; + return $entity instanceof EntityNG ? $entity->getTranslation($langcode) : $entity; } /** * Returns the value for the specified property in the given language. * - * @param \Drupal\Core\TypedData\TranslatableInterface $translation + * @param \Drupal\Core\Entity\EntityInterface $translation * The translation object the property value should be retrieved from. * @param string $property * The property name. @@ -277,7 +276,7 @@ protected function getTranslation(EntityInterface $entity, $langcode) { * @return * The property value. */ - protected function getValue(ComplexDataInterface $translation, $property, $langcode) { + protected function getValue(EntityInterface $translation, $property, $langcode) { $key = $property == 'user_id' ? 'target_id' : 'value'; // @todo remove EntityBCDecorator condition once EntityBCDecorator is gone. if (($translation instanceof EntityInterface) && !($translation instanceof EntityNG) && !($translation instanceof EntityBCDecorator)) { diff --git a/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php b/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php index cecc676..65a50c2 100644 --- a/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php +++ b/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php @@ -69,7 +69,7 @@ public function generate(EntityInterface $entity, FieldInstance $instance, $lang // Early-return if no editor is available. $formatter_id = entity_get_render_display($entity, $view_mode)->getFormatter($instance['field_name'])->getPluginId(); - $items = $entity->getTranslation($langcode, FALSE)->get($field_name)->getValue(); + $items = $entity->getTranslation($langcode)->get($field_name)->getValue(); $editor_id = $this->editorSelector->getEditor($formatter_id, $instance, $items); if (!isset($editor_id)) { return array('access' => FALSE); diff --git a/core/modules/editor/lib/Drupal/editor/EditorController.php b/core/modules/editor/lib/Drupal/editor/EditorController.php index 2968454..22164b6 100644 --- a/core/modules/editor/lib/Drupal/editor/EditorController.php +++ b/core/modules/editor/lib/Drupal/editor/EditorController.php @@ -37,7 +37,7 @@ public function getUntransformedText(EntityInterface $entity, $field_name, $lang $response = new AjaxResponse(); // Direct text editing is only supported for single-valued fields. - $field = $entity->getTranslation($langcode, FALSE)->$field_name; + $field = $entity->getTranslation($langcode)->$field_name; $editable_text = check_markup($field->value, $field->format, $langcode, FALSE, array(FILTER_TYPE_TRANSFORM_REVERSIBLE, FILTER_TYPE_TRANSFORM_IRREVERSIBLE)); $response->addCommand(new GetUntransformedTextCommand($editable_text)); diff --git a/core/modules/field/field.module b/core/modules/field/field.module index bae2be4..36ba66d 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -235,9 +235,7 @@ function field_system_info_alter(&$info, $file, $type) { function field_entity_create(EntityInterface $entity) { $info = $entity->entityInfo(); if (!empty($info['fieldable'])) { - foreach ($entity->getTranslationLanguages() as $langcode => $language) { - field_populate_default_values($entity, $langcode); - } + field_populate_default_values($entity, $entity->language()->langcode); } } diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldTestBase.php b/core/modules/field/lib/Drupal/field/Tests/FieldTestBase.php index 949a108..1287cb0 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldTestBase.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldTestBase.php @@ -52,7 +52,7 @@ function assertFieldValues(EntityInterface $entity, $field_name, $langcode, $exp // Re-load the entity to make sure we have the latest changes. entity_get_controller($entity->entityType())->resetCache(array($entity->id())); $e = entity_load($entity->entityType(), $entity->id()); - $field = $values = $e->getTranslation($langcode, FALSE)->$field_name; + $field = $values = $e->getTranslation($langcode)->$field_name; // Filter out empty values so that they don't mess with the assertions. $field->filterEmptyValues(); $values = $field->getValue(); diff --git a/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php b/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php index 0893f43..ceaf8f1 100644 --- a/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php @@ -106,6 +106,7 @@ function testFieldFormTranslationRevisions() { $entity = entity_create($this->entity_type, array()); $available_langcodes = array_flip(field_available_languages($this->entity_type, $this->field)); unset($available_langcodes[Language::LANGCODE_NOT_SPECIFIED]); + unset($available_langcodes[Language::LANGCODE_NOT_APPLICABLE]); $field_name = $this->field['field_name']; // Store the field translations. diff --git a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Tests/FieldSqlStorageTest.php b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Tests/FieldSqlStorageTest.php index 737daf3..e62b777 100644 --- a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Tests/FieldSqlStorageTest.php +++ b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Tests/FieldSqlStorageTest.php @@ -321,7 +321,6 @@ function testFieldAttachSaveMissingData() { // Update: Field translation is missing but field is not empty. Translation // data should survive. - $entity->getTranslation($unavailable_langcode)->{$this->field_name} = mt_rand(1, 127); unset($entity->{$this->field_name}); field_attach_update($entity); $count = db_select($this->table) diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module index fedeb2d..4da7a61 100644 --- a/core/modules/forum/forum.module +++ b/core/modules/forum/forum.module @@ -511,7 +511,7 @@ function forum_field_storage_pre_insert(EntityInterface $entity, &$skip_fields) if ($entity->entityType() == 'node' && $entity->status && _forum_node_check_node_type($entity)) { $query = db_insert('forum_index')->fields(array('nid', 'title', 'tid', 'sticky', 'created', 'comment_count', 'last_comment_timestamp')); foreach ($entity->getTranslationLanguages() as $langcode => $language) { - $translation = $entity->getTranslation($langcode, FALSE); + $translation = $entity->getTranslation($langcode); $query->values(array( 'nid' => $entity->id(), 'title' => $translation->title->value, diff --git a/core/modules/node/lib/Drupal/node/NodeAccessController.php b/core/modules/node/lib/Drupal/node/NodeAccessController.php index d7cfd2c..e36743e 100644 --- a/core/modules/node/lib/Drupal/node/NodeAccessController.php +++ b/core/modules/node/lib/Drupal/node/NodeAccessController.php @@ -40,8 +40,8 @@ protected function checkAccess(EntityInterface $node, $operation, $langcode, Acc $uid = isset($node->uid) ? $node->uid : NULL; // If it is a proper EntityNG object, use the proper methods. if ($node instanceof EntityNG) { - $status = $node->getTranslation($langcode, FALSE)->status->value; - $uid = $node->getTranslation($langcode, FALSE)->uid->value; + $status = $node->getTranslation($langcode)->status->value; + $uid = $node->getTranslation($langcode)->uid->value; } // Check if authors can view their own unpublished nodes. diff --git a/core/modules/node/lib/Drupal/node/NodeStorageController.php b/core/modules/node/lib/Drupal/node/NodeStorageController.php index 524036b..a8b4586 100644 --- a/core/modules/node/lib/Drupal/node/NodeStorageController.php +++ b/core/modules/node/lib/Drupal/node/NodeStorageController.php @@ -75,7 +75,7 @@ protected function attachLoad(&$queried_entities, $load_revision = FALSE) { * Overrides Drupal\Core\Entity\DatabaseStorageController::invokeHook(). */ protected function invokeHook($hook, EntityInterface $node) { - $node = $node->getBCEntity(); + $node = $node->getOriginal()->getBCEntity(); // Inline parent::invokeHook() to pass on BC-entities to node-specific // hooks. diff --git a/core/modules/node/lib/Drupal/node/NodeTranslationController.php b/core/modules/node/lib/Drupal/node/NodeTranslationController.php index 1109724..acac03f 100644 --- a/core/modules/node/lib/Drupal/node/NodeTranslationController.php +++ b/core/modules/node/lib/Drupal/node/NodeTranslationController.php @@ -8,12 +8,12 @@ namespace Drupal\node; use Drupal\Core\Entity\EntityInterface; -use Drupal\content_translation\ContentTranslationController; +use Drupal\content_translation\ContentTranslationControllerNG; /** * Defines the translation controller class for nodes. */ -class NodeTranslationController extends ContentTranslationController { +class NodeTranslationController extends ContentTranslationControllerNG { /** * Overrides ContentTranslationController::getAccess(). diff --git a/core/modules/node/node.admin.inc b/core/modules/node/node.admin.inc index 821f41a..fb9593e 100644 --- a/core/modules/node/node.admin.inc +++ b/core/modules/node/node.admin.inc @@ -103,7 +103,7 @@ function _node_mass_update_helper(NodeInterface $node, array $updates, $langcode $node->original = clone $node; foreach ($langcodes as $langcode) { foreach ($updates as $name => $value) { - $node->getTranslation($langcode, FALSE)->$name = $value; + $node->getTranslation($langcode)->$name = $value; } } $node->save(); diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationFormTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationFormTest.php index 80d00b8..2fea5fc 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationFormTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationFormTest.php @@ -20,7 +20,7 @@ class EntityTranslationFormTest extends WebTestBase { * * @var array */ - public static $modules = array('entity_test', 'locale', 'node'); + public static $modules = array('entity_test', 'language', 'node'); protected $langcodes; @@ -112,7 +112,7 @@ function testEntityFormLanguage() { // Create a body translation and check the form language. $langcode2 = $this->langcodes[1]; - $node->body[$langcode2][0]['value'] = $this->randomName(16); + $node->getTranslation($langcode2)->body->value = $this->randomName(16); $node->save(); $this->drupalGet($langcode2 . '/node/' . $node->nid . '/edit'); $form_langcode = \Drupal::state()->get('entity_test.form_langcode') ?: FALSE; diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php index a235add..6122822 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php @@ -7,9 +7,9 @@ namespace Drupal\system\Tests\Entity; -use InvalidArgumentException; - use Drupal\Core\Language\Language; +use Drupal\Core\TypedData\TranslatableInterface; +use InvalidArgumentException; /** * Tests entity translation. @@ -18,7 +18,7 @@ class EntityTranslationTest extends EntityUnitTestBase { protected $langcodes; - public static $modules = array('language', 'locale'); + public static $modules = array('language', 'entity_test'); public static function getInfo() { return array( @@ -30,6 +30,7 @@ public static function getInfo() { function setUp() { parent::setUp(); + $this->installSchema('system', 'variable'); $this->installSchema('language', 'language'); $this->installSchema('entity_test', array( @@ -46,7 +47,7 @@ function setUp() { entity_test_install(); // Enable translations for the test entity type. - \Drupal::state()->set('entity_test.translation', TRUE); + $this->state->set('entity_test.translation', TRUE); // Create a translatable test field. $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); @@ -161,22 +162,6 @@ protected function assertEntityLanguageMethods($entity_type) { $this->pass('A translation for an invalid language is NULL.'); } - // Try to get an untranslatable value from a translation in strict mode. - try { - $field_name = 'field_test_text'; - $value = $entity->getTranslation($this->langcodes[1])->get($field_name); - $this->fail(format_string('%entity_type: Getting an untranslatable value from a translation in strict mode throws an exception.', array('%entity_type' => $entity_type))); - } - catch (InvalidArgumentException $e) { - $this->pass(format_string('%entity_type: Getting an untranslatable value from a translation in strict mode throws an exception.', array('%entity_type' => $entity_type))); - } - - // Try to get an untranslatable value from a translation in non-strict - // mode. - $entity->set($field_name, array(0 => array('value' => 'default value'))); - $value = $entity->getTranslation($this->langcodes[1], FALSE)->get($field_name)->value; - $this->assertEqual($value, 'default value', format_string('%entity_type: Untranslated value retrieved from translation in non-strict mode.', array('%entity_type' => $entity_type))); - // Try to set a value using an invalid language code. try { $entity->getTranslation('invalid')->set($this->field_name, NULL); @@ -186,17 +171,9 @@ protected function assertEntityLanguageMethods($entity_type) { $this->pass(format_string('%entity_type: Setting a translation for an invalid language throws an exception.', array('%entity_type' => $entity_type))); } - // Try to set an untranslatable value into a translation in strict mode. - try { - $entity->getTranslation($this->langcodes[1])->set($field_name, NULL); - $this->fail(format_string('%entity_type: Setting an untranslatable value into a translation in strict mode throws an exception.', array('%entity_type' => $entity_type))); - } - catch (InvalidArgumentException $e) { - $this->pass(format_string('%entity_type: Setting an untranslatable value into a translation in strict mode throws an exception.', array('%entity_type' => $entity_type))); - } - // Set the value in default language. - $entity->getTranslation($this->langcodes[1], FALSE)->set($field_name, array(0 => array('value' => 'default value2'))); + $field_name = 'field_test_text'; + $entity->getTranslation($this->langcodes[1])->set($field_name, array(0 => array('value' => 'default value2'))); // Get the value. $this->assertEqual($entity->get($field_name)->value, 'default value2', format_string('%entity_type: Untranslated value set into a translation in non-strict mode.', array('%entity_type' => $entity_type))); } @@ -345,4 +322,145 @@ protected function assertMultilingualProperties($entity_type) { $this->assertEqual(count($result), 1, format_string('%entity_type: One entity loaded by name, uid and field value using different language meta conditions.', array('%entity_type' => $entity_type))); } + /** + * Tests the Entity Translation API behavior. + */ + function testEntityTranslationAPI() { + $default_langcode = $this->langcodes[0]; + $langcode = $this->langcodes[1]; + $entity = $this->entityManager + ->getStorageController('entity_test_mul') + ->create(array('name' => $this->randomName())); + + $entity->save(); + $hooks = $this->getHooksInfo(); + $this->assertFalse($hooks, 'No entity translation hooks are fired when creating an entity.'); + + // Verify that we obtain the entity object itself when we attempt to + // retrieve a translation referring to it. + $translation = $entity->getTranslation($langcode); + $this->assertEqual($entity, $translation, 'The translation object corresponding to a non-default language is the entity object itself when the entity is language-neutral.'); + $entity->langcode->value = $default_langcode; + $translation = $entity->getTranslation($default_langcode); + $this->assertEqual($entity, $translation, 'The translation object corresponding to the default language (explicit) is the entity object itself.'); + $translation = $entity->getTranslation(Language::LANGCODE_DEFAULT); + $this->assertEqual($entity, $translation, 'The translation object corresponding to the default language (implicit) is the entity object itself.'); + + // Create a translation and verify that the translation object and the + // original object behave independently. + $name = $default_langcode . '_' . $this->randomName(); + $entity->name->value = $name; + $name_translated = $langcode . '_' . $this->randomName(); + $translation = $entity->addTranslation($langcode); + $this->assertNotEqual($entity, $translation, 'The entity and the translation object differ from one another.'); + $this->assertTrue($entity->hasTranslation($langcode), 'The new translation exists.'); + $this->assertEqual($translation->language()->langcode, $langcode, 'The translation language matches the specified one.'); + $this->assertEqual($translation->getOriginal()->language()->langcode, $default_langcode, 'The original language can still be retrieved.'); + $translation->name->value = $name_translated; + $this->assertEqual($entity->name->value, $name, 'The original name is retained after setting a translated value.'); + $entity->name->value = $name; + $this->assertEqual($translation->name->value, $name_translated, 'The translated name is retained after setting the original value.'); + + // Save the translation and check that the expecte hooks are fired. + $translation->save(); + $hooks = $this->getHooksInfo(); + $this->assertEqual($hooks['entity_translation_insert'], $langcode, 'The generic entity translation insertion hook has fired.'); + $this->assertEqual($hooks['entity_test_mul_translation_insert'], $langcode, 'The entity-type-specific entity translation insertion hook has fired.'); + + // Check that after loading an entity the language is the default one. + $entity = $this->reloadEntity($entity); + $this->assertEqual($entity->language()->langcode, $default_langcode, 'The loaded entity is the original one.'); + + // Add another translation and check that everything works as expected. A + // new translation object can be obtained also by just specifying a valid + // language. + $langcode2 = $this->langcodes[2]; + $translation = $entity->getTranslation($langcode2); + $value = $entity != $translation && $translation->language()->langcode == $langcode2 && $entity->hasTranslation($langcode2); + $this->assertTrue($value, 'A new translation object can be obtained also by specifying a valid language.'); + $this->assertEqual($entity->language()->langcode, $default_langcode, 'The original language has been preserved.'); + $translation->save(); + $hooks = $this->getHooksInfo(); + $this->assertEqual($hooks['entity_translation_insert'], $langcode2, 'The generic entity translation insertion hook has fired.'); + $this->assertEqual($hooks['entity_test_mul_translation_insert'], $langcode2, 'The entity-type-specific entity translation insertion hook has fired.'); + + // Verify that trying to manipulate a translation object referring to a + // removed translation results in exceptions being thrown. + $entity = $this->reloadEntity($entity); + $translation = $entity->getTranslation($langcode2); + $entity->removeTranslation($langcode2); + foreach (array('get', 'set', '__get', '__set', 'createDuplicate') as $method) { + $message = format_string('The @method method raises an exception when trying to manipulate a removed translation.', array('@method' => $method)); + try { + $translation->{$method}('name', $this->randomName()); + $this->fail($message); + } + catch (\Exception $e) { + $this->pass($message); + } + } + + // Verify that deletion hooks are fired when saving an entity with a removed + // translation. + $entity->save(); + $hooks = $this->getHooksInfo(); + $this->assertEqual($hooks['entity_translation_delete'], $langcode2, 'The generic entity translation deletion hook has fired.'); + $this->assertEqual($hooks['entity_test_mul_translation_delete'], $langcode2, 'The entity-type-specific entity translation deletion hook has fired.'); + $entity = $this->reloadEntity($entity); + $this->assertFalse($entity->hasTranslation($langcode2), 'The translation does not appear among available translations after saving the entity.'); + + // Check that removing an invalid translation causes an exception to be + // thrown. + foreach (array($default_langcode, Language::LANGCODE_DEFAULT, $this->randomName()) as $invalid_langcode) { + $message = format_string('Removing an invalid translation (@langcode) causes an exception to be thrown.', array('@langcode' => $invalid_langcode)); + try { + $entity->removeTranslation($invalid_langcode); + $this->fail($message); + } + catch (\Exception $e) { + $this->pass($message); + } + } + + // Check that hooks are fired only when actually storing data. + $entity = $this->reloadEntity($entity); + $entity->addTranslation($langcode2); + $entity->removeTranslation($langcode2); + $entity->save(); + $hooks = $this->getHooksInfo(); + $this->assertFalse($hooks, 'No hooks are run when adding and removing a translation without storing it.'); + + // Verify that entity serialization does not cause stale references to be + // left around. + $entity = $this->reloadEntity($entity); + $translation = $entity->getTranslation($langcode); + $entity = unserialize(serialize($entity)); + $entity->name->value = $this->randomName(); + $name = $default_langcode . '_' . $this->randomName(); + $entity->getTranslation($default_langcode)->name->value = $name; + $this->assertEqual($entity->name->value, $name, 'No stale reference for the translation object corresponding to the original language.'); + $translation2 = $entity->getTranslation($langcode); + $translation2->name->value .= $this->randomName(); + $this->assertNotEqual($translation->name->value, $translation2->name->value, 'No stale reference for the actual translation object.'); + $this->assertEqual($entity, $translation2->getOriginal(), 'No stale reference in the actual translation object.'); + + // Verify that deep-cloning is still available when we are not instantiating + // a translation object, which instead relies on shallow cloning. + $entity = $this->reloadEntity($entity); + $entity->getTranslation($langcode); + $cloned = clone $entity; + $translation = $cloned->getTranslation($langcode); + $this->assertNotEqual($entity, $translation->getOriginal(), 'A cloned entity object has no reference to the original one.'); + + // Check that per-language defaults are properly populated. + $entity = $this->reloadEntity($entity); + $instance_id = implode('.', array($entity->entityType(), $entity->bundle(), $this->field_name)); + $instances = $this->entityManager->getStorageController('field_instance')->load(array($instance_id)); + $instance = reset($instances); + $instance['default_value_function'] = 'entity_test_field_default_value'; + $instance->save(); + $translation = $entity->addTranslation($langcode2); + $this->assertEqual($translation->get($this->field_name)->value, $this->field_name . '_' . $langcode2, 'Language-aware default values correctly populated.'); + } + } diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUnitTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUnitTestBase.php index 664b09c..9ac9cff 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUnitTestBase.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityUnitTestBase.php @@ -8,6 +8,7 @@ namespace Drupal\system\Tests\Entity; use Drupal\simpletest\DrupalUnitTestBase; +use Drupal\Core\Entity\EntityInterface; /** * Defines an abstract test base for entity unit tests. @@ -21,8 +22,26 @@ */ public static $modules = array('entity', 'user', 'system', 'field', 'text', 'field_sql_storage', 'entity_test'); + /** + * The entity manager service. + * + * @var \Drupal\Core\Entity\EntityManager + */ + protected $entityManager; + + /** + * The state service. + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface + */ + protected $state; + public function setUp() { parent::setUp(); + + $this->entityManager = $this->container->get('plugin.manager.entity'); + $this->state = $this->container->get('state'); + $this->installSchema('user', 'users'); $this->installSchema('system', 'sequences'); $this->installSchema('entity_test', 'entity_test'); @@ -62,4 +81,34 @@ protected function createUser($values = array(), $permissions = array()) { return $account; } + /** + * Reloads the given entity from the storage and returns it. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to be reloaded. + * + * @return \Drupal\Core\Entity\EntityInterface + * The reloaded entity. + */ + protected function reloadEntity(EntityInterface $entity) { + $ids = array($entity->id()); + $controller = $this->entityManager->getStorageController($entity->entityType()); + $controller->resetCache($ids); + $entities = $controller->load($ids); + return reset($entities); + } + + /** + * Returns the entity_test hook invocation info. + * + * @return array + * An associative array of arbitrary hook data keyed by hook name. + */ + protected function getHooksInfo() { + $key = 'entity_test.hooks'; + $hooks = $this->state->get($key); + $this->state->set($key, array()); + return $hooks; + } + } diff --git a/core/modules/system/system.module b/core/modules/system/system.module index ae4131d..056b18a 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -2230,11 +2230,6 @@ function system_data_type_info() { 'description' => t('All kind of entities, e.g. nodes, comments or users.'), 'class' => '\Drupal\Core\Entity\Field\Type\EntityWrapper', ), - 'entity_translation' => array( - 'label' => t('Entity translation'), - 'description' => t('A translation of an entity'), - 'class' => '\Drupal\Core\Entity\Field\Type\EntityTranslation', - ), 'boolean_field' => array( 'label' => t('Boolean field item'), 'description' => t('An entity field containing a boolean value.'), diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module index 55d0dac..03dc4dc 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.module +++ b/core/modules/system/tests/modules/entity_test/entity_test.module @@ -7,6 +7,8 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\entity\Plugin\Core\Entity\EntityFormDisplay; +use Drupal\field\Plugin\Core\Entity\Field; +use Drupal\field\Plugin\Core\Entity\FieldInstance; /** * Filter that limits test entity list to revisable ones. @@ -432,3 +434,63 @@ function entity_test_entity_operation_alter(array &$operations, EntityInterface 'weight' => 50, ); } + +/** + * Implements hook_entity_translation_insert(). + */ +function entity_test_entity_translation_insert(EntityInterface $translation) { + _entity_test_record_hooks('entity_translation_insert', $translation->language()->langcode); +} + +/** + * Implements hook_entity_translation_delete(). + */ +function entity_test_entity_translation_delete(EntityInterface $translation) { + _entity_test_record_hooks('entity_translation_delete', $translation->language()->langcode); +} + +/** + * Implements hook_ENTITY_TYPE_translation_insert(). + */ +function entity_test_entity_test_mul_translation_insert(EntityInterface $translation) { + _entity_test_record_hooks('entity_test_mul_translation_insert', $translation->language()->langcode); +} + +/** + * Implements hook_ENTITY_TYPE_translation_delete(). + */ +function entity_test_entity_test_mul_translation_delete(EntityInterface $translation) { + _entity_test_record_hooks('entity_test_mul_translation_delete', $translation->language()->langcode); +} + +/** + * Field default value callback. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity the field belongs to. + * @param \Drupal\field\Plugin\Core\Entity\Field $field + * The field for which default values should be provided. + * @param \Drupal\field\Plugin\Core\Entity\FieldInstance $instance + * The field instance for which default values should be provided. + * @param string $langcode + * The field language code to fill-in with the default value. + */ +function entity_test_field_default_value(EntityInterface $entity, Field $field, FieldInstance $instance, $langcode) { + return array(array('value' => $field['field_name'] . '_' . $langcode)); +} + +/** + * Helper function to be used to record hook invocations. + * + * @param string $hook + * The hook name. + * @param mixed $data + * Arbitrary data associated to the hook invocation. + */ +function _entity_test_record_hooks($hook, $data) { + $state = \Drupal::state(); + $key = 'entity_test.hooks'; + $hooks = $state->get($key); + $hooks[$hook] = $data; + $state->set($key, $hooks); +} diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php index 0ad1da0..7d27c48 100644 --- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php @@ -19,15 +19,12 @@ class EntityTestFormController extends EntityFormControllerNG { */ public function form(array $form, array &$form_state) { $form = parent::form($form, $form_state); - $entity = $this->entity; - $langcode = $this->getFormLangcode($form_state); - $translation = $entity->getTranslation($langcode); $form['name'] = array( '#type' => 'textfield', '#title' => t('Name'), - '#default_value' => $translation->name->value, + '#default_value' => $entity->name->value, '#size' => 60, '#maxlength' => 128, '#required' => TRUE, @@ -37,7 +34,7 @@ public function form(array $form, array &$form_state) { $form['user_id'] = array( '#type' => 'textfield', '#title' => 'UID', - '#default_value' => $translation->user_id->target_id, + '#default_value' => $entity->user_id->target_id, '#size' => 60, '#maxlength' => 128, '#required' => TRUE, @@ -47,7 +44,7 @@ public function form(array $form, array &$form_state) { $form['langcode'] = array( '#title' => t('Language'), '#type' => 'language_select', - '#default_value' => $entity->language()->langcode, + '#default_value' => $entity->getOriginal()->language()->langcode, '#languages' => Language::STATE_ALL, ); diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TermFormController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermFormController.php index 832a0cf..8e98c4f 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/TermFormController.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermFormController.php @@ -47,7 +47,7 @@ public function form(array $form, array &$form_state) { '#type' => 'language_select', '#title' => t('Language'), '#languages' => Language::STATE_ALL, - '#default_value' => $term->langcode->value, + '#default_value' => $term->getOriginal()->language()->langcode, '#access' => !is_null($language_configuration['language_show']) && $language_configuration['language_show'], ); diff --git a/core/modules/user/lib/Drupal/user/ProfileTranslationController.php b/core/modules/user/lib/Drupal/user/ProfileTranslationController.php index 5cb9cc8..3392754 100644 --- a/core/modules/user/lib/Drupal/user/ProfileTranslationController.php +++ b/core/modules/user/lib/Drupal/user/ProfileTranslationController.php @@ -8,12 +8,12 @@ namespace Drupal\user; use Drupal\Core\Entity\EntityInterface; -use Drupal\content_translation\ContentTranslationController; +use Drupal\content_translation\ContentTranslationControllerNG; /** * Defines the translation controller class for terms. */ -class ProfileTranslationController extends ContentTranslationController { +class ProfileTranslationController extends ContentTranslationControllerNG { /** * Overrides ContentTranslationController::entityFormAlter(). diff --git a/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php b/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php index c83e5f7..48df936 100644 --- a/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php +++ b/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php @@ -923,8 +923,9 @@ public function getExportProperties() { /** * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslation(). */ - public function getTranslation($langcode, $strict = TRUE) { - return $this->storage->getTranslation($langcode, $strict); + public function getTranslation($langcode) { + // @todo Revisit this once config entities are converted to NG. + return $this; } /** @@ -1047,6 +1048,41 @@ public function isTranslatable() { } /** + * {@inheritdoc} + */ + public function getOriginal() { + return $this->storage->getOriginal(); + } + + /** + * {@inheritdoc} + */ + public function hasTranslation($langcode) { + return $this->storage->hasTranslation($langcode); + } + + /** + * {@inheritdoc} + */ + public function addTranslation($langcode, array $values = array()) { + return $this->storage->addTranslation($langcode, $values); + } + + /** + * {@inheritdoc} + */ + public function removeTranslation($langcode) { + $this->storage->removeTranslation($langcode); + } + + /** + * {@inheritdoc} + */ + public function initTranslation($langcode) { + $this->storage->initTranslation($langcode); + } + + /** * Implements \Drupal\Core\TypedData\TypedDataInterface::getType(). */ public function getType() {