Commit 8fcd5546 authored by webchick's avatar webchick

Issue #1831444 by das-peter, Berdir, andypost: Added EntityNG: Support for...

Issue #1831444 by das-peter, Berdir, andypost: Added EntityNG: Support for revisions for entity save and delete operations.
parent b20cb1dd
......@@ -552,13 +552,16 @@ protected function saveRevision(EntityInterface $entity) {
$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 @@ protected function postDelete($entities) { }
/**
* 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 @@ public function create(array $values) {
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 @@ protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
/**
* 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 @@ public function save(EntityInterface $entity) {
$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 @@ public function save(EntityInterface $entity) {
}
}
/**
* 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 @@ public function save(EntityInterface $entity) {
* 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 @@ protected function mapToStorageRecord(EntityInterface $entity) {
}
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 @@ protected function preSave(EntityInterface $node) {
/**
* 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 @@ protected function preSaveRevision(array &$record, EntityInterface $entity) {
// 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;
}
}
......
<?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 @@ protected function buildPropertyQuery(QueryInterface $entity_query, array $value
* @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 @@ protected function postSave(EntityInterface $entity, $update) {
$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 @@ public function baseFieldDefinitions() {
'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 @@ public function baseFieldDefinitions() {
'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 @@
* 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 @@ public function __construct(array $values, $entity_type) {
unset($this->id);
unset($this->langcode);
unset($this->uuid);
unset($this->revision_id);
unset($this->name);
unset($this->user_id);
}
......@@ -85,4 +95,10 @@ public function label($langcode = LANGUAGE_DEFAULT) {
return $this->getTranslation($langcode)->name->value;
}
/**
* Implements Drupal\Core\Entity\EntityInterface::getRevisionId().
*/
public function getRevisionId() {
return $this->get('revision_id')->value;
}
}
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