diff --git a/core/lib/Drupal/Core/Field/FieldItemListComputed.php b/core/lib/Drupal/Core/Field/FieldItemListComputed.php new file mode 100644 index 0000000..b00dc30 --- /dev/null +++ b/core/lib/Drupal/Core/Field/FieldItemListComputed.php @@ -0,0 +1,59 @@ +list = []; + $values = $this->computeItemValues(); + if (is_array($values) && !empty($values)) { + foreach ($values AS $delta => $value) { + $this->list[] = $this->createItem($delta, $value); + } + } + } + + /** + * This should compute the values that are used to create the field items. + * + * @return array The values for the field items. + */ + protected abstract function computeItemValues(); + + /** + * This will do nothing as computed lists are readonly bz default. + */ + public function setValue($values, $notify = TRUE) {} + + /** + * This will do nothing as computed lists are readonly bz default. + */ + public function appendItem($value = NULL) {} + + /** + * This will do nothing as computed lists are readonly bz default. + */ + public function defaultValuesForm(array &$form, FormStateInterface $form_state) {} + + /** + * This will do nothing as computed lists are readonly bz default. + */ + public function removeItem($index) {} + + /** + * This will do nothing as computed lists are readonly bz default. + */ + public function set($index, $value) {} +} diff --git a/core/lib/Drupal/Core/Field/FieldItemListComputedInterface.php b/core/lib/Drupal/Core/Field/FieldItemListComputedInterface.php new file mode 100644 index 0000000..6adff61 --- /dev/null +++ b/core/lib/Drupal/Core/Field/FieldItemListComputedInterface.php @@ -0,0 +1,9 @@ +itemDefinition = $definition; return $this; } + + /** + * {@inheritdoc} + */ + public function isComputed() { + if ($this->isComputedByClassInheritance()) { + return TRUE; + } + return parent::isComputed(); + } + + /** + * {@inheritdoc} + * @throws \LogicException + * Thrown if computed is to be set to FALSE for a field that requires to be computed by it's list class. + */ + public function setComputed($computed) { + if (!$computed && $this->isComputedByClassInheritance()) { + throw new \LogicException("The list class used enforces this field to be a computed field."); + } + return parent::setComputed($computed); + } + + /** + * Helper to deduce whether the the field should be computed based on it's list class. + * + * @return bool + * + * @see \Drupal\Core\Field\FieldItemListComputed + */ + protected function isComputedByClassInheritance() { + $class = $this->getClass(); + if (!empty($class)) { + return is_subclass_of($class, '\Drupal\Core\Field\FieldItemListComputedInterface'); + } + return FALSE; + } } diff --git a/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php index 9114527..8849db2 100644 --- a/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php @@ -102,10 +102,7 @@ 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/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php index be5bcd4..365ad58 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php @@ -12,6 +12,7 @@ use Drupal\Core\DependencyInjection\ClassResolverInterface; use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Field\FieldItemListComputedInterface; use Drupal\Core\Plugin\DefaultPluginManager; use Drupal\Core\TypedData\Validation\ExecutionContextFactory; use Drupal\Core\TypedData\Validation\RecursiveValidator; @@ -198,6 +199,9 @@ public function getPropertyInstance(TypedDataInterface $object, $property_name, $property->setContext($property_name, $object); if (isset($value)) { $property->setValue($value, FALSE); + } elseif ($property instanceof FieldItemListComputedInterface) { + // populate the computed list with values as there are no initial values to set. + $property->computeItems(); } return $property; } diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php index 8e0e0b4..5d51f0d 100644 --- a/core/modules/field/src/Entity/FieldConfig.php +++ b/core/modules/field/src/Entity/FieldConfig.php @@ -69,6 +69,13 @@ class FieldConfig extends FieldConfigBase implements FieldConfigInterface { protected $fieldStorage; /** + * Flag indicating whether the field is computed. + * + * @var bool + */ + protected $computed; + + /** * Constructs a FieldConfig object. * * In most cases, Field entities are created via @@ -318,14 +325,23 @@ public function getDisplayOptions($display_context) { * {@inheritdoc} */ public function isReadOnly() { - return FALSE; + return $this->isComputed(); } /** * {@inheritdoc} */ public function isComputed() { - return FALSE; + if (!isset($this->computed)) { + $item_type_definition = \Drupal::typedDataManager() + ->getDefinition('field_item:' . $this->getType()); + if ($item_type_definition && isset($item_type_definition['list_class'])) { + $this->computed = is_subclass_of($item_type_definition['list_class'], '\Drupal\Core\Field\FieldItemListComputedInterface'); + } else { + $this->computed = FALSE; + } + } + return $this->computed; } /** diff --git a/core/modules/field/src/Entity/FieldStorageConfig.php b/core/modules/field/src/Entity/FieldStorageConfig.php index d4c2922..1747582 100644 --- a/core/modules/field/src/Entity/FieldStorageConfig.php +++ b/core/modules/field/src/Entity/FieldStorageConfig.php @@ -225,6 +225,13 @@ class FieldStorageConfig extends ConfigEntityBase implements FieldStorageConfigI protected static $inDeletion = FALSE; /** + * Flag indicating whether the field is computed. + * + * @var bool + */ + protected $computed; + + /** * Constructs a FieldStorageConfig object. * * @param array $values @@ -691,7 +698,7 @@ public function getTargetEntityTypeId() { * {@inheritdoc} */ public function isQueryable() { - return TRUE; + return !$this->isComputed(); } /** @@ -701,6 +708,9 @@ public function isQueryable() { * TRUE if the field has data for any entity; FALSE otherwise. */ public function hasData() { + if ($this->isComputed()) { + return FALSE; + } return \Drupal::entityManager()->getStorage($this->entity_type)->countFieldData($this, TRUE); } @@ -828,4 +838,17 @@ public function setIndexes(array $indexes) { return $this; } + + protected function isComputed() { + if (!isset($this->computed)) { + $item_type_definition = \Drupal::typedDataManager() + ->getDefinition('field_item:' . $this->getType()); + if ($item_type_definition && isset($item_type_definition['list_class'])) { + $this->computed = is_subclass_of($item_type_definition['list_class'], '\Drupal\Core\Field\FieldItemListComputedInterface'); + } else { + $this->computed = FALSE; + } + } + return $this->computed; + } } 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..9f77e96 --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/field_computed_test.module @@ -0,0 +1,56 @@ +id() === 'node') { + $fields = array(); + + $fields['dice_count'] = BaseFieldDefinition::create('integer') + ->setSetting('unsigned', TRUE) + ->setLabel(t('Dice count')) + ->setDisplayOptions('form', array( + 'type' => 'number', + 'weight' => 10, + )) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayOptions('view', array( + 'label' => 'inline', + 'type' => 'number_integer', + 'weight' => 0, + )); + + // set a custom list class an explicitly define the field to be computed + $fields['dice_result'] = BaseFieldDefinition::create('integer') + ->setLabel(t('Dice result')) + ->setClass('\Drupal\field_computed_test\Plugin\Field\FieldType\DiceItemList') + ->setComputed(TRUE) + ->setDisplayOptions('view', array( + 'label' => 'inline', + 'type' => 'number_integer', + 'weight' => 0, + )); + + // set a custom list class and rely on that class to mark the field as computed + $fields['dice_result_v2'] = BaseFieldDefinition::create('integer') + ->setLabel(t('Dice result V2')) + ->setClass('\Drupal\field_computed_test\Plugin\Field\FieldType\DiceItemList') + ->setDisplayOptions('view', array( + 'label' => 'inline', + 'type' => 'number_integer', + 'weight' => 0, + )); + + // use a field type which uses a list class that marks the field as computed + $fields['dice_result_v3'] = BaseFieldDefinition::create('dice') + ->setLabel(t('Dice result V3')) + ->setDisplayOptions('view', array( + 'label' => 'inline', + 'weight' => 0, + )); + + return $fields; + } + +} diff --git a/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldFormatter/DiceFormatter.php b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldFormatter/DiceFormatter.php new file mode 100644 index 0000000..09a106b --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldFormatter/DiceFormatter.php @@ -0,0 +1,32 @@ + $item) { + $elements[$delta] = [ + '#markup' => 'The value: '. $item->value, + ]; + } + return $elements; + } +} diff --git a/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/DiceItem.php b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/DiceItem.php new file mode 100644 index 0000000..44bda1b --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/DiceItem.php @@ -0,0 +1,37 @@ +setLabel(t('Integer value')); + 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/DiceItemList.php b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/DiceItemList.php new file mode 100644 index 0000000..102f91e --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldType/DiceItemList.php @@ -0,0 +1,24 @@ +getEntity()->dice_count->value; + $values = []; + foreach (range(0, $items_count - 1) as $delta) { + $values[$delta] = [ + 'value' => rand(1, 6) + ]; + } + return $values; + } +} diff --git a/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldWidget/DiceWidget.php b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldWidget/DiceWidget.php new file mode 100644 index 0000000..9d9b0f4 --- /dev/null +++ b/core/modules/field/tests/modules/field_computed_test/src/Plugin/Field/FieldWidget/DiceWidget.php @@ -0,0 +1,30 @@ + 'Dummy widget.', + ]; + return $element; + } +} diff --git a/core/modules/field_ui/src/Form/FieldStorageAddForm.php b/core/modules/field_ui/src/Form/FieldStorageAddForm.php index 5f087a6..0a6f191 100644 --- a/core/modules/field_ui/src/Form/FieldStorageAddForm.php +++ b/core/modules/field_ui/src/Form/FieldStorageAddForm.php @@ -361,19 +361,21 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Create the field storage and field. try { $this->entityManager->getStorage('field_storage_config')->create($field_storage_values)->save(); + /** @var \Drupal\field\FieldConfigInterface $field */ $field = $this->entityManager->getStorage('field_config')->create($field_values); $field->save(); $this->configureEntityFormDisplay($values['field_name'], $widget_id); $this->configureEntityViewDisplay($values['field_name'], $formatter_id); - // Always show the field settings step, as the cardinality needs to be // configured for new fields. $route_parameters = array( 'field_config' => $field->id(), ) + FieldUI::getRouteBundleParameter($entity_type, $this->bundle); - $destinations[] = array('route_name' => "entity.field_config.{$this->entityTypeId}_storage_edit_form", 'route_parameters' => $route_parameters); - $destinations[] = array('route_name' => "entity.field_config.{$this->entityTypeId}_field_edit_form", 'route_parameters' => $route_parameters); + if (!$field->isComputed()) { + $destinations[] = array('route_name' => "entity.field_config.{$this->entityTypeId}_storage_edit_form", 'route_parameters' => $route_parameters); + $destinations[] = array('route_name' => "entity.field_config.{$this->entityTypeId}_field_edit_form", 'route_parameters' => $route_parameters); + } $destinations[] = array('route_name' => "entity.{$this->entityTypeId}.field_ui_fields", 'route_parameters' => $route_parameters); // Store new field information for any additional submit handlers. 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 90bd2cc..b39dd5a 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 9689552..165b9e8 100644 --- a/core/tests/Drupal/Tests/Core/Entity/BaseFieldDefinitionTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/BaseFieldDefinitionTest.php @@ -69,8 +69,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/TypedData/EntityAdapterUnitTest.php b/core/tests/Drupal/Tests/Core/Entity/TypedData/EntityAdapterUnitTest.php index 863ae62..1ee84ff 100644 --- a/core/tests/Drupal/Tests/Core/Entity/TypedData/EntityAdapterUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/TypedData/EntityAdapterUnitTest.php @@ -146,8 +146,15 @@ protected function setUp() { ->getMock(); $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([]);