Issue #2431329 by plach: Make (content) translation language available as a field definition

8.0.x
Alex Pott 2015-03-10 20:53:21 +00:00
parent 1483ef1034
commit dc3d8a01b8
25 changed files with 395 additions and 128 deletions

View File

@ -81,6 +81,13 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
*/
protected $langcodeKey;
/**
* The default langcode entity key.
*
* @var string
*/
protected $defaultLangcodeKey;
/**
* Language code identifying the entity active language.
*
@ -144,6 +151,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
$this->entityTypeId = $entity_type;
$this->entityKeys['bundle'] = $bundle ? $bundle : $this->entityTypeId;
$this->langcodeKey = $this->getEntityType()->getKey('langcode');
$this->defaultLangcodeKey = $this->getEntityType()->getKey('default_langcode');
foreach ($values as $key => $value) {
// If the key matches an existing property set the value to the property
@ -242,6 +250,13 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
return $return;
}
/**
* {@inheritdoc}
*/
public function isDefaultTranslation() {
return $this->activeLangcode === LanguageInterface::LANGCODE_DEFAULT;
}
/**
* {@inheritdoc}
*/
@ -543,14 +558,38 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
}
}
// Update the default internal language cache.
if ($name == $this->langcodeKey) {
$this->setDefaultLangcode();
if (isset($this->translations[$this->defaultLangcode])) {
$message = String::format('A translation already exists for the specified language (@langcode).', array('@langcode' => $this->defaultLangcode));
throw new \InvalidArgumentException($message);
}
$this->updateFieldLangcodes($this->defaultLangcode);
switch ($name) {
case $this->langcodeKey:
if ($this->isDefaultTranslation()) {
// Update the default internal language cache.
$this->setDefaultLangcode();
if (isset($this->translations[$this->defaultLangcode])) {
$message = String::format('A translation already exists for the specified language (@langcode).', array('@langcode' => $this->defaultLangcode));
throw new \InvalidArgumentException($message);
}
$this->updateFieldLangcodes($this->defaultLangcode);
}
else {
// @todo Allow the translation language to be changed. See
// https://www.drupal.org/node/2443989.
$items = $this->get($this->langcodeKey);
if ($items->value != $this->activeLangcode) {
$items->setValue($this->activeLangcode, FALSE);
$message = String::format('The translation language cannot be changed (@langcode).', array('@langcode' => $this->activeLangcode));
throw new \LogicException($message);
}
}
break;
case $this->defaultLangcodeKey:
// @todo Use a standard method to make the default_langcode field
// read-only. See https://www.drupal.org/node/2443991.
if (isset($this->values[$this->defaultLangcodeKey])) {
$this->get($this->defaultLangcodeKey)->setValue($this->isDefaultTranslation(), FALSE);
$message = String::format('The default translation flag cannot be changed (@langcode).', array('@langcode' => $this->activeLangcode));
throw new \LogicException($message);
}
break;
}
}
@ -684,6 +723,8 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
$values[$name] = $field->getValue();
}
}
$values[$this->langcodeKey] = $langcode;
$values[$this->defaultLangcodeKey] = FALSE;
$this->translations[$langcode]['status'] = static::TRANSLATION_CREATED;
$translation = $this->getTranslation($langcode);
@ -691,7 +732,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
foreach ($values as $name => $value) {
if (isset($definitions[$name]) && $definitions[$name]->isTranslatable()) {
$translation->$name = $value;
$translation->values[$name][$langcode] = $value;
}
}

View File

