diff --git a/core/lib/Drupal/Core/Field/ComputedFieldItemListTrait.php b/core/lib/Drupal/Core/Field/ComputedFieldItemListTrait.php new file mode 100644 index 0000000..8c0d7b1 --- /dev/null +++ b/core/lib/Drupal/Core/Field/ComputedFieldItemListTrait.php @@ -0,0 +1,176 @@ +valueComputed === FALSE) { + $this->computeValue(); + $this->valueComputed = TRUE; + } + } + + /** + * {@inheritdoc} + */ + public function getValue() { + $this->doComputeValue(); + return parent::getValue(); + } + + /** + * {@inheritdoc} + */ + public function setValue($values, $notify = TRUE) { + parent::setValue($values, $notify); + + // Make sure that subsequent getter calls do not try to compute the values + // again. + $this->valueComputed = TRUE; + } + + /** + * {@inheritdoc} + */ + public function getString() { + $this->doComputeValue(); + return parent::getString(); + } + + /** + * {@inheritdoc} + */ + public function get($index) { + if (!is_numeric($index)) { + throw new \InvalidArgumentException('Unable to get a value with a non-numeric delta in a list.'); + } + + // Unlike the base implementation of + // \Drupal\Core\TypedData\ListInterface::get(), we do not add an empty item + // automatically because computed fields need to behave like non-computed + // ones. For example, calling isEmpty() on a computed field items list + // should return TRUE when the values were computed and the field is truly + // empty. + // @see \Drupal\Core\TypedData\Plugin\DataType\ItemList::get(). + $this->doComputeValue(); + + return isset($this->list[$index]) ? $this->list[$index] : NULL; + } + + /** + * {@inheritdoc} + */ + public function set($index, $value) { + // Ensure that existing values are loaded when setting a value, this also + // ensures that it is possible to set a new value immediately after loading + // an entity. + $this->doComputeValue(); + return parent::set($index, $value); + } + + /** + * {@inheritdoc} + */ + public function appendItem($value = NULL) { + $this->doComputeValue(); + return parent::appendItem($value); + } + + /** + * {@inheritdoc} + */ + public function removeItem($index) { + $this->doComputeValue(); + return parent::removeItem($index); + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $this->doComputeValue(); + return parent::isEmpty(); + } + + /** + * {@inheritdoc} + */ + public function filter($callback) { + $this->doComputeValue(); + return parent::filter($callback); + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) { + $this->doComputeValue(); + return parent::offsetExists($offset); + } + + /** + * {@inheritdoc} + */ + public function getIterator() { + $this->doComputeValue(); + return parent::getIterator(); + } + + /** + * {@inheritdoc} + */ + public function count() { + $this->doComputeValue(); + return parent::count(); + } + + /** + * {@inheritdoc} + */ + public function applyDefaultValue($notify = TRUE) { + // Default values do not make sense for computed fields. However, this + // method can be overridden if needed. + return $this; + } + + /** + * {@inheritdoc} + */ + public function filterEmptyItems() { + // If the values were never computed, for example when the field has not + // been interacted with during the life cycle of an entity, there is no need + // to compute them now. + if ($this->valueComputed === FALSE) { + return $this; + } + + return parent::filterEmptyItems(); + } + +} diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestFieldItemList.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestFieldItemList.php index 864d33f..e3bff76 100644 --- a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestFieldItemList.php +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestFieldItemList.php @@ -2,6 +2,7 @@ namespace Drupal\entity_test\Plugin\Field; +use Drupal\Core\Field\ComputedFieldItemListTrait; use Drupal\Core\Field\FieldItemList; /** @@ -9,29 +10,20 @@ */ class ComputedTestFieldItemList extends FieldItemList { + use ComputedFieldItemListTrait; + /** * Compute the list property from state. */ - protected function computedListProperty() { + protected function computeValue() { + // Count the number of times this method has been executed during the + // lifecycle of an entity. + $execution_count = \Drupal::state()->get('computed_test_field_execution', 0); + \Drupal::state()->set('computed_test_field_execution', ++$execution_count); + foreach (\Drupal::state()->get('entity_test_computed_field_item_list_value', []) as $delta => $item) { $this->list[$delta] = $this->createItem($delta, $item); } } - /** - * {@inheritdoc} - */ - public function get($index) { - $this->computedListProperty(); - return isset($this->list[$index]) ? $this->list[$index] : NULL; - } - - /** - * {@inheritdoc} - */ - public function getIterator() { - $this->computedListProperty(); - return parent::getIterator(); - } - } diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php index 31b1f9e..c6d7772 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php @@ -739,13 +739,134 @@ public function testComputedProperties() { } /** - * Test computed fields. + * Tests all the interaction points of a computed field. */ public function testComputedFields() { + $this->installEntitySchema('entity_test_computed_field'); + \Drupal::state()->set('entity_test_computed_field_item_list_value', ['foo computed']); + // Check that the values are not computed unnecessarily during the life + // cycle of an entity when there the field is not interacted with directly. + \Drupal::state()->set('computed_test_field_execution', 0); + $entity = EntityTestComputedField::create([]); + $this->assertSame(0, \Drupal::state()->get('computed_test_field_execution', 0)); + + $entity->name->value = $this->randomString(); + $this->assertSame(0, \Drupal::state()->get('computed_test_field_execution', 0)); + + $entity->save(); + $this->assertSame(0, \Drupal::state()->get('computed_test_field_execution', 0)); + + // Test \Drupal\Core\TypedData\ComputedItemListTrait::getValue(). + \Drupal::state()->set('computed_test_field_execution', 0); $entity = EntityTestComputedField::create([]); - $this->assertEquals($entity->computed_string_field->value, 'foo computed'); + $this->assertSame([['value' => 'foo computed']], $entity->computed_string_field->getValue()); + + // Check that the values are only computed once. + $this->assertSame(1, \Drupal::state()->get('computed_test_field_execution', 0)); + + // Test \Drupal\Core\TypedData\ComputedItemListTrait::setValue(). This also + // checks that a subsequent getter does not try to re-compute the value. + \Drupal::state()->set('computed_test_field_execution', 0); + $entity = EntityTestComputedField::create([]); + $entity->computed_string_field->setValue([ + ['value' => 'foo computed 1'], + ['value' => 'foo computed 2'], + ]); + $this->assertSame([['value' => 'foo computed 1'], ['value' => 'foo computed 2']], $entity->computed_string_field->getValue()); + + // Check that the values have not been computed when they were explicitly + // set. + $this->assertSame(0, \Drupal::state()->get('computed_test_field_execution', 0)); + + // Test \Drupal\Core\TypedData\ComputedItemListTrait::getString(). + $entity = EntityTestComputedField::create([]); + $this->assertSame('foo computed', $entity->computed_string_field->getString()); + + // Test \Drupal\Core\TypedData\ComputedItemListTrait::get(). + $entity = EntityTestComputedField::create([]); + $this->assertSame('foo computed', $entity->computed_string_field->get(0)->value); + $this->assertEmpty($entity->computed_string_field->get(1)); + + // Test \Drupal\Core\TypedData\ComputedItemListTrait::set(). + $entity = EntityTestComputedField::create([]); + $entity->computed_string_field->set(1, 'foo computed 1'); + $this->assertSame('foo computed', $entity->computed_string_field[0]->value); + $this->assertSame('foo computed 1', $entity->computed_string_field[1]->value); + $entity->computed_string_field->set(0, 'foo computed 0'); + $this->assertSame('foo computed 0', $entity->computed_string_field[0]->value); + $this->assertSame('foo computed 1', $entity->computed_string_field[1]->value); + + // Test \Drupal\Core\TypedData\ComputedItemListTrait::appendItem(). + $entity = EntityTestComputedField::create([]); + $entity->computed_string_field->appendItem('foo computed 1'); + $this->assertSame('foo computed', $entity->computed_string_field[0]->value); + $this->assertSame('foo computed 1', $entity->computed_string_field[1]->value); + + // Test \Drupal\Core\TypedData\ComputedItemListTrait::removeItem(). + $entity = EntityTestComputedField::create([]); + $entity->computed_string_field->removeItem(0); + $this->assertTrue($entity->computed_string_field->isEmpty()); + + // Test \Drupal\Core\TypedData\ComputedItemListTrait::isEmpty(). + \Drupal::state()->set('entity_test_computed_field_item_list_value', []); + $entity = EntityTestComputedField::create([]); + $this->assertTrue($entity->computed_string_field->isEmpty()); + + \Drupal::state()->set('entity_test_computed_field_item_list_value', ['foo computed']); + $entity = EntityTestComputedField::create([]); + $this->assertFalse($entity->computed_string_field->isEmpty()); + + // Test \Drupal\Core\TypedData\ComputedItemListTrait::filter(). + $filter_callback = function ($item) { + return !$item->isEmpty(); + }; + $entity = EntityTestComputedField::create([]); + $entity->computed_string_field->filter($filter_callback); + $this->assertCount(1, $entity->computed_string_field); + + // Add an empty item to the list and check that it is filtered out. + $entity->computed_string_field->appendItem(); + $entity->computed_string_field->filter($filter_callback); + $this->assertCount(1, $entity->computed_string_field); + + $entity->computed_string_field->appendItem('foo computed 1'); + $entity->computed_string_field->filter($filter_callback); + $this->assertCount(2, $entity->computed_string_field); + + // Test \Drupal\Core\TypedData\ComputedItemListTrait::offsetExists(). + $entity = EntityTestComputedField::create([]); + $this->assertTrue($entity->computed_string_field->offsetExists(0)); + $this->assertFalse($entity->computed_string_field->offsetExists(1)); + + // Test \Drupal\Core\TypedData\ComputedItemListTrait::getIterator(). + $entity = EntityTestComputedField::create([]); + foreach ($entity->computed_string_field as $delta => $item) { + $this->assertSame('foo computed', $item->value); + } + + // Test \Drupal\Core\TypedData\ComputedItemListTrait::count(). + $entity = EntityTestComputedField::create([]); + $this->assertCount(1, $entity->computed_string_field); + + // Check that computed items are not auto-created when they have no values. + \Drupal::state()->set('entity_test_computed_field_item_list_value', []); + $entity = EntityTestComputedField::create([]); + $this->assertCount(0, $entity->computed_string_field); + + // Test \Drupal\Core\Field\FieldItemList::equals() for a computed field. + \Drupal::state()->set('entity_test_computed_field_item_list_value', ['foo computed']); + $entity = EntityTestComputedField::create([]); + $computed_item_list1 = $entity->computed_string_field; + + $entity = EntityTestComputedField::create([]); + $computed_item_list2 = $entity->computed_string_field; + + $this->assertTrue($computed_item_list1->equals($computed_item_list2)); + + $computed_item_list2->value = 'foo computed 2'; + $this->assertFalse($computed_item_list1->equals($computed_item_list2)); } /**