Issue #1831444 by das-peter, Berdir, andypost: Added EntityNG: Support for revisions for entity save and delete operations.

8.0.x
webchick 2012-11-21 14:07:53 -08:00
parent b20cb1dd19
commit 8fcd5546ba
8 changed files with 306 additions and 27 deletions

View File

@ -552,13 +552,16 @@ class DatabaseStorageController implements EntityStorageControllerInterface {
$record = (array) $entity; $record = (array) $entity;
// When saving a new revision, set any existing revision ID to NULL so as to // 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 // ensure that a new revision will actually be created.
// revision ID in a separate property for use by hook implementations.
if ($entity->isNewRevision() && $record[$this->revisionKey]) { if ($entity->isNewRevision() && $record[$this->revisionKey]) {
$record[$this->revisionKey] = NULL; $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); $this->preSaveRevision($record, $entity);
$record = (array) $record;
if ($entity->isNewRevision()) { if ($entity->isNewRevision()) {
drupal_write_record($this->revisionTable, $record); drupal_write_record($this->revisionTable, $record);
@ -613,12 +616,12 @@ class DatabaseStorageController implements EntityStorageControllerInterface {
/** /**
* Act on a revision before being saved. * Act on a revision before being saved.
* *
* @param array $record * @param \stdClass $record
* The revision array. * The revision object.
* @param Drupal\Core\Entity\EntityInterface $entity * @param Drupal\Core\Entity\EntityInterface $entity
* The entity object. * 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. * Invokes a hook on behalf of the entity.

View File

@ -105,7 +105,7 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
protected function attachLoad(&$queried_entities, $load_revision = FALSE) { protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
// Now map the record values to the according entity properties and // Now map the record values to the according entity properties and
// activate compatibility mode. // activate compatibility mode.
$queried_entities = $this->mapFromStorageRecords($queried_entities); $queried_entities = $this->mapFromStorageRecords($queried_entities, $load_revision);
// Attach fields. // Attach fields.
if ($this->entityInfo['fieldable']) { if ($this->entityInfo['fieldable']) {
@ -139,10 +139,15 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
/** /**
* Maps from storage records to entity objects. * 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 * @return array
* An array of entity objects implementing the EntityInterface. * 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) { foreach ($records as $id => $record) {
$entity = new $this->entityClass(array(), $this->entityType); $entity = new $this->entityClass(array(), $this->entityType);
@ -181,13 +186,27 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
$entity->updateOriginalValues(); $entity->updateOriginalValues();
if (!$entity->isNew()) { if (!$entity->isNew()) {
if ($entity->isDefaultRevision()) {
$return = drupal_write_record($this->entityInfo['base_table'], $record, $this->idKey); $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->resetCache(array($entity->id()));
$this->postSave($entity, TRUE); $this->postSave($entity, TRUE);
$this->invokeHook('update', $entity); $this->invokeHook('update', $entity);
} }
else { else {
$return = drupal_write_record($this->entityInfo['base_table'], $record); $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. // Reset general caches, but keep caches specific to certain entities.
$this->resetCache(array()); $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(). * Overrides DatabaseStorageController::invokeHook().
* *
@ -217,7 +274,13 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
* afterwards. * afterwards.
*/ */
protected function invokeHook($hook, EntityInterface $entity) { 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); $entity->setCompatibilityMode(TRUE);
$function($this->entityType, $entity); $function($this->entityType, $entity);
$entity->setCompatibilityMode(FALSE); $entity->setCompatibilityMode(FALSE);
@ -239,4 +302,16 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
} }
return $record; 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;
}
} }

View File

