diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php index 6327656..5589dad 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -12,6 +12,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface; +use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldException; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\field\FieldStorageConfigInterface; @@ -201,7 +202,10 @@ public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterfac return FALSE; } - return $this->getSchemaFromStorageDefinition($storage_definition) != $this->loadFieldSchemaData($original); + $current_schema = $this->getSchemaFromStorageDefinition($storage_definition); + $this->processFieldStorageSchema($current_schema); + + return $current_schema != $this->loadFieldSchemaData($original); } /** @@ -852,6 +856,7 @@ protected function loadFieldSchemaData(FieldStorageDefinitionInterface $storage_ * The field schema data array. */ protected function saveFieldSchemaData(FieldStorageDefinitionInterface $storage_definition, $schema) { + $this->processFieldStorageSchema($schema); $this->installedStorageSchema()->set($storage_definition->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition->getName(), $schema); } @@ -1102,6 +1107,23 @@ protected function processIdentifierSchema(&$schema, $key) { } /** + * Processes the schema for a field storage definition. + * + * @param array &$field_storage_schema + * An array that contains the schema data for a field storage definition. + */ + protected function processFieldStorageSchema(array &$field_storage_schema) { + // Clean up some schema properties that should not be taken into account + // after a field storage has been created. + foreach ($field_storage_schema as $table_name => $table_schema) { + foreach ($table_schema['fields'] as $key => $schema) { + unset($field_storage_schema[$table_name]['fields'][$key]['initial']); + unset($field_storage_schema[$table_name]['fields'][$key]['initial_from_field']); + } + } + } + + /** * Performs the specified operation on a field. * * This figures out whether the field is stored in a dedicated or shared table @@ -1613,29 +1635,58 @@ protected function hasNullFieldPropertyData($table_name, $column_name) { * - foreign keys: The schema definition for the foreign keys. * * @throws \Drupal\Core\Field\FieldException - * Exception thrown if the schema contains reserved column names. + * Exception thrown if the schema contains reserved column names or if the + * initial values definition is invalid. */ protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) { $schema = []; + $table_mapping = $this->storage->getTableMapping(); $field_schema = $storage_definition->getSchema(); // Check that the schema does not include forbidden column names. - if (array_intersect(array_keys($field_schema['columns']), $this->storage->getTableMapping()->getReservedColumns())) { + if (array_intersect(array_keys($field_schema['columns']), $table_mapping->getReservedColumns())) { throw new FieldException("Illegal field column names on {$storage_definition->getName()}"); } $field_name = $storage_definition->getName(); $base_table = $this->storage->getBaseTable(); + // Define the initial values, if any. + $initial_value = $initial_value_from_field = []; + $storage_definition_is_new = empty($this->loadFieldSchemaData($storage_definition)); + if ($storage_definition_is_new && $storage_definition instanceof BaseFieldDefinition && $table_mapping->allowsSharedTableStorage($storage_definition)) { + if (($initial_storage_value = $storage_definition->getInitialValue()) && !empty($initial_storage_value)) { + $initial_value = $initial_storage_value[0]; + } + + if ($initial_value_field_name = $storage_definition->getInitialValueFromField()) { + // Check that the field used for populating initial values is valid. We + // must use the last installed version of that, as the new field might + // be created in an update function and the storage definition of the + // "from" field might get changed later. + $last_installed_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id()); + if (!isset($last_installed_storage_definitions[$initial_value_field_name])) { + throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: The field $initial_value_field_name does not exist."); + } + + if ($storage_definition->getType() !== $last_installed_storage_definitions[$initial_value_field_name]->getType()) { + throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: The field types do not match."); + } + + if (!$table_mapping->allowsSharedTableStorage($last_installed_storage_definitions[$initial_value_field_name])) { + throw new FieldException("Illegal initial value definition on {$storage_definition->getName()}: Both fields have to be stored in the shared entity tables."); + } + + $initial_value_from_field = $table_mapping->getColumnNames($initial_value_field_name); + } + } + // A shared table contains rows for entities where the field is empty // (since other fields stored in the same table might not be empty), thus // the only columns that can be 'not null' are those for required - // properties of required fields. However, even those would break in the - // case where a new field is added to a table that contains existing rows. - // For now, we only hardcode 'not null' to a couple "entity keys", in order - // to keep their indexes optimized. - // @todo Revisit once we have support for 'initial' in - // https://www.drupal.org/node/2346019. + // properties of required fields. For now, we only hardcode 'not null' to a + // few "entity keys", in order to keep their indexes optimized. + // @todo Fix this in https://www.drupal.org/node/2841291. $not_null_keys = $this->entityType->getKeys(); // Label fields are not necessarily required. unset($not_null_keys['label']); @@ -1654,6 +1705,14 @@ protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $st $schema['fields'][$schema_field_name] = $column_schema; $schema['fields'][$schema_field_name]['not null'] = in_array($field_name, $not_null_keys); + + // Use the initial value of the field storage, if available. + if ($initial_value && isset($initial_value[$field_column_name])) { + $schema['fields'][$schema_field_name]['initial'] = $initial_value[$field_column_name]; + } + elseif (!empty($initial_value_from_field)) { + $schema['fields'][$schema_field_name]['initial_from_field'] = $initial_value_from_field[$field_column_name]; + } } if (!empty($field_schema['indexes'])) { diff --git a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php index 37cc103..16df7c6 100644 --- a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php +++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php @@ -510,6 +510,83 @@ public function setDefaultValueCallback($callback) { } /** + * Returns the initial value for the field. + * + * @return array + * The initial value for the field, as a numerically indexed array of items, + * each item being a property/value array (array() for no default value). + */ + public function getInitialValue() { + $value = isset($this->definition['initial_value']) ? $this->definition['initial_value'] : []; + + // Normalize into the "array keyed by delta" format. + if (isset($value) && !is_array($value)) { + $value = [ + [$this->getMainPropertyName() => $value], + ]; + } + + return $value; + } + + /** + * Sets an initial value for the field. + * + * @param mixed $value + * The initial value for the field. This can be either: + * - a literal, in which case it will be assigned to the first property of + * the first item; + * - a numerically indexed array of items, each item being a property/value + * array; + * - a non-numerically indexed array, in which case the array is assumed to + * be a property/value array and used as the first item; + * - an empty array for no initial value. + * + * @return $this + */ + public function setInitialValue($value) { + if ($value === NULL) { + $value = []; + } + // Unless the value is an empty array, we may need to transform it. + if (!is_array($value) || !empty($value)) { + if (!is_array($value)) { + $value = [[$this->getMainPropertyName() => $value]]; + } + elseif (is_array($value) && !is_numeric(array_keys($value)[0])) { + $value = [0 => $value]; + } + } + $this->definition['initial_value'] = $value; + + return $this; + } + + /** + * Returns the name of the field that will be used for getting initial values. + * + * @return string|null + * The field name. + */ + public function getInitialValueFromField() { + return isset($this->definition['initial_value_from_field']) ? $this->definition['initial_value_from_field'] : NULL; + } + + /** + * Sets a field that will be used for getting initial values. + * + * @param string $field_name + * The name of the field that will be used for getting initial values. + * + * @return $this + */ + public function setInitialValueFromField($field_name) { + $this->definition['initial_value_from_field'] = $field_name; + + return $this; + } + + /** * {@inheritdoc} */ public function getOptionsProvider($property_name, FieldableEntityInterface $entity) { diff --git a/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php index bb17fe6..86202f6 100644 --- a/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php +++ b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php @@ -5,6 +5,7 @@ use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\Sql\SqlContentEntityStorage; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** @@ -70,6 +71,16 @@ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $ // @todo Forward this to all interested handlers, not only storage, once // iterating handlers is possible: https://www.drupal.org/node/2332857. $storage = $this->entityTypeManager->getStorage($entity_type_id); + + // Entity type definition updates can change the schema by adding or + // removing entity tables (for example when switching an entity type from + // non-revisionable to revisionable), so CRUD operations on a field storage + // definition need to use the last installed entity type schema. + if ($storage instanceof SqlContentEntityStorage + && ($last_installed_entity_type = $this->entityLastInstalledSchemaRepository->getLastInstalledDefinition($entity_type_id))) { + $storage->setEntityType($last_installed_entity_type); + } + if ($storage instanceof FieldStorageDefinitionListenerInterface) { $storage->onFieldStorageDefinitionCreate($storage_definition); } @@ -89,6 +100,16 @@ public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $ // @todo Forward this to all interested handlers, not only storage, once // iterating handlers is possible: https://www.drupal.org/node/2332857. $storage = $this->entityTypeManager->getStorage($entity_type_id); + + // Entity type definition updates can change the schema by adding or + // removing entity tables (for example when switching an entity type from + // non-revisionable to revisionable), so CRUD operations on a field storage + // definition need to use the last installed entity type schema. + if ($storage instanceof SqlContentEntityStorage + && ($last_installed_entity_type = $this->entityLastInstalledSchemaRepository->getLastInstalledDefinition($entity_type_id))) { + $storage->setEntityType($last_installed_entity_type); + } + if ($storage instanceof FieldStorageDefinitionListenerInterface) { $storage->onFieldStorageDefinitionUpdate($storage_definition, $original); } @@ -108,6 +129,16 @@ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $ // @todo Forward this to all interested handlers, not only storage, once // iterating handlers is possible: https://www.drupal.org/node/2332857. $storage = $this->entityTypeManager->getStorage($entity_type_id); + + // Entity type definition updates can change the schema by adding or + // removing entity tables (for example when switching an entity type from + // non-revisionable to revisionable), so CRUD operations on a field storage + // definition need to use the last installed entity type schema. + if ($storage instanceof SqlContentEntityStorage + && ($last_installed_entity_type = $this->entityLastInstalledSchemaRepository->getLastInstalledDefinition($entity_type_id))) { + $storage->setEntityType($last_installed_entity_type); + } + if ($storage instanceof FieldStorageDefinitionListenerInterface) { $storage->onFieldStorageDefinitionDelete($storage_definition); } diff --git a/core/modules/comment/comment.install b/core/modules/comment/comment.install index 47241c2..d9a9ba6 100644 --- a/core/modules/comment/comment.install +++ b/core/modules/comment/comment.install @@ -6,6 +6,7 @@ */ use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\StringTranslation\PluralTranslatableMarkup; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\field\Entity\FieldStorageConfig; @@ -184,3 +185,63 @@ function comment_update_8301() { $entity_type->set('entity_keys', $keys); $definition_update_manager->updateEntityType($entity_type); } + +/** + * Update the status field. + */ +function comment_update_8400() { + // The status field was promoted to an entity key in comment_update_8301(), + // which makes it NOT NULL in the default SQL storage, which means its storage + // definition needs to be updated as well. + $entity_definition_update_manager = \Drupal::service('entity.definition_update_manager'); + $entity_definition_update_manager->updateFieldStorageDefinition($entity_definition_update_manager->getFieldStorageDefinition('status', 'comment')); +} + +/** + * Add the revisionable metadata fields to comments. + */ +function comment_update_8401() { + $definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + + // Add the revisionable metadata fields to the comment entity type. + $entity_type = $definition_update_manager->getEntityType('comment'); + + $revision_metadata_keys = [ + 'revision_user' => 'revision_user', + 'revision_created' => 'revision_created', + 'revision_log_message' => 'revision_log_message' + ]; + $entity_type->set('revision_metadata_keys', $revision_metadata_keys); + + $definition_update_manager->updateEntityType($entity_type); + + // Add the revision metadata fields. + $revision_created = BaseFieldDefinition::create('created') + ->setLabel(t('Revision create time')) + ->setDescription(t('The time that the current revision was created.')) + ->setRevisionable(TRUE); + $definition_update_manager->installFieldStorageDefinition('revision_created', 'comment', 'comment', $revision_created); + + $revision_user = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Revision user')) + ->setDescription(t('The user ID of the author of the current revision.')) + ->setSetting('target_type', 'user') + ->setRevisionable(TRUE); + $definition_update_manager->installFieldStorageDefinition('revision_user', 'comment', 'comment', $revision_user); + + $revision_log_message = BaseFieldDefinition::create('string_long') + ->setLabel(t('Revision log message')) + ->setDescription(t('Briefly describe the changes you have made.')) + ->setRevisionable(TRUE) + ->setDefaultValue('') + ->setDisplayOptions('form', [ + 'type' => 'string_textarea', + 'weight' => 25, + 'settings' => [ + 'rows' => 4, + ], + ]); + $definition_update_manager->installFieldStorageDefinition('revision_log_message', 'comment', 'comment', $revision_log_message); + + return t('Comments have been converted to revisionable.'); +} diff --git a/core/modules/comment/comment.post_update.php b/core/modules/comment/comment.post_update.php new file mode 100644 index 0000000..d526a69 --- /dev/null +++ b/core/modules/comment/comment.post_update.php @@ -0,0 +1,35 @@ +convertToRevisionable( + $sandbox, + [ + 'subject', + 'name', + 'mail', + 'homepage', + 'hostname', + 'created', + 'changed', + ] + ); +} diff --git a/core/modules/comment/src/CommentStorageSchema.php b/core/modules/comment/src/CommentStorageSchema.php index 2106a8e..d2c87af 100644 --- a/core/modules/comment/src/CommentStorageSchema.php +++ b/core/modules/comment/src/CommentStorageSchema.php @@ -17,7 +17,7 @@ class CommentStorageSchema extends SqlContentEntityStorageSchema { protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) { $schema = parent::getEntitySchema($entity_type, $reset); - $schema['comment_field_data']['indexes'] += [ + $schema[$entity_type->getDataTable()]['indexes'] += [ 'comment__status_pid' => ['pid', 'status'], 'comment__num_new' => [ 'entity_id', diff --git a/core/modules/comment/src/Entity/Comment.php b/core/modules/comment/src/Entity/Comment.php index 821b30d..c36a05a 100644 --- a/core/modules/comment/src/Entity/Comment.php +++ b/core/modules/comment/src/Entity/Comment.php @@ -4,10 +4,8 @@ use Drupal\Component\Utility\Number; use Drupal\Core\Cache\Cache; -use Drupal\Core\Entity\ContentEntityBase; use Drupal\comment\CommentInterface; -use Drupal\Core\Entity\EntityChangedTrait; -use Drupal\Core\Entity\EntityPublishedTrait; +use Drupal\Core\Entity\EditorialContentEntityBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; @@ -43,16 +41,24 @@ * }, * base_table = "comment", * data_table = "comment_field_data", + * revision_table = "comment_revision", + * revision_data_table = "comment_field_revision", * uri_callback = "comment_uri", * translatable = TRUE, * entity_keys = { * "id" = "cid", + * "revision" = "revision_id", * "bundle" = "comment_type", * "label" = "subject", * "langcode" = "langcode", * "uuid" = "uuid", * "published" = "status", * }, + * revision_metadata_keys = { + * "revision_user" = "revision_user", + * "revision_created" = "revision_created", + * "revision_log_message" = "revision_log_message", + * }, * links = { * "canonical" = "/comment/{comment}", * "delete-form" = "/comment/{comment}/delete", @@ -66,10 +72,7 @@ * } * ) */ -class Comment extends ContentEntityBase implements CommentInterface { - - use EntityChangedTrait; - use EntityPublishedTrait; +class Comment extends EditorialContentEntityBase implements CommentInterface { /** * The thread for which a lock was acquired. @@ -251,6 +254,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['subject'] = BaseFieldDefinition::create('string') ->setLabel(t('Subject')) ->setTranslatable(TRUE) + ->setRevisionable(TRUE) ->setSetting('max_length', 64) ->setDisplayOptions('form', [ 'type' => 'string_textfield', @@ -270,18 +274,21 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('Name')) ->setDescription(t("The comment author's name.")) ->setTranslatable(TRUE) + ->setRevisionable(TRUE) ->setSetting('max_length', 60) ->setDefaultValue(''); $fields['mail'] = BaseFieldDefinition::create('email') ->setLabel(t('Email')) ->setDescription(t("The comment author's email address.")) - ->setTranslatable(TRUE); + ->setTranslatable(TRUE) + ->setRevisionable(TRUE); $fields['homepage'] = BaseFieldDefinition::create('uri') ->setLabel(t('Homepage')) ->setDescription(t("The comment author's home page address.")) ->setTranslatable(TRUE) + ->setRevisionable(TRUE) // URIs are not length limited by RFC 2616, but we can only store 255 // characters in our comment DB schema. ->setSetting('max_length', 255); @@ -290,17 +297,20 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('Hostname')) ->setDescription(t("The comment author's hostname.")) ->setTranslatable(TRUE) + ->setRevisionable(TRUE) ->setSetting('max_length', 128); $fields['created'] = BaseFieldDefinition::create('created') ->setLabel(t('Created')) ->setDescription(t('The time that the comment was created.')) - ->setTranslatable(TRUE); + ->setTranslatable(TRUE) + ->setRevisionable(TRUE); $fields['changed'] = BaseFieldDefinition::create('changed') ->setLabel(t('Changed')) ->setDescription(t('The time that the comment was last edited.')) - ->setTranslatable(TRUE); + ->setTranslatable(TRUE) + ->setRevisionable(TRUE); $fields['thread'] = BaseFieldDefinition::create('string') ->setLabel(t('Thread place')) diff --git a/core/modules/comment/src/Tests/Update/CommentUpdateTest.php b/core/modules/comment/src/Tests/Update/CommentUpdateTest.php index 3db9c4a..8e2a402 100644 --- a/core/modules/comment/src/Tests/Update/CommentUpdateTest.php +++ b/core/modules/comment/src/Tests/Update/CommentUpdateTest.php @@ -3,10 +3,12 @@ namespace Drupal\comment\Tests\Update; use Drupal\system\Tests\Update\UpdatePathTestBase; +use Drupal\user\Entity\User; /** * Tests that comment settings are properly updated during database updates. * + * @group Update * @group comment */ class CommentUpdateTest extends UpdatePathTestBase { @@ -71,4 +73,53 @@ public function testPublishedEntityKey() { $this->assertTrue(\Drupal::database()->schema()->indexExists('comment_field_data', 'comment__status_comment_type')); } + /** + * Tests the conversion of comments to be revisionable. + * + * @see comment_update_8401() + * @see comment_post_update_make_comment_revisionable() + */ + public function testConversionToRevisionableAndPublishable() { + $this->runUpdates(); + + $spanish = \Drupal::languageManager()->getLanguage('es'); + + // Log in as user 1. + $account = User::load(1); + $account->pass_raw = 'drupal'; + $this->drupalLogin($account); + + // Make sure a translated page exists. + $this->drupalGet('node/8', ['language' => $spanish]); + // Check for text of two comments. + $this->assertText('Hola'); + $this->assertText('Hello'); + + // Check that comments can be created, saved and then loaded. + $storage = \Drupal::entityTypeManager()->getStorage('comment'); + /** @var \Drupal\comment\Entity\Comment $comment */ + $comment = $storage->create([ + 'entity_id' => 8, + 'entity_type' => 'node', + 'field_name' => 'comment', + 'subject' => 'Test subject', + 'comment_body' => ['Test comment body'], + ]); + $comment->save(); + + $storage->resetCache(); + $comment = $storage->loadRevision($comment->getRevisionId()); + + $this->assertEqual('Test subject', $comment->label()); + $this->assertEqual('Test comment body', $comment->comment_body->value); + $this->assertTrue($comment->isPublished()); + } + + /** + * {@inheritdoc} + */ + protected function replaceUser1() { + // Do not replace the user from our dump. + } + } diff --git a/core/modules/content_translation/content_translation.install b/core/modules/content_translation/content_translation.install index dc12d9f..67b161c 100644 --- a/core/modules/content_translation/content_translation.install +++ b/core/modules/content_translation/content_translation.install @@ -5,6 +5,8 @@ * Installation functions for Content Translation module. */ +use \Drupal\Core\Entity\Sql\SqlEntityStorageInterface; +use \Drupal\Core\Language\LanguageInterface; use \Drupal\Core\Url; /** @@ -44,3 +46,57 @@ function content_translation_update_8001() { function content_translation_update_8002() { \Drupal::service('plugin.manager.field.field_type')->clearCachedDefinitions(); } + +/** + * Fix the initial values for content translation metadata fields. + */ +function content_translation_update_8400() { + $database = \Drupal::database(); + /** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */ + $content_translation_manager = \Drupal::service('content_translation.manager'); + /** @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository */ + $last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository'); + $entity_type_manager = \Drupal::entityTypeManager(); + $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + + foreach ($content_translation_manager->getSupportedEntityTypes() as $entity_type_id => $entity_type_definition) { + $storage = $entity_type_manager->getStorage($entity_type_id); + if ($storage instanceof SqlEntityStorageInterface) { + $entity_type = $entity_definition_update_manager->getEntityType($entity_type_id); + $storage_definitions = $last_installed_schema_repository->getLastInstalledFieldStorageDefinitions($entity_type_id); + + // Since the entity type is managed by Content Translation, we can assume + // that it is translatable, so we use the data and revision data tables. + $tables_to_update = [$entity_type->getDataTable()]; + if ($entity_type->isRevisionable()) { + $tables_to_update += [$entity_type->getRevisionDataTable()]; + } + + foreach ($tables_to_update as $table_name) { + // Fix the values of the 'content_translation_source' field. + if (isset($storage_definitions['content_translation_source'])) { + $database->update($table_name) + ->fields(['content_translation_source' => LanguageInterface::LANGCODE_NOT_SPECIFIED]) + ->isNull('content_translation_source') + ->execute(); + } + + // Fix the values of the 'content_translation_outdated' field. + if (isset($storage_definitions['content_translation_outdated'])) { + $database->update($table_name) + ->fields(['content_translation_outdated' => 0]) + ->isNull('content_translation_outdated') + ->execute(); + } + + // Fix the values of the 'content_translation_status' field. + if (isset($storage_definitions['content_translation_status'])) { + $database->update($table_name) + ->fields(['content_translation_status' => 1]) + ->isNull('content_translation_status') + ->execute(); + } + } + } + } +} diff --git a/core/modules/content_translation/src/ContentTranslationHandler.php b/core/modules/content_translation/src/ContentTranslationHandler.php index c1688b9..3f7baa7 100644 --- a/core/modules/content_translation/src/ContentTranslationHandler.php +++ b/core/modules/content_translation/src/ContentTranslationHandler.php @@ -117,14 +117,16 @@ public function getFieldDefinitions() { ->setDescription(t('The source language from which this translation was created.')) ->setDefaultValue(LanguageInterface::LANGCODE_NOT_SPECIFIED) ->setRevisionable(TRUE) - ->setTranslatable(TRUE); + ->setTranslatable(TRUE) + ->setInitialValue(LanguageInterface::LANGCODE_NOT_SPECIFIED); $definitions['content_translation_outdated'] = BaseFieldDefinition::create('boolean') ->setLabel(t('Translation outdated')) ->setDescription(t('A boolean indicating whether this translation needs to be updated.')) ->setDefaultValue(FALSE) ->setRevisionable(TRUE) - ->setTranslatable(TRUE); + ->setTranslatable(TRUE) + ->setInitialValue(0); if (!$this->hasAuthor()) { $definitions['content_translation_uid'] = BaseFieldDefinition::create('entity_reference') @@ -143,7 +145,8 @@ public function getFieldDefinitions() { ->setDescription(t('A boolean indicating whether the translation is visible to non-translators.')) ->setDefaultValue(TRUE) ->setRevisionable(TRUE) - ->setTranslatable(TRUE); + ->setTranslatable(TRUE) + ->setInitialValue(1); } if (!$this->hasCreatedTime()) { diff --git a/core/modules/content_translation/src/Tests/Update/ContentTranslationUpdateTest.php b/core/modules/content_translation/src/Tests/Update/ContentTranslationUpdateTest.php new file mode 100644 index 0000000..85b952c --- /dev/null +++ b/core/modules/content_translation/src/Tests/Update/ContentTranslationUpdateTest.php @@ -0,0 +1,100 @@ +database = \Drupal::database(); + $this->entityDefinitionUpdateManager = \Drupal::entityDefinitionUpdateManager(); + $this->entityManager = \Drupal::entityManager(); + $this->state = \Drupal::state(); + } + + /** + * {@inheritdoc} + */ + public function setDatabaseDumpFiles() { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.0.0-rc1-filled.standard.entity_test_update_mul.php.gz', + ]; + } + + /** + * Tests that initial values for metadata fields are populated correctly. + */ + public function testContentTranslationUpdate8400() { + $this->updateEntityTypeToTranslatable(); + + // The test database dump contains NULL values for + // 'content_translation_source', 'content_translation_outdated' and + // 'content_translation_status' for the first 50 test entities. + // @see _entity_test_update_create_test_entities() + $first_entity_record = $this->database->select('entity_test_update_data', 'etud') + ->fields('etud') + ->condition('etud.id', 1) + ->execute() + ->fetchAllAssoc('id'); + $this->assertNull($first_entity_record[1]->content_translation_source); + $this->assertNull($first_entity_record[1]->content_translation_outdated); + $this->assertNull($first_entity_record[1]->content_translation_status); + + $this->runUpdates(); + + // After running the updates, all those fields should be populated with + // their default values. + $first_entity_record = $this->database->select('entity_test_update_data', 'etud') + ->fields('etud') + ->condition('etud.id', 1) + ->execute() + ->fetchAllAssoc('id'); + $this->assertEqual(LanguageInterface::LANGCODE_NOT_SPECIFIED, $first_entity_record[1]->content_translation_source); + $this->assertEqual(0, $first_entity_record[1]->content_translation_outdated); + $this->assertEqual(1, $first_entity_record[1]->content_translation_status); + } + +} diff --git a/core/modules/migrate/tests/src/Kernel/MigrateBundleTest.php b/core/modules/migrate/tests/src/Kernel/MigrateBundleTest.php index 7463295..780cce8 100644 --- a/core/modules/migrate/tests/src/Kernel/MigrateBundleTest.php +++ b/core/modules/migrate/tests/src/Kernel/MigrateBundleTest.php @@ -18,13 +18,14 @@ class MigrateBundleTest extends MigrateTestBase { * * @var array */ - public static $modules = ['taxonomy', 'text']; + public static $modules = ['taxonomy', 'text', 'user']; /** * {@inheritdoc} */ protected function setUp() { parent::setUp(); + $this->installEntitySchema('user'); $this->installEntitySchema('taxonomy_vocabulary'); $this->installEntitySchema('taxonomy_term'); $this->installConfig(['taxonomy']); diff --git a/core/modules/migrate/tests/src/Kernel/MigrateRollbackEntityConfigTest.php b/core/modules/migrate/tests/src/Kernel/MigrateRollbackEntityConfigTest.php index 76945a7..a639959 100644 --- a/core/modules/migrate/tests/src/Kernel/MigrateRollbackEntityConfigTest.php +++ b/core/modules/migrate/tests/src/Kernel/MigrateRollbackEntityConfigTest.php @@ -17,13 +17,14 @@ class MigrateRollbackEntityConfigTest extends MigrateTestBase { * * @var array */ - public static $modules = ['field', 'taxonomy', 'text', 'language', 'config_translation']; + public static $modules = ['field', 'taxonomy', 'text', 'language', 'config_translation', 'user']; /** * {@inheritdoc} */ protected function setUp() { parent::setUp(); + $this->installEntitySchema('user'); $this->installEntitySchema('taxonomy_vocabulary'); $this->installEntitySchema('taxonomy_term'); $this->installConfig(['taxonomy']); diff --git a/core/modules/migrate/tests/src/Kernel/MigrateRollbackTest.php b/core/modules/migrate/tests/src/Kernel/MigrateRollbackTest.php index 4b78e65..427f5bc 100644 --- a/core/modules/migrate/tests/src/Kernel/MigrateRollbackTest.php +++ b/core/modules/migrate/tests/src/Kernel/MigrateRollbackTest.php @@ -20,13 +20,14 @@ class MigrateRollbackTest extends MigrateTestBase { * * @var array */ - public static $modules = ['field', 'taxonomy', 'text']; + public static $modules = ['field', 'taxonomy', 'text', 'user']; /** * {@inheritdoc} */ protected function setUp() { parent::setUp(); + $this->installEntitySchema('user'); $this->installEntitySchema('taxonomy_vocabulary'); $this->installEntitySchema('taxonomy_term'); $this->installConfig(['taxonomy']); diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php index 9614412..877c703 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php @@ -122,6 +122,9 @@ protected function getExpectedNormalizedEntity() { 'cid' => [ ['value' => 1], ], + 'revision_id' => [ + ['value' => 1], + ], 'uuid' => [ ['value' => $this->entity->uuid()], ], @@ -196,6 +199,13 @@ protected function getExpectedNormalizedEntity() { 'value' => '01/', ], ], + 'revision_created' => [ + [ + 'value' => (int) $this->entity->getRevisionCreationTime(), + ] + ], + 'revision_user' => [], + 'revision_log_message' => [], 'comment_body' => [ [ 'value' => 'The name "llama" was adopted by European settlers from native Peruvians.', diff --git a/core/modules/system/src/Tests/Update/EntityUpdateToRevisionableAndPublishableTest.php b/core/modules/system/src/Tests/Update/EntityUpdateToRevisionableAndPublishableTest.php new file mode 100644 index 0000000..8f480d1 --- /dev/null +++ b/core/modules/system/src/Tests/Update/EntityUpdateToRevisionableAndPublishableTest.php @@ -0,0 +1,216 @@ +entityManager = \Drupal::entityManager(); + $this->entityDefinitionUpdateManager = \Drupal::entityDefinitionUpdateManager(); + $this->lastInstalledSchemaRepository = \Drupal::service('entity.last_installed_schema.repository'); + $this->installedStorageSchema = \Drupal::keyValue('entity.storage_schema.sql'); + $this->state = \Drupal::state(); + } + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles() { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.0.0-rc1-filled.standard.entity_test_update_mul.php.gz', + __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.entity-test-schema-converter-enabled.php', + ]; + } + + /** + * Tests the conversion of an entity type to revisionable and publishable. + */ + public function testConvertToRevisionableAndPublishable() { + // Check that entity type is not revisionable nor publishable prior to + // running the update process. + $entity_test_update = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update'); + $this->assertFalse($entity_test_update->isRevisionable()); + $this->assertFalse($entity_test_update->getKey('published')); + + // Make the entity type revisionable, translatable and publishable. + $this->updateEntityTypeDefinition(); + + $this->enableUpdates('entity_test_update', 'entity_rev_pub_updates', 8400); + $this->runUpdates(); + + /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_test_update */ + $entity_test_update = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update'); + $this->assertTrue($entity_test_update->isRevisionable()); + $this->assertEqual('status', $entity_test_update->getKey('published')); + + /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */ + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update'); + $this->assertEqual(count($storage->loadMultiple()), 102, 'All test entities were found.'); + + // The conversion to revisionable is already tested by + // \Drupal\system\Tests\Entity\Update\SqlContentEntityStorageSchemaConverterTest::testMakeRevisionable() + // so we only need to check that some special cases are handled. + // All the checks implemented here are taking into consideration the special + // conditions in which the test database was created. + // @see _entity_test_update_create_test_entities() + + // The test entity with ID 50 was created before Content Translation was + // enabled, which means it didn't have a 'content_translation_status' field. + // content_translation_update_8400() added values for that field which + // should now be reflected in the entity's 'status' field. + /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */ + $revision = $storage->loadRevision(50); + $this->assertEqual(1, $revision->status->value); + + $translation = $revision->getTranslation('ro'); + $this->assertEqual(1, $translation->status->value); + + // The test entity with ID 100 was created with Content Translation enabled + // and it should have the same values as entity 50. + $revision = $storage->loadRevision(100); + $this->assertEqual(1, $revision->status->value); + + $translation = $revision->getTranslation('ro'); + $this->assertEqual(1, $translation->status->value); + + // The test entity 101 had 'content_translation_status' set to 0 for the + // English (source) language. + $revision = $storage->loadRevision(101); + $this->assertEqual(0, $revision->status->value); + + $translation = $revision->getTranslation('ro'); + $this->assertEqual(1, $translation->status->value); + + // The test entity 102 had 'content_translation_status' set to 0 for the + // Romanian language. + $revision = $storage->loadRevision(102); + $this->assertEqual(1, $revision->status->value); + + $translation = $revision->getTranslation('ro'); + $this->assertEqual(0, $translation->status->value); + } + + /** + * Updates the 'entity_test_update' entity type to revisionable, + * translatable, publishable and adds revision metadata keys. + */ + protected function updateEntityTypeDefinition() { + $entity_type = clone $this->entityManager->getDefinition('entity_test_update'); + + $keys = $entity_type->getKeys(); + $keys['revision'] = 'revision_id'; + $keys['published'] = 'status'; + $entity_type->set('entity_keys', $keys); + + $revision_metadata_keys = [ + 'revision_user' => 'revision_user', + 'revision_created' => 'revision_created', + 'revision_log_message' => 'revision_log_message' + ]; + $entity_type->set('revision_metadata_keys', $revision_metadata_keys); + + + $entity_type->set('translatable', TRUE); + $entity_type->set('data_table', 'entity_test_update_data'); + $entity_type->set('revision_table', 'entity_test_update_revision'); + $entity_type->set('revision_data_table', 'entity_test_update_revision_data'); + + $this->state->set('entity_test_update.entity_type', $entity_type); + + // Also add the status and revision metadata base fields to the entity type. + $status = BaseFieldDefinition::create('boolean') + ->setLabel(t('Publishing status')) + ->setDescription(t('A boolean indicating the published state.')) + ->setRevisionable(TRUE) + ->setTranslatable(TRUE) + ->setRequired(TRUE) + ->setDefaultValue(TRUE); + + $revision_created = BaseFieldDefinition::create('created') + ->setLabel(t('Revision create time')) + ->setDescription(t('The time that the current revision was created.')) + ->setRevisionable(TRUE); + + $revision_user = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Revision user')) + ->setDescription(t('The user ID of the author of the current revision.')) + ->setSetting('target_type', 'user') + ->setRevisionable(TRUE); + + $revision_log_message = BaseFieldDefinition::create('string_long') + ->setLabel(t('Revision log message')) + ->setDescription(t('Briefly describe the changes you have made.')) + ->setRevisionable(TRUE) + ->setDefaultValue('') + ->setDisplayOptions('form', [ + 'type' => 'string_textarea', + 'weight' => 25, + 'settings' => [ + 'rows' => 4, + ], + ]); + + $this->state->set('entity_test_update.additional_base_field_definitions', [ + 'status' => $status, + 'revision_created' => $revision_created, + 'revision_user' => $revision_user, + 'revision_log_message' => $revision_log_message, + ]); + + $this->entityManager->clearCachedDefinitions(); + } + +} diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestUpdate.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestUpdate.php deleted file mode 100644 index 6d72f7a..0000000 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestUpdate.php +++ /dev/null @@ -1,51 +0,0 @@ -get('entity_test_update.additional_base_field_definitions', []); - return $fields; - } - - /** - * {@inheritdoc} - */ - public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { - $fields = parent::bundleFieldDefinitions($entity_type, $bundle, $base_field_definitions); - $fields += \Drupal::state()->get('entity_test_update.additional_bundle_field_definitions.' . $bundle, []); - return $fields; - } - -} diff --git a/core/modules/system/tests/modules/entity_test_update/entity_test_update.install b/core/modules/system/tests/modules/entity_test_update/entity_test_update.install new file mode 100644 index 0000000..1423308 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_update/entity_test_update.install @@ -0,0 +1,10 @@ + 8400, + ]; + + return $dependencies; +} + +/** + * Add the 'published' and revisionable metadata fields to entity_test_update. + */ +function entity_test_update_update_8400() { + $definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + + // Add the published entity key and revisionable metadata fields to the + // entity_test_update entity type. + $entity_type = $definition_update_manager->getEntityType('entity_test_update'); + + $entity_keys = $entity_type->getKeys(); + $entity_keys['published'] = 'status'; + $entity_type->set('entity_keys', $entity_keys); + + $revision_metadata_keys = [ + 'revision_user' => 'revision_user', + 'revision_created' => 'revision_created', + 'revision_log_message' => 'revision_log_message' + ]; + $entity_type->set('revision_metadata_keys', $revision_metadata_keys); + + $definition_update_manager->updateEntityType($entity_type); + + // Add the status field. + $status = BaseFieldDefinition::create('boolean') + ->setLabel(t('Publishing status')) + ->setDescription(t('A boolean indicating the published state.')) + ->setRevisionable(TRUE) + ->setTranslatable(TRUE) + ->setRequired(TRUE) + ->setDefaultValue(TRUE); + + $has_content_translation_status_field = \Drupal::moduleHandler()->moduleExists('content_translation') && $definition_update_manager->getFieldStorageDefinition('content_translation_status', 'entity_test_update'); + if ($has_content_translation_status_field) { + $status->setInitialValueFromField('content_translation_status'); + } + else { + $status->setInitialValue(1); + } + $definition_update_manager->installFieldStorageDefinition('status', 'entity_test_update', 'entity_test_update', $status); + + // Add the revision metadata fields. + $revision_created = BaseFieldDefinition::create('created') + ->setLabel(t('Revision create time')) + ->setDescription(t('The time that the current revision was created.')) + ->setRevisionable(TRUE); + $definition_update_manager->installFieldStorageDefinition('revision_created', 'entity_test_update', 'entity_test_update', $revision_created); + + $revision_user = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Revision user')) + ->setDescription(t('The user ID of the author of the current revision.')) + ->setSetting('target_type', 'user') + ->setRevisionable(TRUE); + $definition_update_manager->installFieldStorageDefinition('revision_user', 'entity_test_update', 'entity_test_update', $revision_user); + + $revision_log_message = BaseFieldDefinition::create('string_long') + ->setLabel(t('Revision log message')) + ->setDescription(t('Briefly describe the changes you have made.')) + ->setRevisionable(TRUE) + ->setDefaultValue('') + ->setDisplayOptions('form', [ + 'type' => 'string_textarea', + 'weight' => 25, + 'settings' => [ + 'rows' => 4, + ], + ]); + $definition_update_manager->installFieldStorageDefinition('revision_log_message', 'entity_test_update', 'entity_test_update', $revision_log_message); + + // Uninstall the 'content_translation_status' field if needed. + $database = \Drupal::database(); + if ($has_content_translation_status_field) { + // First we have to remove the field data. + $database->update($entity_type->getDataTable()) + ->fields(['content_translation_status' => NULL]) + ->execute(); + + // A site may have disabled revisionability for this entity type. + if ($entity_type->isRevisionable()) { + $database->update($entity_type->getRevisionDataTable()) + ->fields(['content_translation_status' => NULL]) + ->execute(); + } + + $content_translation_status = $definition_update_manager->getFieldStorageDefinition('content_translation_status', 'entity_test_update'); + $definition_update_manager->uninstallFieldStorageDefinition($content_translation_status); + } +} diff --git a/core/modules/taxonomy/src/Tests/Views/TaxonomyTermViewTest.php b/core/modules/taxonomy/src/Tests/Views/TaxonomyTermViewTest.php index bfd65cf..08ff643 100644 --- a/core/modules/taxonomy/src/Tests/Views/TaxonomyTermViewTest.php +++ b/core/modules/taxonomy/src/Tests/Views/TaxonomyTermViewTest.php @@ -5,6 +5,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\node\Entity\Node; use Drupal\user\Entity\Role; use Drupal\user\RoleInterface; use Drupal\views\Views; @@ -124,6 +125,11 @@ public function testTaxonomyTermView() { // query anymore. // @see \Drupal\views\Plugin\views\filter\LanguageFilter::query() $node->delete(); + // We also have to remove the nodes created by the parent ::setUp() method + // if we want to be able to uninstall the Content Translation module. + foreach (Node::loadMultiple() as $node) { + $node->delete(); + } \Drupal::service('module_installer')->uninstall(['content_translation', 'language']); $view = Views::getView('taxonomy_term'); diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php index 646ad2e..a066616 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php @@ -11,6 +11,7 @@ use Drupal\Core\Entity\EntityTypeEvents; use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Field\FieldException; use Drupal\Core\Field\FieldStorageDefinitionEvents; use Drupal\Core\Language\LanguageInterface; use Drupal\entity_test_update\Entity\EntityTestUpdate; @@ -776,6 +777,11 @@ public function testBaseFieldEntityKeyUpdateWithExistingData() { // of a NOT NULL constraint. $this->makeBaseFieldEntityKey(); + // Field storage CRUD operations use the last installed entity type + // definition so we need to update it before doing any other field storage + // updates. + $this->entityDefinitionUpdateManager->updateEntityType($this->state->get('entity_test_update.entity_type')); + // Try to apply the update and verify they fail since we have a NULL value. $message = 'An error occurs when trying to enabling NOT NULL constraints with NULL data.'; try { @@ -817,4 +823,119 @@ public function testLongNameFieldIndexes() { $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'Entity and field schema data are correctly detected.'); } + /** + * Tests adding a base field with initial values. + */ + public function testInitialValue() { + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update'); + $db_schema = $this->database->schema(); + + // Create two entities before adding the base field. + /** @var \Drupal\entity_test\Entity\EntityTestUpdate $entity */ + $storage->create()->save(); + $storage->create()->save(); + + // Add a base field with an initial value. + $this->addBaseField(); + $storage_definition = BaseFieldDefinition::create('string') + ->setLabel(t('A new base field')) + ->setInitialValue('test value'); + + $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update."); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); + $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table."); + + // Check that the initial values have been applied. + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update'); + $entities = $storage->loadMultiple(); + $this->assertEquals('test value', $entities[1]->get('new_base_field')->value); + $this->assertEquals('test value', $entities[2]->get('new_base_field')->value); + } + + /** + * Tests adding a base field with initial values inherited from another field. + */ + public function testInitialValueFromField() { + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update'); + $db_schema = $this->database->schema(); + + // Create two entities before adding the base field. + /** @var \Drupal\entity_test\Entity\EntityTestUpdate $entity */ + $storage->create(['name' => 'First entity'])->save(); + $storage->create(['name' => 'Second entity'])->save(); + + // Add a base field with an initial value inherited from another field. + $this->addBaseField(); + $storage_definition = BaseFieldDefinition::create('string') + ->setLabel(t('A new base field')) + ->setInitialValueFromField('name'); + + $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update."); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); + $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table."); + + // Check that the initial values have been applied. + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update'); + $entities = $storage->loadMultiple(); + $this->assertEquals('First entity', $entities[1]->get('new_base_field')->value); + $this->assertEquals('Second entity', $entities[2]->get('new_base_field')->value); + } + + /** + * Tests the error handling when using initial values from another field. + */ + public function testInitialValueFromFieldErrorHandling() { + // Check that setting invalid values for 'initial value from field' doesn't + // work. + try { + $this->addBaseField(); + $storage_definition = BaseFieldDefinition::create('string') + ->setLabel(t('A new base field')) + ->setInitialValueFromField('field_that_does_not_exist'); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); + $this->fail('Using a non-existent field as initial value does not work.'); + } + catch (FieldException $e) { + $this->assertEquals('Illegal initial value definition on new_base_field: The field field_that_does_not_exist does not exist.', $e->getMessage()); + $this->pass('Using a non-existent field as initial value does not work.'); + } + + try { + $this->addBaseField(); + $storage_definition = BaseFieldDefinition::create('integer') + ->setLabel(t('A new base field')) + ->setInitialValueFromField('name'); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); + $this->fail('Using a field of a different type as initial value does not work.'); + } + catch (FieldException $e) { + $this->assertEquals('Illegal initial value definition on new_base_field: The field types do not match.', $e->getMessage()); + $this->pass('Using a field of a different type as initial value does not work.'); + } + + try { + // Add a base field that will not be stored in the shared tables. + $initial_field = BaseFieldDefinition::create('string') + ->setName('initial_field') + ->setLabel(t('An initial field')) + ->setCardinality(2); + $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field]); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('initial_field', 'entity_test_update', 'entity_test', $initial_field); + + // Now add the base field which will try to use the previously added field + // as the source of its initial values. + $new_base_field = BaseFieldDefinition::create('string') + ->setName('new_base_field') + ->setLabel(t('A new base field')) + ->setInitialValueFromField('initial_field'); + $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field, 'new_base_field' => $new_base_field]); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $new_base_field); + $this->fail('Using a field that is not stored in the shared tables as initial value does not work.'); + } + catch (FieldException $e) { + $this->assertEquals('Illegal initial value definition on new_base_field: Both fields have to be stored in the shared entity tables.', $e->getMessage()); + $this->pass('Using a field that is not stored in the shared tables as initial value does not work.'); + } + } + } diff --git a/core/tests/Drupal/Tests/Core/Entity/BaseFieldDefinitionTest.php b/core/tests/Drupal/Tests/Core/Entity/BaseFieldDefinitionTest.php index 341ed9c..616f6ba 100644 --- a/core/tests/Drupal/Tests/Core/Entity/BaseFieldDefinitionTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/BaseFieldDefinitionTest.php @@ -195,6 +195,52 @@ public function testFieldDefaultValue() { } /** + * Tests field initial value. + * + * @covers ::getInitialValue + * @covers ::setInitialValue + */ + public function testFieldInitialValue() { + $definition = BaseFieldDefinition::create($this->fieldType); + $default_value = [ + 'value' => $this->randomMachineName(), + ]; + $expected_default_value = [$default_value]; + $definition->setInitialValue($default_value); + $entity = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase') + ->disableOriginalConstructor() + ->getMock(); + // Set the field item list class to be used to avoid requiring the typed + // data manager to retrieve it. + $definition->setClass('Drupal\Core\Field\FieldItemList'); + $this->assertEquals($expected_default_value, $definition->getInitialValue($entity)); + + $data_definition = $this->getMockBuilder('Drupal\Core\TypedData\DataDefinition') + ->disableOriginalConstructor() + ->getMock(); + $data_definition->expects($this->any()) + ->method('getClass') + ->will($this->returnValue('Drupal\Core\Field\FieldItemBase')); + $definition->setItemDefinition($data_definition); + + // Set default value only with a literal. + $definition->setInitialValue($default_value['value']); + $this->assertEquals($expected_default_value, $definition->getInitialValue($entity)); + + // Set default value with an indexed array. + $definition->setInitialValue($expected_default_value); + $this->assertEquals($expected_default_value, $definition->getInitialValue($entity)); + + // Set default value with an empty array. + $definition->setInitialValue([]); + $this->assertEquals([], $definition->getInitialValue($entity)); + + // Set default value with NULL. + $definition->setInitialValue(NULL); + $this->assertEquals([], $definition->getInitialValue($entity)); + } + + /** * Tests field translatable methods. * * @covers ::isTranslatable