@ -401,14 +401,35 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
protected function buildBaseFieldDefinitions($entity_type_id) {
$entity_type = $this->getDefinition($entity_type_id);
$class = $entity_type->getClass();
$keys = array_filter($entity_type->getKeys());
// Fail with an exception for non-fieldable entity types.
if (!$entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
throw new \LogicException(String::format('Getting the base fields is not supported for entity type @type.', array('@type' => $entity_type->getLabel())));
}
// Retrieve base field definitions and assign them the entity type provider.
// Retrieve base field definitions.
/** @var FieldStorageDefinitionInterface[] $base_field_definitions */
$base_field_definitions = $class::baseFieldDefinitions($entity_type);
// Make sure translatable entity types are correctly defined.
if ($entity_type->isTranslatable()) {
// The langcode field should always be translatable if the entity type is.
if (isset($keys['langcode']) && isset($base_field_definitions[$keys['langcode']])) {
$base_field_definitions[$keys['langcode']]->setTranslatable(TRUE);
}
// A default_langcode field should always be defined.
if (!isset($base_field_definitions[$keys['default_langcode']])) {
$base_field_definitions[$keys['default_langcode']] = BaseFieldDefinition::create('boolean')
->setLabel($this->t('Default translation'))
->setDescription($this->t('A flag indicating whether this is the default translation.'))
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setDefaultValue(TRUE);
}
}
// Assign base field definitions the entity type provider.
$provider = $entity_type->getProvider();
foreach ($base_field_definitions as $definition) {
// @todo Remove this check once FieldDefinitionInterface exposes a proper
@ -450,15 +471,32 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
// Ensure defined entity keys are there and have proper revisionable and
// translatable values.
$keys = array_filter($entity_type->getKeys());
foreach ($keys as $key => $field_name) {
if (isset($base_field_definitions[$field_name]) && in_array($key, array('id', 'revision', 'uuid', 'bundle')) && $base_field_definitions[$field_name]->isRevisionable()) {
throw new \LogicException(String::format('The @field field cannot be revisionable as it is used as @key entity key.', array('@field' => $base_field_definitions[$field_name]->getLabel(), '@key' => $key)));
foreach (array_intersect_key($keys, array_flip(['id', 'revision', 'uuid', 'bundle'])) as $key => $field_name) {
if (!isset($base_field_definitions[$field_name])) {
throw new \LogicException(String::format('The @field field definition does not exist and it is used as @key entity key.', array(
'@field' => $base_field_definitions[$field_name]->getLabel(),
'@key' => $key,
)));
}
if (isset($base_field_definitions[$field_name]) && in_array($key, array('id', 'revision', 'uuid', 'bundle', 'langcode')) && $base_field_definitions[$field_name]->isTranslatable()) {
throw new \LogicException(String::format('The @field field cannot be translatable as it is used as @key entity key.', array('@field' => $base_field_definitions[$field_name]->getLabel(), '@key' => $key)));
if ($base_field_definitions[$field_name]->isRevisionable()) {
throw new \LogicException(String::format('The @field field cannot be revisionable as it is used as @key entity key.', array(
'@field' => $base_field_definitions[$field_name]->getLabel(),
'@key' => $key,
)));
}
if ($base_field_definitions[$field_name]->isTranslatable()) {
throw new \LogicException(String::format('The @field field cannot be translatable as it is used as @key entity key.', array(
'@field' => $base_field_definitions[$field_name]->getLabel(),
'@key' => $key,
)));
}
}
// Make sure translatable entity types define the "langcode" field properly.
if ($entity_type->isTranslatable() && (!isset($keys['langcode']) || !isset($base_field_definitions[$keys['langcode']]) || !$base_field_definitions[$keys['langcode']]->isTranslatable())) {
throw new \LogicException(String::format('The @entity_type entity type cannot be translatable as it does not define a translatable "langcode" field.', array('@entity_type' => $entity_type->getLabel())));
}
return $base_field_definitions;
}

View File

@ -238,6 +238,7 @@ class EntityType implements EntityTypeInterface {
'revision' => '',
'bundle' => '',
'langcode' => '',
'default_langcode' => 'default_langcode',
);
$this->handlers += array(
'access' => 'Drupal\Core\Entity\EntityAccessControlHandler',

View File

@ -192,6 +192,14 @@ interface FieldableEntityInterface extends EntityInterface {
*
* @param string $field_name
* The name of the field which is changed.
*
* @throws \InvalidArgumentException
* When trying to assign a value to the language field that matches an
* existing translation.
* @throws \LogicException
* When trying to change:
* - The language of a translation.
* - The value of the flag identifying the default translation object.
*/
public function onChange($field_name);

View File

@ -182,8 +182,8 @@ class Tables implements TablesInterface {
$table = $this->ensureEntityTable($index_prefix, $sql_column, $type, $langcode, $base_table, $entity_id_field, $entity_tables);
// If there is a field storage (some specifiers are not, like
// default_langcode), check for case sensitivity.
// If there is a field storage (some specifiers are not), check for case
// sensitivity.
if ($field_storage) {
$column = $field_storage->getMainPropertyName();
$base_field_property_definitions = $field_storage->getPropertyDefinitions();

View File

@ -66,6 +66,13 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
*/
protected $langcodeKey = FALSE;
/**
* The default language entity key.
*
* @var string
*/
protected $defaultLangcodeKey = FALSE;
/**
* The base table of the entity.
*
@ -200,7 +207,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
if ($translatable) {
$this->dataTable = $this->entityType->getDataTable() ?: $this->entityTypeId . '_field_data';
$this->langcodeKey = $this->entityType->getKey('langcode');
$this->defaultLangcodeKey = $this->entityType->getKey('default_langcode') ?: 'default_langcode';
$this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
}
if ($revisionable && $translatable) {
$this->revisionDataTable = $this->entityType->getRevisionDataTable() ?: $this->entityTypeId . '_field_revision';
@ -342,11 +349,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
// the data table.
$table_mapping
->setFieldNames($this->baseTable, $key_fields)
->setFieldNames($this->dataTable, array_values(array_diff($all_fields, array($this->uuidKey))))
// Add the denormalized 'default_langcode' field to the mapping. Its
// value is identical to the query expression
// "base_table.langcode = data_table.langcode"
->setExtraColumns($this->dataTable, array('default_langcode'));
->setFieldNames($this->dataTable, array_values(array_diff($all_fields, array($this->uuidKey))));
}
elseif ($revisionable && $translatable) {
// The revisionable multilingual layout stores key field values in the
@ -356,31 +359,20 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
// holds the data field values for all non-revisionable fields. The data
// field values of revisionable fields are denormalized in the data
// table, as well.
$table_mapping->setFieldNames($this->baseTable, array_values(array_diff($key_fields, array($this->langcodeKey))));
$table_mapping->setFieldNames($this->baseTable, array_values($key_fields));
// Like in the multilingual, non-revisionable case the UUID is not
// in the data table. Additionally, do not store revision metadata
// fields in the data table.
$data_fields = array_values(array_diff($all_fields, array($this->uuidKey), $revision_metadata_fields));
$table_mapping
->setFieldNames($this->dataTable, $data_fields)
// Add the denormalized 'default_langcode' field to the mapping. Its
// value is identical to the query expression
// "base_langcode = data_table.langcode" where "base_langcode" is
// the language code of the default revision.
->setExtraColumns($this->dataTable, array('default_langcode'));
$table_mapping->setFieldNames($this->dataTable, $data_fields);
$revision_base_fields = array_merge(array($this->idKey, $this->revisionKey, $this->langcodeKey), $revision_metadata_fields);
$table_mapping->setFieldNames($this->revisionTable, $revision_base_fields);
$revision_data_key_fields = array($this->idKey, $this->revisionKey, $this->langcodeKey);
$revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, array($this->langcodeKey));
$table_mapping
->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields))
// Add the denormalized 'default_langcode' field to the mapping. Its
// value is identical to the query expression
// "revision_table.langcode = data_table.langcode".
->setExtraColumns($this->revisionDataTable, array('default_langcode'));
$table_mapping->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
}
// Add dedicated tables.
@ -673,12 +665,14 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
$table_mapping = $this->getTableMapping();
if ($this->revisionDataTable) {
// Find revisioned fields that are not entity keys.
$fields = array_diff($table_mapping->getFieldNames($this->revisionDataTable), $table_mapping->getFieldNames($this->baseTable));
// Find revisioned fields that are not entity keys. Exclude the langcode
// key as the base table holds only the default language.
$base_fields = array_diff($table_mapping->getFieldNames($this->baseTable), array($this->langcodeKey));
$fields = array_diff($table_mapping->getFieldNames($this->revisionDataTable), $base_fields);
// Find fields that are not revisioned or entity keys. Data fields have
// the same value regardless of entity revision.
$data_fields = array_diff($table_mapping->getFieldNames($this->dataTable), $fields, $table_mapping->getFieldNames($this->baseTable));
$data_fields = array_diff($table_mapping->getFieldNames($this->dataTable), $fields, $base_fields);
if ($data_fields) {
$fields = array_merge($fields, $data_fields);
$query->leftJoin($this->dataTable, 'data', "(revision.$this->idKey = data.$this->idKey)");
@ -703,10 +697,9 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
// Field values in default language are stored with
// LanguageInterface::LANGCODE_DEFAULT as key.
$langcode = empty($values['default_langcode']) ? $values[$this->langcodeKey] : LanguageInterface::LANGCODE_DEFAULT;
$langcode = empty($values[$this->defaultLangcodeKey]) ? $values[$this->langcodeKey] : LanguageInterface::LANGCODE_DEFAULT;
$translations[$id][$langcode] = TRUE;
foreach ($fields as $field_name) {
$columns = $table_mapping->getColumnNames($field_name);
// Do not key single-column fields by property name.
@ -776,13 +769,13 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
// apply to the default language. See http://drupal.org/node/1866330.
// Default to the original entity language if not explicitly specified
// otherwise.
if (!array_key_exists('default_langcode', $values)) {
$values['default_langcode'] = 1;
if (!array_key_exists($this->defaultLangcodeKey, $values)) {
$values[$this->defaultLangcodeKey] = 1;
}
// If the 'default_langcode' flag is explicitly not set, we do not care
// whether the queried values are in the original entity language or not.
elseif ($values['default_langcode'] === NULL) {
unset($values['default_langcode']);
elseif ($values[$this->defaultLangcodeKey] === NULL) {
unset($values[$this->defaultLangcodeKey]);
}
}
@ -1156,8 +1149,6 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
$table_name = $this->dataTable;
}
$record = $this->mapToStorageRecord($entity, $table_name);
$record->{$this->langcodeKey} = $entity->language()->getId();
$record->default_langcode = intval($record->{$this->langcodeKey} == $entity->getUntranslated()->language()->getId());
return $record;
}
@ -1171,7 +1162,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
* The revision id.
*/
protected function saveRevision(EntityInterface $entity) {
$record = $this->mapToStorageRecord($entity, $this->revisionTable);
$record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
$entity->preSaveRevision($this, $record);

View File

@ -9,6 +9,7 @@ namespace Drupal\Core\Entity\Sql;
use Drupal\Component\Utility\String;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\DatabaseException;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageException;
@ -163,7 +164,6 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
$storage_definition->hasCustomStorage() != $original->hasCustomStorage() ||
$storage_definition->getSchema() != $original->getSchema() ||
$storage_definition->isRevisionable() != $original->isRevisionable() ||
$storage_definition->isTranslatable() != $original->isTranslatable() ||
$table_mapping->allowsSharedTableStorage($storage_definition) != $table_mapping->allowsSharedTableStorage($original) ||
$table_mapping->requiresDedicatedTableStorage($storage_definition) != $table_mapping->requiresDedicatedTableStorage($original)
) {
@ -386,8 +386,17 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
// Only configurable fields currently support purging, so prevent deletion
// of ones we can't purge if they have existing data.
// @todo Add purging to all fields: https://www.drupal.org/node/2282119.
if (!($storage_definition instanceof FieldStorageConfigInterface) && $this->storage->countFieldData($storage_definition, TRUE)) {
throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field with data that can\'t be purged.');
try {
if (!($storage_definition instanceof FieldStorageConfigInterface) && $this->storage->countFieldData($storage_definition, TRUE)) {
throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field with data that cannot be purged.');
}
}
catch (DatabaseException $e) {
// This may happen when changing field storage schema, since we are not
// able to use a table mapping matching the passed storage definition.
// @todo Revisit this once we are able to instantiate the table mapping
// properly. See https://www.drupal.org/node/2274017.
return;
}
// Retrieve a table mapping which contains the deleted field still.
@ -506,13 +515,6 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
$schema[$table_name] = array_merge_recursive($schema[$table_name], $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names));
}
}
// Add the schema for extra fields.
foreach ($table_mapping->getExtraColumns($table_name) as $column_name) {
if ($column_name == 'default_langcode') {
$this->addDefaultLangcodeSchema($schema[$table_name]);
}
}
}
// Process tables after having gathered field information.
@ -726,25 +728,6 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
return $foreign_keys;
}
/**
* Returns the schema for the 'default_langcode' metadata field.
*
* @param array $schema
* The table schema to add the field schema to, passed by reference.
*
* @return array
* A schema field array for the 'default_langcode' metadata field.
*/
protected function addDefaultLangcodeSchema(&$schema) {
$schema['fields']['default_langcode'] = array(
'description' => 'Boolean indicating whether field values are in the default entity language.',
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 1,
);
}
/**
* Loads stored schema data for the given entity type definition.
*

View File

@ -13,13 +13,21 @@ namespace Drupal\Core\TypedData;
interface TranslatableInterface {
/**
* Returns the default language.
* Returns the translation language.
*
* @return \Drupal\Core\Language\LanguageInterface
* The language object.
*/
public function language();
/**
* Checks whether the translation is the default one.
*
* @return bool
* TRUE if the translation is the default one, FALSE otherwise.
*/
public function isDefaultTranslation();
/**
* Returns the languages the data is translated to.
*

View File

@ -167,6 +167,7 @@ class BlockContent extends ContentEntityBase implements BlockContentInterface {
$fields['langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Language'))
->setDescription(t('The custom block language code.'))
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('view', array(
'type' => 'hidden',

View File

@ -224,6 +224,7 @@ class Comment extends ContentEntityBase implements CommentInterface {
$fields['langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Language'))
->setDescription(t('The comment language code.'))
->setTranslatable(TRUE)
->setDisplayOptions('view', array(
'type' => 'hidden',
))

View File

@ -102,7 +102,7 @@ function _content_translation_form_language_content_settings_form_alter(array &$
foreach ($fields as $field_name => $definition) {
// Allow to configure only fields supporting multilingual storage.
// We skip our own fields as they are always translatable.
if (!empty($storage_definitions[$field_name]) && $storage_definitions[$field_name]->isTranslatable() && $storage_definitions[$field_name]->getProvider() != 'content_translation') {
if (!empty($storage_definitions[$field_name]) && $storage_definitions[$field_name]->isTranslatable() && $storage_definitions[$field_name]->getProvider() != 'content_translation' && $field_name != $entity_type->getKey('langcode') && $field_name != $entity_type->getKey('default_langcode')) {
$form['settings'][$entity_type_id][$bundle]['fields'][$field_name] = array(
'#label' => $definition->getLabel(),
'#type' => 'checkbox',

View File

@ -360,6 +360,7 @@ class MenuLinkContent extends ContentEntityBase implements MenuLinkContentInterf
$fields['langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Language'))
->setDescription(t('The menu link language code.'))
->setTranslatable(TRUE)
->setDisplayOptions('view', array(
'type' => 'hidden',
))

View File

@ -353,6 +353,7 @@ class Node extends ContentEntityBase implements NodeInterface {
$fields['langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Language'))
->setDescription(t('The node language code.'))
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('view', array(
'type' => 'hidden',

View File

@ -95,21 +95,24 @@ class EntitySerializationTest extends NormalizerTestBase {
'type' => array(
array('value' => 'entity_test_mulrev'),
),
'created' => array(
array('value' => $this->entity->created->value),
),
'user_id' => array(
array('target_id' => $this->values['user_id']),
),
'revision_id' => array(
array('value' => 1),
),
'default_langcode' => array(
array('value' => TRUE),
),
'field_test_text' => array(
array(
'value' => $this->values['field_test_text']['value'],
'format' => $this->values['field_test_text']['format'],
),
),
'created' => array(
array('value' => $this->entity->created->value),
),
);
$normalized = $this->serializer->normalize($this->entity);
@ -175,10 +178,11 @@ class EntitySerializationTest extends NormalizerTestBase {
'langcode' => '<langcode><value>en</value></langcode>',
'name' => '<name><value>' . $this->values['name'] . '</value></name>',
'type' => '<type><value>entity_test_mulrev</value></type>',
'created' => '<created><value>' . $this->entity->created->value . '</value></created>',
'user_id' => '<user_id><target_id>' . $this->values['user_id'] . '</target_id></user_id>',
'revision_id' => '<revision_id><value>' . $this->entity->getRevisionId() . '</value></revision_id>',
'default_langcode' => '<default_langcode><value>1</value></default_langcode>',
'field_test_text' => '<field_test_text><value>' . $this->values['field_test_text']['value'] . '</value><format>' . $this->values['field_test_text']['format'] . '</format></field_test_text>',
'created' => '<created><value>' . $this->entity->created->value . '</value></created>',
);
// Sort it in the same order as normalised.
$expected = array_merge($normalized, $expected);

View File

@ -163,6 +163,7 @@ class Shortcut extends ContentEntityBase implements ShortcutInterface {
$fields['langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Language'))
->setDescription(t('The language code of the shortcut.'))
->setTranslatable(TRUE)
->setDisplayOptions('view', array(
'type' => 'hidden',
))

View File

@ -8,6 +8,7 @@
namespace Drupal\system\Tests\Entity;
use Drupal\Component\Utility\String;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\entity_test\Entity\EntityTestMulRev;
use Drupal\language\Entity\ConfigurableLanguage;
@ -147,6 +148,7 @@ class EntityTranslationTest extends EntityLanguageTestBase {
*/
protected function doTestMultilingualProperties($entity_type) {
$langcode_key = $this->entityManager->getDefinition($entity_type)->getKey('langcode');
$default_langcode_key = $this->entityManager->getDefinition($entity_type)->getKey('default_langcode');
$name = $this->randomMachineName();
$uid = mt_rand(0, 127);
$langcode = $this->langcodes[0];
@ -249,16 +251,16 @@ class EntityTranslationTest extends EntityLanguageTestBase {
$this->assertEqual(count($entities), 2, format_string('%entity_type: Two entities correctly loaded by name.', array('%entity_type' => $entity_type)));
// @todo The default language condition should go away in favor of an
// explicit parameter.
$entities = entity_load_multiple_by_properties($entity_type, array('name' => $properties[$langcode]['name'][0], 'default_langcode' => 0));
$entities = entity_load_multiple_by_properties($entity_type, array('name' => $properties[$langcode]['name'][0], $default_langcode_key => 0));
$this->assertEqual(count($entities), 1, format_string('%entity_type: One entity correctly loaded by name translation.', array('%entity_type' => $entity_type)));
$entities = entity_load_multiple_by_properties($entity_type, array($langcode_key => $default_langcode, 'name' => $name));
$this->assertEqual(count($entities), 1, format_string('%entity_type: One entity correctly loaded by name and language.', array('%entity_type' => $entity_type)));
$entities = entity_load_multiple_by_properties($entity_type, array($langcode_key => $langcode, 'name' => $properties[$langcode]['name'][0]));
$this->assertEqual(count($entities), 0, format_string('%entity_type: No entity loaded by name translation specifying the translation language.', array('%entity_type' => $entity_type)));
$entities = entity_load_multiple_by_properties($entity_type, array($langcode_key => $langcode, 'name' => $properties[$langcode]['name'][0], 'default_langcode' => 0));
$entities = entity_load_multiple_by_properties($entity_type, array($langcode_key => $langcode, 'name' => $properties[$langcode]['name'][0], $default_langcode_key => 0));
$this->assertEqual(count($entities), 1, format_string('%entity_type: One entity loaded by name translation and language specifying to look for translations.', array('%entity_type' => $entity_type)));
$entities = entity_load_multiple_by_properties($entity_type, array('user_id' => $properties[$langcode]['user_id'][0], 'default_langcode' => NULL));
$entities = entity_load_multiple_by_properties($entity_type, array('user_id' => $properties[$langcode]['user_id'][0], $default_langcode_key => NULL));
$this->assertEqual(count($entities), 2, format_string('%entity_type: Two entities loaded by uid without caring about property translatability.', array('%entity_type' => $entity_type)));
// Test property conditions and orders with multiple languages in the same
@ -313,6 +315,9 @@ class EntityTranslationTest extends EntityLanguageTestBase {
$default_langcode = $this->langcodes[0];
$langcode = $this->langcodes[1];
$langcode_key = $this->entityManager->getDefinition($entity_type)->getKey('langcode');
$default_langcode_key = $this->entityManager->getDefinition($entity_type)->getKey('default_langcode');
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $this->entityManager
->getStorage($entity_type)
->create(array('name' => $this->randomMachineName(), $langcode_key => LanguageInterface::LANGCODE_NOT_SPECIFIED));
@ -324,12 +329,13 @@ class EntityTranslationTest extends EntityLanguageTestBase {
// 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.');
$this->assertIdentical($entity, $translation, 'The translation object corresponding to a non-default language is the entity object itself when the entity is language-neutral.');
$entity->{$langcode_key}->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.');
$this->assertIdentical($entity, $translation, 'The translation object corresponding to the default language (explicit) is the entity object itself.');
$translation = $entity->getTranslation(LanguageInterface::LANGCODE_DEFAULT);
$this->assertEqual($entity, $translation, 'The translation object corresponding to the default language (implicit) is the entity object itself.');
$this->assertIdentical($entity, $translation, 'The translation object corresponding to the default language (implicit) is the entity object itself.');
$this->assertTrue($entity->{$default_langcode_key}->value, 'The translation object is the default one.');
// Create a translation and verify that the translation object and the
// original object behave independently.
@ -340,18 +346,57 @@ class EntityTranslationTest extends EntityLanguageTestBase {
$this->assertNotIdentical($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()->getId(), $langcode, 'The translation language matches the specified one.');
$this->assertEqual($translation->{$langcode_key}->value, $langcode, 'The translation field language value matches the specified one.');
$this->assertFalse($translation->{$default_langcode_key}->value, 'The translation object is not the default one.');
$this->assertEqual($translation->getUntranslated()->language()->getId(), $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.
// Save the translation and check that the expected 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_type . '_translation_insert'], $langcode, 'The entity-type-specific entity translation insertion hook has fired.');
// Verify that changing translation language causes an exception to be
// thrown.
$message = 'The translation language cannot be changed.';
try {
$translation->{$langcode_key}->value = $this->langcodes[2];
$this->fail($message);
}
catch (\LogicException $e) {
$this->pass($message);
}
// Verify that reassigning the same translation language is allowed.
$message = 'The translation language can be reassigned the same value.';
try {
$translation->{$langcode_key}->value = $langcode;
$this->pass($message);
}
catch (\LogicException $e) {
$this->fail($message);
}
// Verify that changing the default translation flag causes an exception to
// be thrown.
$message = 'The default translation flag cannot be changed.';
foreach ($entity->getTranslationLanguages() as $t_langcode => $language) {
$translation = $entity->getTranslation($t_langcode);
$default = $translation->isDefaultTranslation();
try {
$translation->{$default_langcode_key}->value = !$default;
$this->fail($message);
}
catch (\LogicException $e) {
$this->pass($message);
}
$this->assertEqual($translation->{$default_langcode_key}->value, $default);
}
// Check that after loading an entity the language is the default one.
$entity = $this->reloadEntity($entity);
$this->assertEqual($entity->language()->getId(), $default_langcode, 'The loaded entity is the original one.');
@ -588,9 +633,16 @@ class EntityTranslationTest extends EntityLanguageTestBase {
$this->assertTrue(!isset($base_field_definitions['id']->translatable), 'Translatability for the <em>id</em> field is not defined.');
$this->assertFalse($definitions['id']->isTranslatable(), 'Field translatability is disabled by default.');
// Check that entity ids and langcode fields cannot be translatable.
foreach (array('id', 'uuid', 'revision_id', 'type', 'langcode') as $name) {
$this->state->set('entity_test.field_definitions.translatable', array($name => TRUE));
// Check that entity id keys have the expect translatability.
$translatable_fields = array(
'id' => TRUE,
'uuid' => TRUE,
'revision_id' => TRUE,
'type' => TRUE,
'langcode' => FALSE,
);
foreach ($translatable_fields as $name => $translatable) {
$this->state->set('entity_test.field_definitions.translatable', array($name => $translatable));
$this->entityManager->clearCachedFieldDefinitions();
$message = format_string('Field %field cannot be translatable.', array('%field' => $name));

View File

@ -77,7 +77,8 @@ class EntityTest extends ContentEntityBase implements EntityOwnerInterface {
$fields['langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Language code'))
->setDescription(t('The language code of the test entity.'));
->setDescription(t('The language code of the test entity.'))
->setTranslatable(TRUE);
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))

View File

@ -34,6 +34,7 @@ use Drupal\Core\Entity\EntityTypeInterface;
* "bundle" = "type",
* "label" = "name",
* "langcode" = "custom_langcode_key",
* "default_langcode" = "custom_default_langcode_key",
* },
* links = {
* "canonical" = "/entity_test_mul_langcode_key/manage/{entity_test_mul_langcode_key}",

View File

@ -121,6 +121,7 @@ class Term extends ContentEntityBase implements TermInterface {
$fields['langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Language'))
->setDescription(t('The term language code.'))
->setTranslatable(TRUE)
->setDisplayOptions('view', array(
'type' => 'hidden',
))

View File

@ -345,7 +345,7 @@ abstract class AccountForm extends ContentEntityForm {
* The current state of the form.
*/
public function syncUserLangcode($entity_type_id, UserInterface $user, array &$form, FormStateInterface &$form_state) {
$user->langcode = $user->preferred_langcode;
$user->getUntranslated()->langcode = $user->preferred_langcode;
}
/**

View File

@ -474,7 +474,8 @@ class User extends ContentEntityBase implements UserInterface {
$fields['langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Language code'))
->setDescription(t('The user language code.'));
->setDescription(t('The user language code.'))
->setTranslatable(TRUE);
$fields['preferred_langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Preferred language code'))

View File

@ -36,6 +36,20 @@ class EntityManagerTest extends UnitTestCase {
*/
protected $entityManager;
/**
* The entity type definition.
*
* @var \Drupal\Core\Entity\EntityTypeInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $entityType;
/**
* An instance of the test entity.
*
* @var \Drupal\Tests\Core\Entity\EntityManagerTestEntity|\PHPUnit_Framework_MockObject_MockObject
*/
protected $entity;
/**
* The plugin discovery.
*
@ -146,17 +160,48 @@ class EntityManagerTest extends UnitTestCase {
$this->formBuilder = $this->getMock('Drupal\Core\Form\FormBuilderInterface');
$this->controllerResolver = $this->getClassResolverStub();
$this->container = $this->getContainerWithCacheTagsInvalidator($this->cacheTagsInvalidator);
$this->discovery = $this->getMock('Drupal\Component\Plugin\Discovery\DiscoveryInterface');
$this->typedDataManager = $this->getMockBuilder('\Drupal\Core\TypedData\TypedDataManager')
->disableOriginalConstructor()
->getMock();
$map = [
['field_item:boolean', TRUE, ['class' => 'Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem']],
];
$this->typedDataManager->expects($this->any())
->method('getDefinition')
->willReturnMap($map);
$this->eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$this->installedDefinitions = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreInterface');
$this->container = $this->getContainerWithCacheTagsInvalidator($this->cacheTagsInvalidator);
$this->container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
\Drupal::setContainer($this->container);
$field_type_manager = $this->getMock('Drupal\Core\Field\FieldTypePluginManagerInterface');
$field_type_manager->expects($this->any())
->method('getDefaultStorageSettings')
->willReturn(array());
$field_type_manager->expects($this->any())
->method('getDefaultFieldSettings')
->willReturn(array());
$string_translation = $this->getMock('Drupal\Core\StringTranslation\TranslationInterface');
$map = [
['cache_tags.invalidator', 1, $this->cacheTagsInvalidator],
['plugin.manager.field.field_type', 1, $field_type_manager],
['string_translation', 1, $string_translation],
['typed_data_manager', 1, $this->typedDataManager],
];
$this->container->expects($this->any())
->method('get')
->willReturnMap($map);
}
/**
@ -521,6 +566,100 @@ class EntityManagerTest extends UnitTestCase {
$this->assertSame($expected, $this->entityManager->getFieldStorageDefinitions('test_entity_type'));
}
/**
* Tests the getBaseFieldDefinitions() method with a translatable entity type.
*
* @covers ::getBaseFieldDefinitions
* @covers ::buildBaseFieldDefinitions
*
* @dataProvider providerTestGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode
*/
public function testGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode($default_langcode_key) {
$this->setUpEntityWithFieldDefinition(FALSE, 'id', array('langcode' => 'langcode', 'default_langcode' => $default_langcode_key));
$field_definition = $this->getMockBuilder('Drupal\Core\Field\BaseFieldDefinition')
->disableOriginalConstructor()
->getMock();
$field_definition->expects($this->atLeastOnce())
->method('isTranslatable')
->willReturn(TRUE);
$entity_class = get_class($this->entity);
$entity_class::$baseFieldDefinitions += array('langcode' => $field_definition);
$this->entityType->expects($this->atLeastOnce())
->method('isTranslatable')
->willReturn(TRUE);
$definitions = $this->entityManager->getBaseFieldDefinitions('test_entity_type');
$this->assertTrue(isset($definitions[$default_langcode_key]));
}
/**
* Provides test data for testGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode().
*
* @return array
* Test data.
*/
public function providerTestGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode() {
return [
['default_langcode'],
['custom_default_langcode_key'],
];
}
/**
* Tests the getBaseFieldDefinitions() method with a translatable entity type.
*
* @covers ::getBaseFieldDefinitions
* @covers ::buildBaseFieldDefinitions
*
* @expectedException \LogicException
* @expectedExceptionMessage The Test entity type cannot be translatable as it does not define a translatable "langcode" field.
*
* @dataProvider providerTestGetBaseFieldDefinitionsTranslatableEntityTypeLangcode
*/
public function testGetBaseFieldDefinitionsTranslatableEntityTypeLangcode($provide_key, $provide_field, $translatable) {
$keys = $provide_key ? array('langcode' => 'langcode') : array();
$this->setUpEntityWithFieldDefinition(FALSE, 'id', $keys);
if ($provide_field) {
$field_definition = $this->getMockBuilder('Drupal\Core\Field\BaseFieldDefinition')
->disableOriginalConstructor()
->getMock();
$field_definition->expects($this->any())
->method('isTranslatable')
->willReturn($translatable);
$entity_class = get_class($this->entity);
$entity_class::$baseFieldDefinitions += array('langcode' => $field_definition);
}
$this->entityType->expects($this->atLeastOnce())
->method('isTranslatable')
->willReturn(TRUE);
$this->entityType->expects($this->atLeastOnce())
->method('getLabel')
->willReturn('Test');
$this->entityManager->getBaseFieldDefinitions('test_entity_type');
}
/**
* Provides test data for testGetBaseFieldDefinitionsTranslatableEntityTypeLangcode().
*
* @return array
* Test data.
*/
public function providerTestGetBaseFieldDefinitionsTranslatableEntityTypeLangcode() {
return [
[FALSE, TRUE, TRUE],
[TRUE, FALSE, TRUE],
[TRUE, TRUE, FALSE],
];
}
/**
* Tests the getBaseFieldDefinitions() method with caching.
*
@ -667,6 +806,10 @@ class EntityManagerTest extends UnitTestCase {
public function testGetBaseFieldDefinitionsInvalidDefinition() {
$langcode_definition = $this->setUpEntityWithFieldDefinition(FALSE, 'langcode', array('langcode' => 'langcode'));
$langcode_definition->expects($this->once())
->method('isTranslatable')
->will($this->returnValue(FALSE));
$this->entityType->expects($this->any())
->method('isTranslatable')
->will($this->returnValue(TRUE));
@ -725,19 +868,20 @@ class EntityManagerTest extends UnitTestCase {
* A field definition object.
*/
protected function setUpEntityWithFieldDefinition($custom_invoke_all = FALSE, $field_definition_id = 'id', $entity_keys = array()) {
$entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
$entity = $this->getMockBuilder('Drupal\Tests\Core\Entity\EntityManagerTestEntity')
$this->entityType = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
$this->entity = $this->getMockBuilder('Drupal\Tests\Core\Entity\EntityManagerTestEntity')
->disableOriginalConstructor()
->getMockForAbstractClass();
$entity_class = get_class($entity);
$entity_class = get_class($this->entity);
$entity_type->expects($this->any())
$this->entityType->expects($this->any())
->method('getClass')
->will($this->returnValue($entity_class));
$entity_type->expects($this->any())
$this->entityType->expects($this->any())
->method('getKeys')
->will($this->returnValue($entity_keys));
$entity_type->expects($this->any())
->will($this->returnValue($entity_keys + array('default_langcode' => 'default_langcode')));
$this->entityType->expects($this->any())
->method('isSubclassOf')
->with($this->equalTo('\Drupal\Core\Entity\FieldableEntityInterface'))
->will($this->returnValue(TRUE));
@ -762,14 +906,14 @@ class EntityManagerTest extends UnitTestCase {
$override_entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
$override_entity_type->expects($this->any())
->method('getClass')
->will($this->returnValue(get_class($entity)));
->will($this->returnValue(get_class($this->entity)));
$override_entity_type->expects($this->any())
->method('getHandlerClass')
->with('storage')
->will($this->returnValue('\Drupal\Tests\Core\Entity\TestConfigEntityStorage'));
$this->setUpEntityManager(array('test_entity_type' => $entity_type, 'base_field_override' => $override_entity_type));
$this->setUpEntityManager(array('test_entity_type' => $this->entityType, 'base_field_override' => $override_entity_type));
return $field_definition;
}
@ -1059,7 +1203,7 @@ class EntityManagerTest extends UnitTestCase {
->will($this->returnValue($entity_class));
$entity_type->expects($this->any())
->method('getKeys')
->will($this->returnValue(array()));
->will($this->returnValue(array('default_langcode' => 'default_langcode')));
$entity_type->expects($this->any())
->method('id')
->will($this->returnValue('test_entity_type'));
@ -1202,7 +1346,7 @@ class EntityManagerTest extends UnitTestCase {
->will($this->returnValue($entity_class));
$entity_type->expects($this->any())
->method('getKeys')
->will($this->returnValue(array()));
->will($this->returnValue(array('default_langcode' => 'default_langcode')));
$entity_type->expects($this->any())
->method('id')
->will($this->returnValue('test_entity_type'));

View File

@ -38,7 +38,7 @@ class EntityTypeTest extends UnitTestCase {
*/
public function testGetKeys($entity_keys, $expected) {
$entity_type = $this->setUpEntityType(array('entity_keys' => $entity_keys));
$this->assertSame($expected, $entity_type->getKeys());
$this->assertSame($expected + ['default_langcode' => 'default_langcode'], $entity_type->getKeys());
}
/**

View File

@ -89,7 +89,6 @@ class SqlContentEntityStorageSchemaTest extends UnitTestCase {
* @covers ::getFieldUniqueKeys
* @covers ::getFieldForeignKeys
* @covers ::getFieldSchemaData
* @covers ::addDefaultLangcodeSchema
* @covers ::processBaseTable
* @covers ::processIdentifierSchema
*/
@ -321,13 +320,6 @@ class SqlContentEntityStorageSchemaTest extends UnitTestCase {
'type' => 'int',
'not null' => FALSE,
),
'default_langcode' => array(
'description' => 'Boolean indicating whether field values are in the default entity language.',
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 1,
),
),
'primary key' => array('id'),
'unique keys' => array(

View File

@ -643,7 +643,6 @@ class SqlContentEntityStorageTest extends UnitTestCase {
$expected = array();
$actual = $mapping->getExtraColumns('entity_test');
$this->assertEquals($expected, $actual);
$expected = array('default_langcode');
$actual = $mapping->getExtraColumns('entity_test_field_data');
$this->assertEquals($expected, $actual);
}
@ -704,7 +703,6 @@ class SqlContentEntityStorageTest extends UnitTestCase {
$expected = array();
$actual = $mapping->getExtraColumns('entity_test');
$this->assertEquals($expected, $actual);
$expected = array('default_langcode');
$actual = $mapping->getExtraColumns('entity_test_field_data');
$this->assertEquals($expected, $actual);
}
@ -761,13 +759,13 @@ class SqlContentEntityStorageTest extends UnitTestCase {
);
$this->assertEquals($expected, $mapping->getTableNames());
// The language code is not stored on the base table, but on the revision
// table.
// The default language code is stored on the base table.
$expected = array_values(array_filter(array(
$entity_keys['id'],
$entity_keys['revision'],
$entity_keys['bundle'],
$entity_keys['uuid'],
$entity_keys['langcode'],
)));
$actual = $mapping->getFieldNames('entity_test');
$this->assertEquals($expected, $actual);
@ -803,7 +801,6 @@ class SqlContentEntityStorageTest extends UnitTestCase {
$this->assertEquals($expected, $actual);
$actual = $mapping->getExtraColumns('entity_test_revision');
$this->assertEquals($expected, $actual);
$expected = array('default_langcode');
$actual = $mapping->getExtraColumns('entity_test_field_data');
$this->assertEquals($expected, $actual);
$actual = $mapping->getExtraColumns('entity_test_field_revision');
@ -891,13 +888,13 @@ class SqlContentEntityStorageTest extends UnitTestCase {
);
$this->assertEquals($expected, $mapping->getTableNames());
// The language code is not stored on the base table, but on the revision
// table.
// The default language code is not stored on the base table.
$expected = array_values(array_filter(array(
$entity_keys['id'],
$entity_keys['revision'],
$entity_keys['bundle'],
$entity_keys['uuid'],
$entity_keys['langcode'],
)));
$actual = $mapping->getFieldNames('entity_test');
$this->assertEquals($expected, $actual);
@ -933,7 +930,6 @@ class SqlContentEntityStorageTest extends UnitTestCase {
$this->assertEquals($expected, $actual);
$actual = $mapping->getExtraColumns('entity_test_revision');
$this->assertEquals($expected, $actual);
$expected = array('default_langcode');
$actual = $mapping->getExtraColumns('entity_test_field_data');
$this->assertEquals($expected, $actual);
$actual = $mapping->getExtraColumns('entity_test_field_revision');