From 979bb625cefb89a893dfe03e3fc8cca6e243c9f4 Mon Sep 17 00:00:00 2001 From: catch Date: Sun, 30 Mar 2014 15:02:28 +0200 Subject: [PATCH] Issue #2226027 by tim.plunkett: ConfigEntityBase::preSave() tries to load the original entity but instead loads itself. --- .../Core/Config/Entity/ConfigEntityBase.php | 3 +- .../Config/Entity/ConfigEntityStorage.php | 26 +- core/lib/Drupal/Core/Entity/Entity.php | 2 + .../Drupal/breakpoint/Entity/Breakpoint.php | 19 +- .../breakpoint/Entity/BreakpointGroup.php | 13 +- .../breakpoint/Tests/BreakpointAPITest.php | 6 +- .../Tests/BreakpointGroupAPITest.php | 2 + .../lib/Drupal/field/FieldConfigStorage.php | 8 +- .../field/FieldInstanceConfigStorage.php | 8 +- .../Drupal/filter/Tests/FilterCrudTest.php | 7 +- .../Entity/ConfigEntityBaseUnitTest.php | 2 + .../Config/Entity/ConfigEntityStorageTest.php | 723 ++++++++++++++++++ 12 files changed, 793 insertions(+), 26 deletions(-) create mode 100644 core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index e5d943c4c5e..dd6481eec91 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -278,8 +278,9 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface throw new ConfigDuplicateUUIDException(String::format('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))); } + // If this entity is not new, load the original entity for comparison. if (!$this->isNew()) { - $original = $storage->loadUnchanged($this->id()); + $original = $storage->loadUnchanged($this->getOriginalId()); // Ensure that the UUID cannot be changed for an existing entity. if ($original && ($original->uuid() != $this->uuid())) { throw new ConfigDuplicateUUIDException(String::format('Attempt to save a configuration entity %id with UUID %uuid when this entity already exists with UUID %original_uuid', array('%id' => $this->id(), '%uuid' => $this->uuid(), '%original_uuid' => $original->uuid()))); diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php index 0ab9a60f625..f807abda586 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php @@ -18,6 +18,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Component\Uuid\UuidInterface; +use Drupal\Core\Language\LanguageManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -65,6 +66,13 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora */ protected $configStorage; + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + /** * Constructs a ConfigEntityStorage object. * @@ -76,8 +84,10 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora * The config storage service. * @param \Drupal\Component\Uuid\UuidInterface $uuid_service * The UUID service. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. */ - public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, StorageInterface $config_storage, UuidInterface $uuid_service) { + public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, StorageInterface $config_storage, UuidInterface $uuid_service, LanguageManagerInterface $language_manager) { parent::__construct($entity_type); $this->idKey = $this->entityType->getKey('id'); @@ -86,6 +96,7 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora $this->configFactory = $config_factory; $this->configStorage = $config_storage; $this->uuidService = $uuid_service; + $this->languageManager = $language_manager; } /** @@ -96,7 +107,8 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora $entity_type, $container->get('config.factory'), $container->get('config.storage'), - $container->get('uuid') + $container->get('uuid'), + $container->get('language_manager') ); } @@ -228,7 +240,7 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora $class::preCreate($this, $values); // Set default language to site default if not provided. - $values += array('langcode' => language_default()->id); + $values += array('langcode' => $this->languageManager->getDefaultLanguage()->id); $entity = new $class($values, $this->entityTypeId); // Mark this entity as new, so isNew() returns TRUE. This does not check @@ -331,9 +343,6 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora $config->save(); $entity->postSave($this, TRUE); $this->invokeHook('update', $entity); - - // Immediately update the original ID. - $entity->setOriginalId($entity->id()); } else { $return = SAVED_NEW; @@ -343,6 +352,11 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora $this->invokeHook('insert', $entity); } + // After saving, this is now the "original entity", and subsequent saves + // will be updates instead of inserts, and updates must always be able to + // correctly identify the original entity. + $entity->setOriginalId($entity->id()); + unset($entity->original); return $return; diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index 2245ad7662e..534048c1482 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -314,7 +314,9 @@ abstract class Entity extends DependencySerialization implements EntityInterface public function createDuplicate() { $duplicate = clone $this; $entity_type = $this->getEntityType(); + // Reset the entity ID and indicate that this is a new entity. $duplicate->{$entity_type->getKey('id')} = NULL; + $duplicate->enforceIsNew(); // Check if the entity type supports UUIDs and generate a new one if so. if ($entity_type->hasKey('uuid')) { diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/Breakpoint.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/Breakpoint.php index e2008944211..5fa026074c4 100644 --- a/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/Breakpoint.php +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/Breakpoint.php @@ -105,6 +105,18 @@ class Breakpoint extends ConfigEntityBase implements BreakpointInterface { */ public $multipliers = array(); + /** + * {@inheritdoc} + */ + public function id() { + // If no ID is specified, build one from the properties that uniquely define + // this breakpoint. + if (!isset($this->id)) { + $this->id = $this->sourceType . '.' . $this->source . '.' . $this->name; + } + return $this->id; + } + /** * Overrides Drupal\config\ConfigEntityBase::save(). */ @@ -114,13 +126,6 @@ class Breakpoint extends ConfigEntityBase implements BreakpointInterface { throw new InvalidBreakpointException('Invalid data detected.'); } - // Build an id if none is set. - // Since a particular name can be used by multiple theme/modules we need - // to make a unique id. - if (empty($this->id)) { - $this->id = $this->sourceType . '.' . $this->source . '.' . $this->name; - } - // Set the label if none is set. if (empty($this->label)) { $this->label = $this->name; diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/BreakpointGroup.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/BreakpointGroup.php index 9840657d4ad..595ca76489e 100644 --- a/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/BreakpointGroup.php +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Entity/BreakpointGroup.php @@ -104,10 +104,19 @@ class BreakpointGroup extends ConfigEntityBase implements BreakpointGroupInterfa if (!$this->isValid()) { throw new InvalidBreakpointException('Invalid data detected.'); } - if (empty($this->id)) { + parent::save(); + } + + /** + * {@inheritdoc} + */ + public function id() { + // If no ID is specified, build one from the properties that uniquely define + // this breakpoint group. + if (!isset($this->id)) { $this->id = $this->sourceType . '.' . $this->source . '.' . $this->name; } - parent::save(); + return $this->id; } /** diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointAPITest.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointAPITest.php index 85d0711fc17..e1322800780 100644 --- a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointAPITest.php +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointAPITest.php @@ -46,7 +46,7 @@ class BreakpointAPITest extends BreakpointTestBase { $this->assertTrue($exception, 'breakpoint_config_name: An exception is thrown when an invalid sourceType is entered.'); // Try an invalid source. - $breakpoint->id = ''; + $breakpoint = $breakpoint->createDuplicate(); $breakpoint->sourceType = Breakpoint::SOURCE_TYPE_USER_DEFINED; $breakpoint->source = 'custom*_module source'; @@ -60,7 +60,7 @@ class BreakpointAPITest extends BreakpointTestBase { $this->assertTrue($exception, 'breakpoint_config_name: An exception is thrown when an invalid source is entered.'); // Try an invalid name (make sure there is at least once capital letter). - $breakpoint->id = ''; + $breakpoint = $breakpoint->createDuplicate(); $breakpoint->source = 'custom_module'; $breakpoint->name = drupal_ucfirst($this->randomName()); @@ -74,7 +74,7 @@ class BreakpointAPITest extends BreakpointTestBase { $this->assertTrue($exception, 'breakpoint_config_name: An exception is thrown when an invalid name is entered.'); // Try a valid breakpoint. - $breakpoint->id = ''; + $breakpoint = $breakpoint->createDuplicate(); $breakpoint->name = drupal_strtolower($this->randomName()); $breakpoint->mediaQuery = 'all'; diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupAPITest.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupAPITest.php index 3751c813b65..7a563f6e984 100644 --- a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupAPITest.php +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupAPITest.php @@ -49,6 +49,7 @@ class BreakpointGroupAPITest extends BreakpointGroupTestBase { $this->assertTrue($exception, 'An exception is thrown when an invalid sourceType is entered.'); // Try an invalid source. + $breakpoint_group = $breakpoint_group->createDuplicate(); $breakpoint_group->name = ''; $breakpoint_group->sourceType = Breakpoint::SOURCE_TYPE_USER_DEFINED; $breakpoint_group->source = 'custom*_module source'; @@ -63,6 +64,7 @@ class BreakpointGroupAPITest extends BreakpointGroupTestBase { $this->assertTrue($exception, 'An exception is thrown when an invalid source is entered.'); // Try a valid breakpoint_group. + $breakpoint_group = $breakpoint_group->createDuplicate(); $breakpoint_group->name = 'test'; $breakpoint_group->source = 'custom_module_source'; diff --git a/core/modules/field/lib/Drupal/field/FieldConfigStorage.php b/core/modules/field/lib/Drupal/field/FieldConfigStorage.php index 09fa1196d09..79caf5484a0 100644 --- a/core/modules/field/lib/Drupal/field/FieldConfigStorage.php +++ b/core/modules/field/lib/Drupal/field/FieldConfigStorage.php @@ -13,6 +13,7 @@ use Drupal\Core\Config\Entity\ConfigEntityStorage; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\Query\QueryFactory; +use Drupal\Core\Language\LanguageManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\StorageInterface; @@ -56,6 +57,8 @@ class FieldConfigStorage extends ConfigEntityStorage { * The config storage service. * @param \Drupal\Component\Uuid\UuidInterface $uuid_service * The UUID service. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager. * @param \Drupal\Core\Extension\ModuleHandler $module_handler @@ -63,8 +66,8 @@ class FieldConfigStorage extends ConfigEntityStorage { * @param \Drupal\Core\KeyValueStore\StateInterface $state * The state key value store. */ - public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, StorageInterface $config_storage, UuidInterface $uuid_service, EntityManagerInterface $entity_manager, ModuleHandler $module_handler, StateInterface $state) { - parent::__construct($entity_type, $config_factory, $config_storage, $uuid_service); + public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, StorageInterface $config_storage, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, EntityManagerInterface $entity_manager, ModuleHandler $module_handler, StateInterface $state) { + parent::__construct($entity_type, $config_factory, $config_storage, $uuid_service, $language_manager); $this->entityManager = $entity_manager; $this->moduleHandler = $module_handler; $this->state = $state; @@ -79,6 +82,7 @@ class FieldConfigStorage extends ConfigEntityStorage { $container->get('config.factory'), $container->get('config.storage'), $container->get('uuid'), + $container->get('language_manager'), $container->get('entity.manager'), $container->get('module_handler'), $container->get('state') diff --git a/core/modules/field/lib/Drupal/field/FieldInstanceConfigStorage.php b/core/modules/field/lib/Drupal/field/FieldInstanceConfigStorage.php index 1942ef5b87d..9d9826adb1a 100644 --- a/core/modules/field/lib/Drupal/field/FieldInstanceConfigStorage.php +++ b/core/modules/field/lib/Drupal/field/FieldInstanceConfigStorage.php @@ -12,6 +12,7 @@ use Drupal\Core\Config\Entity\ConfigEntityStorage; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\Query\QueryFactory; +use Drupal\Core\Language\LanguageManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Component\Uuid\UuidInterface; @@ -54,13 +55,15 @@ class FieldInstanceConfigStorage extends ConfigEntityStorage { * The config storage service. * @param \Drupal\Component\Uuid\UuidInterface $uuid_service * The UUID service. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager. * @param \Drupal\Core\KeyValueStore\StateInterface $state * The state key value store. */ - public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, StorageInterface $config_storage, UuidInterface $uuid_service, EntityManagerInterface $entity_manager, StateInterface $state) { - parent::__construct($entity_type, $config_factory, $config_storage, $uuid_service); + public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, StorageInterface $config_storage, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, EntityManagerInterface $entity_manager, StateInterface $state) { + parent::__construct($entity_type, $config_factory, $config_storage, $uuid_service, $language_manager); $this->entityManager = $entity_manager; $this->state = $state; } @@ -74,6 +77,7 @@ class FieldInstanceConfigStorage extends ConfigEntityStorage { $container->get('config.factory'), $container->get('config.storage'), $container->get('uuid'), + $container->get('language_manager'), $container->get('entity.manager'), $container->get('state') ); diff --git a/core/modules/filter/lib/Drupal/filter/Tests/FilterCrudTest.php b/core/modules/filter/lib/Drupal/filter/Tests/FilterCrudTest.php index 83203368b4c..a6456918fd1 100644 --- a/core/modules/filter/lib/Drupal/filter/Tests/FilterCrudTest.php +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterCrudTest.php @@ -41,9 +41,10 @@ class FilterCrudTest extends DrupalUnitTestBase { $this->verifyTextFormat($format); // Add another text format specifying all possible properties. - $format = entity_create('filter_format'); - $format->format = 'custom_format'; - $format->name = 'Custom format'; + $format = entity_create('filter_format', array( + 'format' => 'custom_format', + 'name' => 'Custom format', + )); $format->setFilterConfig('filter_url', array( 'status' => 1, 'settings' => array( diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php index dcdfb76480b..a23f032831f 100644 --- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php @@ -369,6 +369,8 @@ class ConfigEntityBaseUnitTest extends UnitTestCase { $duplicate = $this->entity->createDuplicate(); $this->assertInstanceOf('\Drupal\Core\Entity\Entity', $duplicate); $this->assertNotSame($this->entity, $duplicate); + $this->assertFalse($this->entity->isNew()); + $this->assertTrue($duplicate->isNew()); $this->assertNull($duplicate->id()); $this->assertNull($duplicate->getOriginalId()); $this->assertNotEquals($this->entity->uuid(), $duplicate->uuid()); diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php new file mode 100644 index 00000000000..918e5964d86 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php @@ -0,0 +1,723 @@ + 'ConfigEntityStorage unit test', + 'description' => 'Tests \Drupal\Core\Config\Entity\ConfigEntityStorage', + 'group' => 'Configuration', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->entityType = $this->getMock('Drupal\Core\Entity\EntityTypeInterface'); + $this->entityType->expects($this->any()) + ->method('getKey') + ->will($this->returnValueMap(array( + array('id', 'id'), + array('uuid', 'uuid'), + ))); + $this->entityType->expects($this->any()) + ->method('id') + ->will($this->returnValue('test_entity_type')); + $this->entityType->expects($this->any()) + ->method('getConfigPrefix') + ->will($this->returnValue('the_config_prefix')); + + $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + + $this->uuidService = $this->getMock('Drupal\Component\Uuid\UuidInterface'); + + $this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface'); + $this->languageManager->expects($this->any()) + ->method('getDefaultLanguage') + ->will($this->returnValue(new Language(array('langcode' => 'en')))); + + $this->configStorage = $this->getConfigStorageStub(array()); + + $this->configFactory = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface'); + + $this->entityQuery = $this->getMock('Drupal\Core\Entity\Query\QueryInterface'); + + $this->entityStorage = $this->getMockBuilder('Drupal\Core\Config\Entity\ConfigEntityStorage') + ->setConstructorArgs(array($this->entityType, $this->configFactory, $this->configStorage, $this->uuidService, $this->languageManager)) + ->setMethods(array('getQuery')) + ->getMock(); + $this->entityStorage->expects($this->any()) + ->method('getQuery') + ->will($this->returnValue($this->entityQuery)); + $this->entityStorage->setModuleHandler($this->moduleHandler); + } + + /** + * @covers ::create() + */ + public function testCreateWithPredefinedUuid() { + $this->entityType->expects($this->atLeastOnce()) + ->method('getClass') + ->will($this->returnValue(get_class($this->getMockEntity()))); + + $this->moduleHandler->expects($this->at(0)) + ->method('invokeAll') + ->with('test_entity_type_create'); + $this->moduleHandler->expects($this->at(1)) + ->method('invokeAll') + ->with('entity_create'); + $this->uuidService->expects($this->never()) + ->method('generate'); + + $entity = $this->entityStorage->create(array('id' => 'foo', 'uuid' => 'baz')); + $this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity); + $this->assertSame('foo', $entity->id()); + $this->assertSame('baz', $entity->uuid()); + } + + /** + * @covers ::create() + * + * @return \Drupal\Core\Entity\EntityInterface + */ + public function testCreate() { + $this->entityType->expects($this->atLeastOnce()) + ->method('getClass') + ->will($this->returnValue(get_class($this->getMockEntity()))); + + $this->moduleHandler->expects($this->at(0)) + ->method('invokeAll') + ->with('test_entity_type_create'); + $this->moduleHandler->expects($this->at(1)) + ->method('invokeAll') + ->with('entity_create'); + $this->uuidService->expects($this->once()) + ->method('generate') + ->will($this->returnValue('bar')); + + $entity = $this->entityStorage->create(array('id' => 'foo')); + $this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity); + $this->assertSame('foo', $entity->id()); + $this->assertSame('bar', $entity->uuid()); + return $entity; + } + + /** + * @covers ::save() + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * + * @return \Drupal\Core\Entity\EntityInterface + * + * @depends testCreate + */ + public function testSaveInsert(EntityInterface $entity) { + $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config_object->expects($this->atLeastOnce()) + ->method('isNew') + ->will($this->returnValue(TRUE)); + $config_object->expects($this->exactly(4)) + ->method('set'); + $config_object->expects($this->once()) + ->method('save'); + + $this->configFactory->expects($this->once()) + ->method('get') + ->with('the_config_prefix.foo') + ->will($this->returnValue($config_object)); + + $this->moduleHandler->expects($this->at(0)) + ->method('invokeAll') + ->with('test_entity_type_presave'); + $this->moduleHandler->expects($this->at(1)) + ->method('invokeAll') + ->with('entity_presave'); + $this->moduleHandler->expects($this->at(2)) + ->method('invokeAll') + ->with('test_entity_type_insert'); + $this->moduleHandler->expects($this->at(3)) + ->method('invokeAll') + ->with('entity_insert'); + + $this->entityQuery->expects($this->once()) + ->method('condition') + ->with('uuid', 'bar') + ->will($this->returnSelf()); + $this->entityQuery->expects($this->once()) + ->method('execute') + ->will($this->returnValue(array())); + + $return = $this->entityStorage->save($entity); + $this->assertSame(SAVED_NEW, $return); + return $entity; + } + + /** + * @covers ::save() + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * + * @return \Drupal\Core\Entity\EntityInterface + * + * @depends testSaveInsert + */ + public function testSaveUpdate(EntityInterface $entity) { + $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config_object->expects($this->atLeastOnce()) + ->method('isNew') + ->will($this->returnValue(FALSE)); + $config_object->expects($this->exactly(4)) + ->method('set'); + $config_object->expects($this->once()) + ->method('save'); + + $this->configFactory->expects($this->exactly(2)) + ->method('loadMultiple') + ->with(array('the_config_prefix.foo')) + ->will($this->returnValue(array())); + $this->configFactory->expects($this->once()) + ->method('get') + ->with('the_config_prefix.foo') + ->will($this->returnValue($config_object)); + + $this->moduleHandler->expects($this->at(0)) + ->method('invokeAll') + ->with('test_entity_type_presave'); + $this->moduleHandler->expects($this->at(1)) + ->method('invokeAll') + ->with('entity_presave'); + $this->moduleHandler->expects($this->at(2)) + ->method('invokeAll') + ->with('test_entity_type_update'); + $this->moduleHandler->expects($this->at(3)) + ->method('invokeAll') + ->with('entity_update'); + + $this->entityQuery->expects($this->once()) + ->method('condition') + ->with('uuid', 'bar') + ->will($this->returnSelf()); + $this->entityQuery->expects($this->once()) + ->method('execute') + ->will($this->returnValue(array($entity->id()))); + + $return = $this->entityStorage->save($entity); + $this->assertSame(SAVED_UPDATED, $return); + return $entity; + } + + /** + * @covers ::save() + * + * @depends testSaveInsert + */ + public function testSaveRename(ConfigEntityInterface $entity) { + $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config_object->expects($this->atLeastOnce()) + ->method('isNew') + ->will($this->returnValue(FALSE)); + $config_object->expects($this->exactly(4)) + ->method('set'); + $config_object->expects($this->once()) + ->method('save'); + + $this->configFactory->expects($this->once()) + ->method('rename') + ->will($this->returnValue($config_object)); + $this->configFactory->expects($this->exactly(2)) + ->method('loadMultiple') + ->with(array('the_config_prefix.foo')) + ->will($this->returnValue(array())); + $this->configFactory->expects($this->once()) + ->method('get') + ->with('the_config_prefix.foo') + ->will($this->returnValue($config_object)); + + // Performing a rename does not change the original ID until saving. + $this->assertSame('foo', $entity->getOriginalId()); + $entity->set('id', 'bar'); + $this->assertSame('foo', $entity->getOriginalId()); + + $this->entityQuery->expects($this->once()) + ->method('condition') + ->with('uuid', 'bar') + ->will($this->returnSelf()); + $this->entityQuery->expects($this->once()) + ->method('execute') + ->will($this->returnValue(array($entity->id()))); + + $return = $this->entityStorage->save($entity); + $this->assertSame(SAVED_UPDATED, $return); + $this->assertSame('bar', $entity->getOriginalId()); + } + + /** + * @covers ::save() + * + * @expectedException \Drupal\Core\Entity\EntityMalformedException + * @expectedExceptionMessage The entity does not have an ID. + */ + public function testSaveInvalid() { + $entity = $this->getMockEntity(); + $this->entityStorage->save($entity); + } + + /** + * @covers ::save() + * + * @expectedException \Drupal\Core\Entity\EntityStorageException + */ + public function testSaveDuplicate() { + $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config_object->expects($this->atLeastOnce()) + ->method('isNew') + ->will($this->returnValue(FALSE)); + $config_object->expects($this->never()) + ->method('set'); + $config_object->expects($this->never()) + ->method('save'); + + $this->configFactory->expects($this->once()) + ->method('get') + ->with('the_config_prefix.foo') + ->will($this->returnValue($config_object)); + + $entity = $this->getMockEntity(array('id' => 'foo')); + $entity->enforceIsNew(); + + $this->entityStorage->save($entity); + } + + /** + * @covers ::save() + * + * @expectedException \Drupal\Core\Config\ConfigDuplicateUUIDException + * @expectedExceptionMessage when this UUID is already used for + */ + public function testSaveMismatch() { + $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config_object->expects($this->atLeastOnce()) + ->method('isNew') + ->will($this->returnValue(TRUE)); + $config_object->expects($this->never()) + ->method('save'); + + $this->configFactory->expects($this->once()) + ->method('get') + ->with('the_config_prefix.foo') + ->will($this->returnValue($config_object)); + + $this->entityQuery->expects($this->once()) + ->method('condition') + ->will($this->returnSelf()); + $this->entityQuery->expects($this->once()) + ->method('execute') + ->will($this->returnValue(array('baz'))); + + $entity = $this->getMockEntity(array('id' => 'foo')); + $this->entityStorage->save($entity); + } + + /** + * @covers ::save() + */ + public function testSaveNoMismatch() { + $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config_object->expects($this->atLeastOnce()) + ->method('isNew') + ->will($this->returnValue(TRUE)); + $config_object->expects($this->once()) + ->method('save'); + + $this->configFactory->expects($this->once()) + ->method('get') + ->with('the_config_prefix.baz') + ->will($this->returnValue($config_object)); + $this->configFactory->expects($this->once()) + ->method('rename') + ->will($this->returnValue($config_object)); + + $this->entityQuery->expects($this->once()) + ->method('condition') + ->will($this->returnSelf()); + $this->entityQuery->expects($this->once()) + ->method('execute') + ->will($this->returnValue(array('baz'))); + + $entity = $this->getMockEntity(array('id' => 'foo')); + $entity->enforceIsNew(); + $entity->setOriginalId('baz'); + $this->entityStorage->save($entity); + } + + /** + * @covers ::save() + * + * @expectedException \Drupal\Core\Config\ConfigDuplicateUUIDException + * @expectedExceptionMessage when this entity already exists with UUID + */ + public function testSaveChangedUuid() { + $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config_object->expects($this->atLeastOnce()) + ->method('isNew') + ->will($this->returnValue(FALSE)); + $config_object->expects($this->never()) + ->method('save'); + $config_object->expects($this->exactly(2)) + ->method('get') + ->will($this->returnValueMap(array( + array('', array('id' => 'foo')), + array('id', 'foo'), + ))); + + $this->configFactory->expects($this->at(1)) + ->method('loadMultiple') + ->with(array('the_config_prefix.foo')) + ->will($this->returnValue(array())); + $this->configFactory->expects($this->at(2)) + ->method('loadMultiple') + ->with(array('the_config_prefix.foo')) + ->will($this->returnValue(array($config_object))); + $this->configFactory->expects($this->once()) + ->method('get') + ->with('the_config_prefix.foo') + ->will($this->returnValue($config_object)); + $this->configFactory->expects($this->never()) + ->method('rename') + ->will($this->returnValue($config_object)); + + $this->moduleHandler->expects($this->exactly(2)) + ->method('getImplementations') + ->will($this->returnValue(array())); + + $this->entityQuery->expects($this->once()) + ->method('condition') + ->will($this->returnSelf()); + $this->entityQuery->expects($this->once()) + ->method('execute') + ->will($this->returnValue(array('foo'))); + + $entity = $this->getMockEntity(array('id' => 'foo')); + $this->entityType->expects($this->atLeastOnce()) + ->method('getClass') + ->will($this->returnValue(get_class($entity))); + + $entity->set('uuid', 'baz'); + $this->entityStorage->save($entity); + } + + /** + * @covers ::load() + * @covers ::postLoad() + * @covers ::buildQuery() + */ + public function testLoad() { + $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config_object->expects($this->exactly(2)) + ->method('get') + ->will($this->returnValueMap(array( + array('', array('id' => 'foo')), + array('id', 'foo'), + ))); + + $this->configFactory->expects($this->once()) + ->method('loadMultiple') + ->with(array('the_config_prefix.foo')) + ->will($this->returnValue(array($config_object))); + $this->moduleHandler->expects($this->exactly(2)) + ->method('getImplementations') + ->will($this->returnValue(array())); + + $this->entityType->expects($this->atLeastOnce()) + ->method('getClass') + ->will($this->returnValue(get_class($this->getMockEntity()))); + + $entity = $this->entityStorage->load('foo'); + $this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity); + $this->assertSame('foo', $entity->id()); + } + + /** + * @covers ::loadMultiple() + * @covers ::postLoad() + * @covers ::buildQuery() + */ + public function testLoadMultipleAll() { + $foo_config_object = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $foo_config_object->expects($this->exactly(2)) + ->method('get') + ->will($this->returnValueMap(array( + array('', array('id' => 'foo')), + array('id', 'foo'), + ))); + $bar_config_object = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $bar_config_object->expects($this->exactly(2)) + ->method('get') + ->will($this->returnValueMap(array( + array('', array('id' => 'bar')), + array('id', 'bar'), + ))); + + $this->configFactory->expects($this->once()) + ->method('loadMultiple') + ->with(array()) + ->will($this->returnValue(array($foo_config_object, $bar_config_object))); + $this->moduleHandler->expects($this->exactly(2)) + ->method('getImplementations') + ->will($this->returnValue(array())); + + $this->entityType->expects($this->atLeastOnce()) + ->method('getClass') + ->will($this->returnValue(get_class($this->getMockEntity()))); + + $entities = $this->entityStorage->loadMultiple(); + $expected['foo'] = 'foo'; + $expected['bar'] = 'bar'; + foreach ($entities as $id => $entity) { + $this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity); + $this->assertSame($id, $entity->id()); + $this->assertSame($expected[$id], $entity->id()); + } + } + + /** + * @covers ::loadMultiple() + * @covers ::postLoad() + * @covers ::buildQuery() + */ + public function testLoadMultipleIds() { + $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config_object->expects($this->exactly(2)) + ->method('get') + ->will($this->returnValueMap(array( + array('', array('id' => 'foo')), + array('id', 'foo'), + ))); + + $this->configFactory->expects($this->once()) + ->method('loadMultiple') + ->with(array('the_config_prefix.foo')) + ->will($this->returnValue(array($config_object))); + $this->moduleHandler->expects($this->exactly(2)) + ->method('getImplementations') + ->will($this->returnValue(array())); + $this->entityType->expects($this->atLeastOnce()) + ->method('getClass') + ->will($this->returnValue(get_class($this->getMockEntity()))); + + $entities = $this->entityStorage->loadMultiple(array('foo')); + foreach ($entities as $id => $entity) { + $this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity); + $this->assertSame($id, $entity->id()); + } + } + + /** + * @covers ::loadRevision() + */ + public function testLoadRevision() { + $this->assertSame(FALSE, $this->entityStorage->loadRevision(1)); + } + + /** + * @covers ::deleteRevision() + */ + public function testDeleteRevision() { + $this->assertSame(NULL, $this->entityStorage->deleteRevision(1)); + } + + /** + * @covers ::delete() + */ + public function testDelete() { + $this->entityType->expects($this->atLeastOnce()) + ->method('getClass') + ->will($this->returnValue(get_class($this->getMockEntity()))); + + $entities = array(); + $configs = array(); + $config_map = array(); + foreach (array('foo', 'bar') as $id) { + $entity = $this->getMockEntity(array('id' => $id)); + $entities[] = $entity; + $config_object = $this->getMockBuilder('Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + $config_object->expects($this->once()) + ->method('delete'); + $configs[] = $config_object; + $config_map[] = array("the_config_prefix.$id", $config_object); + } + + $this->configFactory->expects($this->exactly(2)) + ->method('get') + ->will($this->returnValueMap($config_map)); + + $this->moduleHandler->expects($this->at(0)) + ->method('invokeAll') + ->with('test_entity_type_predelete'); + $this->moduleHandler->expects($this->at(1)) + ->method('invokeAll') + ->with('entity_predelete'); + $this->moduleHandler->expects($this->at(2)) + ->method('invokeAll') + ->with('test_entity_type_predelete'); + $this->moduleHandler->expects($this->at(3)) + ->method('invokeAll') + ->with('entity_predelete'); + $this->moduleHandler->expects($this->at(4)) + ->method('invokeAll') + ->with('test_entity_type_delete'); + $this->moduleHandler->expects($this->at(5)) + ->method('invokeAll') + ->with('entity_delete'); + $this->moduleHandler->expects($this->at(6)) + ->method('invokeAll') + ->with('test_entity_type_delete'); + $this->moduleHandler->expects($this->at(7)) + ->method('invokeAll') + ->with('entity_delete'); + + $this->entityStorage->delete($entities); + } + + /** + * @covers ::delete() + */ + public function testDeleteNothing() { + $this->moduleHandler->expects($this->never()) + ->method($this->anything()); + $this->configFactory->expects($this->never()) + ->method('get'); + + $this->entityStorage->delete(array()); + } + + /** + * Creates an entity with specific methods mocked. + * + * @param array $values + * (optional) Values to pass to the constructor. + * @param array $methods + * (optional) The methods to mock. + * + * @return \Drupal\Core\Entity\EntityInterface|\PHPUnit_Framework_MockObject_MockObject + */ + public function getMockEntity(array $values = array(), $methods = array()) { + $methods[] = 'onSaveOrDelete'; + $methods[] = 'onUpdateBundleEntity'; + return $this->getMockForAbstractClass('Drupal\Core\Config\Entity\ConfigEntityBase', array($values, 'test_entity_type'), '', TRUE, TRUE, TRUE, $methods); + } + +} + +} +namespace { + if (!defined('SAVED_NEW')) { + define('SAVED_NEW', 1); + } + if (!defined('SAVED_UPDATED')) { + define('SAVED_UPDATED', 2); + } +}