diff --git a/core/lib/Drupal/Core/TypedData/ComputedItemListTrait.php b/core/lib/Drupal/Core/TypedData/ComputedItemListTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..f6660c4cca6886bdbaed43f8fc1ae27943107848 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/ComputedItemListTrait.php @@ -0,0 +1,151 @@ +valueComputed === FALSE) { + $this->computeValue(); + $this->valueComputed = TRUE; + } + } + + /** + * {@inheritdoc} + */ + public function getValue() { + $this->ensureComputedValue(); + 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->ensureComputedValue(); + 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 item lists need to behave like + // non-computed ones. For example, calling isEmpty() on a computed item list + // should return TRUE when the values were computed and the item list is + // truly empty. + // @see \Drupal\Core\TypedData\Plugin\DataType\ItemList::get(). + $this->ensureComputedValue(); + + return isset($this->list[$index]) ? $this->list[$index] : NULL; + } + + /** + * {@inheritdoc} + */ + public function set($index, $value) { + $this->ensureComputedValue(); + return parent::set($index, $value); + } + + /** + * {@inheritdoc} + */ + public function appendItem($value = NULL) { + $this->ensureComputedValue(); + return parent::appendItem($value); + } + + /** + * {@inheritdoc} + */ + public function removeItem($index) { + $this->ensureComputedValue(); + return parent::removeItem($index); + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $this->ensureComputedValue(); + return parent::isEmpty(); + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) { + $this->ensureComputedValue(); + return parent::offsetExists($offset); + } + + /** + * {@inheritdoc} + */ + public function getIterator() { + $this->ensureComputedValue(); + return parent::getIterator(); + } + + /** + * {@inheritdoc} + */ + public function count() { + $this->ensureComputedValue(); + return parent::count(); + } + + /** + * {@inheritdoc} + */ + public function applyDefaultValue($notify = TRUE) { + // Default values do not make sense for computed item lists. However, this + // method can be overridden if needed. + return $this; + } + +} diff --git a/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php index 4f756dadf2540b9b15e9d4e37f5d353f1b02c67e..0d58b717db06b6db534f48081694a46b90317a51 100644 --- a/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php @@ -98,7 +98,10 @@ public function get($index) { throw new \InvalidArgumentException('Unable to get a value with a non-numeric delta in a list.'); } // Automatically create the first item for computed fields. + // @deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. + // Use \Drupal\Core\TypedData\ComputedItemListTrait instead. if ($index == 0 && !isset($this->list[0]) && $this->definition->isComputed()) { + @trigger_error('Automatically creating the first item for computed fields is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. Use \Drupal\Core\TypedData\ComputedItemListTrait instead.', E_USER_DEPRECATED); $this->list[0] = $this->createItem(0); } return isset($this->list[$index]) ? $this->list[$index] : NULL; 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 864d33fe06a881aac06e3d474ba63bae3c46b637..0dfa3e1ec9a284a7b41660f0849ed9e05f775f6e 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\TypedData\ComputedItemListTrait; use Drupal\Core\Field\FieldItemList; /** @@ -9,29 +10,20 @@ */ class ComputedTestFieldItemList extends FieldItemList { + use ComputedItemListTrait; + /** * 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 31b1f9e9e94d1d98d669f252f4eabc4d03204922..a42a74baae1eded269ba737258095503bdbe62b1 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php @@ -739,13 +739,135 @@ 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 lifecycle + // of an entity when 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); + + // Add a non-empty item to the list and check that it is not filtered out. + $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)); } /** diff --git a/core/tests/Drupal/Tests/Listeners/DeprecationListener.php b/core/tests/Drupal/Tests/Listeners/DeprecationListener.php index a037c0224d4807a67979223a068180bf88c83923..3673376d61e111421aa2f88acba7064b9f3b3f5d 100644 --- a/core/tests/Drupal/Tests/Listeners/DeprecationListener.php +++ b/core/tests/Drupal/Tests/Listeners/DeprecationListener.php @@ -110,6 +110,7 @@ public static function getSkippedDeprecations() { 'The Drupal\config_translation\Plugin\migrate\source\d6\I18nProfileField is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. Instead, use Drupal\config_translation\Plugin\migrate\source\d6\ProfileFieldTranslation', 'The Drupal\migrate_drupal\Plugin\migrate\source\d6\i18nVariable is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. Instead, use Drupal\migrate_drupal\Plugin\migrate\source\d6\VariableTranslation', 'Implicit cacheability metadata bubbling (onto the global render context) in normalizers is deprecated since Drupal 8.5.0 and will be removed in Drupal 9.0.0. Use the "cacheability" serialization context instead, for explicit cacheability metadata bubbling. See https://www.drupal.org/node/2918937', + 'Automatically creating the first item for computed fields is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. Use \Drupal\Core\TypedData\ComputedItemListTrait instead.', ]; }