diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 0fbeeed..aee1d31 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -587,6 +587,9 @@ field.field_settings.entity_reference: type: mapping label: 'Entity reference field settings' mapping: + serialize_embedded_entities: + type: boolean + label: 'Serialize entity references together with the parent entity' handler: type: string label: 'Reference method' diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index e242544..684840e 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -156,6 +156,13 @@ protected $loadedRevisionId; /** + * Whether to serialize the complete entity structure. + * + * @var bool + */ + public $serializeCompleteEntityStructure = FALSE; + + /** * {@inheritdoc} */ public function __construct(array $values, $entity_type, $bundle = FALSE, $translations = array()) { @@ -454,6 +461,34 @@ public function __sleep() { /** * {@inheritdoc} */ + public function __wakeup() { + parent::__wakeup(); + // If the entity has been serialized with the flag + // "serializeCompleteEntityStructure" set to TRUE then the entity has been + // deeply serialized and in order to allow normal serialization on the + // unserialized entity we have to unset the flag. + $this->serializeCompleteEntityStructure = FALSE; + } + + /** + * {@inheritdoc} + */ + public function serializeWithCompleteStructure() { + // When running a deep serialization the flag + // "serializeCompleteEntityStructure" has to be set in order to serialize + // referenced entities as well, which are referenced by field having the + // setting "serialize_embedded_entities" set to TRUE. + $this->serializeCompleteEntityStructure = TRUE; + $serialized = serialize($this); + // After the deep serialization is ready we have to unset the flag in order + // to allow for normal serialization afterwards. + $this->serializeCompleteEntityStructure = FALSE; + return $serialized; + } + + /** + * {@inheritdoc} + */ public function id() { return $this->getEntityKey('id'); } @@ -823,6 +858,7 @@ protected function initializeTranslation($langcode) { $translation->enforceIsNew = &$this->enforceIsNew; $translation->newRevision = &$this->newRevision; $translation->entityKeys = &$this->entityKeys; + $translation->serializeCompleteEntityStructure = &$this->serializeCompleteEntityStructure; $translation->translatableEntityKeys = &$this->translatableEntityKeys; $translation->translationInitialize = FALSE; $translation->typedData = NULL; @@ -1100,6 +1136,12 @@ public function __clone() { // overwriting the original reference with one pointing to a copy of it. $original_revision_id = $this->loadedRevisionId; $this->loadedRevisionId = &$original_revision_id; + + // Ensure the serializeCompleteEntityStructure property is actually + // cloned by overwriting the original reference with one pointing to a + // copy of it. + $serializeCompleteEntityStructure = $this->serializeCompleteEntityStructure; + $this->serializeCompleteEntityStructure = &$serializeCompleteEntityStructure; } } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php index 8893a8b..fbf8940 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php @@ -74,4 +74,12 @@ public function getLoadedRevisionId(); */ public function updateLoadedRevisionId(); + /** + * Returns a deeply serialized entity. + * + * @return string + * The deeply serialized entity. + */ + public function serializeWithCompleteStructure(); + } diff --git a/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php b/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php index d21ac96..7b4435c 100644 --- a/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php +++ b/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php @@ -13,6 +13,36 @@ class EntityReferenceFieldItemList extends FieldItemList implements EntityRefere /** * {@inheritdoc} */ + public function getValue($include_computed = FALSE) { + $values = array(); + $serialize_embedded_entities = $this->isDeepSerializationActive(); + if (!$include_computed) { + $include_computed = $serialize_embedded_entities; + } + foreach ($this->list as $delta => $item) { + $values[$delta] = $item->getValue($include_computed); + // If deep serialization is active then flag the referenced entities for + // deep serialization as well. + if (isset($values[$delta]['entity']) && $serialize_embedded_entities) { + $values[$delta]['entity']->serializeCompleteEntityStructure = TRUE; + } + } + return $values; + } + + /** + * Checks if deep serialization is active. + * + * @return bool + * TRUE if deep serialization is active, FALSE otherwise. + */ + protected function isDeepSerializationActive() { + return $this->getFieldDefinition()->getSetting('serialize_embedded_entities') && $this->getEntity()->serializeCompleteEntityStructure; + } + + /** + * {@inheritdoc} + */ public function getConstraints() { $constraints = parent::getConstraints(); $constraint_manager = $this->getTypedDataManager()->getValidationConstraintManager(); diff --git a/core/lib/Drupal/Core/Field/EntityReferenceInlineWidgetBase.php b/core/lib/Drupal/Core/Field/EntityReferenceInlineWidgetBase.php new file mode 100644 index 0000000..251129c --- /dev/null +++ b/core/lib/Drupal/Core/Field/EntityReferenceInlineWidgetBase.php @@ -0,0 +1,19 @@ +getSetting('serialize_embedded_entities'); + } + +} diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php index 3babf42..2d817ae 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php @@ -54,6 +54,7 @@ public static function defaultStorageSettings() { */ public static function defaultFieldSettings() { return array( + 'serialize_embedded_entities' => FALSE, 'handler' => 'default', 'handler_settings' => array(), ) + parent::defaultFieldSettings(); @@ -190,7 +191,7 @@ public function setValue($values, $notify = TRUE) { // If the entity has been saved and we're trying to set both the // target_id and the entity values with a non-null target ID, then the // value for target_id should match the ID of the entity value. - if (!$this->entity->isNew() && $values['target_id'] !== NULL && ($entity_id !== $values['target_id'])) { + if (!$this->entity->isNew() && $values['target_id'] !== NULL && ($entity_id != $values['target_id'])) { throw new \InvalidArgumentException('The target id and entity passed to the entity reference item do not match.'); } } @@ -205,12 +206,12 @@ public function setValue($values, $notify = TRUE) { /** * {@inheritdoc} */ - public function getValue() { + public function getValue($include_computed = FALSE) { $values = parent::getValue(); // If there is an unsaved entity, return it as part of the field item values // to ensure idempotency of getValue() / setValue(). - if ($this->hasNewEntity()) { + if ($include_computed || $this->hasNewEntity()) { $values['entity'] = $this->entity; } return $values; @@ -340,6 +341,14 @@ public function fieldSettingsForm(array $form, FormStateInterface $form_state) { '#element_validate' => array(array(get_class($this), 'fieldSettingsFormValidate')), ); + + $form['serialize_embedded_entities'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Serialize entity references together with the parent entity'), + '#description' => $this->t('If the field widget to be used for this field is an entity reference inline widget then the setting must be set.'), + '#default_value' => $field->getSetting('serialize_embedded_entities'), + ); + $form['handler'] = array( '#type' => 'details', '#title' => t('Reference type'), diff --git a/core/modules/field/field.install b/core/modules/field/field.install index ecd2b31..6d7c48b 100644 --- a/core/modules/field/field.install +++ b/core/modules/field/field.install @@ -6,6 +6,7 @@ */ use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; +use Drupal\file\Plugin\Field\FieldType\FileItem; /** * Removes the stale 'target_bundle' storage setting on entity_reference fields. @@ -104,3 +105,39 @@ function field_update_8003() { } } } + +/** + * @addtogroup updates-8.3.x + * @{ + */ + +/** + * Populate the new 'serialize_embedded_entities' setting for entity reference + * fields. + */ +function field_update_8300() { + $config = \Drupal::configFactory(); + /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */ + $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); + + // Iterate over all fields. + foreach ($config->listAll('field.field.') as $field_id) { + $field = $config->getEditable($field_id); + $class = $field_type_manager->getPluginClass($field->get('field_type')); + + // Deal only with entity reference fields and descendants. + if ($class == EntityReferenceItem::class || is_subclass_of($class, EntityReferenceItem::class)) { + $settings = $field->get('settings'); + + if (!isset($settings['serialize_embedded_entities'])) { + $settings['serialize_embedded_entities'] = is_subclass_of($class, FileItem::class) ? TRUE : FALSE; + } + + $field->set('settings', $settings)->save(TRUE); + } + } +} + +/** + * @} End of "addtogroup updates-8.3.x". + */ diff --git a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldInstanceTest.php b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldInstanceTest.php index b3fac3f..951b27b 100644 --- a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldInstanceTest.php +++ b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldInstanceTest.php @@ -81,6 +81,7 @@ public function testFieldInstanceMigration() { 'uri_scheme' => 'public', 'handler' => 'default:file', 'handler_settings' => array(), + 'serialize_embedded_entities' => true, ); $field_settings = $field->getSettings(); ksort($expected); diff --git a/core/modules/file/config/schema/file.schema.yml b/core/modules/file/config/schema/file.schema.yml index b9f8918..0b49c88 100644 --- a/core/modules/file/config/schema/file.schema.yml +++ b/core/modules/file/config/schema/file.schema.yml @@ -43,6 +43,9 @@ field.value.file: base_file_field_field_settings: type: mapping mapping: + serialize_embedded_entities: + type: boolean + label: 'Serialize entity references together with the parent entity' handler: type: string label: 'Reference method' diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php index 0a9a3b3..1820ea5 100644 --- a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php +++ b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php @@ -49,6 +49,7 @@ public static function defaultFieldSettings() { 'file_directory' => '[date:custom:Y]-[date:custom:m]', 'max_filesize' => '', 'description_field' => 0, + 'serialize_embedded_entities' => TRUE, ) + parent::defaultFieldSettings(); } diff --git a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php index 8ada107..510c4bd 100644 --- a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php +++ b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php @@ -3,10 +3,10 @@ namespace Drupal\file\Plugin\Field\FieldWidget; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Field\EntityReferenceInlineWidgetBase; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; -use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Render\Element; @@ -27,7 +27,7 @@ * } * ) */ -class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface { +class FileWidget extends EntityReferenceInlineWidgetBase implements ContainerFactoryPluginInterface { /** * {@inheritdoc} diff --git a/core/modules/forum/config/optional/field.field.node.forum.taxonomy_forums.yml b/core/modules/forum/config/optional/field.field.node.forum.taxonomy_forums.yml index f2a30f2..150d28f 100644 --- a/core/modules/forum/config/optional/field.field.node.forum.taxonomy_forums.yml +++ b/core/modules/forum/config/optional/field.field.node.forum.taxonomy_forums.yml @@ -16,6 +16,7 @@ translatable: true default_value: { } default_value_callback: '' settings: + serialize_embedded_entities: false handler: 'default:taxonomy_term' handler_settings: target_bundles: diff --git a/core/profiles/standard/config/install/field.field.node.article.field_image.yml b/core/profiles/standard/config/install/field.field.node.article.field_image.yml index b4b1c14..b9e8bfc 100644 --- a/core/profiles/standard/config/install/field.field.node.article.field_image.yml +++ b/core/profiles/standard/config/install/field.field.node.article.field_image.yml @@ -32,6 +32,7 @@ settings: title: '' width: null height: null + serialize_embedded_entities: true handler: 'default:file' handler_settings: { } field_type: image diff --git a/core/profiles/standard/config/install/field.field.node.article.field_tags.yml b/core/profiles/standard/config/install/field.field.node.article.field_tags.yml index 1b9c4cc..fee79a8 100644 --- a/core/profiles/standard/config/install/field.field.node.article.field_tags.yml +++ b/core/profiles/standard/config/install/field.field.node.article.field_tags.yml @@ -16,6 +16,7 @@ translatable: true default_value: { } default_value_callback: '' settings: + serialize_embedded_entities: false handler: 'default:taxonomy_term' handler_settings: target_bundles: diff --git a/core/profiles/standard/config/install/field.field.user.user.user_picture.yml b/core/profiles/standard/config/install/field.field.user.user.user_picture.yml index b2e61f6..4779b99 100644 --- a/core/profiles/standard/config/install/field.field.user.user.user_picture.yml +++ b/core/profiles/standard/config/install/field.field.user.user.user_picture.yml @@ -32,6 +32,7 @@ settings: height: null alt_field_required: false title_field_required: false + serialize_embedded_entities: true handler: 'default:file' handler_settings: { } field_type: image diff --git a/core/tests/Drupal/KernelTests/Core/Entity/ContentEntitySerializationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntitySerializationTest.php new file mode 100644 index 0000000..00c325e --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntitySerializationTest.php @@ -0,0 +1,119 @@ +entityTypeId = 'entity_test_mul'; + $this->installEntitySchema($this->entityTypeId); + $this->entityTestMulStorage = $this->entityManager->getStorage($this->entityTypeId); + } + + /** + * Tests regular and deep serialization. + */ + public function testSerialization() { + $field_name = 'field_test_entity_reference'; + // Create the test entity reference field. + FieldStorageConfig::create([ + 'field_name' => $field_name, + 'entity_type' => $this->entityTypeId, + 'type' => 'entity_reference', + 'settings' => [ + 'target_type' => $this->entityTypeId, + ], + ])->save(); + FieldConfig::create([ + 'entity_type' => $this->entityTypeId, + 'field_name' => 'field_test_entity_reference', + 'bundle' => $this->entityTypeId, + 'translatable' => TRUE, + ])->save(); + + $this->doTestSerialization(FALSE, $field_name); + $this->doTestSerialization(TRUE, $field_name); + } + + /** + * Tests regular or deep serialization based on the parameter. + * + * @param bool $deep_serialization + * Whether to test regular or deep serialization. + * @param bool $field_name + * The name of the entity reference field. + */ + protected function doTestSerialization($deep_serialization, $field_name) { + // Check that the 'target_bundle' setting contains the custom bundle. + $field_config = FieldConfig::loadByName($this->entityTypeId, $this->entityTypeId, $field_name); + $field_config->setSetting('serialize_embedded_entities', $deep_serialization) + ->save(); + + $user = $this->createUser(); + $initial_entity_name = 'test test'; + + // Create the test entities. + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity_level_zero */ + $entity_level_zero = $this->entityTestMulStorage->create([ + 'name' => $initial_entity_name, + 'user_id' => $user->id(), + 'language' => 'en', + ]); + $entity_level_zero->save(); + $entity_level_one = $entity_level_zero->createDuplicate(); + $entity_level_one->save(); + $entity_level_two = $entity_level_zero->createDuplicate(); + $entity_level_two->save(); + + $entity_level_zero->$field_name->appendItem($entity_level_one); + $entity_level_zero->save(); + $entity_level_one->$field_name->appendItem($entity_level_two); + $entity_level_one->save(); + + $entity_level_zero->name = 'entity level zero'; + $entity_level_zero->$field_name->entity->name = 'entity level one'; + $entity_level_zero->$field_name->entity->$field_name->entity->name = 'entity level two'; + + $this->entityTestMulStorage->resetCache(); + + $serialized_entity = $entity_level_zero->serializeWithCompleteStructure(); + $entity_level_zero = unserialize($serialized_entity); + $this->assertEquals('entity level zero', $entity_level_zero->label()); + $this->assertEquals($deep_serialization ? 'entity level one' : $initial_entity_name, $entity_level_zero->$field_name->entity->label()); + $this->assertEquals($deep_serialization ? 'entity level two' : $initial_entity_name, $entity_level_zero->$field_name->entity->$field_name->entity->label()); + } + +}