Commit d988a30f authored by Dries's avatar Dries

Issue #1810370 by plach, kfritsche, berdir, alexpott, xjm, chx: Entity...

Issue #1810370 by plach, kfritsche, berdir, alexpott, xjm, chx: Entity Translation API improvements.
parent 0329b221
......@@ -240,6 +240,35 @@ function hook_entity_update(Drupal\Core\Entity\EntityInterface $entity) {
->execute();
}
/**
* 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->getUntranslated()->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.
*
......
......@@ -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->invokeTranslationHooks($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()->id;
$default_langcode = $entity->getUntranslated()->language()->id;
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;
......@@ -511,7 +515,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 +538,10 @@ protected function mapToRevisionStorageRecord(ComplexDataInterface $entity) {
* The record to store.
*/
protected function mapToDataStorageRecord(EntityInterface $entity, $langcode) {
$default_langcode = $entity->language()->id;
$default_langcode = $entity->getUntranslated()->language()->id;
// 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']);
......
......@@ -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,46 @@ public static function postLoad(EntityStorageControllerInterface $storage_contro
public function preSaveRevision(EntityStorageControllerInterface $storage_controller, \stdClass $record) {
}
/**
* {@inheritdoc}
*/
public function getUntranslated() {
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()) {
// @todo Config entities do not support entity translation hence we need to
// move the TranslatableInterface implementation to EntityNG. See
// http://drupal.org/node/2004244
}
/**
* {@inheritdoc}
*/
public function removeTranslation($langcode) {
// @todo Config entities do not support entity translation hence we need to
// move the TranslatableInterface implementation to EntityNG. See
// http://drupal.org/node/2004244
}
/**
* {@inheritdoc}
*/
public function initTranslation($langcode) {
// @todo Config entities do not support entity translation hence we need to
// move the TranslatableInterface implementation to EntityNG. See
// http://drupal.org/node/2004244
}
}
......@@ -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()->id;
$langcode = $this->decorated->getUntranslated()->language()->id;
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 getUntranslated() {
return $this->decorated->getUntranslated();
}
/**
* 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);
}
}
......@@ -146,6 +146,11 @@ protected function init(array &$form_state) {
// module-provided form handlers there.
$form_state['controller'] = $this;
// Ensure we act on the translation object corresponding to the current form
// language.
$this->entity = $this->getTranslatedEntity($form_state);
// Prepare the entity to be presented in the entity form.
$this->prepareEntity();
$form_display = entity_get_render_form_display($this->entity, $this->getOperation());
......@@ -188,7 +193,7 @@ public function form(array $form, array &$form_state) {
// new entities.
$form['langcode'] = array(
'#type' => 'value',
'#value' => !$entity->isNew() ? $entity->langcode : language_default()->id,
'#value' => !$entity->isNew() ? $entity->getUntranslated()->language()->id : language_default()->id,
);
}
return $form;
......@@ -393,7 +398,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'];
......@@ -402,6 +406,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)->id;
$fallback = language_multilingual() ? language_fallback_get_candidates() : array();
while (!empty($langcode) && !isset($translations[$langcode])) {
......@@ -411,14 +416,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()->id;
return !empty($langcode) ? $langcode : $entity->getUntranslated()->language()->id;
}
/**
* Implements \Drupal\Core\Entity\EntityFormControllerInterface::isDefaultFormLangcode().
*/
public function isDefaultFormLangcode(array $form_state) {
return $this->getFormLangcode($form_state) == $this->entity->language()->id;
return $this->getFormLangcode($form_state) == $this->entity->getUntranslated()->language()->id;
}
/**
......@@ -447,13 +452,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()->id == $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'];
......@@ -489,10 +492,22 @@ public function buildEntity(array $form, array &$form_state) {
* Implements \Drupal\Core\Entity\EntityFormControllerInterface::getEntity().
*/
public function getEntity() {
// @todo Pick the proper translation object based on the form language here.
return $this->entity;
}
/**
* Returns the translation object corresponding to the form language.
*
* @param array $form_state
* A keyed array containing the current state of the form.
*/
protected function getTranslatedEntity(array $form_state) {
$langcode = $this->getFormLangcode($form_state);
$translation = $this->entity->getTranslation($langcode);
// Ensure that the entity object is a BC entity if the original one is.
return $this->entity instanceof EntityBCDecorator ? $translation->getBCEntity() : $translation;
}
/**
* Implements \Drupal\Core\Entity\EntityFormControllerInterface::setEntity().
*/
......@@ -521,7 +536,7 @@ protected function prepareInvokeAll($hook, array &$form_state) {
if (function_exists($function)) {
// Ensure we pass an updated translation object and form display at
// each invocation, since they depend on form state which is alterable.
$args = array($this->getEntity(), $this->getFormDisplay($form_state), $this->operation, &$form_state);
$args = array($this->getTranslatedEntity($form_state), $this->getFormDisplay($form_state), $this->operation, &$form_state);
call_user_func_array($function, $args);
}
}
......
......@@ -61,11 +61,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;
}
}
......
......@@ -323,4 +323,15 @@ public function getNGEntity();
*/
public function isTranslatable();
/**
* 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);
}
This diff is collapsed.
......@@ -234,4 +234,26 @@ public function invokeFieldItemPrepareCache(EntityInterface $entity) {
}
}
/**
* Checks translation statuses and invoke the related hooks if needed.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity being saved.
*/
protected function invokeTranslationHooks(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));
}
}
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Entity\Plugin\DataType\EntityTranslation.
*/
namespace Drupal\Core\Entity\Plugin\DataType;
use Drupal\Core\TypedData\Annotation\DataType;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\AccessibleInterface;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\TypedData;
use ArrayIterator;
use IteratorAggregate;
use InvalidArgumentException;
/**
* Allows accessing and updating translated entity fields.
*
* Via this object translated entity fields may be read and updated in the same
* way as untranslatable entity fields on the entity object.
*
* @DataType(
* id = "entity_translation",
* label = @Translation("Entity translation"),
* description = @Translation("A translation of an entity.")
* )
*/
class EntityTranslation extends TypedData implements IteratorAggregate, AccessibleInterface, ComplexDataInterface {
/**
* The array of translated fields, each being an instance of
* \Drupal\Core\Entity\FieldInterface.
*
* @var array
*/
protected $fields = array();
/**
* Whether the entity translation acts in strict mode.
*
* @var boolean
*/
protected $strict = TRUE;
/**
* Returns whether the entity translation acts in strict mode.
*
* @return boolean
* Whether the entity translation acts in strict mode.
*/
public function getStrictMode() {
return $this->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);
}
}
......@@ -15,7 +15,7 @@ interface TranslatableInterface {
/**
* 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,51 @@ 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 translatable object referring to the original language.
*