Commit 189108a1 authored by catch's avatar catch

Issue #1833334 by das-peter, plach, Berdir, fago: EntityNG: integrate a...

Issue #1833334 by das-peter, plach, Berdir, fago: EntityNG: integrate a dynamic property data table handling.
parent db1122d6
......@@ -9,6 +9,7 @@
use PDO;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\DatabaseStorageController;
use Drupal\Core\Entity\EntityStorageException;
......@@ -36,6 +37,13 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
*/
protected $bundleKey;
/**
* The table that stores properties, if the entity has multilingual support.
*
* @var string
*/
protected $dataTable;
/**
* Overrides DatabaseStorageController::__construct().
*/
......@@ -44,6 +52,11 @@ public function __construct($entityType) {
$this->bundleKey = !empty($this->entityInfo['entity_keys']['bundle']) ? $this->entityInfo['entity_keys']['bundle'] : FALSE;
$this->entityClass = $this->entityInfo['class'];
// Check if the entity type has a dedicated table for properties.
if (!empty($this->entityInfo['data_table'])) {
$this->dataTable = $this->entityInfo['data_table'];
}
// Work-a-round to let load() get stdClass storage records without having to
// override it. We map storage records to entities in
// DatabaseStorageControllerNG:: mapFromStorageRecords().
......@@ -94,6 +107,34 @@ public function create(array $values) {
return $entity;
}
/**
* Builds an entity query.
*
* @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
* EntityQuery instance.
* @param array $values
* An associative array of properties of the entity, where the keys are the
* property names and the values are the values those properties must have.
*/
protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
if ($this->dataTable) {
// @todo We should not be using a condition to specify whether conditions
// apply to the default language. See http://drupal.org/node/1866330.
// Default to the original entity language if not explicitly specified
// otherwise.
if (!array_key_exists('default_langcode', $values)) {
$values['default_langcode'] = 1;
}
// If the 'default_langcode' flag is explicitly not set, we do not care
// whether the queried values are in the original entity language or not.
elseif ($values['default_langcode'] === NULL) {
unset($values['default_langcode']);
}
}
parent::buildPropertyQuery($entity_query, $values);
}
/**
* Overrides DatabaseStorageController::attachLoad().
*
......@@ -146,7 +187,7 @@ protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
* An array of entity objects implementing the EntityInterface.
*/
protected function mapFromStorageRecords(array $records, $load_revision = FALSE) {
$entities = array();
foreach ($records as $id => $record) {
$values = array();
foreach ($record as $name => $value) {
......@@ -155,9 +196,59 @@ protected function mapFromStorageRecords(array $records, $load_revision = FALSE)
}
$bundle = $this->bundleKey ? $record->{$this->bundleKey} : FALSE;
// Turn the record into an entity class.
$records[$id] = new $this->entityClass($values, $this->entityType, $bundle);
$entities[$id] = new $this->entityClass($values, $this->entityType, $bundle);
}
$this->attachPropertyData($entities, $load_revision);
return $entities;
}
/**
* Attaches property data in all languages for translatable properties.
*
* @param array &$entities
* Associative array of entities, keyed on the entity ID.
* @param boolean $load_revision
* (optional) TRUE if the revision should be loaded, defaults to FALSE.
*/
protected function attachPropertyData(array &$entities, $load_revision = FALSE) {
if ($this->dataTable) {
$query = db_select($this->dataTable, 'data', array('fetch' => PDO::FETCH_ASSOC))
->fields('data')
->condition($this->idKey, array_keys($entities))
->orderBy('data.' . $this->idKey);
if ($load_revision) {
// Get revision ID's.
$revision_ids = array();
foreach ($entities as $id => $entity) {
$revision_ids[] = $entity->get($this->revisionKey)->value;
}
$query->condition($this->revisionKey, $revision_ids);
}
$data = $query->execute();
// Fetch the field definitions to check which field is translatable.
$field_definition = $this->getFieldDefinitions(array());
$data_fields = array_flip($this->entityInfo['schema_fields_sql']['data_table']);
foreach ($data as $values) {
$id = $values[$this->idKey];
// Field values in default language are stored with LANGUAGE_DEFAULT as
// key.
$langcode = empty($values['default_langcode']) ? $values['langcode'] : LANGUAGE_DEFAULT;
$translation = $entities[$id]->getTranslation($langcode);
foreach ($field_definition as $name => $definition) {
// Set translatable properties only.
if (isset($data_fields[$name]) && !empty($definition['translatable'])) {
$translation->{$name}->value = $values[$name];
}
// Avoid initializing configurable fields before loading them.
elseif (!empty($definition['configurable'])) {
unset($entities[$id]->fields[$name]);
}
}
}
}
return $records;
}
/**
......@@ -191,6 +282,9 @@ public function save(EntityInterface $entity) {
if ($this->revisionKey) {
$record->{$this->revisionKey} = $this->saveRevision($entity);
}
if ($this->dataTable) {
$this->savePropertyData($entity);
}
$this->resetCache(array($entity->id()));
$this->postSave($entity, TRUE);
$this->invokeHook('update', $entity);
......@@ -201,10 +295,14 @@ public function save(EntityInterface $entity) {
$entity->{$this->idKey}->value = $record->{$this->idKey};
$record->{$this->revisionKey} = $this->saveRevision($entity);
}
$entity->{$this->idKey}->value = $record->{$this->idKey};
if ($this->dataTable) {
$this->savePropertyData($entity);
}
// Reset general caches, but keep caches specific to certain entities.
$this->resetCache(array());
$entity->{$this->idKey}->value = $record->{$this->idKey};
$entity->enforceIsNew(FALSE);
$this->postSave($entity, FALSE);
$this->invokeHook('insert', $entity);
......@@ -262,6 +360,31 @@ protected function saveRevision(EntityInterface $entity) {
return $record->{$this->revisionKey};
}
/**
* Stores the entity property language-aware data.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
*/
protected function savePropertyData(EntityInterface $entity) {
// Delete and insert to handle removed values.
db_delete($this->dataTable)
->condition($this->idKey, $entity->id())
->execute();
$query = db_insert($this->dataTable);
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
$record = $this->mapToDataStorageRecord($entity, $langcode);
$values = (array) $record;
$query
->fields(array_keys($values))
->values($values);
}
$query->execute();
}
/**
* Overrides DatabaseStorageController::invokeHook().
*
......@@ -286,6 +409,12 @@ protected function invokeHook($hook, EntityInterface $entity) {
/**
* Maps from an entity object to the storage record of the base table.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
*
* @return \stdClass
* The record to store.
*/
protected function mapToStorageRecord(EntityInterface $entity) {
$record = new \stdClass();
......@@ -297,6 +426,12 @@ protected function mapToStorageRecord(EntityInterface $entity) {
/**
* Maps from an entity object to the storage record of the revision table.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
*
* @return \stdClass
* The record to store.
*/
protected function mapToRevisionStorageRecord(EntityInterface $entity) {
$record = new \stdClass();
......@@ -305,4 +440,81 @@ protected function mapToRevisionStorageRecord(EntityInterface $entity) {
}
return $record;
}
/**
* Maps from an entity object to the storage record of the data table.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
* @param $langcode
* The language code of the translation to get.
*
* @return \stdClass
* The record to store.
*/
protected function mapToDataStorageRecord(EntityInterface $entity, $langcode) {
$default_langcode = $entity->language()->langcode;
// Don't use strict mode, this way there's no need to do checks here, as
// non-translatable properties are replicated for each language.
$translation = $entity->getTranslation($langcode, FALSE);
$record = new \stdClass();
foreach ($this->entityInfo['schema_fields_sql']['data_table'] as $name) {
$record->$name = $translation->$name->value;
}
$record->langcode = $langcode;
$record->default_langcode = intval($default_langcode == $langcode);
return $record;
}
/**
* Overwrites \Drupal\Core\Entity\DatabaseStorageController::delete().
*/
public function delete(array $entities) {
if (!$entities) {
// If no IDs or invalid IDs were passed, do nothing.
return;
}
$transaction = db_transaction();
try {
$this->preDelete($entities);
foreach ($entities as $id => $entity) {
$this->invokeHook('predelete', $entity);
}
$ids = array_keys($entities);
db_delete($this->entityInfo['base_table'])
->condition($this->idKey, $ids)
->execute();
if ($this->revisionKey) {
db_delete($this->revisionTable)
->condition($this->idKey, $ids)
->execute();
}
if ($this->dataTable) {
db_delete($this->dataTable)
->condition($this->idKey, $ids)
->execute();
}
// Reset the cache as soon as the changes have been applied.
$this->resetCache($ids);
$this->postDelete($entities);
foreach ($entities as $id => $entity) {
$this->invokeHook('delete', $entity);
}
// Ignore slave server temporarily.
db_ignore_slave();
}
catch (Exception $e) {
$transaction->rollback();
watchdog_exception($this->entityType, $e);
throw new EntityStorageException($e->getMessage, $e->getCode, $e);
}
}
}
......@@ -187,6 +187,7 @@ public function submit(array $form, array &$form_state) {
// Remove button and internal Form API values from submitted values.
form_state_values_clean($form_state);
$this->updateFormLangcode($form_state);
$this->submitEntityLanguage($form, $form_state);
$entity = $this->buildEntity($form, $form_state);
$this->setEntity($entity, $form_state);
......@@ -246,24 +247,32 @@ public function getFormLangcode(array $form_state) {
/**
* Implements EntityFormControllerInterface::isDefaultFormLangcode().
*/
public function isDefaultFormLangcode($form_state) {
public function isDefaultFormLangcode(array $form_state) {
return $this->getFormLangcode($form_state) == $this->getEntity($form_state)->language()->langcode;
}
/**
* Handle possible entity language changes.
* Updates the form language to reflect any change to the entity language.
*
* @param array $form
* An associative array containing the structure of the form.
* @param array $form_state
* A reference to a keyed array containing the current state of the form.
* A keyed array containing the current state of the form.
*/
protected function submitEntityLanguage(array $form, array &$form_state) {
protected function updateFormLangcode(array $form_state) {
// Update the form language as it might have changed.
if (isset($form_state['values']['langcode']) && $this->isDefaultFormLangcode($form_state)) {
$form_state['langcode'] = $form_state['values']['langcode'];
}
}
/**
* Handle possible entity language changes.
*
* @param array $form
* An associative array containing the structure of the form.
* @param array $form_state
* A reference to a keyed array containing the current state of the form.
*/
protected function submitEntityLanguage(array $form, array &$form_state) {
$entity = $this->getEntity($form_state);
$entity_type = $entity->entityType();
......
......@@ -47,12 +47,12 @@ public function getFormLangcode(array $form_state);
* Checks whether the current form language matches the entity one.
*
* @param array $form_state
* A reference to a keyed array containing the current state of the form.
* A keyed array containing the current state of the form.
*
* @return boolean
* Returns TRUE if the entity form language matches the entity one.
*/
public function isDefaultFormLangcode($form_state);
public function isDefaultFormLangcode(array $form_state);
/**
* Returns the operation identifying the form controller.
......
......@@ -47,6 +47,16 @@ public function validate(array $form, array &$form_state) {
form_execute_handlers('validate', $form, $form_state);
}
/**
* Overrides EntityFormController::submitEntityLanguage().
*/
protected function submitEntityLanguage(array $form, array &$form_state) {
// Nothing to do here, as original field values are always stored with
// LANGUAGE_DEFAULT language.
// @todo Delete this method when merging EntityFormControllerNG with
// EntityFormController.
}
/**
* Overrides EntityFormController::buildEntity().
*/
......
......@@ -294,6 +294,9 @@ public function processDefinition(&$definition, $plugin_id) {
// Drupal\Core\Entity\DatabaseStorageControllerInterface::buildQuery().
if (isset($definition['base_table'])) {
$definition['schema_fields_sql']['base_table'] = drupal_schema_fields_sql($definition['base_table']);
if (isset($definition['data_table'])) {
$definition['schema_fields_sql']['data_table'] = drupal_schema_fields_sql($definition['data_table']);
}
if (isset($definition['revision_table'])) {
$definition['schema_fields_sql']['revision_table'] = drupal_schema_fields_sql($definition['revision_table']);
}
......
......@@ -102,7 +102,7 @@ protected function init() {
}
/**
* Magic __wakeup() implemenation.
* Magic __wakeup() implementation.
*/
public function __wakeup() {
$this->init();
......
......@@ -32,43 +32,58 @@ public static function getInfo() {
/**
* Tests basic CRUD functionality of the Entity API.
*/
function testCRUD() {
public function testCRUD() {
$user1 = $this->drupalCreateUser();
// All entity variations have to have the same results.
foreach (entity_test_entity_types() as $entity_type) {
$this->assertCRUD($entity_type, $user1);
}
}
/**
* Executes a test set for a defined entity type and user.
*
* @param string $entity_type
* The entity type to run the tests with.
* @param \Drupal\user\Plugin\Core\Entity\User $user1
* The user to run the tests with.
*/
protected function assertCRUD($entity_type, \Drupal\user\Plugin\Core\Entity\User $user1) {
// Create some test entities.
$entity = entity_create('entity_test', array('name' => 'test', 'user_id' => $user1->uid));
$entity = entity_create($entity_type, array('name' => 'test', 'user_id' => $user1->uid));
$entity->save();
$entity = entity_create('entity_test', array('name' => 'test2', 'user_id' => $user1->uid));
$entity = entity_create($entity_type, array('name' => 'test2', 'user_id' => $user1->uid));
$entity->save();
$entity = entity_create('entity_test', array('name' => 'test', 'user_id' => NULL));
$entity = entity_create($entity_type, array('name' => 'test', 'user_id' => NULL));
$entity->save();
$entities = array_values(entity_load_multiple_by_properties('entity_test', array('name' => 'test')));
$this->assertEqual($entities[0]->name->value, 'test', 'Created and loaded entity.');
$this->assertEqual($entities[1]->name->value, 'test', 'Created and loaded entity.');
$entities = array_values(entity_load_multiple_by_properties($entity_type, array('name' => 'test')));
$this->assertEqual($entities[0]->name->value, 'test', format_string('%entity_type: Created and loaded entity', array('%entity_type' => $entity_type)));
$this->assertEqual($entities[1]->name->value, 'test', format_string('%entity_type: Created and loaded entity', array('%entity_type' => $entity_type)));
// Test loading a single entity.
$loaded_entity = entity_test_load($entity->id());
$this->assertEqual($loaded_entity->id(), $entity->id(), 'Loaded a single entity by id.');
$loaded_entity = entity_load($entity_type, $entity->id());
$this->assertEqual($loaded_entity->id(), $entity->id(), format_string('%entity_type: Loaded a single entity by id.', array('%entity_type' => $entity_type)));
// Test deleting an entity.
$entities = array_values(entity_load_multiple_by_properties('entity_test', array('name' => 'test2')));
$entities = array_values(entity_load_multiple_by_properties($entity_type, array('name' => 'test2')));
$entities[0]->delete();
$entities = array_values(entity_load_multiple_by_properties('entity_test', array('name' => 'test2')));
$this->assertEqual($entities, array(), 'Entity deleted.');
$entities = array_values(entity_load_multiple_by_properties($entity_type, array('name' => 'test2')));
$this->assertEqual($entities, array(), format_string('%entity_type: Entity deleted.', array('%entity_type' => $entity_type)));
// Test updating an entity.
$entities = array_values(entity_load_multiple_by_properties('entity_test', array('name' => 'test')));
$entities = array_values(entity_load_multiple_by_properties($entity_type, array('name' => 'test')));
$entities[0]->name->value = 'test3';
$entities[0]->save();
$entity = entity_test_load($entities[0]->id());
$this->assertEqual($entity->name->value, 'test3', 'Entity updated.');
$entity = entity_load($entity_type, $entities[0]->id());
$this->assertEqual($entity->name->value, 'test3', format_string('%entity_type: Entity updated.', array('%entity_type' => $entity_type)));
// Try deleting multiple test entities by deleting all.
$ids = array_keys(entity_test_load_multiple());
entity_test_delete_multiple($ids);
$ids = array_keys(entity_load_multiple($entity_type));
entity_delete_multiple($entity_type, $ids);
$all = entity_test_load_multiple();
$this->assertTrue(empty($all), 'Deleted all entities.');
$all = entity_load_multiple($entity_type);
$this->assertTrue(empty($all), format_string('%entity_type: Deleted all entities.', array('%entity_type' => $entity_type)));
}
}
......@@ -39,6 +39,19 @@ function setUp() {
* Tests basic form CRUD functionality.
*/
function testFormCRUD() {
// All entity variations have to have the same results.
foreach (entity_test_entity_types() as $entity_type) {
$this->assertFormCRUD($entity_type);
}
}
/**
* Executes the form CRUD tests for the given entity type.
*
* @param string $entity_type
* The entity type to run the tests with.
*/
protected function assertFormCRUD($entity_type) {
$langcode = LANGUAGE_NOT_SPECIFIED;
$name1 = $this->randomName(8);
$name2 = $this->randomName(10);
......@@ -49,28 +62,27 @@ function testFormCRUD() {
"field_test_text[$langcode][0][value]" => $this->randomName(16),
);
$this->drupalPost('entity-test/add', $edit, t('Save'));
$entity = $this->loadEntityByName($name1);
$this->assertTrue($entity, 'Entity found in the database.');
$this->drupalPost($entity_type . '/add', $edit, t('Save'));
$entity = $this->loadEntityByName($entity_type, $name1);
$this->assertTrue($entity, format_string('%entity_type: Entity found in the database.', array('%entity_type' => $entity_type)));
$edit['name'] = $name2;
$this->drupalPost('entity-test/manage/' . $entity->id() . '/edit', $edit, t('Save'));
$entity = $this->loadEntityByName($name1);
$this->assertFalse($entity, 'The entity has been modified.');
$entity = $this->loadEntityByName($name2);
$this->assertTrue($entity, 'Modified entity found in the database.');
$this->assertNotEqual($entity->name->value, $name1, 'The entity name has been modified.');
$this->drupalPost($entity_type . '/manage/' . $entity->id() . '/edit', $edit, t('Save'));
$entity = $this->loadEntityByName($entity_type, $name1);
$this->assertFalse($entity, format_string('%entity_type: The entity has been modified.', array('%entity_type' => $entity_type)));
$entity = $this->loadEntityByName($entity_type, $name2);
$this->assertTrue($entity, format_string('%entity_type: Modified entity found in the database.', array('%entity_type' => $entity_type)));
$this->assertNotEqual($entity->name->value, $name1, format_string('%entity_type: The entity name has been modified.', array('%entity_type' => $entity_type)));
$this->drupalPost('entity-test/manage/' . $entity->id() . '/edit', array(), t('Delete'));
$entity = $this->loadEntityByName($name2);
$this->assertFalse($entity, 'Entity not found in the database.');
$this->drupalPost($entity_type . '/manage/' . $entity->id() . '/edit', array(), t('Delete'));
$entity = $this->loadEntityByName($entity_type, $name2);
$this->assertFalse($entity, format_string('%entity_type: Entity not found in the database.', array('%entity_type' => $entity_type)));
}
/**
* Loads a test entity by name always resetting the storage controller cache.
*/
protected function loadEntityByName($name) {
$entity_type = 'entity_test';
protected function loadEntityByName($entity_type, $name) {
// Always load the entity from the database to ensure that changes are
// correctly picked up.
entity_get_controller($entity_type)->resetCache();
......
......@@ -47,8 +47,22 @@ public function setUp() {
*/
public function testRevisions() {
// All revisable entity variations have to have the same results.
foreach (entity_test_entity_types(ENTITY_TEST_TYPES_REVISABLE) as $entity_type) {
$this->assertRevisions($entity_type);
}
}
/**
* Executes the revision tests for the given entity type.
*
* @param string $entity_type
* The entity type to run the tests with.
*/
protected function assertRevisions($entity_type) {
// Create initial entity.
$entity = entity_create('entity_test', array(
$entity = entity_create($entity_type, array(
'name' => 'foo',
'user_id' => $this->web_user->uid,
));
......@@ -67,7 +81,7 @@ public function testRevisions() {
$legacy_name = $entity->name->value;
$legacy_text = $entity->field_test_text->value;
$entity = entity_test_load($entity->id->value);
$entity = entity_load($entity_type, $entity->id->value);
<