diff --git a/core/lib/Drupal/Core/Field/ComputedFieldItemListBase.php b/core/lib/Drupal/Core/Field/ComputedFieldItemListBase.php new file mode 100644 index 0000000..74da4e0 --- /dev/null +++ b/core/lib/Drupal/Core/Field/ComputedFieldItemListBase.php @@ -0,0 +1,86 @@ +setComputedValues(); + + $errors = $this->validate()->getIterator(); + if ($errors->current()) { + throw new FieldException($errors->current()->getMessage()); + } + return isset($this->list[$index]) ? $this->list[$index] : NULL; + } + + /** + * {@inheritdoc} + */ + public function getIterator() { + $this->setComputedValues(); + return parent::getIterator(); + } + + /** + * Gets the computed values. + * + * @return array + * The array generated by ::computeValues. + */ + protected function getComputedValues() { + return $this->computedValues; + } + + /** + * Sets the 'list' property with computed values. + * + * @throws \Drupal\Core\Field\FieldException + * If the array returned is not a sequential array of values. + */ + protected function setComputedValues() { + if ($this->getComputedValues() === FALSE) { + $this->computedValues = $this->computeValues(); + if ($this->computedValues !== NULL) { + if (!is_array($this->computedValues) || (array_keys($this->computedValues) !== range(0, count($this->computedValues) - 1))) { + throw new FieldException('A sequential array of values is excepted.'); + } + } + } + if (!empty($this->computedValues)) { + foreach ($this->getComputedValues() as $index => $value) { + $this->list[$index] = $this->createItem($index, $value); + } + } + } + +} 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..e4f1d2b --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/field_computed_test.module @@ -0,0 +1,91 @@ +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 'invalid_computed_timestamp': + $fields['invalid_computed_timestamp'] = BaseFieldDefinition::create('timestamp') + ->setComputed(TRUE) + ->setClass(ComputedValuesItemList::class) + ->setSetting('field', 'invalid_computed_timestamp') + ->setLabel(t('Non valid computed integer')); + 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_existing_computed_entity_reference': + $fields['non_existing_computed_entity_reference'] = BaseFieldDefinition::create('entity_reference') + ->setComputed(TRUE) + ->setSetting('target_type', 'entity_test') + ->setClass(ComputedValuesItemList::class) + ->setSetting('field', 'non_existing_computed_entity_reference') + ->setLabel(t('Non Existing Computed Entity Reference')); + break; + + case 'invalid_cardinality': + $fields['invalid_cardinality'] = BaseFieldDefinition::create('integer') + ->setComputed(TRUE) + ->setSetting('field', 'invalid_cardinality') + ->setClass(ComputedValuesItemList::class) + ->setCardinality(3) + ->setLabel(t('Non valid cardinality')); + break; + + case 'multiplier': + $fields['multiplier'] = BaseFieldDefinition::create('multiplier') + ->setLabel(t('Multiplied integer')) + ->setSetting('factor', 3); + break; + + case 'dice': + $fields['dice'] = BaseFieldDefinition::create('dice') + ->setLabel(t('Dice')) + ->setSetting('min', 1) + ->setSetting('max', 6); + break; + } + } + return $fields; +} diff --git a/core/modules/field/tests/modules/field_computed_test/src/ComputedValuesItemList.php b/core/modules/field/tests/modules/field_computed_test/src/ComputedValuesItemList.php new file mode 100644 index 0000000..2364d12 --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/src/ComputedValuesItemList.php @@ -0,0 +1,68 @@ +getSetting('field')) { + case 'valid_computed_timestamp': + $values = [ + 0 => \Drupal::time()->getRequestTime(), + ]; + break; + + case 'invalid_computed_timestamp': + $values = [0 => 'A']; + break; + + case 'valid_computed_entity_reference': + $parent = $this->getEntity(); + if (!$parent->isNew()) { + $values = [0 => (integer) $parent->id() === 1 ? 2 : 1]; + } + break; + + case 'non_existing_computed_entity_reference': + $parent = $this->getEntity(); + if (!$parent->isNew()) { + $values = [0 => 3]; + } + break; + + case 'invalid_cardinality': + $values = [1, 2, 3, 4]; + break; + } + return $values; + } + + /** + * Force the entity reference fields to be computed mutliple times. + * + * The default behaviour saves the result to a class variable and only + * computes it once. + * + * For the entity reference fields, since the entity we're using doesn't have + * an entity ID until it's actually saved, we need to repeat the computing. + */ + protected function setComputedValues() { + $field = $this->getSetting('field'); + if ($field == 'valid_computed_entity_reference' || $field == 'non_existing_computed_entity_reference') { + $this->computedValues = FALSE; + } + parent::setComputedValues(); + } + +} diff --git a/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldFormatter/MultiplierFormatter.php b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldFormatter/MultiplierFormatter.php new file mode 100644 index 0000000..87e9304 --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldFormatter/MultiplierFormatter.php @@ -0,0 +1,31 @@ + $item) { + $items[$delta]->value = $item->multiplied; + } + return parent::viewElements($items, $langcode); + } + +} diff --git a/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/Dice.php b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/Dice.php new file mode 100644 index 0000000..a21b2f0 --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/Dice.php @@ -0,0 +1,48 @@ +setComputed(TRUE) + ->setClass(DiceProcessor::class); + return $properties; + } + + /** + * {@inheritdoc} + */ + public static function schema(FieldStorageDefinitionInterface $field_definition) { + return [ + 'columns' => [], + ]; + } + +} diff --git a/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/DiceProcessor.php b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/DiceProcessor.php new file mode 100644 index 0000000..ea42b30 --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/DiceProcessor.php @@ -0,0 +1,23 @@ +getParent(); + $min = $parent->getFieldDefinition()->getSetting('min'); + $max = $parent->getFieldDefinition()->getSetting('max'); + return rand($min, $max); + } + +} 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..ed94e8e --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/Multiplier.php @@ -0,0 +1,59 @@ +setLabel(t('multiplied')) + ->setComputed(TRUE) + ->setClass(MultiplierProcessor::class); + return $properties; + } + + /** + * {@inheritdoc} + */ + public static function defaultFieldSettings() { + return [ + 'factor' => 1, + ] + parent::defaultFieldSettings(); + } + + /** + * {@inheritdoc} + */ + public function fieldSettingsForm(array $form, FormStateInterface $form_state) { + $element = parent::fieldSettingsForm($form, $form_state); + $settings = $this->getSettings(); + $element['factor'] = [ + '#type' => 'number', + '#title' => t('Factor'), + '#default_value' => $settings['factor'], + '#description' => t('Multiply the original value with this factor.'), + ]; + return $element; + } + +} 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..64759c8 --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/MultiplierProcessor.php @@ -0,0 +1,32 @@ +multipliedValue !== NULL && $this->value !== NULL) { + return $this->multipliedValue; + } + $item = $this->getParent(); + $factor = $item->getDataDefinition()->getSetting('factor'); + $this->multipliedValue = $item->value * $factor; + return $this->multipliedValue; + } + +} diff --git a/core/modules/field/tests/src/Kernel/FieldComputedFieldItemListTest.php b/core/modules/field/tests/src/Kernel/FieldComputedFieldItemListTest.php new file mode 100644 index 0000000..1163b5e --- /dev/null +++ b/core/modules/field/tests/src/Kernel/FieldComputedFieldItemListTest.php @@ -0,0 +1,185 @@ +installSchema('system', ['sequences', 'key_value']); + $this->installConfig(['field', 'system']); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('user'); + $entitiesData = $this->getTestDataForEntities($this->getName()); + $this->createTestEntities($entitiesData); + } + + /** + * 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 testNonExistingComputedEntityReference() { + $referencedEntity = $this->entities[0]->get('non_existing_computed_entity_reference')->entity; + $this->assertNull($referencedEntity); + } + + /** + * Test that a wrong value ('A') for a 'timestamp' field throws an exception. + * + * @expectedException \Drupal\Core\Field\FieldException + * @expectedExceptionMessage This value should be a valid number. + */ + public function testNonValidComputedTimestamp() { + $entitiesData = [ + [ + 'type' => 'invalid_computed_timestamp', + 'title' => 'Entity with a non valid computed timestamp', + ], + ]; + $this->createTestEntities($entitiesData); + } + + /** + * Test that the computed values (4) in a field with cardinality '3' throws an exception. + * + * @expectedException \Drupal\Core\Field\FieldException + * @expectedExceptionMessage this field cannot hold more than 3 values + */ + public function testNonValidCardinality() { + $entitiesData = [ + [ + 'type' => 'invalid_cardinality', + 'title' => 'Entity with a non valid cardinality', + ], + ]; + $this->createTestEntities($entitiesData); + } + + /** + * Get an array of test data for the creation of entities. + * + * @param $methodName + * The test method for which the entities have to be created. + * + * @return array + * An array of data for creation of test entities. + */ + protected function getTestDataForEntities($methodName) { + $entitiesData = []; + switch ($methodName) { + case "testValidComputedTimestamp": + $entitiesData = [ + [ + 'type' => 'valid_computed_timestamp', + 'title' => 'Entity with valid computed timestamp', + ], + ]; + break; + + case "testInvalidComputedTimestamp": + $entitiesData = [ + [ + 'type' => 'invalid_computed_timestamp', + 'title' => 'Entity with an invalid computed timestamp', + ], + ]; + break; + + case "testValidComputedEntityReference": + $entitiesData = [ + [ + 'type' => 'valid_computed_entity_reference', + 'title' => 'Entity A', + ], + [ + 'type' => 'valid_computed_entity_reference', + 'title' => 'Entity B', + ], + ]; + break; + + case "testNonExistingComputedEntityReference": + $entitiesData = [ + [ + 'type' => 'non_existing_computed_entity_reference', + 'title' => 'Entity A', + ], + [ + 'type' => 'non_existing_computed_entity_reference', + 'title' => 'Entity B', + ], + ]; + break; + + } + return $entitiesData; + } + + /** + * Create some test entities based on an array of data. + * + * @param $entitiesData + * An array of data for creating entities with computed fields for specific + * bundles. + * + * @see field_computed_test.module + */ + protected function createTestEntities(array $entitiesData) { + foreach ($entitiesData as $item) { + $entity = EntityTest::create([ + 'type' => $item['type'], + 'title' => $item['title'], + ]); + $entity->save(); + $this->entities[] = $entity; + } + } + +} diff --git a/core/modules/field/tests/src/Kernel/FieldComputedFieldTypeTest.php b/core/modules/field/tests/src/Kernel/FieldComputedFieldTypeTest.php new file mode 100644 index 0000000..47fac01 --- /dev/null +++ b/core/modules/field/tests/src/Kernel/FieldComputedFieldTypeTest.php @@ -0,0 +1,75 @@ +installSchema('system', ['sequences', 'key_value']); + $this->installConfig(['field', 'system']); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('user'); + } + + /** + * Test that the processed Multiplier field works correctly. + * + * The factor is a setting saved in the field definition config entity. + */ + public function testMultiplierFieldType() { + $originalValue = rand(1, 10); + $entity = EntityTest::create([ + 'type' => 'multiplier', + 'title' => $this->randomString(), + ]); + $entity->set('multiplier', $originalValue); + $entity->save(); + $factor = $entity->get('multiplier')->getFieldDefinition()->getSetting('factor'); + $multipliedValue = $entity->get('multiplier')->multiplied; + $this->assertEquals($originalValue * $factor, $multipliedValue); + } + + /** + * Test that the processed Multiplier field works correctly. + * + * The factor is a setting saved in the field definition config entity. + */ + public function testDiceFieldType() { + $entity = EntityTest::create([ + 'type' => 'dice', + 'title' => $this->randomString(), + ]); + $entity->save(); + $min = $entity->get('dice')->getFieldDefinition()->getSetting('min'); + $max = $entity->get('dice')->getFieldDefinition()->getSetting('max'); + $value = $entity->get('dice')->value; + $this->assertTrue($value >= $min && $value <= $max, "The value (" . $value . ")is a random computed number between the max and min settings of the dice field."); + } + +} 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([]);