Issue #2208617 by tim.plunkett: Add key value entity storage.

8.0.x
Nathaniel Catchpole 2014-04-30 11:08:22 +01:00
parent 773a992920
commit 95c69f5b9b
11 changed files with 1329 additions and 4 deletions

View File

@ -364,6 +364,9 @@ services:
entity.query.sql: entity.query.sql:
class: Drupal\Core\Entity\Query\Sql\QueryFactory class: Drupal\Core\Entity\Query\Sql\QueryFactory
arguments: ['@database'] arguments: ['@database']
entity.query.keyvalue:
class: Drupal\Core\Entity\KeyValueStore\Query\QueryFactory
arguments: ['@keyvalue']
router.dumper: router.dumper:
class: Drupal\Core\Routing\MatcherDumper class: Drupal\Core\Routing\MatcherDumper
arguments: ['@database', '@state'] arguments: ['@database', '@state']

View File

@ -0,0 +1,209 @@
<?php
/**
* @file
* Contains \Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage.
*/
namespace Drupal\Core\Entity\KeyValueStore;
use Drupal\Component\Utility\String;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Config\Entity\Exception\ConfigEntityIdLengthException;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityMalformedException;
use Drupal\Core\Entity\EntityStorageBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a key value backend for entities.
*
* @todo Entities that depend on auto-incrementing serial IDs need to explicitly
* provide an ID until a generic wrapper around the functionality provided by
* \Drupal\Core\Database\Connection::nextId() is added and used.
* @todo Revisions are currently not supported.
*/
class KeyValueEntityStorage extends EntityStorageBase {
/**
* Length limit of the entity ID.
*/
const MAX_ID_LENGTH = 128;
/**
* The key value store.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $keyValueStore;
/**
* The UUID service.
*
* @var \Drupal\Component\Uuid\UuidInterface
*/
protected $uuidService;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Constructs a new KeyValueEntityStorage.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value_store
* The key value store.
* @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, KeyValueStoreInterface $key_value_store, UuidInterface $uuid_service, LanguageManagerInterface $language_manager) {
parent::__construct($entity_type);
$this->keyValueStore = $key_value_store;
$this->uuidService = $uuid_service;
$this->languageManager = $language_manager;
// Check if the entity type supports UUIDs.
$this->uuidKey = $this->entityType->getKey('uuid');
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('keyvalue')->get('entity_storage__' . $entity_type->id()),
$container->get('uuid'),
$container->get('language_manager')
);
}
/**
* {@inheritdoc}
*/
public function doCreate(array $values = array()) {
// Set default language to site default if not provided.
$values += array('langcode' => $this->languageManager->getDefaultLanguage()->id);
$entity = new $this->entityClass($values, $this->entityTypeId);
// @todo This is handled by ContentEntityStorageBase, which assumes
// ContentEntityInterface. The current approach in
// https://drupal.org/node/1867228 improves this but does not solve it
// completely.
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]);
}
}
return $entity;
}
/**
* {@inheritdoc}
*/
public function doLoadMultiple(array $ids = NULL) {
if (empty($ids)) {
$entities = $this->keyValueStore->getAll();
}
else {
$entities = $this->keyValueStore->getMultiple($ids);
}
return $this->mapFromStorageRecords($entities);
}
/**
* {@inheritdoc}
*/
public function loadRevision($revision_id) {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function deleteRevision($revision_id) {
return NULL;
}
/**
* {@inheritdoc}
*/
public function doDelete($entities) {
$entity_ids = array();
foreach ($entities as $entity) {
$entity_ids[] = $entity->id();
}
$this->keyValueStore->deleteMultiple($entity_ids);
}
/**
* {@inheritdoc}
*/
public function save(EntityInterface $entity) {
$id = $entity->id();
if ($id === NULL || $id === '') {
throw new EntityMalformedException('The entity does not have an ID.');
}
// Check the entity ID length.
// @todo This is not config-specific, but serial IDs will likely never hit
// this limit. Consider renaming the exception class.
if (strlen($entity->id()) > static::MAX_ID_LENGTH) {
throw new ConfigEntityIdLengthException(String::format('Entity ID @id exceeds maximum allowed length of @length characters.', array(
'@id' => $entity->id(),
'@length' => static::MAX_ID_LENGTH,
)));
}
return parent::save($entity);
}
/**
* {@inheritdoc}
*/
protected function doSave($id, EntityInterface $entity) {
$is_new = $entity->isNew();
// Save the entity data in the key value store.
$this->keyValueStore->set($entity->id(), $entity->toArray());
// If this is a rename, delete the original entity.
if ($this->has($id, $entity) && $id !== $entity->id()) {
$this->keyValueStore->delete($id);
}
return $is_new ? SAVED_NEW : SAVED_UPDATED;
}
/**
* {@inheritdoc}
*/
protected function has($id, EntityInterface $entity) {
return $this->keyValueStore->has($id);
}
/**
* {@inheritdoc}
*/
public function getQueryServicename() {
return 'entity.query.keyvalue';
}
}

