Issue #1831444 by das-peter, Berdir, andypost: Added EntityNG: Support for revisions for entity save and delete operations.
parent
b20cb1dd19
commit
8fcd5546ba
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.'),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue