diff --git a/core/lib/Drupal/Core/TypedData/ComputedItemListTrait.php b/core/lib/Drupal/Core/TypedData/ComputedItemListTrait.php new file mode 100644 index 0000000..a9b5673 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/ComputedItemListTrait.php @@ -0,0 +1,148 @@ +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) { + $this->doComputeValue(); + return parent::get($index); + } + + /** + * {@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; + } + +} diff --git a/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php index 4f756da..8bec55c 100644 --- a/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php @@ -97,10 +97,6 @@ public function get($index) { if (!is_numeric($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. - if ($index == 0 && !isset($this->list[0]) && $this->definition->isComputed()) { - $this->list[0] = $this->createItem(0); - } return isset($this->list[$index]) ? $this->list[$index] : NULL; } diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php b/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php index a98e718..8488c7e 100644 --- a/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php +++ b/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php @@ -5,12 +5,43 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Field\FieldItemList; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\TypedData\ComputedItemListTrait; /** * Represents a configurable entity path field. */ class PathFieldItemList extends FieldItemList { + use ComputedItemListTrait; + + /** + * {@inheritdoc} + */ + protected function computeValue() { + $entity = $this->getEntity(); + if (!$entity->isNew()) { + // @todo Support loading languge neutral aliases in + // https://www.drupal.org/node/2511968. + $alias = \Drupal::service('path.alias_storage')->load([ + 'source' => '/' . $entity->toUrl()->getInternalPath(), + 'langcode' => $this->getLangcode(), + ]); + + if ($alias) { + $value = $alias; + } + else { + // If there is no existing alias, default the langcode to the current + // language. + // @todo Set the langcode to not specified for untranslatable fields + // in https://www.drupal.org/node/2689459. + $value = ['langcode' => $this->getLangcode()]; + } + + $this->list[0] = $this->createItem(0, $value); + } + } + /** * {@inheritdoc} */ @@ -34,42 +65,4 @@ public function delete() { \Drupal::service('path.alias_storage')->delete($conditions); } - /** - * {@inheritdoc} - */ - public function getValue($include_computed = FALSE) { - $this->ensureLoaded(); - return parent::getValue($include_computed); - } - - /** - * {@inheritdoc} - */ - public function isEmpty() { - $this->ensureLoaded(); - return parent::isEmpty(); - } - - /** - * {@inheritdoc} - */ - public function getIterator() { - $this->ensureLoaded(); - return parent::getIterator(); - } - - /** - * Automatically create the first item for computed fields. - * - * This ensures that ::getValue() and ::isEmpty() calls will behave like a - * non-computed field. - * - * @todo: Move this to the base class in https://www.drupal.org/node/2392845. - */ - protected function ensureLoaded() { - if (!isset($this->list[0]) && $this->definition->isComputed()) { - $this->list[0] = $this->createItem(0); - } - } - } diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php index 9b5b67d..9cc570c 100644 --- a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php +++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php @@ -24,13 +24,6 @@ class PathItem extends FieldItemBase { /** - * Whether the alias has been loaded from the alias storage service yet. - * - * @var bool - */ - protected $isLoaded = FALSE; - - /** * {@inheritdoc} */ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { @@ -46,14 +39,6 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel /** * {@inheritdoc} */ - public function __get($name) { - $this->ensureLoaded(); - return parent::__get($name); - } - - /** - * {@inheritdoc} - */ public static function schema(FieldStorageDefinitionInterface $field_definition) { return []; } @@ -61,30 +46,6 @@ public static function schema(FieldStorageDefinitionInterface $field_definition) /** * {@inheritdoc} */ - public function getValue() { - $this->ensureLoaded(); - return parent::getValue(); - } - - /** - * {@inheritdoc} - */ - public function isEmpty() { - $this->ensureLoaded(); - return parent::isEmpty(); - } - - /** - * {@inheritdoc} - */ - public function getIterator() { - $this->ensureLoaded(); - return parent::getIterator(); - } - - /** - * {@inheritdoc} - */ public function preSave() { $this->alias = trim($this->alias); } @@ -92,36 +53,6 @@ public function preSave() { /** * {@inheritdoc} */ - public function __set($name, $value) { - // Also ensure that existing values are loaded when setting a value, this - // ensures that it is possible to set a new value immediately after loading - // an entity. - $this->ensureLoaded(); - parent::__set($name, $value); - } - - /** - * {@inheritdoc} - */ - public function set($property_name, $value, $notify = TRUE) { - // Also ensure that existing values are loaded when setting a value, this - // ensures that it is possible to set a new value immediately after loading - // an entity. - $this->ensureLoaded(); - return parent::set($property_name, $value, $notify); - } - - /** - * {@inheritdoc} - */ - public function get($property_name) { - $this->ensureLoaded(); - return parent::get($property_name); - } - - /** - * {@inheritdoc} - */ public function postSave($update) { if (!$update) { if ($this->alias) { @@ -160,38 +91,4 @@ public static function mainPropertyName() { return 'alias'; } - /** - * Ensures the alias properties are loaded if available. - * - * This ensures that the properties will always be loaded and act like - * non-computed fields when calling ::__get() and getValue(). - * - * @todo: Determine if this should be moved to the base class in - * https://www.drupal.org/node/2392845. - */ - protected function ensureLoaded() { - if (!$this->isLoaded) { - $entity = $this->getEntity(); - if (!$entity->isNew()) { - // @todo Support loading languge neutral aliases in - // https://www.drupal.org/node/2511968. - $alias = \Drupal::service('path.alias_storage')->load([ - 'source' => '/' . $entity->toUrl()->getInternalPath(), - 'langcode' => $this->getLangcode(), - ]); - if ($alias) { - $this->setValue($alias); - } - else { - // If there is no existing alias, default the langcode to the current - // language. - // @todo Set the langcode to not specified for untranslatable fields - // in https://www.drupal.org/node/2689459. - $this->langcode = $this->getLangcode(); - } - } - $this->isLoaded = TRUE; - } - } - } 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..cacc6e0 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 @@ -3,35 +3,22 @@ namespace Drupal\entity_test\Plugin\Field; use Drupal\Core\Field\FieldItemList; +use Drupal\Core\TypedData\ComputedItemListTrait; /** * A computed field item list. */ class ComputedTestFieldItemList extends FieldItemList { + use ComputedItemListTrait; + /** * Compute the list property from state. */ - protected function computedListProperty() { + protected function computeValue() { 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..adb04ab 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php @@ -739,13 +739,98 @@ public function testComputedProperties() { } /** - * Test computed fields. + * Tests all the interaction points of a computed field. */ public function testComputedFields() { \Drupal::state()->set('entity_test_computed_field_item_list_value', ['foo computed']); + // Test \Drupal\Core\TypedData\ComputedItemListTrait::getValue(). $entity = EntityTestComputedField::create([]); - $this->assertEquals($entity->computed_string_field->value, 'foo computed'); + $this->assertSame([['value' => 'foo computed']], $entity->computed_string_field->getValue()); + + // Test \Drupal\Core\TypedData\ComputedItemListTrait::setValue(). This also + // checks that a subsequent getter does not try to re-compute the value. + $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()); + + // 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); } /**