Commit 3deb640a authored by webchick's avatar webchick

Issue #2297817 by alexpott, pounard, Berdir, yched, Fabianx, plach,...

Issue #2297817 by alexpott, pounard, Berdir, yched, Fabianx, plach, mkalkbrenner: Do not attempt field storage write when field content did not change
parent 1daa951e
......@@ -206,4 +206,36 @@ protected function invokeFieldMethod($method, ContentEntityInterface $entity) {
}
}
/**
* Checks whether the field values changed compared to the original entity.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* Field definition of field to compare for changes.
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* Entity to check for field changes.
* @param \Drupal\Core\Entity\ContentEntityInterface $original
* Original entity to compare against.
*
* @return bool
* True if the field value changed from the original entity.
*/
protected function hasFieldValueChanged(FieldDefinitionInterface $field_definition, ContentEntityInterface $entity, ContentEntityInterface $original) {
$field_name = $field_definition->getName();
$langcodes = array_keys($entity->getTranslationLanguages());
if ($langcodes !== array_keys($original->getTranslationLanguages())) {
// If the list of langcodes has changed, we need to save.
return TRUE;
}
foreach ($langcodes as $langcode) {
$items = $entity->getTranslation($langcode)->get($field_name)->filterEmptyItems();
$original_items = $original->getTranslation($langcode)->get($field_name)->filterEmptyItems();
// If the field items are not equal, we need to save.
if (!$items->equals($original_items)) {
return TRUE;
}
}
return FALSE;
}
}
......@@ -156,8 +156,8 @@ public function label() {
* {@inheritdoc}
*/
public function urlInfo($rel = 'canonical', array $options = []) {
if ($this->isNew()) {
throw new EntityMalformedException(sprintf('The "%s" entity type has not been saved, and cannot have a URI.', $this->getEntityTypeId()));
if ($this->id() === NULL) {
throw new EntityMalformedException(sprintf('The "%s" entity cannot have a URI as it does have an ID', $this->getEntityTypeId()));
}
// The links array might contain URI templates set in annotations.
......
......@@ -22,6 +22,7 @@
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\field\FieldStorageConfigInterface;
......@@ -960,9 +961,6 @@ protected function doSave($id, EntityInterface $entity) {
if ($this->revisionDataTable) {
$this->saveToSharedTables($entity, $this->revisionDataTable);
}
if ($this->revisionTable) {
$entity->setNewRevision(FALSE);
}
}
else {
// Ensure the entity is still seen as new after assigning it an id,
......@@ -990,11 +988,6 @@ protected function doSave($id, EntityInterface $entity) {
if ($this->revisionDataTable) {
$this->saveToSharedTables($entity, $this->revisionDataTable);
}
$entity->enforceIsNew(FALSE);
if ($this->revisionTable) {
$entity->setNewRevision(FALSE);
}
}
$this->invokeFieldMethod($is_new ? 'insert' : 'update', $entity);
$this->saveToDedicatedTables($entity, !$is_new);
......@@ -1002,6 +995,10 @@ protected function doSave($id, EntityInterface $entity) {
if (!$is_new && $this->dataTable) {
$this->invokeTranslationHooks($entity);
}
$entity->enforceIsNew(FALSE);
if ($this->revisionTable) {
$entity->setNewRevision(FALSE);
}
return $return;
}
......@@ -1317,11 +1314,20 @@ protected function saveToDedicatedTables(ContentEntityInterface $entity, $update
$vid = $id;
}
$original = !empty($entity->original) ? $entity->original: NULL;
foreach ($this->entityManager->getFieldDefinitions($entity_type, $bundle) as $field_name => $field_definition) {
$storage_definition = $field_definition->getFieldStorageDefinition();
if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
continue;
}
// When updating an existing revision, keep the existing records if the
// field values did not change.
if (!$entity->isNewRevision() && $original && !$this->hasFieldValueChanged($field_definition, $entity, $original)) {
continue;
}
$table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
$revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
......
......@@ -98,6 +98,7 @@ public function filterEmptyItems() {
$this->filter(function ($item) {
return !$item->isEmpty();
});
return $this;
}
/**
......@@ -370,4 +371,38 @@ protected function defaultValueWidget(FormStateInterface $form_state) {
return $form_state->get('default_value_widget');
}
/**
* {@inheritdoc}
*/
public function equals(FieldItemListInterface $list_to_compare) {
$columns = $this->getFieldDefinition()->getFieldStorageDefinition()->getColumns();
$count1 = count($this);
$count2 = count($list_to_compare);
if ($count1 === 0 && $count2 === 0) {
// Both are empty we can safely assume that it did not change.
return TRUE;
}
if ($count1 !== $count2) {
// One of them is empty but not the other one so the value changed.
return FALSE;
}
$value1 = $this->getValue();
$value2 = $list_to_compare->getValue();
if ($value1 === $value2) {
return TRUE;
}
// If the values are not equal ensure a consistent order of field item
// properties and remove properties which will not be saved.
$callback = function (&$value) use ($columns) {
if (is_array($value)) {
$value = array_intersect_key($value, $columns);
ksort($value);
}
};
array_walk($value1, $callback);
array_walk($value2, $callback);
return $value1 === $value2;
}
}
......@@ -94,6 +94,8 @@ public function defaultAccess($operation = 'view', AccountInterface $account = N
/**
* Filters out empty field items and re-numbers the item deltas.
*
* @return $this
*/
public function filterEmptyItems();
......@@ -258,4 +260,15 @@ public function defaultValuesFormSubmit(array $element, array &$form, FormStateI
*/
public static function processDefaultValue($default_value, FieldableEntityInterface $entity, FieldDefinitionInterface $definition);
/**
* Determines equality to another object implementing FieldItemListInterface.
*
* @param \Drupal\Core\Field\FieldItemListInterface $list_to_compare
* The field item list to compare to.
*
* @return bool
* TRUE if the field item lists are equal, FALSE if not.
*/
public function equals(FieldItemListInterface $list_to_compare);
}
......@@ -460,6 +460,19 @@ protected function doTestEntityTranslationAPI($entity_type) {
$hooks = $this->getHooksInfo();
$this->assertFalse($hooks, 'No hooks are run when adding and removing a translation without storing it.');
// Check that hooks are fired only when actually storing data.
$entity = $this->reloadEntity($entity);
$entity->addTranslation($langcode2);
$entity->save();
$entity = $this->reloadEntity($entity);
$this->assertTrue($entity->hasTranslation($langcode2), 'Entity has translation after adding one and saving.');
$entity->removeTranslation($langcode2);
$entity->save();
$entity = $this->reloadEntity($entity);
$this->assertFalse($entity->hasTranslation($langcode2), 'Entity does not have translation after removing it and saving.');
// Reset hook firing information.
$this->getHooksInfo();
// Verify that entity serialization does not cause stale references to be
// left around.
$entity = $this->reloadEntity($entity);
......
<?php
/**
* @file Contains \Drupal\Tests\Core\Field\FieldItemListTest.
*/
namespace Drupal\Tests\Core\Field;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemList;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Core\Field\FieldItemList
* @group Field
*/
class FieldItemListTest extends UnitTestCase {
/**
* @covers ::equals
*
* @dataProvider providerTestEquals
*/
public function testEquals($expected, FieldItemInterface $first_field_item = NULL, FieldItemInterface $second_field_item = NULL) {
// Mock the field type manager and place it in the container.
$field_type_manager = $this->getMock('Drupal\Core\Field\FieldTypePluginManagerInterface');
$container = new ContainerBuilder();
$container->set('plugin.manager.field.field_type', $field_type_manager);
\Drupal::setContainer($container);
$field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
$field_storage_definition->expects($this->any())
->method('getColumns')
->willReturn([0 => '0', 1 => '1']);
$field_definition = $this->getMock('Drupal\Core\Field\FieldDefinitionInterface');
$field_definition->expects($this->any())
->method('getFieldStorageDefinition')
->willReturn($field_storage_definition);
$field_list_a = new FieldItemList($field_definition);
$field_list_b = new FieldItemList($field_definition);
// Set up the mocking necessary for creating field items.
$field_type_manager->expects($this->any())
->method('createFieldItem')
->willReturnOnConsecutiveCalls($first_field_item, $second_field_item);
// Set the field item values.
if ($first_field_item instanceof FieldItemInterface) {
$field_list_a->setValue($first_field_item);
}
if ($second_field_item instanceof FieldItemInterface) {
$field_list_b->setValue($second_field_item);
}
$this->assertEquals($expected, $field_list_a->equals($field_list_b));
}
/**
* Data provider for testEquals.
*/
public function providerTestEquals() {
// Tests field item lists with no values.
$datasets[] = [TRUE];
/** @var \Drupal\Core\Field\FieldItemBase $field_item_a */
$field_item_a = $this->getMockForAbstractClass('Drupal\Core\Field\FieldItemBase', [], '', FALSE);
$field_item_a->setValue([1]);
// Tests field item lists where one has a value and one does not.
$datasets[] = [FALSE, $field_item_a];
// Tests field item lists where both have the same value.
$datasets[] = [TRUE, $field_item_a, $field_item_a];
/** @var \Drupal\Core\Field\FieldItemBase $fv */
$field_item_b = $this->getMockForAbstractClass('Drupal\Core\Field\FieldItemBase', [], '', FALSE);
$field_item_b->setValue([2]);
// Tests field item lists where both have the different values.
$datasets[] = [FALSE, $field_item_a, $field_item_b];
/** @var \Drupal\Core\Field\FieldItemBase $fv */
$field_item_c = $this->getMockForAbstractClass('Drupal\Core\Field\FieldItemBase', [], '', FALSE);
$field_item_c->setValue(['0' => 1, '1' => 2]);
$field_item_d = $this->getMockForAbstractClass('Drupal\Core\Field\FieldItemBase', [], '', FALSE);
$field_item_d->setValue(['1' => 2, '0' => 1]);
// Tests field item lists where both have the differently ordered values.
$datasets[] = [TRUE, $field_item_c, $field_item_d];
return $datasets;
}
/**
* @covers ::equals
*/
public function testEqualsEmptyItems() {
/** @var \Drupal\Core\Field\FieldItemBase $fv */
$first_field_item = $this->getMockForAbstractClass('Drupal\Core\Field\FieldItemBase', [], '', FALSE);
$first_field_item->setValue(['0' => 1, '1' => 2]);
$second_field_item = $this->getMockForAbstractClass('Drupal\Core\Field\FieldItemBase', [], '', FALSE);
$second_field_item->setValue(['1' => 2, '0' => 1]);
$empty_field_item = $this->getMockForAbstractClass('Drupal\Core\Field\FieldItemBase', [], '', FALSE);
// Mock the field type manager and place it in the container.
$field_type_manager = $this->getMock('Drupal\Core\Field\FieldTypePluginManagerInterface');
$container = new ContainerBuilder();
$container->set('plugin.manager.field.field_type', $field_type_manager);
\Drupal::setContainer($container);
$field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
$field_storage_definition->expects($this->any())
->method('getColumns')
->willReturn([0 => '0', 1 => '1']);
$field_definition = $this->getMock('Drupal\Core\Field\FieldDefinitionInterface');
$field_definition->expects($this->any())
->method('getFieldStorageDefinition')
->willReturn($field_storage_definition);
$field_list_a = new FieldItemList($field_definition);
$field_list_b = new FieldItemList($field_definition);
// Set up the mocking necessary for creating field items.
$field_type_manager->expects($this->any())
->method('createFieldItem')
->willReturnOnConsecutiveCalls($first_field_item, $second_field_item, $empty_field_item, $empty_field_item);
// Set the field item values.
$field_list_a->setValue($first_field_item);
$field_list_b->setValue($second_field_item);
$field_list_a->appendItem($empty_field_item);
// Field list A has an empty item.
$this->assertEquals(FALSE, $field_list_a->equals($field_list_b));
// Field lists A and B have empty items.
$field_list_b->appendItem($empty_field_item);
$this->assertEquals(TRUE, $field_list_a->equals($field_list_b));
// Field list B has an empty item.
$field_list_a->filterEmptyItems();
$this->assertEquals(FALSE, $field_list_a->equals($field_list_b));
// Neither field lists A and B have empty items.
$field_list_b->filterEmptyItems();
$this->assertEquals(TRUE, $field_list_a->equals($field_list_b));
}
}
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