@ -18,10 +18,10 @@ class TestEntityController extends DatabaseStorageController {
/** /**
* Overrides Drupal\Core\Entity\DatabaseStorageController::preSaveRevision(). * 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. // Allow for predefined revision ids.
if (!empty($record['use_provided_revision_id'])) { if (!empty($record->use_provided_revision_id)) {
$record['ftvid'] = $record['use_provided_revision_id']; $record->ftvid = $record->use_provided_revision_id;
} }
} }

View File

@ -100,7 +100,7 @@ class NodeStorageController extends DatabaseStorageController {
/** /**
* Overrides Drupal\Core\Entity\DatabaseStorageController::preSaveRevision(). * Overrides Drupal\Core\Entity\DatabaseStorageController::preSaveRevision().
*/ */
protected function preSaveRevision(array &$record, EntityInterface $entity) { protected function preSaveRevision(\stdClass $record, EntityInterface $entity) {
if ($entity->isNewRevision()) { if ($entity->isNewRevision()) {
// When inserting either a new node or a new node revision, $node->log // 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 // 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. // empty string in that case.
// @todo: Make the {node_revision}.log column nullable so that we can // @todo: Make the {node_revision}.log column nullable so that we can
// remove this check. // remove this check.
if (!isset($record['log'])) { if (!isset($record->log)) {
$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 // 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 // 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 // $node->log is unset, drupal_write_record() will not attempt to update
// the existing database column when re-saving the revision; therefore, // the existing database column when re-saving the revision; therefore,
// this code allows us to avoid clobbering an existing log entry with an // this code allows us to avoid clobbering an existing log entry with an
// empty one. // empty one.
unset($record['log']); unset($record->log);
} }
if ($entity->isNewRevision()) { if ($entity->isNewRevision()) {
$record['timestamp'] = REQUEST_TIME; $record->timestamp = REQUEST_TIME;
$record['uid'] = isset($record['revision_uid']) ? $record['revision_uid'] : $GLOBALS['user']->uid; $record->uid = isset($record->revision_uid) ? $record->revision_uid : $GLOBALS['user']->uid;
} }
} }

View File

@ -0,0 +1,99 @@
<?php
/**
* @file
* Definition of Drupal\system\Tests\Entity\EntityRevisionsTest.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\simpletest\WebTestBase;
/**
* Tests for the basic revisioning functionality of entities.
*/
class EntityRevisionsTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('entity_test');
public static function getInfo() {
return array(
'name' => '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.');
}
}

View File

@ -43,6 +43,13 @@ function entity_test_schema() {
'not null' => TRUE, 'not null' => TRUE,
'description' => 'Primary Key: Unique entity-test item ID.', '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( 'uuid' => array(
'description' => 'Unique Key: Universally unique identifier for this entity.', 'description' => 'Unique Key: Universally unique identifier for this entity.',
'type' => 'varchar', 'type' => 'varchar',
@ -71,6 +78,13 @@ function entity_test_schema() {
'not null' => TRUE, 'not null' => TRUE,
'description' => 'The {entity_test}.id of the test entity.', '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( 'langcode' => array(
'description' => 'The {language}.langcode of this variant of this test entity.', 'description' => 'The {language}.langcode of this variant of this test entity.',
'type' => 'varchar', 'type' => 'varchar',
@ -108,5 +122,57 @@ function entity_test_schema() {
), ),
'primary key' => array('id', 'langcode'), '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; return $schema;
} }

View File

@ -47,23 +47,31 @@ class EntityTestStorageController extends DatabaseStorageControllerNG {
* @return array * @return array
* An array of entity objects implementing the EntityInterface. * An array of entity objects implementing the EntityInterface.
*/ */
protected function mapFromStorageRecords(array $records) { protected function mapFromStorageRecords(array $records, $load_revision = FALSE) {
$records = parent::mapFromStorageRecords($records); $records = parent::mapFromStorageRecords($records, $load_revision);
// Load data of translatable properties. // Load data of translatable properties.
$this->attachPropertyData($records); $this->attachPropertyData($records, $load_revision);
return $records; return $records;
} }
/** /**
* Attaches property data in all languages for translatable properties. * Attaches property data in all languages for translatable properties.
*/ */
protected function attachPropertyData(&$queried_entities) { protected function attachPropertyData(&$queried_entities, $load_revision = FALSE) {
$data = db_select('entity_test_property_data', 'data', array('fetch' => PDO::FETCH_ASSOC)) $query = db_select('entity_test_property_data', 'data', array('fetch' => PDO::FETCH_ASSOC))
->fields('data') ->fields('data')
->condition('id', array_keys($queried_entities)) ->condition('id', array_keys($queried_entities))
->orderBy('data.id') ->orderBy('data.id');
->execute(); 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) { foreach ($data as $values) {
$id = $values['id']; $id = $values['id'];
@ -96,6 +104,7 @@ class EntityTestStorageController extends DatabaseStorageControllerNG {
$values = array( $values = array(
'id' => $entity->id(), 'id' => $entity->id(),
'revision_id' => $entity->getRevisionId(),
'langcode' => $langcode, 'langcode' => $langcode,
'default_langcode' => intval($default_langcode == $langcode), 'default_langcode' => intval($default_langcode == $langcode),
'name' => $translation->name->value, 'name' => $translation->name->value,
@ -129,6 +138,12 @@ class EntityTestStorageController extends DatabaseStorageControllerNG {
'type' => 'integer_field', 'type' => 'integer_field',
'read-only' => TRUE, '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( $fields['uuid'] = array(
'label' => t('UUID'), 'label' => t('UUID'),
'description' => t('The UUID of the test entity.'), '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.'), 'description' => t('The language code of the test entity.'),
'type' => 'language_field', '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( $fields['name'] = array(
'label' => t('Name'), 'label' => t('Name'),
'description' => t('The name of the test entity.'), 'description' => t('The name of the test entity.'),

View File

@ -26,10 +26,12 @@ use Drupal\Core\Annotation\Translation;
* translation_controller_class = "Drupal\entity_test\EntityTestTranslationController", * translation_controller_class = "Drupal\entity_test\EntityTestTranslationController",
* base_table = "entity_test", * base_table = "entity_test",
* data_table = "entity_test_property_data", * data_table = "entity_test_property_data",
* revision_table = "entity_test_property_revision",
* fieldable = TRUE, * fieldable = TRUE,
* entity_keys = { * entity_keys = {
* "id" = "id", * "id" = "id",
* "uuid" = "uuid" * "uuid" = "uuid",
* "revision" = "revision_id"
* }, * },
* menu_base_path = "entity-test/manage/%entity_test" * menu_base_path = "entity-test/manage/%entity_test"
* ) * )
@ -50,6 +52,13 @@ class EntityTest extends EntityNG {
*/ */
public $uuid; public $uuid;
/**
* The entity revision id.
*
* @var \Drupal\Core\Entity\Field\FieldInterface
*/
public $revision_id;
/** /**
* The name of the test entity. * The name of the test entity.
* *
@ -74,6 +83,7 @@ class EntityTest extends EntityNG {
unset($this->id); unset($this->id);
unset($this->langcode); unset($this->langcode);
unset($this->uuid); unset($this->uuid);
unset($this->revision_id);
unset($this->name); unset($this->name);
unset($this->user_id); unset($this->user_id);
} }
@ -85,4 +95,10 @@ class EntityTest extends EntityNG {
return $this->getTranslation($langcode)->name->value; return $this->getTranslation($langcode)->name->value;
} }
/**
* Implements Drupal\Core\Entity\EntityInterface::getRevisionId().
*/
public function getRevisionId() {
return $this->get('revision_id')->value;
}
} }