From 8fcd5546ba5a8914adc3af83d5b53652e7e34032 Mon Sep 17 00:00:00 2001 From: webchick Date: Wed, 21 Nov 2012 14:07:53 -0800 Subject: [PATCH] Issue #1831444 by das-peter, Berdir, andypost: Added EntityNG: Support for revisions for entity save and delete operations. --- .../Core/Entity/DatabaseStorageController.php | 13 ++- .../Entity/DatabaseStorageControllerNG.php | 83 +++++++++++++++- .../field_test/TestEntityController.php | 6 +- .../lib/Drupal/node/NodeStorageController.php | 14 +-- .../Tests/Entity/EntityRevisionsTest.php | 99 +++++++++++++++++++ .../modules/entity_test/entity_test.install | 66 +++++++++++++ .../EntityTestStorageController.php | 34 +++++-- .../Plugin/Core/Entity/EntityTest.php | 18 +++- 8 files changed, 306 insertions(+), 27 deletions(-) create mode 100644 core/modules/system/lib/Drupal/system/Tests/Entity/EntityRevisionsTest.php diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php index 267e10c6329..bf515dea189 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php @@ -552,13 +552,16 @@ class DatabaseStorageController implements EntityStorageControllerInterface { $record = (array) $entity; // When saving a new revision, set any existing revision ID to NULL so as to - // ensure that a new revision will actually be created, then store the old - // revision ID in a separate property for use by hook implementations. + // ensure that a new revision will actually be created. if ($entity->isNewRevision() && $record[$this->revisionKey]) { $record[$this->revisionKey] = NULL; } + // Cast to object as preSaveRevision() expects one to be compatible with the + // upcoming NG storage controller. + $record = (object) $record; $this->preSaveRevision($record, $entity); + $record = (array) $record; if ($entity->isNewRevision()) { drupal_write_record($this->revisionTable, $record); @@ -613,12 +616,12 @@ class DatabaseStorageController implements EntityStorageControllerInterface { /** * Act on a revision before being saved. * - * @param array $record - * The revision array. + * @param \stdClass $record + * The revision object. * @param Drupal\Core\Entity\EntityInterface $entity * The entity object. */ - protected function preSaveRevision(array &$record, EntityInterface $entity) { } + protected function preSaveRevision(\stdClass $record, EntityInterface $entity) { } /** * Invokes a hook on behalf of the entity. diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php index 6f22d8393d8..10a7a77e722 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php @@ -105,7 +105,7 @@ class DatabaseStorageControllerNG extends DatabaseStorageController { protected function attachLoad(&$queried_entities, $load_revision = FALSE) { // Now map the record values to the according entity properties and // activate compatibility mode. - $queried_entities = $this->mapFromStorageRecords($queried_entities); + $queried_entities = $this->mapFromStorageRecords($queried_entities, $load_revision); // Attach fields. if ($this->entityInfo['fieldable']) { @@ -139,10 +139,15 @@ class DatabaseStorageControllerNG extends DatabaseStorageController { /** * Maps from storage records to entity objects. * + * @param array $records + * Associative array of query results, keyed on the entity ID. + * @param boolean $load_revision + * (optional) TRUE if the revision should be loaded, defaults to FALSE. + * * @return array * An array of entity objects implementing the EntityInterface. */ - protected function mapFromStorageRecords(array $records) { + protected function mapFromStorageRecords(array $records, $load_revision = FALSE) { foreach ($records as $id => $record) { $entity = new $this->entityClass(array(), $this->entityType); @@ -181,13 +186,27 @@ class DatabaseStorageControllerNG extends DatabaseStorageController { $entity->updateOriginalValues(); if (!$entity->isNew()) { - $return = drupal_write_record($this->entityInfo['base_table'], $record, $this->idKey); + if ($entity->isDefaultRevision()) { + $return = drupal_write_record($this->entityInfo['base_table'], $record, $this->idKey); + } + else { + // @todo, should a different value be returned when saving an entity + // with $isDefaultRevision = FALSE? + $return = FALSE; + } + if ($this->revisionKey) { + $record->{$this->revisionKey} = $this->saveRevision($entity); + } $this->resetCache(array($entity->id())); $this->postSave($entity, TRUE); $this->invokeHook('update', $entity); } else { $return = drupal_write_record($this->entityInfo['base_table'], $record); + if ($this->revisionKey) { + $entity->{$this->idKey}->value = $record->{$this->idKey}; + $record->{$this->revisionKey} = $this->saveRevision($entity); + } // Reset general caches, but keep caches specific to certain entities. $this->resetCache(array()); @@ -210,6 +229,44 @@ class DatabaseStorageControllerNG extends DatabaseStorageController { } } + /** + * Saves an entity revision. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity object. + * + * @return integer + * The revision id. + */ + protected function saveRevision(EntityInterface $entity) { + $record = $this->mapToRevisionStorageRecord($entity); + + // When saving a new revision, set any existing revision ID to NULL so as to + // ensure that a new revision will actually be created. + if ($entity->isNewRevision() && isset($record->{$this->revisionKey})) { + $record->{$this->revisionKey} = NULL; + } + + $this->preSaveRevision($record, $entity); + + if ($entity->isNewRevision()) { + drupal_write_record($this->revisionTable, $record); + if ($entity->isDefaultRevision()) { + db_update($this->entityInfo['base_table']) + ->fields(array($this->revisionKey => $record->{$this->revisionKey})) + ->condition($this->idKey, $record->{$this->idKey}) + ->execute(); + } + $entity->setNewRevision(FALSE); + } + else { + drupal_write_record($this->revisionTable, $record, $this->revisionKey); + } + // Make sure to update the new revision key for the entity. + $entity->{$this->revisionKey}->value = $record->{$this->revisionKey}; + return $record->{$this->revisionKey}; + } + /** * Overrides DatabaseStorageController::invokeHook(). * @@ -217,7 +274,13 @@ class DatabaseStorageControllerNG extends DatabaseStorageController { * afterwards. */ protected function invokeHook($hook, EntityInterface $entity) { - if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $hook)) { + $function = 'field_attach_' . $hook; + // @todo: field_attach_delete_revision() is named the wrong way round, + // consider renaming it. + if ($function == 'field_attach_revision_delete') { + $function = 'field_attach_delete_revision'; + } + if (!empty($this->entityInfo['fieldable']) && function_exists($function)) { $entity->setCompatibilityMode(TRUE); $function($this->entityType, $entity); $entity->setCompatibilityMode(FALSE); @@ -239,4 +302,16 @@ class DatabaseStorageControllerNG extends DatabaseStorageController { } return $record; } + + /** + * Maps from an entity object to the storage record of the revision table. + */ + protected function mapToRevisionStorageRecord(EntityInterface $entity) { + $record = new \stdClass(); + foreach ($this->entityInfo['schema_fields_sql']['revision_table'] as $name) { + $record->$name = $entity->$name->value; + } + + return $record; + } } diff --git a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/TestEntityController.php b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/TestEntityController.php index ab62a9a86f2..0fe760226b6 100644 --- a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/TestEntityController.php +++ b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/TestEntityController.php @@ -18,10 +18,10 @@ class TestEntityController extends DatabaseStorageController { /** * Overrides Drupal\Core\Entity\DatabaseStorageController::preSaveRevision(). */ - public function preSaveRevision(array &$record, EntityInterface $entity) { + public function preSaveRevision(\stdClass $record, EntityInterface $entity) { // Allow for predefined revision ids. - if (!empty($record['use_provided_revision_id'])) { - $record['ftvid'] = $record['use_provided_revision_id']; + if (!empty($record->use_provided_revision_id)) { + $record->ftvid = $record->use_provided_revision_id; } } diff --git a/core/modules/node/lib/Drupal/node/NodeStorageController.php b/core/modules/node/lib/Drupal/node/NodeStorageController.php index 1a56fe02be8..d855384e95a 100644 --- a/core/modules/node/lib/Drupal/node/NodeStorageController.php +++ b/core/modules/node/lib/Drupal/node/NodeStorageController.php @@ -100,7 +100,7 @@ class NodeStorageController extends DatabaseStorageController { /** * Overrides Drupal\Core\Entity\DatabaseStorageController::preSaveRevision(). */ - protected function preSaveRevision(array &$record, EntityInterface $entity) { + protected function preSaveRevision(\stdClass $record, EntityInterface $entity) { if ($entity->isNewRevision()) { // When inserting either a new node or a new node revision, $node->log // must be set because {node_revision}.log is a text column and therefore @@ -110,23 +110,23 @@ class NodeStorageController extends DatabaseStorageController { // empty string in that case. // @todo: Make the {node_revision}.log column nullable so that we can // remove this check. - if (!isset($record['log'])) { - $record['log'] = ''; + if (!isset($record->log)) { + $record->log = ''; } } - elseif (!isset($record['log']) || $record['log'] === '') { + elseif (!isset($record->log) || $record->log === '') { // If we are updating an existing node without adding a new revision, we // need to make sure $node->log is unset whenever it is empty. As long as // $node->log is unset, drupal_write_record() will not attempt to update // the existing database column when re-saving the revision; therefore, // this code allows us to avoid clobbering an existing log entry with an // empty one. - unset($record['log']); + unset($record->log); } if ($entity->isNewRevision()) { - $record['timestamp'] = REQUEST_TIME; - $record['uid'] = isset($record['revision_uid']) ? $record['revision_uid'] : $GLOBALS['user']->uid; + $record->timestamp = REQUEST_TIME; + $record->uid = isset($record->revision_uid) ? $record->revision_uid : $GLOBALS['user']->uid; } } diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityRevisionsTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityRevisionsTest.php new file mode 100644 index 00000000000..c94972dbcef --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityRevisionsTest.php @@ -0,0 +1,99 @@ + 'Entity revisions', + 'description' => 'Create a entity with revisions and test viewing, saving, reverting, and deleting revisions.', + 'group' => 'Entity API', + ); + } + + public function setUp() { + parent::setUp(); + + // Create and login user. + $this->web_user = $this->drupalCreateUser(array( + 'view revisions', + 'revert revisions', + 'delete revisions', + 'administer entity_test content', + )); + $this->drupalLogin($this->web_user); + } + + /** + * Check node revision related operations. + */ + public function testRevisions() { + + // Create initial entity. + $entity = entity_create('entity_test', array( + 'name' => 'foo', + 'user_id' => $this->web_user->uid, + )); + $entity->field_test_text->value = 'bar'; + $entity->save(); + + $entities = array(); + $names = array(); + $texts = array(); + $revision_ids = array(); + + // Create three revisions. + $revision_count = 3; + for ($i = 0; $i < $revision_count; $i++) { + $legacy_revision_id = $entity->revision_id->value; + $legacy_name = $entity->name->value; + $legacy_text = $entity->field_test_text->value; + + $entity = entity_test_load($entity->id->value); + $entity->setNewRevision(TRUE); + $names[] = $entity->name->value = $this->randomName(32); + $texts[] = $entity->field_test_text->value = $this->randomName(32); + $entity->save(); + $revision_ids[] = $entity->revision_id->value; + + // Check that the fields and properties contain new content. + $this->assertTrue($entity->revision_id->value > $legacy_revision_id, 'Revision ID changed.'); + $this->assertNotEqual($entity->name->value, $legacy_name, 'Name changed.'); + $this->assertNotEqual($entity->field_test_text->value, $legacy_text, 'Text changed.'); + } + + for ($i = 0; $i < $revision_count; $i++) { + // Load specific revision. + $entity_revision = entity_revision_load('entity_test', $revision_ids[$i]); + + // Check if properties and fields contain the revision specific content. + $this->assertEqual($entity_revision->revision_id->value, $revision_ids[$i], 'Revision ID matches.'); + $this->assertEqual($entity_revision->name->value, $names[$i], 'Name matches.'); + $this->assertEqual($entity_revision->field_test_text->value, $texts[$i], 'Text matches.'); + } + + // Confirm the correct revision text appears in the edit form. + $entity = entity_load('entity_test', $entity->id->value); + $this->drupalGet('entity-test/manage/' . $entity->id->value); + $this->assertFieldById('edit-name', $entity->name->value, 'Name matches in UI.'); + $this->assertFieldById('edit-field-test-text-und-0-value', $entity->field_test_text->value, 'Text matches in UI.'); + } +} diff --git a/core/modules/system/tests/modules/entity_test/entity_test.install b/core/modules/system/tests/modules/entity_test/entity_test.install index 4122d01a454..617986a1aaf 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.install +++ b/core/modules/system/tests/modules/entity_test/entity_test.install @@ -43,6 +43,13 @@ function entity_test_schema() { 'not null' => TRUE, 'description' => 'Primary Key: Unique entity-test item ID.', ), + 'revision_id' => array( + 'description' => 'The current {entity_test_property_revision}.revision_id version identifier.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), 'uuid' => array( 'description' => 'Unique Key: Universally unique identifier for this entity.', 'type' => 'varchar', @@ -71,6 +78,13 @@ function entity_test_schema() { 'not null' => TRUE, 'description' => 'The {entity_test}.id of the test entity.', ), + 'revision_id' => array( + 'description' => 'The current {entity_test_property_revision}.revision_id version identifier.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), 'langcode' => array( 'description' => 'The {language}.langcode of this variant of this test entity.', 'type' => 'varchar', @@ -108,5 +122,57 @@ function entity_test_schema() { ), 'primary key' => array('id', 'langcode'), ); + $schema['entity_test_property_revision'] = array( + 'description' => 'Stores entity_test item property revisions.', + 'fields' => array( + 'id' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The {entity_test}.id of the test entity.', + ), + 'revision_id' => array( + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The primary identifier for this version.', + ), + 'langcode' => array( + 'description' => 'The {language}.langcode of this variant of this test entity.', + 'type' => 'varchar', + 'length' => 12, + 'not null' => TRUE, + 'default' => '', + ), + 'default_langcode' => array( + 'description' => 'Boolean indicating whether the current variant is in the original entity language.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 1, + ), + 'name' => array( + 'description' => 'The name of the test entity.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + 'user_id' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'default' => NULL, + 'description' => "The {users}.uid of the associated user.", + ), + ), + 'indexes' => array( + 'user_id' => array('user_id'), + ), + 'foreign keys' => array( + 'user_id' => array('users' => 'uid'), + 'id' => array('entity_test' => 'id'), + ), + 'primary key' => array('revision_id', 'id', 'langcode'), + ); return $schema; } diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php index da502b88fc3..e3b45fa9827 100644 --- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php @@ -47,23 +47,31 @@ class EntityTestStorageController extends DatabaseStorageControllerNG { * @return array * An array of entity objects implementing the EntityInterface. */ - protected function mapFromStorageRecords(array $records) { - $records = parent::mapFromStorageRecords($records); + protected function mapFromStorageRecords(array $records, $load_revision = FALSE) { + $records = parent::mapFromStorageRecords($records, $load_revision); // Load data of translatable properties. - $this->attachPropertyData($records); + $this->attachPropertyData($records, $load_revision); return $records; } /** * Attaches property data in all languages for translatable properties. */ - protected function attachPropertyData(&$queried_entities) { - $data = db_select('entity_test_property_data', 'data', array('fetch' => PDO::FETCH_ASSOC)) + protected function attachPropertyData(&$queried_entities, $load_revision = FALSE) { + $query = db_select('entity_test_property_data', 'data', array('fetch' => PDO::FETCH_ASSOC)) ->fields('data') ->condition('id', array_keys($queried_entities)) - ->orderBy('data.id') - ->execute(); + ->orderBy('data.id'); + if ($load_revision) { + // Get revision id's. + $revision_ids = array(); + foreach ($queried_entities as $id => $entity) { + $revision_ids[] = $entity->get('revision_id')->value; + } + $query->condition('revision_id', $revision_ids); + } + $data = $query->execute(); foreach ($data as $values) { $id = $values['id']; @@ -96,6 +104,7 @@ class EntityTestStorageController extends DatabaseStorageControllerNG { $values = array( 'id' => $entity->id(), + 'revision_id' => $entity->getRevisionId(), 'langcode' => $langcode, 'default_langcode' => intval($default_langcode == $langcode), 'name' => $translation->name->value, @@ -129,6 +138,12 @@ class EntityTestStorageController extends DatabaseStorageControllerNG { 'type' => 'integer_field', 'read-only' => TRUE, ); + $fields['revision_id'] = array( + 'label' => t('ID'), + 'description' => t('The version id of the test entity.'), + 'type' => 'integer_field', + 'read-only' => TRUE, + ); $fields['uuid'] = array( 'label' => t('UUID'), 'description' => t('The UUID of the test entity.'), @@ -139,6 +154,11 @@ class EntityTestStorageController extends DatabaseStorageControllerNG { 'description' => t('The language code of the test entity.'), 'type' => 'language_field', ); + $fields['default_langcode'] = array( + 'label' => t('Default language'), + 'description' => t('Flag to inditcate whether this is the default language.'), + 'type' => 'boolean_field', + ); $fields['name'] = array( 'label' => t('Name'), 'description' => t('The name of the test entity.'), diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php index bf81594f692..9b4d3688d44 100644 --- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php @@ -26,10 +26,12 @@ use Drupal\Core\Annotation\Translation; * translation_controller_class = "Drupal\entity_test\EntityTestTranslationController", * base_table = "entity_test", * data_table = "entity_test_property_data", + * revision_table = "entity_test_property_revision", * fieldable = TRUE, * entity_keys = { * "id" = "id", - * "uuid" = "uuid" + * "uuid" = "uuid", + * "revision" = "revision_id" * }, * menu_base_path = "entity-test/manage/%entity_test" * ) @@ -50,6 +52,13 @@ class EntityTest extends EntityNG { */ public $uuid; + /** + * The entity revision id. + * + * @var \Drupal\Core\Entity\Field\FieldInterface + */ + public $revision_id; + /** * The name of the test entity. * @@ -74,6 +83,7 @@ class EntityTest extends EntityNG { unset($this->id); unset($this->langcode); unset($this->uuid); + unset($this->revision_id); unset($this->name); unset($this->user_id); } @@ -85,4 +95,10 @@ class EntityTest extends EntityNG { return $this->getTranslation($langcode)->name->value; } + /** + * Implements Drupal\Core\Entity\EntityInterface::getRevisionId(). + */ + public function getRevisionId() { + return $this->get('revision_id')->value; + } }