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..5013086 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -156,6 +156,17 @@ protected $loadedRevisionId; /** + * Whether to serialize the complete entity structure. + * + * Depending on the field setting "serialize_embedded_entities" of entity + * reference fields a deeply serialized entity might contain the referenced + * entities. + * + * @var bool + */ + protected $deepSerialization = FALSE; + + /** * {@inheritdoc} */ public function __construct(array $values, $entity_type, $bundle = FALSE, $translations = array()) { @@ -440,7 +451,7 @@ public function __sleep() { // Get the values of instantiated field objects, only serialize the values. foreach ($this->fields as $name => $fields) { foreach ($fields as $langcode => $field) { - $this->values[$name][$langcode] = $field->getValue(); + $this->values[$name][$langcode] = $field->getSerializationValue($this->deepSerialization); } } $this->fields = array(); @@ -448,12 +459,38 @@ public function __sleep() { $this->languages = NULL; $this->clearTranslationCache(); + // As during the serialization process referenced entities might also have + // the "deepSerialization" flag set the only place of unsetting it remains + // in the sleep method right after the entity is prepared for the + // serialization and before executing it. + // @see \Drupal\Core\Field\EntityReferenceFieldItemList::getSerializationValue(). + $this->deepSerialization = FALSE; + return parent::__sleep(); } /** * {@inheritdoc} */ + public function deepSerialize() { + // When running a deep serialization the flag "deepSerialization" 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->deepSerialization = TRUE; + return serialize($this); + } + + /** + * {@inheritdoc} + */ + public function setDeepSerialization() { + $this->deepSerialization = TRUE; + } + + /** + * {@inheritdoc} + */ public function id() { return $this->getEntityKey('id'); } @@ -823,6 +860,7 @@ protected function initializeTranslation($langcode) { $translation->enforceIsNew = &$this->enforceIsNew; $translation->newRevision = &$this->newRevision; $translation->entityKeys = &$this->entityKeys; + $translation->deepSerialization = &$this->deepSerialization; $translation->translatableEntityKeys = &$this->translatableEntityKeys; $translation->translationInitialize = FALSE; $translation->typedData = NULL; @@ -1100,6 +1138,11 @@ 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 deepSerialization property is actually cloned by + // overwriting the original reference with one pointing to a copy of it. + $deepSerialization = $this->deepSerialization; + $this->deepSerialization = &$deepSerialization; } } diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php b/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php index d21a2a8..3f98226 100644 --- a/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php +++ b/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php @@ -236,4 +236,23 @@ public function isValidationRequired(); */ public function setValidationRequired($required); + /** + * Returns a deeply serialized entity. + * + * @return string + * The deeply serialized entity. + */ + public function deepSerialize(); + + /** + * Flags the entity for deep serialization. + * + * After calling this method and serializing the entity e.g. + * "serialize($entity);" a deep serialization will be performed and the flag + * will be automatically reset. + * + * @return $this + */ + public function setDeepSerialization(); + } diff --git a/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php b/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php index d21ac96..d9860f1 100644 --- a/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php +++ b/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php @@ -13,6 +13,26 @@ class EntityReferenceFieldItemList extends FieldItemList implements EntityRefere /** * {@inheritdoc} */ + public function getSerializationValue($deep_serialization) { + if ($deep_serialization) { + $serialize_embedded_entities = $this->getFieldDefinition()->getSetting('serialize_embedded_entities'); + $values = $this->getValue($serialize_embedded_entities); + foreach ($values as $delta => $item_values) { + if (isset($values[$delta]['entity'])) { + $values[$delta]['entity']->setDeepSerialization(); + } + } + } + else { + $values = parent::getSerializationValue($deep_serialization); + } + + return $values; + } + + /** + * {@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/FieldItemList.php b/core/lib/Drupal/Core/Field/FieldItemList.php index 97dd493..ea530d1 100644 --- a/core/lib/Drupal/Core/Field/FieldItemList.php +++ b/core/lib/Drupal/Core/Field/FieldItemList.php @@ -121,6 +121,13 @@ public function setValue($values, $notify = TRUE) { /** * {@inheritdoc} */ + public function getSerializationValue($deep_serialization) { + return $this->getValue(); + } + + /** + * {@inheritdoc} + */ public function __get($property_name) { // For empty fields, $entity->field->property is NULL. if ($item = $this->first()) { diff --git a/core/lib/Drupal/Core/Field/FieldItemListInterface.php b/core/lib/Drupal/Core/Field/FieldItemListInterface.php index 4e77202..dfeeede 100644 --- a/core/lib/Drupal/Core/Field/FieldItemListInterface.php +++ b/core/lib/Drupal/Core/Field/FieldItemListInterface.php @@ -271,4 +271,14 @@ public static function processDefaultValue($default_value, FieldableEntityInterf */ public function equals(FieldItemListInterface $list_to_compare); + /** + * Gets the data value prepared for serialization. + * + * @param bool $deep_serialization + * Whether a deep serialization is running. + * + * @return mixed + */ + public function getSerializationValue($deep_serialization); + } 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..8832c2c 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; diff --git a/core/modules/field/field.install b/core/modules/field/field.install index ecd2b31..e692d9d 100644 --- a/core/modules/field/field.install +++ b/core/modules/field/field.install @@ -104,3 +104,40 @@ 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_factory = \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_factory->listAll('field.field.') as $field_id) { + $field_config = $config_factory->getEditable($field_id); + $field_type = $field_config->get('field_type'); + $class = $field_type_manager->getPluginClass($field_type); + + // Deal only with entity reference fields and descendants. + if (($class == EntityReferenceItem::class || is_subclass_of($class, EntityReferenceItem::class)) && (strpos($field_type, 'entity_reference') === 0)) { + $settings = $field_config->get('settings'); + + if (!isset($settings['serialize_embedded_entities'])) { + $settings['serialize_embedded_entities'] = FALSE; + } + + $field_config->set('settings', $settings)->save(TRUE); + } + } +} + +/** + * @} End of "addtogroup updates-8.3.x". + */ 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_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/tests/Drupal/KernelTests/Core/Entity/ContentEntitySerializationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntitySerializationTest.php new file mode 100644 index 0000000..c0c92c5 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntitySerializationTest.php @@ -0,0 +1,126 @@ +installEntitySchema($this->entityTypeId); + $this->entityStorage = $this->entityManager->getStorage($this->entityTypeId); + + // Create the test entity reference field. + FieldStorageConfig::create([ + 'field_name' => $this->entityReferenceFieldName, + '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(); + } + + /** + * Tests regular and deep serialization. + */ + public function testSerialization() { + $this->doTestSerialization(FALSE); + $this->doTestSerialization(TRUE); + } + + /** + * Tests regular or deep serialization based on the parameter. + * + * @param bool $deep_serialization + * Whether to test regular or deep serialization. + */ + protected function doTestSerialization($deep_serialization) { + $field_name = $this->entityReferenceFieldName; + // 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->entityStorage->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(); + + // Change the entity values in memory but do not persist them into the + // storage. + $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'; + + $serialized_entity = $deep_serialization ? $entity_level_zero->deepSerialize() : serialize($entity_level_zero); + $entity_level_zero = unserialize($serialized_entity); + + // If regular serialization is tested then the changes on the referenced + // entities should be lost, but kept if deep serialization is tested. + $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()); + } + +}