View File

@ -0,0 +1,17 @@
<?php
/**
* @file
* Contains \Drupal\Core\Entity\KeyValueStore\Query\Condition.
*/
namespace Drupal\Core\Entity\KeyValueStore\Query;
use Drupal\Core\Config\Entity\Query\Condition as ConditionParent;
/**
* Defines the condition class for the key value entity query.
*/
class Condition extends ConditionParent {
}

View File

@ -0,0 +1,78 @@
<?php
/**
* @file
* Contains \Drupal\Core\Entity\KeyValueStore\Query\Query.
*/
namespace Drupal\Core\Entity\KeyValueStore\Query;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Query\QueryBase;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
/**
* Defines the entity query for entities stored in a key value backend.
*/
class Query extends QueryBase {
/**
* The key value factory.
*
* @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface
*/
protected $keyValueFactory;
/**
* Constructs a new Query.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param string $conjunction
* - AND: all of the conditions on the query need to match.
* - OR: at least one of the conditions on the query need to match.
* @param array $namespaces
* List of potential namespaces of the classes belonging to this query.
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
* The key value factory.
*/
public function __construct(EntityTypeInterface $entity_type, $conjunction, array $namespaces, KeyValueFactoryInterface $key_value_factory) {
parent::__construct($entity_type, $conjunction, $namespaces);
$this->keyValueFactory = $key_value_factory;
}
/**
* {@inheritdoc}
*/
public function execute() {
// Load the relevant records.
$records = $this->keyValueFactory->get('entity_storage__' . $this->entityTypeId)->getAll();
// Apply conditions.
$result = $this->condition->compile($records);
// Apply sort settings.
foreach ($this->sort as $sort) {
$direction = $sort['direction'] == 'ASC' ? -1 : 1;
$field = $sort['field'];
uasort($result, function($a, $b) use ($field, $direction) {
return ($a[$field] <= $b[$field]) ? $direction : -$direction;
});
}
// Let the pager do its work.
$this->initializePager();
if ($this->range) {
$result = array_slice($result, $this->range['start'], $this->range['length'], TRUE);
}
if ($this->count) {
return count($result);
}
// Create the expected structure of entity_id => entity_id.
$entity_ids = array_keys($result);
return array_combine($entity_ids, $entity_ids);
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* @file
* Contains \Drupal\Core\Entity\KeyValueStore\Query\QueryFactory.
*/
namespace Drupal\Core\Entity\KeyValueStore\Query;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Query\QueryException;
use Drupal\Core\Entity\Query\QueryFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
/**
* Provides a factory for creating the key value entity query.
*/
class QueryFactory implements QueryFactoryInterface {
/**
* The key value factory.
*
* @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface
*/
protected $keyValueFactory;
/**
* The namespace of this class, the parent class etc.
*
* @var array
*/
protected $namespaces;
/**
* Constructs a QueryFactory object.
*
*/
public function __construct(KeyValueFactoryInterface $key_value_factory) {
$this->keyValueFactory = $key_value_factory;
$this->namespaces = Query::getNamespaces($this);
}
/**
* {@inheritdoc}
*/
public function get(EntityTypeInterface $entity_type, $conjunction) {
return new Query($entity_type, $conjunction, $this->namespaces, $this->keyValueFactory);
}
/**
* {@inheritdoc}
*/
public function getAggregate(EntityTypeInterface $entity_type, $conjunction) {
throw new QueryException('Aggregation over key-value entity storage is not supported');
}
}

View File

@ -20,6 +20,11 @@ use Drupal\simpletest\WebTestBase;
*/ */
class ConfigEntityTest extends WebTestBase { class ConfigEntityTest extends WebTestBase {
/**
* The maximum length for the entity storage used in this test.
*/
const MAX_ID_LENGTH = ConfigEntityStorage::MAX_ID_LENGTH;
/** /**
* Modules to enable. * Modules to enable.
* *
@ -164,7 +169,7 @@ class ConfigEntityTest extends WebTestBase {
// Test with an ID of the maximum allowed length. // Test with an ID of the maximum allowed length.
$id_length_config_test = entity_create('config_test', array( $id_length_config_test = entity_create('config_test', array(
'id' => $this->randomName(ConfigEntityStorage::MAX_ID_LENGTH), 'id' => $this->randomName(static::MAX_ID_LENGTH),
)); ));
try { try {
$id_length_config_test->save(); $id_length_config_test->save();
@ -178,19 +183,19 @@ class ConfigEntityTest extends WebTestBase {
// Test with an ID exeeding the maximum allowed length. // Test with an ID exeeding the maximum allowed length.
$id_length_config_test = entity_create('config_test', array( $id_length_config_test = entity_create('config_test', array(
'id' => $this->randomName(ConfigEntityStorage::MAX_ID_LENGTH + 1), 'id' => $this->randomName(static::MAX_ID_LENGTH + 1),
)); ));
try { try {
$status = $id_length_config_test->save(); $status = $id_length_config_test->save();
$this->fail(String::format("config_test entity with ID length @length exceeding the maximum allowed length of @max saved successfully", array( $this->fail(String::format("config_test entity with ID length @length exceeding the maximum allowed length of @max saved successfully", array(
'@length' => strlen($id_length_config_test->id), '@length' => strlen($id_length_config_test->id),
'@max' => ConfigEntityStorage::MAX_ID_LENGTH, '@max' => static::MAX_ID_LENGTH,
))); )));
} }
catch (ConfigEntityIdLengthException $e) { catch (ConfigEntityIdLengthException $e) {
$this->pass(String::format("config_test entity with ID length @length exceeding the maximum allowed length of @max failed to save", array( $this->pass(String::format("config_test entity with ID length @length exceeding the maximum allowed length of @max failed to save", array(
'@length' => strlen($id_length_config_test->id), '@length' => strlen($id_length_config_test->id),
'@max' => ConfigEntityStorage::MAX_ID_LENGTH, '@max' => static::MAX_ID_LENGTH,
))); )));
} }

View File

@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\KeyValueStore\KeyValueConfigEntityStorageTest.
*/
namespace Drupal\system\Tests\KeyValueStore;
use Drupal\config\Tests\ConfigEntityTest;
use Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage;
/**
* Tests config entity CRUD with key value entity storage.
*/
class KeyValueConfigEntityStorageTest extends ConfigEntityTest {
/**
* {@inheritdoc}
*/
const MAX_ID_LENGTH = KeyValueEntityStorage::MAX_ID_LENGTH;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('keyvalue_test');
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => 'KeyValueEntityStorage config entity test',
'description' => 'Tests KeyValueEntityStorage for config entities.',
'group' => 'Entity API',
);
}
}

View File

@ -0,0 +1,164 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\KeyValueStore\KeyValueContentEntityStorageTest.
*/
namespace Drupal\system\Tests\KeyValueStore;
use Drupal\Core\Entity\EntityMalformedException;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\simpletest\DrupalUnitTestBase;
/**
* Tests content entity CRUD with key value entity storage.
*/
class KeyValueContentEntityStorageTest extends DrupalUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('entity', 'user', 'entity_test', 'keyvalue_test');
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => '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);
}
}
}

