diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index afddb22..34772fc 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -443,7 +443,9 @@ public function __sleep() { // Get the values of instantiated field objects, only serialize the values. foreach ($this->fields as $name => $fields) { foreach ($fields as $langcode => $field) { - $this->values[$name][$langcode] = $field->getValue(); + if(!$field->isComputed()){ + $this->values[$name][$langcode] = $field->getValue(); + } } } $this->fields = []; @@ -486,7 +488,7 @@ public function hasField($field_name) { * {@inheritdoc} */ public function get($field_name) { - if (!isset($this->fields[$field_name][$this->activeLangcode])) { + if (!isset($this->fields[$field_name][$this->activeLangcode]) || $this->fields[$field_name][$this->activeLangcode]->isComputed()) { return $this->getTranslatedField($field_name, $this->activeLangcode); } return $this->fields[$field_name][$this->activeLangcode]; @@ -503,7 +505,9 @@ protected function getTranslatedField($name, $langcode) { } // Populate $this->fields to speed-up further look-ups and to keep track of // fields objects, possibly holding changes to field values. - if (!isset($this->fields[$name][$langcode])) { + // Computed fields are always computed as their value might be subject to + // changes in the entities' lifecycle. + if (!isset($this->fields[$name][$langcode]) || $this->fields[$name][$langcode]->isComputed()) { $definition = $this->getFieldDefinition($name); if (!$definition) { throw new \InvalidArgumentException("Field $name is unknown."); diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php index 9d6b8d6..2596cf3 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -108,7 +108,7 @@ protected function initFieldValues(ContentEntityInterface $entity, array $values if (isset($values[$name])) { $entity->$name = $values[$name]; } - elseif (!array_key_exists($name, $values)) { + elseif (!array_key_exists($name, $values) && !$field->isComputed()) { $entity->get($name)->applyDefaultValue(); } } diff --git a/core/lib/Drupal/Core/Field/FieldItemList.php b/core/lib/Drupal/Core/Field/FieldItemList.php index a1a1ebd..a93d777 100644 --- a/core/lib/Drupal/Core/Field/FieldItemList.php +++ b/core/lib/Drupal/Core/Field/FieldItemList.php @@ -7,7 +7,9 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\TypedData\DataDefinitionInterface; use Drupal\Core\TypedData\Plugin\DataType\ItemList; +use Drupal\Core\TypedData\TypedDataInterface; /** * Represents an entity field; that is, a list of field item objects. @@ -160,6 +162,24 @@ public function __unset($property_name) { /** * {@inheritdoc} */ + public function isComputed() { + return $this->getFieldDefinition()->isComputed(); + } + + /** + * {@inheritdoc} + */ + public function setComputedValues() { + $this->setValue($this->getValue()); + $errors = $this->validate()->getIterator(); + if ($errors->current()) { + throw new FieldException($errors->current()->getMessage()); + } + } + + /** + * {@inheritdoc} + */ public function access($operation = 'view', AccountInterface $account = NULL, $return_as_object = FALSE) { $access_control_handler = \Drupal::entityManager()->getAccessControlHandler($this->getEntity()->getEntityTypeId()); return $access_control_handler->fieldAccess($operation, $this->getFieldDefinition(), $account, $this, $return_as_object); diff --git a/core/lib/Drupal/Core/Field/FieldItemListInterface.php b/core/lib/Drupal/Core/Field/FieldItemListInterface.php index c350432..562d845 100644 --- a/core/lib/Drupal/Core/Field/FieldItemListInterface.php +++ b/core/lib/Drupal/Core/Field/FieldItemListInterface.php @@ -125,6 +125,19 @@ public function __isset($property_name); public function __unset($property_name); /** + * Determines if the field item List is computed. + * + * @return bool + * TRUE if the field item list is computed. + */ + public function isComputed(); + + /** + * Populate the computed list with values. + */ + public function setComputedValues(); + + /** * Defines custom presave behavior for field values. * * This method is called during the process of saving an entity, just before diff --git a/core/lib/Drupal/Core/TypedData/TypedData.php b/core/lib/Drupal/Core/TypedData/TypedData.php index 243d48a..4ba75fb 100644 --- a/core/lib/Drupal/Core/TypedData/TypedData.php +++ b/core/lib/Drupal/Core/TypedData/TypedData.php @@ -112,6 +112,13 @@ public function setValue($value, $notify = TRUE) { /** * {@inheritdoc} */ + public function isComputed(){ + return $this->getDataDefinition()->isComputed(); + } + + /** + * {@inheritdoc} + */ public function getString() { return (string) $this->getValue(); } diff --git a/core/lib/Drupal/Core/TypedData/TypedDataInterface.php b/core/lib/Drupal/Core/TypedData/TypedDataInterface.php index a6892d2..248bfdb 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataInterface.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataInterface.php @@ -47,6 +47,14 @@ public function getDataDefinition(); public function getValue(); /** + * Checks if typed data is computed. + * + * @return boolean + * TRUE if computed. + */ + public function isComputed(); + + /** * Sets the data value. * * @param mixed|null $value diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php index dbf0d5f..9e6c5cc 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php @@ -7,6 +7,8 @@ use Drupal\Core\DependencyInjection\ClassResolverInterface; use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Field\FieldItemList; +use Drupal\Core\Field\FieldItemListComputedInterface; use Drupal\Core\Plugin\DefaultPluginManager; use Drupal\Core\TypedData\Validation\ExecutionContextFactory; use Drupal\Core\TypedData\Validation\RecursiveValidator; @@ -200,6 +202,11 @@ public function getPropertyInstance(TypedDataInterface $object, $property_name, if (isset($value)) { $property->setValue($value, FALSE); } + elseif ($property instanceof FieldItemList && $property->isComputed()) { + // Populate the computed list with values as there are no initial values + // to set. + $property->setComputedValues(); + } return $property; } diff --git a/core/modules/field/tests/modules/field_computed_test/field_computed_test.info.yml b/core/modules/field/tests/modules/field_computed_test/field_computed_test.info.yml new file mode 100644 index 0000000..42f6216 --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/field_computed_test.info.yml @@ -0,0 +1,6 @@ +name: 'Field Computed Test' +type: module +description: 'Support module for the computed field tests.' +core: 8.x +package: Testing +version: VERSION diff --git a/core/modules/field/tests/modules/field_computed_test/field_computed_test.module b/core/modules/field/tests/modules/field_computed_test/field_computed_test.module new file mode 100644 index 0000000..6a29dd7 --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/field_computed_test.module @@ -0,0 +1,63 @@ +id() === 'entity_test') { + + // Add fields to separate bundles so we can test them individually. + switch ($bundle) { + case 'valid_computed_timestamp': + $fields['valid_computed_timestamp'] = BaseFieldDefinition::create('timestamp') + ->setComputed(TRUE) + ->setClass(ComputedValuesItemList::class) + ->setSetting('field', 'valid_computed_timestamp') + ->setLabel(t('Request Time')); + break; + case 'valid_computed_entity_reference': + $fields['valid_computed_entity_reference'] = BaseFieldDefinition::create('entity_reference') + ->setComputed(TRUE) + ->setSetting('target_type', 'entity_test') + ->setClass(ComputedValuesItemList::class) + ->setSetting('field', 'valid_computed_entity_reference') + ->setLabel(t('Valid Computed Entity Reference')); + break; + case 'non_valid_computed_timestamp': + $fields['non_valid_computed_timestamp'] = BaseFieldDefinition::create('timestamp') + ->setComputed(TRUE) + ->setClass(ComputedValuesItemList::class) + ->setSetting('field', 'non_valid_computed_timestamp') + ->setLabel(t('Non valid computed integer')); + break; + case 'non_valid_computed_entity_reference': + $fields['non_valid_computed_entity_reference'] = BaseFieldDefinition::create('entity_reference') + ->setComputed(TRUE) + ->setSetting('target_type', 'entity_test') + ->setClass(ComputedValuesItemList::class) + ->setSetting('field', 'non_valid_computed_entity_reference') + ->setLabel(t('Valid Computed Entity Reference')); + break; + case 'multiplier': + $fields['multiplier'] = BaseFieldDefinition::create('multiplier') + ->setLabel(t('Multiplied integer')) + ->setSetting('factor', 3); + break; + } + } + return $fields; +} \ No newline at end of file diff --git a/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/ComputedValuesItemList.php b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/ComputedValuesItemList.php new file mode 100644 index 0000000..c8ed354 --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/ComputedValuesItemList.php @@ -0,0 +1,63 @@ +getSetting('field')) { + case 'valid_computed_timestamp': + return [ + 0 => [ + 'value' => \Drupal::time()->getRequestTime(), + ], + ]; + break; + case 'valid_computed_entity_reference': + $parent = $this->getEntity(); + if (!$parent->isNew()) { + return [ + 0 => [ + 'target_id' => $parent->id() == 1 ? 2 : 1, + ], + ]; + } + break; + case 'non_valid_computed_entity_reference': + $parent = $this->getEntity(); + if (!$parent->isNew()) { + return [ + 0 => [ + 'target_id' => '3', // Non existing entity reference. + ], + ]; + } + break; + case 'non_valid_computed_timestamp': + return [ + 0 => [ + 'value' => 'A', + ], + ]; + break; + case 'non_valid_computed_entity_reference': + return [ + 0 => [ + 'target_id' => 3, + ], + ]; + break; + } + } + + +} diff --git a/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/Multiplier.php b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/Multiplier.php new file mode 100644 index 0000000..7d24a19 --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/Multiplier.php @@ -0,0 +1,50 @@ +setLabel(t('Original value')); + $properties['multipliedValue'] = DataDefinition::create('integer') + ->setLabel(t('Multiplied value')) + ->setComputed(TRUE) + ->setClass(MultiplierProcessor::class); + return $properties; + } + + + /** + * {@inheritdoc} + */ + public static function schema(FieldStorageDefinitionInterface $field_definition) { + return [ + 'columns' => [ + 'value' => [ + 'type' => 'int', + ], + ], + 'indexes' => [ + 'format' => ['value'], + ], + ]; + } +} \ No newline at end of file diff --git a/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/MultiplierProcessor.php b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/MultiplierProcessor.php new file mode 100644 index 0000000..0a20ada --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/MultiplierProcessor.php @@ -0,0 +1,31 @@ +multipliedValue !== NULL && $this->value !== NULL) { + return $this->multipliedValue; + } + $item = $this->getParent(); + $factor = $item->getDataDefinition()->getSetting('factor'); + + $this->multipliedValue = $item->value * $factor; + return $this->multipliedValue; + } + +} \ No newline at end of file diff --git a/core/modules/field/tests/src/Kernel/FieldComputedTest.php b/core/modules/field/tests/src/Kernel/FieldComputedTest.php new file mode 100644 index 0000000..9211b1c --- /dev/null +++ b/core/modules/field/tests/src/Kernel/FieldComputedTest.php @@ -0,0 +1,175 @@ +installSchema('system', ['sequences', 'key_value']); + $this->installConfig(['field', 'system']); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('user'); + + $data = $this->getTestDataForEntities($this->getName()); + + $this->createTestEntities($data); + } + + /** + * Test a valid computed 'timestamp' field. + */ + public function testValidComputedTimestamp() { + $request_time = \Drupal::time()->getRequestTime(); + $value = $this->entities[0]->get('valid_computed_timestamp')->value; + $this->assertEquals($request_time, $value); + } + + /** + * Test a valid computed entityreference field. + */ + public function testValidComputedEntityReference() { + $referencedEntity = $this->entities[0]->get('valid_computed_entity_reference')->entity; + $this->assertInstanceOf(EntityTest::class, $referencedEntity); + $this->assertEquals($this->entities[1]->id(), $referencedEntity->id()); + } + + /** + * Test that a non existing entity reference returns NULL. + */ + public function testNonValidComputedEntityReference(){ + $referencedEntity = $this->entities[0]->get('non_valid_computed_entity_reference')->entity; + $this->assertNull($referencedEntity); + } + + /** + * @expectedException \Drupal\Core\Field\FieldException + * @expectedExceptionMessage This value should be a valid number. + */ + public function testNonValidComputedTimestamp() { + $data = [ + [ + 'type' => 'non_valid_computed_timestamp', + 'title' => 'Entity with a non valid computed timestamp', + ], + ]; + $this->createTestEntities($data); + } + + + /** + * Test that the processed Multiplier works correctly. + */ + public function testMultiplierFieldType(){ + $originalValue = 9; + $factor = 3; + $multipliedValue = $this->entities[0]->get('multiplier')->multipliedValue; + $this->assertEquals($originalValue*$factor, $multipliedValue); + } + + + + /** + * Get an array of test data for the creation of entities. + * + * @param $methodName + * + * @return array Data for creation of test entities. + */ + private function getTestDataForEntities($methodName){ + $data = []; + switch ($methodName) { + case "testValidComputedTimestamp": + $data = [ + [ + 'type' => 'valid_computed_timestamp', + 'title' => 'Entity with valid computed timestamp', + ], + ]; + break; + case "testNonValidComputedEntityReference": + $data = [ + [ + 'type' => 'non_valid_computed_entity_reference', + 'title' => 'Entity A', + ], + [ + 'type' => 'non_valid_computed_entity_reference', + 'title' => 'Entity B', + ], + ]; + break; + case "testValidComputedEntityReference": + $data = [ + [ + 'type' => 'valid_computed_entity_reference', + 'title' => 'Entity A', + ], + [ + 'type' => 'valid_computed_entity_reference', + 'title' => 'Entity B', + ], + ]; + break; + case "testMultiplierFieldType": + $data = [ + [ + 'type' => 'multiplier', + 'title' => 'Entity with multiplier field' + ], + ]; + break; + } + return $data; + } + + /** + * Create some Test Entities based on an array of data. + */ + private function createTestEntities($data) { + foreach ($data as $item) { + $entity = EntityTest::create([ + 'type' => $item['type'], + 'title' => $item['title'], + ]); + if($entity->bundle() == 'multiplier'){ + $entity->set('multiplier', 9); + } + $entity->save(); + $this->entities[] = $entity; + } + } +} \ No newline at end of file diff --git a/core/modules/views/tests/src/Unit/Plugin/field/FieldTest.php b/core/modules/views/tests/src/Unit/Plugin/field/FieldTest.php index 2b929f3..1f4bfa4 100644 --- a/core/modules/views/tests/src/Unit/Plugin/field/FieldTest.php +++ b/core/modules/views/tests/src/Unit/Plugin/field/FieldTest.php @@ -83,6 +83,13 @@ protected function setUp() { ->method('getDefaultFieldSettings') ->willReturn([]); + $typed_data_manager = $this->getMock('Drupal\Component\Plugin\PluginManagerInterface'); + // @todo: maybe use a reasonable argument + $typed_data_manager->expects($this->any()) + ->method('getDefinition') + ->with($this->anything()) + ->will($this->returnValue(['list_class' => '\Drupal\Core\Field\FieldItemList'])); + $this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface'); $this->renderer = $this->getMock('Drupal\Core\Render\RendererInterface'); @@ -94,6 +101,7 @@ protected function setUp() { $this->container = new ContainerBuilder(); $this->container->set('plugin.manager.field.field_type', $this->fieldTypePluginManager); + $this->container->set('typed_data_manager', $typed_data_manager); \Drupal::setContainer($this->container); } diff --git a/core/tests/Drupal/Tests/Core/Entity/BaseFieldDefinitionTest.php b/core/tests/Drupal/Tests/Core/Entity/BaseFieldDefinitionTest.php index 616f6ba..1f2cab1 100644 --- a/core/tests/Drupal/Tests/Core/Entity/BaseFieldDefinitionTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/BaseFieldDefinitionTest.php @@ -64,8 +64,16 @@ protected function setUp() { ->with($this->fieldType) ->will($this->returnValue($this->fieldTypeDefinition['field_settings'])); + $typed_data_manager = $this->getMock('Drupal\Component\Plugin\PluginManagerInterface'); + // @todo: maybe use a reasonable argument + $typed_data_manager->expects($this->any()) + ->method('getDefinition') + ->with($this->anything()) + ->will($this->returnValue(['list_class' => '\Drupal\Core\Field\FieldItemList'])); + $container = new ContainerBuilder(); $container->set('plugin.manager.field.field_type', $field_type_manager); + $container->set('typed_data_manager', $typed_data_manager); \Drupal::setContainer($container); } diff --git a/core/tests/Drupal/Tests/Core/Entity/ContentEntityBaseUnitTest.php b/core/tests/Drupal/Tests/Core/Entity/ContentEntityBaseUnitTest.php index 541ec88..5a511cd 100644 --- a/core/tests/Drupal/Tests/Core/Entity/ContentEntityBaseUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/ContentEntityBaseUnitTest.php @@ -472,9 +472,12 @@ public function testLabel() { * - Fields array for $fields. */ public function providerGet() { + $items = $this->getMockBuilder('Drupal\Core\Field\FieldItemList') + ->disableOriginalConstructor() + ->getMock(); return [ // Populated fields array. - ['result', 'field_name', 'langcode', ['field_name' => ['langcode' => 'result']]], + [$items, 'field_name', 'langcode', ['field_name' => ['langcode' => $items]]], // Incomplete fields array. ['getTranslatedField_result', 'field_name', 'langcode', ['field_name' => 'no_langcode']], // Empty fields array. diff --git a/core/tests/Drupal/Tests/Core/Entity/TypedData/EntityAdapterUnitTest.php b/core/tests/Drupal/Tests/Core/Entity/TypedData/EntityAdapterUnitTest.php index 953daac..d5832d7 100644 --- a/core/tests/Drupal/Tests/Core/Entity/TypedData/EntityAdapterUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/TypedData/EntityAdapterUnitTest.php @@ -141,8 +141,15 @@ protected function setUp() { $this->typedDataManager = $this->getMock(TypedDataManagerInterface::class); $this->typedDataManager->expects($this->any()) ->method('getDefinition') - ->with('entity') - ->will($this->returnValue(['class' => '\Drupal\Core\Entity\Plugin\DataType\EntityAdapter'])); + ->will($this->returnCallback(function($plugin_id){ + switch ($plugin_id) { + case 'entity': + return ['class' => '\Drupal\Core\Entity\Plugin\DataType\EntityAdapter']; + default: + // @todo: maybe use a reasonable argument value + return ['list_class' => '\Drupal\Core\Field\FieldItemList']; + } + })); $this->typedDataManager->expects($this->any()) ->method('getDefaultConstraints') ->willReturn([]);