diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index 18e0260..a370552 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -230,7 +230,7 @@ public function preSave(EntityStorageControllerInterface $storage_controller) { ->condition('uuid', $this->uuid()) ->execute(); $matched_entity = reset($matching_entities); - if (!empty($matched_entity) && ($matched_entity != $this->id())) { + if (!empty($matched_entity) && ($matched_entity != $this->id()) && $matched_entity != $this->getOriginalId()) { throw new ConfigDuplicateUUIDException(format_string('Attempt to save a configuration entity %id with UUID %uuid when this UUID is already used for %matched', array('%id' => $this->id(), '%uuid' => $this->uuid(), '%matched' => $matched_entity))); } diff --git a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php index 0412a1f..a95aaef 100644 --- a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\String; use Drupal\Component\Uuid\UuidInterface; use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityMalformedException; use Drupal\Core\Entity\EntityStorageControllerBase; @@ -17,6 +18,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\KeyValueStore\KeyValueStoreInterface; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\TypedData\TypedDataInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -102,6 +104,21 @@ public function create(array $values = array()) { $values += array('langcode' => $this->languageManager->getDefaultLanguage()->id); $entity = new $entity_class($values, $this->entityTypeId); + + // @todo This is handled by FieldableEntityStorageControllerBase, which + // assumes ContentEntityInterface, which is awful :( + if ($entity instanceof ContentEntityInterface) { + foreach ($entity as $name => $field) { + if (isset($values[$name])) { + $entity->$name = $values[$name]; + } + elseif (!array_key_exists($name, $values)) { + $entity->get($name)->applyDefaultValue(); + } + unset($values[$name]); + } + } + $entity->enforceIsNew(); // Assign a new UUID if there is none yet. @@ -194,30 +211,69 @@ public function save(EntityInterface $entity) { if ($id === NULL || $id === '') { throw new EntityMalformedException('The entity does not have an ID.'); } + + // If this entity has a concept of 'original ID', use that. + if ($entity instanceof ConfigEntityInterface && $entity->getOriginalId() !== NULL) { + $id = $entity->getOriginalId(); + } + + // Track if this entity is new. + $is_new = $entity->isNew(); + // Track if this entity exists already. + $id_exists = $this->keyValue->has($id); + + // A new entity should not already exist. + if ($id_exists && $is_new) { + throw new EntityStorageException(String::format('@type entity with ID @id already exists.', array('@type' => $this->entityTypeId, '@id' => $id))); + } + + // Load the original entity, if any. + if ($id_exists && !isset($entity->original)) { + $entity->original = $this->loadUnchanged($id); + } + + // If this is a rename, delete the original entity. + if ($id_exists && $id !== $entity->id()) { + $this->keyValue->delete($entity->original->id()); + } + + // Allow code to run before saving. $entity->preSave($this); $this->invokeHook('presave', $entity); - $hook = !$entity->isNew() ? 'update' : 'insert'; - // Prefer the entity normalizer. // @todo Always use normalization after https://drupal.org/node/2216569. if ($this->normalizer) { $data = $this->normalizer->normalize($entity); } - // We know how to handle ConfigEntity. elseif ($entity instanceof ConfigEntityInterface) { $data = $entity->getExportProperties(); } + elseif ($entity instanceof ContentEntityInterface) { + $data = $entity->getPropertyValues(); + } else { throw new EntityStorageException(String::format('Cannot normalize entity of type @entity_type, enable the serialization module.', array('@entity_type' => $this->getEntityTypeId()))); } + // Save the entity data in the key value store. $this->keyValue->set($entity->id(), $data); + + // The entity is no longer new. $entity->enforceIsNew(FALSE); - $entity->postSave($this, FALSE); - $this->invokeHook($hook, $entity); - return $hook == 'update' ? SAVED_UPDATED : SAVED_NEW; + // Allow code to run after saving. + $entity->postSave($this, !$is_new); + $this->invokeHook($is_new ? 'insert' : 'update', $entity); + + // If this entity has a concept of 'original ID', update it. + if ($id_exists && $entity instanceof ConfigEntityInterface) { + $entity->setOriginalId($entity->id()); + } + + unset($entity->original); + + return $is_new ? SAVED_NEW : SAVED_UPDATED; } /** diff --git a/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/KeyValueConfigEntityStorageTest.php b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/KeyValueConfigEntityStorageTest.php new file mode 100644 index 0000000..a814c50 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/KeyValueConfigEntityStorageTest.php @@ -0,0 +1,35 @@ + 'KeyValueEntityStorage config entity test', + 'description' => 'Tests KeyValueEntityStorage for config entities.', + 'group' => 'Entity API', + ); + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/KeyValueContentEntityStorageTest.php b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/KeyValueContentEntityStorageTest.php new file mode 100644 index 0000000..726deac --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/KeyValueContentEntityStorageTest.php @@ -0,0 +1,164 @@ + 'KeyValueEntityStorage content entity test', + 'description' => 'Tests KeyValueEntityStorage for content entities.', + 'group' => 'Entity API', + ); + } + + /** + * Tests CRUD operations. + */ + function testCRUD() { + $default_langcode = language_default()->id; + // Verify default properties on a newly created empty entity. + $empty = entity_create('entity_test_label'); + $this->assertIdentical($empty->id->value, NULL); + $this->assertIdentical($empty->name->value, NULL); + $this->assertTrue($empty->uuid->value); + $this->assertIdentical($empty->langcode->value, $default_langcode); + + // Verify ConfigEntity properties/methods on the newly created empty entity. + $this->assertIdentical($empty->isNew(), TRUE); + $this->assertIdentical($empty->bundle(), 'entity_test_label'); + $this->assertIdentical($empty->id(), NULL); + $this->assertTrue($empty->uuid()); + $this->assertIdentical($empty->label(), NULL); + + // Verify Entity properties/methods on the newly created empty entity. + $this->assertIdentical($empty->getEntityTypeId(), 'entity_test_label'); + // The URI can only be checked after saving. + try { + $empty->urlInfo(); + $this->fail('EntityMalformedException was thrown.'); + } + catch (EntityMalformedException $e) { + $this->pass('EntityMalformedException was thrown.'); + } + + // Verify that an empty entity cannot be saved. + try { + $empty->save(); + $this->fail('EntityMalformedException was thrown.'); + } + catch (EntityMalformedException $e) { + $this->pass('EntityMalformedException was thrown.'); + } + + // Verify that an entity with an empty ID string is considered empty, too. + $empty_id = entity_create('entity_test_label', array( + 'id' => '', + )); + $this->assertIdentical($empty_id->isNew(), TRUE); + try { + $empty_id->save(); + $this->fail('EntityMalformedException was thrown.'); + } + catch (EntityMalformedException $e) { + $this->pass('EntityMalformedException was thrown.'); + } + + // Verify properties on a newly created entity. + $entity_test = entity_create('entity_test_label', $expected = array( + 'id' => $this->randomName(), + 'name' => $this->randomString(), + )); + $this->assertIdentical($entity_test->id->value, $expected['id']); + $this->assertTrue($entity_test->uuid->value); + $this->assertNotEqual($entity_test->uuid->value, $empty->uuid->value); + $this->assertIdentical($entity_test->name->value, $expected['name']); + $this->assertIdentical($entity_test->langcode->value, $default_langcode); + + // Verify methods on the newly created entity. + $this->assertIdentical($entity_test->isNew(), TRUE); + $this->assertIdentical($entity_test->id(), $expected['id']); + $this->assertTrue($entity_test->uuid()); + $expected['uuid'] = $entity_test->uuid(); + $this->assertIdentical($entity_test->label(), $expected['name']); + + // Verify that the entity can be saved. + try { + $status = $entity_test->save(); + $this->pass('EntityMalformedException was not thrown.'); + } + catch (EntityMalformedException $e) { + $this->fail('EntityMalformedException was not thrown.'); + } + + // Verify that the correct status is returned and properties did not change. + $this->assertIdentical($status, SAVED_NEW); + $this->assertIdentical($entity_test->id(), $expected['id']); + $this->assertIdentical($entity_test->uuid(), $expected['uuid']); + $this->assertIdentical($entity_test->label(), $expected['name']); + $this->assertIdentical($entity_test->isNew(), FALSE); + + // Save again, and verify correct status and properties again. + $status = $entity_test->save(); + $this->assertIdentical($status, SAVED_UPDATED); + $this->assertIdentical($entity_test->id(), $expected['id']); + $this->assertIdentical($entity_test->uuid(), $expected['uuid']); + $this->assertIdentical($entity_test->label(), $expected['name']); + $this->assertIdentical($entity_test->isNew(), FALSE); + + // Ensure that creating an entity with the same id as an existing one is not + // possible. + $same_id = entity_create('entity_test_label', array( + 'id' => $entity_test->id(), + )); + $this->assertIdentical($same_id->isNew(), TRUE); + try { + $same_id->save(); + $this->fail('Not possible to overwrite an entity entity.'); + } catch (EntityStorageException $e) { + $this->pass('Not possible to overwrite an entity entity.'); + } + + // Verify that renaming the ID returns correct status and properties. + $ids = array($expected['id'], 'second_' . $this->randomName(4), 'third_' . $this->randomName(4)); + for ($i = 1; $i < 3; $i++) { + $old_id = $ids[$i - 1]; + $new_id = $ids[$i]; + // Before renaming, everything should point to the current ID. + $this->assertIdentical($entity_test->id(), $old_id); + + // Rename. + $entity_test->id = $new_id; + $this->assertIdentical($entity_test->id(), $new_id); + $status = $entity_test->save(); + $this->assertIdentical($status, SAVED_UPDATED); + $this->assertIdentical($entity_test->isNew(), FALSE); + + // Verify that originalID points to new ID directly after renaming. + $this->assertIdentical($entity_test->id(), $new_id); + } + } + +} diff --git a/core/modules/system/tests/modules/keyvalue_test/keyvalue_test.info.yml b/core/modules/system/tests/modules/keyvalue_test/keyvalue_test.info.yml new file mode 100644 index 0000000..41cfb33 --- /dev/null +++ b/core/modules/system/tests/modules/keyvalue_test/keyvalue_test.info.yml @@ -0,0 +1,10 @@ +name: 'KeyValue tests' +type: module +description: 'A support module to test key value storage.' +core: 8.x +package: Testing +version: VERSION +hidden: true +dependencies: + - config_test + - entity_test diff --git a/core/modules/system/tests/modules/keyvalue_test/keyvalue_test.module b/core/modules/system/tests/modules/keyvalue_test/keyvalue_test.module new file mode 100644 index 0000000..96404a1 --- /dev/null +++ b/core/modules/system/tests/modules/keyvalue_test/keyvalue_test.module @@ -0,0 +1,19 @@ +setStorageClass('Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage'); + } + if (isset($entity_types['entity_test_label'])) { + $entity_types['entity_test_label']->setStorageClass('Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage'); + } +} diff --git a/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php index c0fc209..e86b7f2 100644 --- a/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php @@ -8,6 +8,7 @@ namespace Drupal\Tests\Core\Entity\KeyValueStore { use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Language\Language; use Drupal\Tests\UnitTestCase; use Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage; @@ -272,6 +273,26 @@ public function testSaveConfigEntityWithoutNormalizer() { } /** + * @covers ::save() + */ + public function testSaveContentEntityWithoutNormalizer() { + $expected = array('id' => 'foo'); + $this->entityStorage = new KeyValueEntityStorage($this->entityType, $this->keyValueStore, $this->uuidService, $this->languageManager); + $this->entityStorage->setModuleHandler($this->moduleHandler); + $this->keyValueStore->expects($this->once()) + ->method('set') + ->with('foo', $expected); + $entity = $this->getMock('Drupal\Tests\Core\Entity\KeyValueStore\TestContentEntityInterface'); + $entity->expects($this->atLeastOnce()) + ->method('id') + ->will($this->returnValue('foo')); + $entity->expects($this->once()) + ->method('getPropertyValues') + ->will($this->returnValue($expected)); + $this->entityStorage->save($entity); + } + + /** * @covers ::load() */ public function testLoad() { @@ -395,6 +416,14 @@ protected function getMockEntity($methods = array()) { } +/** + * Provides a testable version of ContentEntityInterface. + * + * @see https://github.com/sebastianbergmann/phpunit-mock-objects/commit/96a6794 + */ +interface TestContentEntityInterface extends \Iterator, ContentEntityInterface { +} + } namespace { if (!defined('SAVED_NEW')) {