View File

@ -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

View File

@ -0,0 +1,21 @@
<?php
/**
* @file
* Sets up the key value entity storage.
*/
/**
* Implements hook_entity_type_alter().
*/
function keyvalue_test_entity_type_alter(array &$entity_types) {
/** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
if (isset($entity_types['config_test'])) {
$entity_types['config_test']->setStorageClass('Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage');
}
if (isset($entity_types['entity_test_label'])) {
$entity_types['entity_test_label']->setStorageClass('Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage');
$entity_keys = $entity_types['entity_test_label']->getKeys();
$entity_types['entity_test_label']->set('entity_keys', $entity_keys + array('uuid' => 'uuid'));
}
}

View File

@ -0,0 +1,720 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Entity\KeyValueStore\KeyValueEntityStorageTest.
*/
namespace Drupal\Tests\Core\Entity\KeyValueStore {
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\Language;
use Drupal\Tests\UnitTestCase;
use Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage;
/**
* @coversDefaultClass \Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage
*
* @group Drupal
* @group KeyValueEntityStorage
*/
class KeyValueEntityStorageTest extends UnitTestCase {
/**
* The entity type.
*
* @var \Drupal\Core\Entity\EntityTypeInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $entityType;
/**
* The key value store.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $keyValueStore;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $moduleHandler;
/**
* The UUID service.
*
* @var \Drupal\Component\Uuid\UuidInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $uuidService;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $languageManager;
/**
* @var \Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage
*/
protected $entityStorage;
/**
* The mocked entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $entityManager;
/**
* The mocked cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $cacheBackend;
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => 'KeyValueEntityStorage',
'description' => 'Tests \Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage',
'group' => 'Entity',
);
}
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->entityType = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
}
/**
* Prepares the key value entity storage.
*
* @covers ::__construct()
*
* @param string $uuid_key
* (optional) The entity key used for the UUID. Defaults to 'uuid'.
*/
protected function setUpKeyValueEntityStorage($uuid_key = 'uuid') {
$this->entityType->expects($this->atLeastOnce())
->method('getKey')
->will($this->returnValueMap(array(
array('id', 'id'),
array('uuid', $uuid_key),
)));
$this->entityType->expects($this->atLeastOnce())
->method('id')
->will($this->returnValue('test_entity_type'));
$this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
$this->entityManager->expects($this->any())
->method('getDefinition')
->with('test_entity_type')
->will($this->returnValue($this->entityType));
$this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
$this->keyValueStore = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreInterface');
$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->entityStorage = new KeyValueEntityStorage($this->entityType, $this->keyValueStore, $this->uuidService, $this->languageManager);
$this->entityStorage->setModuleHandler($this->moduleHandler);
$container = new ContainerBuilder();
$container->set('entity.manager', $this->entityManager);
$container->set('language_manager', $this->languageManager);
$container->set('cache.test', $this->cacheBackend);
$container->setParameter('cache_bins', array('cache.test' => 'test'));
\Drupal::setContainer($container);
}
/**
* @covers ::create()
* @covers ::doCreate()
*/
public function testCreateWithPredefinedUuid() {
$this->entityType->expects($this->once())
->method('getClass')
->will($this->returnValue(get_class($this->getMockEntity())));
$this->setUpKeyValueEntityStorage();
$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()
* @covers ::doCreate()
*/
public function testCreateWithoutUuidKey() {
// Set up the entity storage to expect no UUID key.
$this->entityType->expects($this->once())
->method('getClass')
->will($this->returnValue(get_class($this->getMockEntity())));
$this->setUpKeyValueEntityStorage(NULL);
$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()
* @covers ::doCreate()
*
* @return \Drupal\Core\Entity\EntityInterface
*/
public function testCreate() {
$entity = $this->getMockEntity('Drupal\Core\Entity\Entity', array(), array('toArray'));
$this->entityType->expects($this->once())
->method('getClass')
->will($this->returnValue(get_class($entity)));
$this->setUpKeyValueEntityStorage();
$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()
* @covers ::doSave()
*
* @param \Drupal\Core\Entity\EntityInterface $entity
*
* @return \Drupal\Core\Entity\EntityInterface
*
* @depends testCreate
*/
public function testSaveInsert(EntityInterface $entity) {
$this->entityType->expects($this->once())
->method('getClass')
->will($this->returnValue(get_class($entity)));
$this->setUpKeyValueEntityStorage();
$expected = array('id' => 'foo');
$this->keyValueStore->expects($this->exactly(2))
->method('has')
->with('foo')
->will($this->returnValue(FALSE));
$this->keyValueStore->expects($this->never())
->method('getMultiple');
$this->keyValueStore->expects($this->never())
->method('delete');
$entity->expects($this->atLeastOnce())
->method('toArray')
->will($this->returnValue($expected));
$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->keyValueStore->expects($this->once())
->method('set')
->with('foo', $expected);
$return = $this->entityStorage->save($entity);
$this->assertSame(SAVED_NEW, $return);
return $entity;
}
/**
* @covers ::save()
* @covers ::doSave()
*
* @param \Drupal\Core\Entity\EntityInterface $entity
*
* @return \Drupal\Core\Entity\EntityInterface
*
* @depends testSaveInsert
*/
public function testSaveUpdate(EntityInterface $entity) {
$this->entityType->expects($this->once())
->method('getClass')
->will($this->returnValue(get_class($entity)));
$this->setUpKeyValueEntityStorage();
$expected = array('id' => 'foo');
$this->keyValueStore->expects($this->exactly(2))
->method('has')
->with('foo')
->will($this->returnValue(TRUE));
$this->keyValueStore->expects($this->once())
->method('getMultiple')
->with(array('foo'))
->will($this->returnValue(array(array('id' => 'foo'))));
$this->keyValueStore->expects($this->never())
->method('delete');
$this->moduleHandler->expects($this->at(0))
->method('getImplementations')
->with('entity_load')
->will($this->returnValue(array()));
$this->moduleHandler->expects($this->at(1))
->method('getImplementations')
->with('test_entity_type_load')
->will($this->returnValue(array()));
$this->moduleHandler->expects($this->at(2))
->method('invokeAll')
->with('test_entity_type_presave');
$this->moduleHandler->expects($this->at(3))
->method('invokeAll')
->with('entity_presave');
$this->moduleHandler->expects($this->at(4))
->method('invokeAll')
->with('test_entity_type_update');
$this->moduleHandler->expects($this->at(5))
->method('invokeAll')
->with('entity_update');
$this->keyValueStore->expects($this->once())
->method('set')
->with('foo', $expected);
$return = $this->entityStorage->save($entity);
$this->assertSame(SAVED_UPDATED, $return);
return $entity;
}
/**
* @covers ::save()
* @covers ::doSave()
*/
public function testSaveConfigEntity() {
$this->setUpKeyValueEntityStorage();
$entity = $this->getMockEntity('Drupal\Core\Config\Entity\ConfigEntityBase', array(array('id' => 'foo')), array(
'toArray',
'preSave',
));
$entity->enforceIsNew();
// When creating a new entity, the ID is tracked as the original ID.
$this->assertSame('foo', $entity->getOriginalId());
$expected = array('id' => 'foo');
$entity->expects($this->once())
->method('toArray')
->will($this->returnValue($expected));
$this->keyValueStore->expects($this->exactly(2))
->method('has')
->with('foo')
->will($this->returnValue(FALSE));
$this->keyValueStore->expects($this->once())
->method('set')
->with('foo', $expected);
$this->keyValueStore->expects($this->never())
->method('delete');
$return = $this->entityStorage->save($entity);
$this->assertSame(SAVED_NEW, $return);
return $entity;
}
/**
* @covers ::save()
* @covers ::doSave()
*
* @depends testSaveConfigEntity
*/
public function testSaveRenameConfigEntity(ConfigEntityInterface $entity) {
$this->entityType->expects($this->once())
->method('getClass')
->will($this->returnValue(get_class($entity)));
$this->setUpKeyValueEntityStorage();
$this->moduleHandler->expects($this->at(0))
->method('getImplementations')
->with('entity_load')
->will($this->returnValue(array()));
$this->moduleHandler->expects($this->at(1))
->method('getImplementations')
->with('test_entity_type_load')
->will($this->returnValue(array()));
$expected = array('id' => 'foo');
$entity->expects($this->once())
->method('toArray')
->will($this->returnValue($expected));
$this->keyValueStore->expects($this->exactly(2))
->method('has')
->with('foo')
->will($this->returnValue(TRUE));
$this->keyValueStore->expects($this->once())
->method('getMultiple')
->with(array('foo'))
->will($this->returnValue(array(array('id' => 'foo'))));
$this->keyValueStore->expects($this->once())
->method('delete')
->with('foo');
$this->keyValueStore->expects($this->once())
->method('set')
->with('bar', $expected);
// 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());
$return = $this->entityStorage->save($entity);
$this->assertSame(SAVED_UPDATED, $return);
$this->assertSame('bar', $entity->getOriginalId());
}
/**
* @covers ::save()
* @covers ::doSave()
*/
public function testSaveContentEntity() {
$this->entityType->expects($this->any())
->method('getKeys')
->will($this->returnValue(array(
'id' => 'id',
)));
$this->setUpKeyValueEntityStorage();
$expected = array('id' => 'foo');
$this->keyValueStore->expects($this->exactly(2))
->method('has')
->with('foo')
->will($this->returnValue(FALSE));
$this->keyValueStore->expects($this->once())
->method('set')
->with('foo', $expected);
$this->keyValueStore->expects($this->never())
->method('delete');
$entity = $this->getMockEntity('Drupal\Core\Entity\ContentEntityBase', array(), array(
'onSaveOrDelete',
'toArray',
'id',
));
$entity->expects($this->atLeastOnce())
->method('id')
->will($this->returnValue('foo'));
$entity->expects($this->once())
->method('toArray')
->will($this->returnValue($expected));
$this->entityStorage->save($entity);
}
/**
* @covers ::save()
* @covers ::doSave()
*
* @expectedException \Drupal\Core\Entity\EntityMalformedException
* @expectedExceptionMessage The entity does not have an ID.
*/
public function testSaveInvalid() {
$this->setUpKeyValueEntityStorage();
$entity = $this->getMockEntity('Drupal\Core\Config\Entity\ConfigEntityBase');
$this->entityStorage->save($entity);
$this->keyValueStore->expects($this->never())
->method('has');
$this->keyValueStore->expects($this->never())
->method('set');
$this->keyValueStore->expects($this->never())
->method('delete');
}
/**
* @covers ::save()
* @covers ::doSave()
*
* @expectedException \Drupal\Core\Entity\EntityStorageException
* @expectedExceptionMessage test_entity_type entity with ID foo already exists
*/
public function testSaveDuplicate() {
$this->setUpKeyValueEntityStorage();
$entity = $this->getMockEntity('Drupal\Core\Entity\Entity', array(array('id' => 'foo')));
$entity->enforceIsNew();
$this->keyValueStore->expects($this->once())
->method('has')
->will($this->returnValue(TRUE));
$this->keyValueStore->expects($this->never())
->method('set');
$this->keyValueStore->expects($this->never())
->method('delete');
$this->entityStorage->save($entity);
}
/**
* @covers ::load()
* @covers ::postLoad()
*/
public function testLoad() {
$entity = $this->getMockEntity();
$this->entityType->expects($this->once())
->method('getClass')
->will($this->returnValue(get_class($entity)));
$this->setUpKeyValueEntityStorage();
$this->keyValueStore->expects($this->once())
->method('getMultiple')
->with(array('foo'))
->will($this->returnValue(array(array('id' => 'foo'))));
$this->moduleHandler->expects($this->at(0))
->method('getImplementations')
->with('entity_load')
->will($this->returnValue(array()));
$this->moduleHandler->expects($this->at(1))
->method('getImplementations')
->with('test_entity_type_load')
->will($this->returnValue(array()));
$entity = $this->entityStorage->load('foo');
$this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity);
$this->assertSame('foo', $entity->id());
}
/**
* @covers ::load()
*/
public function testLoadMissingEntity() {
$this->entityType->expects($this->once())
->method('getClass');
$this->setUpKeyValueEntityStorage();
$this->keyValueStore->expects($this->once())
->method('getMultiple')
->with(array('foo'))
->will($this->returnValue(array()));
$this->moduleHandler->expects($this->never())
->method('getImplementations');
$entity = $this->entityStorage->load('foo');
$this->assertNull($entity);
}
/**
* @covers ::loadMultiple()
* @covers ::postLoad()
* @covers ::mapFromStorageRecords()
* @covers ::doLoadMultiple()
*/
public function testLoadMultipleAll() {
$expected['foo'] = $this->getMockEntity('Drupal\Core\Entity\Entity', array(array('id' => 'foo')));
$expected['bar'] = $this->getMockEntity('Drupal\Core\Entity\Entity', array(array('id' => 'bar')));
$this->entityType->expects($this->once())
->method('getClass')
->will($this->returnValue(get_class(reset($expected))));
$this->setUpKeyValueEntityStorage();
$this->keyValueStore->expects($this->once())
->method('getAll')
->will($this->returnValue(array(array('id' => 'foo'), array('id' => 'bar'))));
$this->moduleHandler->expects($this->at(0))
->method('getImplementations')
->with('entity_load')
->will($this->returnValue(array()));
$this->moduleHandler->expects($this->at(1))
->method('getImplementations')
->with('test_entity_type_load')
->will($this->returnValue(array()));
$entities = $this->entityStorage->loadMultiple();
foreach ($entities as $id => $entity) {
$this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity);
$this->assertSame($id, $entity->id());
$this->assertSame($id, $expected[$id]->id());
}
}
/**
* @covers ::loadMultiple()
* @covers ::postLoad()
* @covers ::mapFromStorageRecords()
* @covers ::doLoadMultiple()
*/
public function testLoadMultipleIds() {
$entity = $this->getMockEntity('Drupal\Core\Entity\Entity', array(array('id' => 'foo')));
$this->entityType->expects($this->once())
->method('getClass')
->will($this->returnValue(get_class($entity)));
$this->setUpKeyValueEntityStorage();
$expected[] = $entity;
$this->keyValueStore->expects($this->once())
->method('getMultiple')
->with(array('foo'))
->will($this->returnValue(array(array('id' => 'foo'))));
$this->moduleHandler->expects($this->at(0))
->method('getImplementations')
->with('entity_load')
->will($this->returnValue(array()));
$this->moduleHandler->expects($this->at(1))
->method('getImplementations')
->with('test_entity_type_load')
->will($this->returnValue(array()));
$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->setUpKeyValueEntityStorage();
$this->assertSame(FALSE, $this->entityStorage->loadRevision(1));
}
/**
* @covers ::deleteRevision()
*/
public function testDeleteRevision() {
$this->setUpKeyValueEntityStorage();
$this->assertSame(NULL, $this->entityStorage->deleteRevision(1));
}
/**
* @covers ::delete()
* @covers ::doDelete()
*/
public function testDelete() {
$entities['foo'] = $this->getMockEntity('Drupal\Core\Entity\Entity', array(array('id' => 'foo')));
$entities['bar'] = $this->getMockEntity('Drupal\Core\Entity\Entity', array(array('id' => 'bar')));
$this->entityType->expects($this->once())
->method('getClass')
->will($this->returnValue(get_class(reset($entities))));
$this->setUpKeyValueEntityStorage();
$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->keyValueStore->expects($this->once())
->method('deleteMultiple')
->with(array('foo', 'bar'));
$this->entityStorage->delete($entities);
}
/**
* @covers ::delete()
* @covers ::doDelete()
*/
public function testDeleteNothing() {
$this->setUpKeyValueEntityStorage();
$this->moduleHandler->expects($this->never())
->method($this->anything());
$this->keyValueStore->expects($this->never())
->method('delete');
$this->keyValueStore->expects($this->never())
->method('deleteMultiple');
$this->entityStorage->delete(array());
}
/**
* Creates an entity with specific methods mocked.
*
* @param string $class
* (optional) The concrete entity class to mock. Defaults to
* 'Drupal\Core\Entity\Entity'.
* @param array $arguments
* (optional) Arguments to pass to the constructor. An empty set of values
* and an entity type ID will be provided.
* @param array $methods
* (optional) The methods to mock.
*
* @return \Drupal\Core\Entity\EntityInterface|\PHPUnit_Framework_MockObject_MockObject
*/
public function getMockEntity($class = 'Drupal\Core\Entity\Entity', array $arguments = array(), $methods = array()) {
// Ensure the entity is passed at least an array of values and an entity
// type ID
if (!isset($arguments[0])) {
$arguments[0] = array();
}
if (!isset($arguments[1])) {
$arguments[1] = 'test_entity_type';
}
return $this->getMockForAbstractClass($class, $arguments, '', TRUE, TRUE, TRUE, $methods);
}
}
}
namespace {
if (!defined('SAVED_NEW')) {
define('SAVED_NEW', 1);
}
if (!defined('SAVED_UPDATED')) {
define('SAVED_UPDATED', 2);
}
}