diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 1d69be2..d0d8ee0 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -149,6 +149,13 @@ protected $validationRequired = FALSE; /** + * Whether to serialize the initialized fields when serializing the entity. + * + * @var bool + */ + public $serializeComputedFieldValues = FALSE; + + /** * {@inheritdoc} */ public function __construct(array $values, $entity_type, $bundle = FALSE, $translations = array()) { @@ -410,10 +417,17 @@ protected function clearTranslationCache() { * {@inheritdoc} */ public function __sleep() { + if ($this->serializeComputedFieldValues) { + foreach ($this->referencedEntities() as $entity) { + if ($entity instanceof ContentEntityInterface) { + $entity->serializeComputedFieldValues = TRUE; + } + } + } // 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->getValue($this->serializeComputedFieldValues); } } $this->fields = array(); @@ -427,6 +441,14 @@ public function __sleep() { /** * {@inheritdoc} */ + public function __wakeup() { + $this->serializeComputedFieldValues = FALSE; + parent::__wakeup(); + } + + /** + * {@inheritdoc} + */ public function id() { return $this->getEntityKey('id'); } 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 63bd90d..0fc514e 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php @@ -204,12 +204,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/lib/Drupal/Core/Form/FormCache.php b/core/lib/Drupal/Core/Form/FormCache.php index 13e5430..a7196e8 100644 --- a/core/lib/Drupal/Core/Form/FormCache.php +++ b/core/lib/Drupal/Core/Form/FormCache.php @@ -4,6 +4,7 @@ use Drupal\Component\Utility\Crypt; use Drupal\Core\Access\CsrfTokenGenerator; +use Drupal\Core\Entity\ContentEntityFormInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface; use Drupal\Core\PageCache\RequestPolicyInterface; @@ -192,9 +193,20 @@ public function setCache($form_build_id, $form, FormStateInterface $form_state) $this->keyValueExpirableFactory->get('form')->setWithExpire($form_build_id, $form, $expire); } + // If the form state contains a content entity form object then the entity + // should be cached together with the computed field values otherwise when + // unserialized the entity might differ from the initial entity used - e.g. + // when referenced entities have changed. + if (($form_object = $form_state->getFormObject()) && ($form_object instanceof ContentEntityFormInterface)) { + $entity = $form_object->getEntity(); + $entity->serializeComputedFieldValues = TRUE; + } if ($data = $form_state->getCacheableArray()) { $this->keyValueExpirableFactory->get('form_state')->setWithExpire($form_build_id, $data, $expire); } + if (isset($entity)) { + $entity->serializeComputedFieldValues = FALSE; + } } /** diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntitySerializationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntitySerializationTest.php new file mode 100644 index 0000000..747336c --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntitySerializationTest.php @@ -0,0 +1,185 @@ +installEntitySchema('entity_test_rev'); + + // Create the entity reference fields. + $this->createEntityReferenceField( + $this->entityType, + $this->bundle, + $this->fieldName, + 'Field test', + $this->referencedEntityType, + 'default', + array('target_bundles' => array($this->bundle)), + FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED + ); + + $this->createEntityReferenceField( + $this->referencedEntityType, + $this->bundle, + $this->fieldName, + 'Field test', + $this->referencedEntityType, + 'default', + array('target_bundles' => array($this->bundle)), + FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED + ); + + } + + /** + * Tests the regular entity serialization. + */ + public function testEntitySerialization() { + $entity = $this->container->get('entity_type.manager') + ->getStorage($this->entityType) + ->create(['type' => $this->bundle, 'name' => 'test entity']); + + $serialized_entity = serialize($entity); + $entity = unserialize($serialized_entity); + + $this->assertEquals('test entity', $entity->label()); + } + + /** + * Tests the regular entity serialization with references. + */ + public function testRegularEntitySerializationWithEntityReferences() { + $entity = $this->createEntity(); + + // Assert that the regular serialization will not serialize references. + $original_label = $entity->{$this->fieldName}->entity->label(); + $entity->{$this->fieldName}->entity->name = 'test referenced entity changed'; + $serialized_entity = serialize($entity); + $entity = unserialize($serialized_entity); + $this->assertEquals($original_label, $entity->{$this->fieldName}->entity->label()); + } + + /** + * Tests entity serialization with flag for serializing references. + */ + public function testEntitySerializationWithSerializingEntityReferences() { + $entity = $this->createEntity(); + + // Assert that using the "serializeComputedFieldValues" flag the + // serialization will serialize references. + $original_label = $entity->{$this->fieldName}->entity->label(); + $new_label = 'test referenced entity changed'; + $this->assertNotEquals($original_label, $new_label); + + $entity->{$this->fieldName}->entity->name = $new_label; + $entity->serializeComputedFieldValues = TRUE; + $serialized_entity = serialize($entity); + $entity = unserialize($serialized_entity); + $this->assertEquals($new_label, $entity->{$this->fieldName}->entity->label()); + // Assert that after unserializing the entity the + // "serializeComputedFieldValues" flag is reset. + $this->assertTrue(empty($entity->serializeComputedFieldValues)); + $this->assertTrue(empty($entity->{$this->fieldName}->entity->serializeComputedFieldValues)); + } + + /** + * Tests entity serialization with flag for serializing references - complex. + */ + public function testEntitySerializationWithSerializingEntityReferencesDeeperStructure() { + $entity = $this->createEntity(); + + // Assert that using the "serializeComputedFieldValues" flag the + // serialization will serialize references on a deeper level as well. + $original_label_first_reference = $entity->{$this->fieldName}->entity->label(); + $new_label_first_reference = 'test referenced entity changed'; + $this->assertNotEquals($original_label_first_reference, $new_label_first_reference); + $original_label_second_reference = $entity->{$this->fieldName}->entity->{$this->fieldName}->entity->label(); + $new_label_second_reference = 'test referenced entity 2 changed'; + $this->assertNotEquals($original_label_second_reference, $new_label_second_reference); + + $entity->{$this->fieldName}->entity->name = $new_label_first_reference; + $entity->{$this->fieldName}->entity->{$this->fieldName}->entity->name = $new_label_second_reference; + $entity->serializeComputedFieldValues = TRUE; + $serialized_entity = serialize($entity); + $entity = unserialize($serialized_entity); + $this->assertEquals($new_label_first_reference, $entity->{$this->fieldName}->entity->label()); + $this->assertEquals($new_label_second_reference, $entity->{$this->fieldName}->entity->{$this->fieldName}->entity->label()); + // Assert that after unserializing the entity the + // "serializeComputedFieldValues" flag is reset. + $this->assertTrue(empty($entity->serializeComputedFieldValues)); + $this->assertTrue(empty($entity->{$this->fieldName}->entity->serializeComputedFieldValues)); + $this->assertTrue(empty($entity->{$this->fieldName}->entity->{$this->fieldName}->entity->serializeComputedFieldValues)); + } + + /** + * Creates and returns an entity with references. + * + * @return \Drupal\Core\Entity\EntityTypeInterface + * The entity object. + */ + protected function createEntity() { + $referenced_entity = $this->container->get('entity_type.manager') + ->getStorage($this->referencedEntityType) + ->create(['type' => $this->bundle, 'name' => 'test referenced entity']); + $referenced_entity->save(); + + $referenced_entity_2 = $this->container->get('entity_type.manager') + ->getStorage($this->referencedEntityType) + ->create(['type' => $this->bundle, 'name' => 'test referenced entity 2']); + $referenced_entity_2->save(); + + $entity = $this->container->get('entity_type.manager') + ->getStorage($this->entityType) + ->create(['type' => $this->bundle, 'name' => 'test entity']); + + $entity->{$this->fieldName}->entity = $referenced_entity; + $referenced_entity->{$this->fieldName}->entity = $referenced_entity_2; + + return $entity; + } + +}