Commit 95c69f5b authored by catch's avatar catch

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

parent 773a9929
......@@ -364,6 +364,9 @@ services:
entity.query.sql:
class: Drupal\Core\Entity\Query\Sql\QueryFactory
arguments: ['@database']
entity.query.keyvalue:
class: Drupal\Core\Entity\KeyValueStore\Query\QueryFactory
arguments: ['@keyvalue']
router.dumper:
class: Drupal\Core\Routing\MatcherDumper
arguments: ['@database', '@state']
......
<?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';
}
}
<?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 {
}
<?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);
}
}
<?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');
}
}
......@@ -20,6 +20,11 @@
*/
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.
*
......@@ -164,7 +169,7 @@ function testCRUD() {
// Test with an ID of the maximum allowed length.
$id_length_config_test = entity_create('config_test', array(
'id' => $this->randomName(ConfigEntityStorage::MAX_ID_LENGTH),
'id' => $this->randomName(static::MAX_ID_LENGTH),
));
try {
$id_length_config_test->save();
......@@ -178,19 +183,19 @@ function testCRUD() {
// Test with an ID exeeding the maximum allowed length.
$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 {
$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(
'@length' => strlen($id_length_config_test->id),
'@max' => ConfigEntityStorage::MAX_ID_LENGTH,
'@max' => static::MAX_ID_LENGTH,
)));
}
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(
'@length' => strlen($id_length_config_test->id),
'@max' => ConfigEntityStorage::MAX_ID_LENGTH,
'@max' => static::MAX_ID_LENGTH,
)));
}
......
<?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',
);
}
}
<?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);
}
}
}
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
<?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'));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment