diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 44fe8a3..b215787 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -157,6 +157,18 @@ protected $loadedRevisionId; /** + * Whether to serialize the complete entity structure. + * + * If set to TRUE when the entity is being serialized a deep serialization + * will be performed and the flag will be automatically set back to FALSE. + * + * @var bool + * + * @see \Drupal\Core\Entity\FieldableEntityInterface::setDeepSerialization() + */ + protected $deepSerialization = FALSE; + + /** * {@inheritdoc} */ public function __construct(array $values, $entity_type, $bundle = FALSE, $translations = []) { @@ -443,7 +455,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 = []; @@ -451,12 +463,26 @@ 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 setDeepSerialization($deep_serialization) { + $this->deepSerialization = $deep_serialization; + } + + /** + * {@inheritdoc} + */ public function id() { return $this->getEntityKey('id'); } @@ -826,6 +852,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; @@ -1094,8 +1121,8 @@ public function __clone() { // Ensure that the following properties are actually cloned by // overwriting the original references with ones pointing to copies of - // them: enforceIsNew, newRevision, loadedRevisionId, fields, entityKeys and - // translatableEntityKeys. + // them: enforceIsNew, newRevision, loadedRevisionId, fields, entityKeys, + // translatableEntityKeys and deepSerialization. $enforce_is_new = $this->enforceIsNew; $this->enforceIsNew = &$enforce_is_new; @@ -1114,6 +1141,9 @@ public function __clone() { $translatable_entity_keys = $this->translatableEntityKeys; $this->translatableEntityKeys = &$translatable_entity_keys; + $deepSerialization = $this->deepSerialization; + $this->deepSerialization = &$deepSerialization; + foreach ($this->fields as $name => $values) { $this->fields[$name] = []; // Untranslatable fields may have multiple references for the same field diff --git a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php index d045152..ad38b9d3 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php @@ -71,4 +71,34 @@ public function getLoadedRevisionId(); */ public function updateLoadedRevisionId(); + /** + * Flags the entity for deep serialization. + * + * When an entity is deeply serialized then its field items will have the + * opportunity to return also e.g. computed properties in order to serialize + * the whole entity structure and cover changes made on computed properties, + * which are not yet saved instead of only returning the metadata needed to + * re-compute/load the computed property. An example for this are entity + * reference fields, which during deep serialization will return their + * referenced entities. In this case not only the changes on the main entity + * will be serialized, but also the changes made on the referenced entities. + * + * Example usage: + * -Alter the title of a referenced entity and serialize the parent entity + * through deep serialization in order to serialize the change made on the + * referenced entity. + * @code + * $entity->entity_reference_field->entity->setTitle('new entity reference title'); + * $entity->setDeepSerialization(TRUE); + * serialize($entity); + * @endcode + * + * @param bool $deep_serialization + * TRUE if deep serialization should be performed when the entity is being + * serialized, FALSE otherwise. Defaults to TRUE. + * + * @return $this + */ + public function setDeepSerialization($deep_serialization); + } diff --git a/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php b/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php index 0a53d98..f4fc99b 100644 --- a/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php +++ b/core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Field; +use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Form\FormStateInterface; @@ -13,6 +14,25 @@ class EntityReferenceFieldItemList extends FieldItemList implements EntityRefere /** * {@inheritdoc} */ + public function getSerializationValue($deep_serialization) { + if ($deep_serialization) { + $values = $this->getValue(TRUE); + foreach ($values as $delta => $item_values) { + if (isset($values[$delta]['entity']) && ($values[$delta]['entity'] instanceof ContentEntityInterface)) { + $values[$delta]['entity']->setDeepSerialization(TRUE); + } + } + } + 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/FieldItemBase.php b/core/lib/Drupal/Core/Field/FieldItemBase.php index 84d73ec..139463e 100644 --- a/core/lib/Drupal/Core/Field/FieldItemBase.php +++ b/core/lib/Drupal/Core/Field/FieldItemBase.php @@ -101,6 +101,43 @@ protected function getSetting($setting_name) { /** * {@inheritdoc} */ + public function getValue($include_computed = FALSE) { + $value = parent::getValue(); + + // Include computed properties if the condition of + // ::includeComputedProperties() is fulfilled. + if ($this->includeComputedProperties($include_computed)) { + // Iterate only over computed properties and include them, as the parent + // method is not including them. + foreach ($this->properties as $name => $property) { + $definition = $property->getDataDefinition(); + if ($definition->isComputed()) { + $value[$name] = $property->getValue(); + } + } + } + return $value; + } + + /** + * Checks if included properties have to be included. + * + * Helper method for ::getValue() to check if included properties have to be + * included. + * + * @param bool $include_computed + * Whether to include computed properties as provided to ::getValue(). + * + * @return bool + * TRUE if computed properties have to be included. FALSE otherwise. + */ + protected function includeComputedProperties($include_computed) { + return $include_computed && !$this->isEmpty(); + } + + /** + * {@inheritdoc} + */ public function setValue($values, $notify = TRUE) { // Treat the values as property value of the first property, if no array is // given. diff --git a/core/lib/Drupal/Core/Field/FieldItemList.php b/core/lib/Drupal/Core/Field/FieldItemList.php index a1a1ebd..bbcd55f 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 c350432..fd5cd68 100644 --- a/core/lib/Drupal/Core/Field/FieldItemListInterface.php +++ b/core/lib/Drupal/Core/Field/FieldItemListInterface.php @@ -271,4 +271,18 @@ 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 + * Depending on whether deep serialization is running or not the returned + * value might be either the data value as returned by ::getValue() if deep + * serialization is not running or additionally contain computed properties + * next to the data values if deep serialization is running. + */ + 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 f9922b9..c39e92a 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php @@ -208,15 +208,10 @@ public function setValue($values, $notify = TRUE) { /** * {@inheritdoc} */ - public function getValue() { - $values = parent::getValue(); - + protected function includeComputedProperties($include_computed) { // If there is an unsaved entity, return it as part of the field item values // to ensure idempotency of getValue() / setValue(). - if ($this->hasNewEntity()) { - $values['entity'] = $this->entity; - } - return $values; + return $this->hasNewEntity() ?: parent::includeComputedProperties($include_computed); } /** 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..c109886 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntitySerializationTest.php @@ -0,0 +1,127 @@ +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' => $this->entityReferenceFieldName, + '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; + $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'; + + if ($deep_serialization) { + $entity_level_zero->setDeepSerialization(TRUE); + } + + // Serialize and then unserialize the entity in order to check that the + // entity has been properly serialized depending on the used serialization + // method - regular or deep serialization. + $entity_level_zero = unserialize(serialize($entity_level_zero)); + + // 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()); + } + +}