diff --git a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php index 7cd56e2578..38288acebf 100644 --- a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php +++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php @@ -5,9 +5,9 @@ use Drupal\Core\Cache\UnchangingCacheableDependencyTrait; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\Entity\BaseFieldOverride; +use Drupal\Core\Field\TypedData\FieldDefinitionOptionsProviderTrait; use Drupal\Core\Field\TypedData\FieldItemDataDefinition; use Drupal\Core\TypedData\ListDataDefinition; -use Drupal\Core\TypedData\OptionsProviderInterface; /** * A class for defining entity fields. @@ -16,6 +16,7 @@ class BaseFieldDefinition extends ListDataDefinition implements FieldDefinitionI use UnchangingCacheableDependencyTrait; use FieldInputValueNormalizerTrait; + use FieldDefinitionOptionsProviderTrait; /** * The field type. @@ -554,18 +555,25 @@ public function setInitialValueFromField($field_name, $default_value = NULL) { } /** - * {@inheritdoc} + * Sets options providers for field item properties. + * + * @param string $property_name + * The property for which to specify the options provider. + * @param string|null $provider_definition + * The options provider definition; e.g. the class name. See + * \Drupal\Core\TypedData\TypedDataManager::getOptionsProvider() for + * supported notations. If NULL is given, the set options provider + * definition is unset. + * + * @return $this + * + * @see ::getPropertyDefinitions() */ - public function getOptionsProvider($property_name, FieldableEntityInterface $entity) { - // If the field item class implements the interface, create an orphaned - // runtime item object, so that it can be used as the options provider - // without modifying the entity being worked on. - if (is_subclass_of($this->getItemDefinition()->getClass(), OptionsProviderInterface::class)) { - $items = $entity->get($this->getName()); - return \Drupal::service('plugin.manager.field.field_type')->createFieldItem($items, 0); - } - // @todo: Allow setting custom options provider, see - // https://www.drupal.org/node/2002138. + public function setOptionsProviderDefinition($property_name, $provider_definition = NULL) { + $this->checkOptionsProviderDefinition($property_name, $provider_definition); + // Set the options provider and let getPropertyDefinitions() apply it. + $this->definition['options_provider'][$property_name] = $provider_definition; + return $this; } /** @@ -582,11 +590,23 @@ public function getPropertyDefinition($name) { /** * {@inheritdoc} + * + * @see ::setOptionsProviderDefinition() */ public function getPropertyDefinitions() { if (!isset($this->propertyDefinitions)) { $class = $this->getItemDefinition()->getClass(); $this->propertyDefinitions = $class::propertyDefinitions($this); + $this->addLegacyOptionsProvider($this->propertyDefinitions); + $this->addFieldStorageDefinitionContext($this->propertyDefinitions); + + // Incorporate any options providers that have been specified. + if (!empty($this->definition['options_provider'])) { + $provider_definitions = array_intersect_key($this->definition['options_provider'], $this->propertyDefinitions); + foreach ($provider_definitions as $name => $provider_definition) { + $this->propertyDefinitions[$name]->setOptionsProviderDefinition($provider_definition); + } + } } return $this->propertyDefinitions; } diff --git a/core/lib/Drupal/Core/Field/FieldConfigBase.php b/core/lib/Drupal/Core/Field/FieldConfigBase.php index 922eba4f25..0c85ebb88e 100644 --- a/core/lib/Drupal/Core/Field/FieldConfigBase.php +++ b/core/lib/Drupal/Core/Field/FieldConfigBase.php @@ -5,6 +5,7 @@ use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Field\TypedData\FieldDefinitionOptionsProviderTrait; use Drupal\Core\Field\TypedData\FieldItemDataDefinition; /** @@ -12,6 +13,7 @@ */ abstract class FieldConfigBase extends ConfigEntityBase implements FieldConfigInterface { + use FieldDefinitionOptionsProviderTrait; use FieldInputValueNormalizerTrait; /** @@ -191,6 +193,13 @@ abstract class FieldConfigBase extends ConfigEntityBase implements FieldConfigIn */ protected $propertyConstraints = []; + /** + * The set options provider context. + * + * @var array[] + */ + protected $optionsProviderContext; + /** * {@inheritdoc} */ @@ -594,4 +603,27 @@ public function isInternal() { return $this->isComputed(); } + /** + * {@inheritdoc} + */ + public function getOptionsProviderContext() { + return $this->optionsProviderContext; + } + + /** + * {@inheritdoc} + */ + public function setOptionsProviderContext($interface, $method, $arguments) { + $this->optionsProviderContext[$interface][$method] = $arguments; + return $this; + } + + /** + * {@inheritdoc} + */ + public function removeOptionsProviderContext($interface) { + unset($this->optionsProviderContext[$interface]); + return $this; + } + } diff --git a/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php b/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php index c99a11a832..370de296c4 100644 --- a/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php +++ b/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php @@ -266,4 +266,32 @@ public function getConfig($bundle); */ public function getUniqueIdentifier(); + /** + * {@inheritdoc} + * + * @param string|null $property_name + * (optional) The name of the property to get options for; e.g., 'value'. + * If omitted, options for the field item's main property are provided. + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * (optional) The entity for which the options should be provided. + * @param int $delta + * (optional) If the entity is passed, the delta of the item for which to + * return the options provider. Defaults to the first item. + * + * @return \Drupal\Core\TypedData\OptionsProviderInterface|null + * An options provider, or NULL if no options are defined. + * + * @see ::getOptionsProviderDefinition() + */ + public function getOptionsProvider($property_name = NULL, FieldableEntityInterface $entity = NULL, $delta = 0); + + /** + * {@inheritdoc} + * + * @param string|null $property_name + * (optional) The name of the property to get options for; e.g., 'value'. + * If omitted, options for the field item's main property are provided. + */ + public function getOptionsProviderDefinition($property_name = NULL); + } diff --git a/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php b/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php index 34cc058462..417fdfa61d 100644 --- a/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php +++ b/core/lib/Drupal/Core/Field/FieldStorageDefinitionInterface.php @@ -131,15 +131,30 @@ public function getDescription(); /** * Gets an options provider for the given field item property. * - * @param string $property_name - * The name of the property to get options for; e.g., 'value'. + * @param string|null $property_name + * (optional) The name of the property to get options for; e.g., 'value'. + * If omitted, options for the field item's main property are provided. * @param \Drupal\Core\Entity\FieldableEntityInterface $entity - * The entity for which the options should be provided. + * (optional) The entity for which the options should be provided. + * @param int $delta + * (optional) If the entity is passed, the delta of the item for which to + * return the options provider. Defaults to the first item. * * @return \Drupal\Core\TypedData\OptionsProviderInterface|null * An options provider, or NULL if no options are defined. + * + * @see ::getOptionsProviderDefinition() + */ + public function getOptionsProvider($property_name = NULL, FieldableEntityInterface $entity = NULL, $delta = 0); + + /** + * {@inheritdoc} + * + * @param string|null $property_name + * (optional) The name of the property to get options for; e.g., 'value'. + * If omitted, options for the field item's main property are provided. */ - public function getOptionsProvider($property_name, FieldableEntityInterface $entity); + public function getOptionsProviderDefinition($property_name = NULL); /** * Returns whether the field can contain multiple items. diff --git a/core/lib/Drupal/Core/Field/TypedData/FieldDefinitionOptionsProviderTrait.php b/core/lib/Drupal/Core/Field/TypedData/FieldDefinitionOptionsProviderTrait.php new file mode 100644 index 0000000000..8784e4bb90 --- /dev/null +++ b/core/lib/Drupal/Core/Field/TypedData/FieldDefinitionOptionsProviderTrait.php @@ -0,0 +1,169 @@ +getRelatedFieldStorageDefinition() + ->getPropertyDefinition($property_name); + + if (!$property_definition) { + throw new \InvalidArgumentException(String::format('Options provider for the unknown property @name of field @field given.', [ + '@name' => $property_name, + '@field' => $this->getName(), + ])); + } + if (!method_exists($property_definition, 'setOptionsProviderDefinition')) { + throw new \InvalidArgumentException(String::format('Unable to set an options provider for the property @name of field @field as the property definition class does not support it.', [ + '@name' => $property_name, + '@field' => $this->getName(), + ])); + } + } + + /** + * Implements \Drupal\Core\Field\FieldDefinitionInterface::getOptionsProvider(). + * Implements \Drupal\Core\Field\FieldStorageDefinitionInterface::getOptionsProvider(). + */ + public function getOptionsProvider($property_name = NULL, FieldableEntityInterface $entity = NULL, $delta = 0) { + // In order to be compatible with + // \Drupal\Core\TypedData\DataDefinitionInterface::getOptionsProvider() we + // must support an optional $data argument instead of the $property_name. + // @todo: Fix by resolving https://www.drupal.org/node/2268049. + if (isset($property_name) && $property_name instanceof TypedDataInterface) { + $entity = $property_name->getRoot(); + $property_name = NULL; + } + $field_storage_definition = $this->getRelatedFieldStorageDefinition(); + $property_name = $property_name ?: $field_storage_definition->getMainPropertyName(); + $property_definition = $field_storage_definition->getPropertyDefinition($property_name); + + if (!isset($property_definition)) { + throw new \InvalidArgumentException(SafeMarkup::format('Invalid property name %property given.', ['%property' => $property_name])); + } + + if ($entity) { + $field_item_list = $entity->get($this->getName()); + // Pass on an empty property object even if no data is set at the given + // delta as this is required by the legacy API support. + // @see \Drupal\Core\Field\TypedData\LegacyOptionsProvider + $item = isset($field_item_list[$delta]) ? $field_item_list[$delta] : + \Drupal::service('plugin.manager.field.field_type')->createFieldItem($field_item_list, 0); + $property = $item->get($property_name); + } + else { + $property = NULL; + } + return $property_definition->getOptionsProvider($property); + } + + /** + * Implements \Drupal\Core\Field\FieldDefinitionInterface::getOptionsProviderDefinition(). + * Implements \Drupal\Core\Field\FieldStorageDefinitionInterface::getOptionsProviderDefinition(). + */ + public function getOptionsProviderDefinition($property_name = NULL) { + $property_name = $property_name ?: $this->getRelatedFieldStorageDefinition()->getMainPropertyName(); + return $this + ->getRelatedFieldStorageDefinition() + ->getPropertyDefinition($property_name) + ->getOptionsProviderDefinition(); + } + + /** + * Gets the field storage definition related to the object. + * + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface + * The field storage definition. + */ + private function getRelatedFieldStorageDefinition() { + if ($this instanceof FieldStorageDefinitionInterface) { + return $this; + } + elseif ($this instanceof FieldDefinitionInterface) { + // On field definition interface, there is already a getter we can re-use. + return $this->getFieldStorageDefinition(); + } + throw new \LogicException('The object must either implement FieldStorageDefinitionInterface of FieldDefinitionInterace.'); + } + + /** + * Returns the field type plugin manager. + * + * @return \Drupal\Core\Field\FieldTypePluginManagerInterface + * The field type plugin manager. + */ + protected function getFieldTypePluginManager() { + return \Drupal::service('plugin.manager.field.field_type'); + } + + /** + * Adds a legacy options provider definition if necessary. + * + * For BC we support field item classes implementing the options provider + * interface. + * + * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $property_definitions + * The field item property definitions. + */ + protected function addLegacyOptionsProvider(array $property_definitions) { + $main_property = $this->getRelatedFieldStorageDefinition()->getMainPropertyName(); + if (!isset($property_definitions[$main_property])) { + return; + } + $property = $property_definitions[$main_property]; + $type_definition = $this->getFieldTypePluginManager() + ->getDefinition($this->getType()); + + // For BC, if the field item class implements the options provider + // interface, specify a class that proxies it through. + if (is_subclass_of($type_definition['class'], OptionsProviderInterface::class) && !$property->getOptionsProviderDefinition()) { + $property->setOptionsProviderDefinition(LegacyOptionsProvider::class); + } + } + + /** + * Adds the field storage definition as available options provider context. + * + * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $property_definitions + * The properties to which to add the context. + */ + protected function addFieldStorageDefinitionContext(array $property_definitions) { + $field_storage_definition = $this->getRelatedFieldStorageDefinition(); + foreach ($property_definitions as $name => $property_definition) { + $property_definition->setOptionsProviderContext( + FieldStorageDefinitionAwareOptionsProviderInterface::class, + 'setFieldStorageDefinition', + [$field_storage_definition] + ); + } + } + +} diff --git a/core/lib/Drupal/Core/Field/TypedData/FieldStorageDefinitionAwareOptionsProviderInterface.php b/core/lib/Drupal/Core/Field/TypedData/FieldStorageDefinitionAwareOptionsProviderInterface.php new file mode 100644 index 0000000000..f7bbb2190a --- /dev/null +++ b/core/lib/Drupal/Core/Field/TypedData/FieldStorageDefinitionAwareOptionsProviderInterface.php @@ -0,0 +1,23 @@ +fieldStorageDefinition = $definition; + return $this; + } + + /** + * Gets the field storage definition. + * + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface + * The field storage definition. + * + * @throws \LogicException + * Thrown when the field storage definition is not available. + */ + public function getFieldStorageDefinition() { + if (!$this->fieldStorageDefinition) { + throw new \LogicException("No field storage definition available. This options provider must be used on field definitions."); + } + return $this->fieldStorageDefinition; + } + +} diff --git a/core/lib/Drupal/Core/Field/TypedData/LegacyOptionsProvider.php b/core/lib/Drupal/Core/Field/TypedData/LegacyOptionsProvider.php new file mode 100644 index 0000000000..f9d03aa10e --- /dev/null +++ b/core/lib/Drupal/Core/Field/TypedData/LegacyOptionsProvider.php @@ -0,0 +1,94 @@ +fieldItem = isset($data) ? $data->getParent() : NULL; + return $this; + } + + /** + * Returns a field item of the field, possibly being a dummy item. + * + * @return \Drupal\Core\Field\FieldItemInterface|\Drupal\Core\TypedData\OptionsProviderInterface + * The field item. + */ + protected function getFieldItem() { + if (!isset($this->fieldItem)) { + $field_definition = $this->getFieldStorageDefinition(); + // We support two legacy cases here: + // - Base field definitions, in which case the object is the field + // definition also. + // - Field storage config, for which we cannot know the bundle if no data + // is passed what previously was not required. If the legacy options + // provider is used in this non-legacy way, throw an exception. + if ($field_definition instanceof BaseFieldDefinition) { + $items = \Drupal::typedDataManager() + ->create($field_definition); + $this->fieldItem = $items[0]; + } + elseif ($field_definition instanceof FieldStorageConfig) { + throw new \LogicException(SafeMarkup::format('The options provider defined for field %field implements the Legacy API and does not support non-legacy usage without data being passed. Convert the options provider to the latest API in order to allow this usage.', + ['%field' => $field_definition->id()])); + } + else { + throw new \LogicException("Legacy options provider is used in some non-legacy scenario."); + } + } + return $this->fieldItem; + } + + /** + * {@inheritdoc} + */ + public function getPossibleValues(AccountInterface $account = NULL) { + return $this->getFieldItem()->getPossibleValues($account); + } + + /** + * {@inheritdoc} + */ + public function getPossibleOptions(AccountInterface $account = NULL) { + return $this->getFieldItem()->getPossibleOptions($account); + } + + /** + * {@inheritdoc} + */ + public function getSettableValues(AccountInterface $account = NULL) { + return $this->getFieldItem()->getSettableValues($account); + } + + /** + * {@inheritdoc} + */ + public function getSettableOptions(AccountInterface $account = NULL) { + return $this->getFieldItem()->getSettableOptions($account); + } + +} diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextAwareOptionsProviderInterface.php b/core/lib/Drupal/Core/Plugin/Context/ContextAwareOptionsProviderInterface.php new file mode 100644 index 0000000000..48c3cb825b --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Context/ContextAwareOptionsProviderInterface.php @@ -0,0 +1,27 @@ +optionsProviderDefinition)) { + $data_definition = $this->getDataDefinition() + ->setOptionsProviderContext(ContextAwareOptionsProviderInterface::class, 'setContexts', [$contexts]); + + $provider = \Drupal::typedDataManager() + ->getOptionsProvider($data_definition); + return $provider; + } + } + + /** + * {@inheritdoc} + */ + public function getOptionsProviderDefinition() { + return $this->optionsProviderDefinition; + } + + /** + * Defines the options provider to be used. + * + * See \Drupal\Core\TypedData\TypedDataManager::getOptionsProvider() for + * supported definitions. In addition, specified option provider classes may + * implement \Drupal\Core\Plugin\Context\ContextAwareOptionsProviderInterface + * in order to provide options depending on the available contexts. + * + * @param string|null $provider_definition + * The options provider definition; e.g. the class name. + * + * @return $this + * + * @see ::getOptionsProviderDefinition() + */ + public function setOptionsProviderDefinition($provider_definition) { + $this->optionsProviderDefinition = $provider_definition; + return $this; + } + /** * {@inheritdoc} */ @@ -250,7 +298,8 @@ public function getDataDefinition() { } $definition->setLabel($this->getLabel()) ->setDescription($this->getDescription()) - ->setRequired($this->isRequired()); + ->setRequired($this->isRequired()) + ->setOptionsProviderDefinition($this->optionsProviderDefinition); $constraints = $definition->getConstraints() + $this->getConstraints(); $definition->setConstraints($constraints); return $definition; diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextDefinitionInterface.php b/core/lib/Drupal/Core/Plugin/Context/ContextDefinitionInterface.php index 2f66fe9ef4..44154efab4 100644 --- a/core/lib/Drupal/Core/Plugin/Context/ContextDefinitionInterface.php +++ b/core/lib/Drupal/Core/Plugin/Context/ContextDefinitionInterface.php @@ -12,6 +12,30 @@ */ interface ContextDefinitionInterface extends ComponentContextDefinitionInterface { + /** + * Returns an options provider if there are defined options. + * + * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts + * (optional) The array of contexts to which the defined context belongs. + * + * @return \Drupal\Core\TypedData\OptionsProviderInterface|null + * The options provider, or NULL if no options are defined. + * + * @see ::getOptionsProviderDefinition() + */ + public function getOptionsProvider(array $contexts = NULL); + + /** + * Returns the set options provider definition. + * + * @return string|null + * The options provider definition, or NULL if no options provider has been + * defined. + * + * @see ::getOptionsProvider() + */ + public function getOptionsProviderDefinition(); + /** * Returns the data definition of the defined context. * diff --git a/core/lib/Drupal/Core/TypedData/DataDefinition.php b/core/lib/Drupal/Core/TypedData/DataDefinition.php index 23b2f09878..787ad820e4 100644 --- a/core/lib/Drupal/Core/TypedData/DataDefinition.php +++ b/core/lib/Drupal/Core/TypedData/DataDefinition.php @@ -255,6 +255,62 @@ public function setSetting($setting_name, $value) { return $this; } + /** + * {@inheritdoc} + */ + public function getOptionsProvider($data = NULL) { + return \Drupal::typedDataManager() + ->getOptionsProvider($this, $data); + } + + /** + * {@inheritdoc} + */ + public function getOptionsProviderDefinition() { + return isset($this->definition['options_provider']) ? $this->definition['options_provider'] : NULL; + } + + /** + * Defines the options provider to be used. + * + * See \Drupal\Core\TypedData\TypedDataManager::getOptionsProvider() for + * supported definition notations. + * + * @param string|null $provider_definition + * The options provider definition; e.g. the class name. + * + * @return $this + * + * @see ::getOptionsProviderDefinition() + */ + public function setOptionsProviderDefinition($provider_definition) { + $this->definition['options_provider'] = $provider_definition; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getOptionsProviderContext() { + return isset($this->definition['options_provider_context']) ? $this->definition['options_provider_context'] : NULL; + } + + /** + * {@inheritdoc} + */ + public function setOptionsProviderContext($interface, $method, $arguments) { + $this->definition['options_provider_context'][$interface][$method] = $arguments; + return $this; + } + + /** + * {@inheritdoc} + */ + public function removeOptionsProviderContext($interface) { + unset($this->definition['options_provider_context'][$interface]); + return $this; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php index d12ccfdef2..eaec15ce08 100644 --- a/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php +++ b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php @@ -144,6 +144,81 @@ public function getSettings(); */ public function getSetting($setting_name); + /** + * Returns the options provider definition. + * + * If not specified, the default defined by the data type will be returned. + * + * @return string|null + * The options provider definition, or NULL if no options provider has been + * defined. + * + * @see ::getOptionsProvider() + */ + public function getOptionsProviderDefinition(); + + /** + * Returns an options provider if there are defined options. + * + * @param \Drupal\Core\TypedData\TypedDataInterface $data + * (optional) The data object for which to get the option provider. + * + * @return \Drupal\Core\TypedData\OptionsProviderInterface|null + * The options provider, or NULL if no options are defined. + * + * @see ::getOptionsProviderDefinition() + */ + public function getOptionsProvider($data = NULL); + + /** + * Gets all options provider context. + * + * @return array + * A nested array of options provider context. The array is keyed by + * interface and method names and contains the provided arguments. + * + * @see ::setOptionsProviderContext() + */ + public function getOptionsProviderContext(); + + /** + * Allows providing contextual information to options providers. + * + * If the defined options provider implements the specified interface, the + * given method is invoked with the provided arguments in order to provide the + * additional context to the options provider object. + * + * This is usually used by the creator of the object in order to allow the + * options provider to take additional context into account. For example, when + * a field definition creates the property definition objects, it sets itself + * as context available to the options providers of the properties. + * + * @param string $interface + * The interface the options provider needs to implement. + * @param string $method + * The method to call on the options provider. + * @param mixed[] $arguments + * The arguments to call the method with. + * + * @return $this + * + * @see ::getOptionsProviderContext() + * @see ::removeOptionsProviderContext() + */ + public function setOptionsProviderContext($interface, $method, $arguments); + + /** + * Removes any options provider context associated with the given interface. + * + * @param string $interface + * The interface with which the context is associated with. + * + * @return $this + * + * @see ::setOptionsProviderContext() + */ + public function removeOptionsProviderContext($interface); + /** * Returns an array of validation constraints. * diff --git a/core/lib/Drupal/Core/TypedData/Options/CallableOptionsProvider.php b/core/lib/Drupal/Core/TypedData/Options/CallableOptionsProvider.php new file mode 100644 index 0000000000..ec7ff8affa --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Options/CallableOptionsProvider.php @@ -0,0 +1,36 @@ +callable = $callable; + } + + /** + * {@inheritdoc} + */ + public function getPossibleOptions(AccountInterface $account = NULL) { + return call_user_func($this->callable, $account); + } + +} diff --git a/core/lib/Drupal/Core/TypedData/Options/DefinitionAwareOptionsProviderInterface.php b/core/lib/Drupal/Core/TypedData/Options/DefinitionAwareOptionsProviderInterface.php new file mode 100644 index 0000000000..4d058f18aa --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Options/DefinitionAwareOptionsProviderInterface.php @@ -0,0 +1,26 @@ +definition = $definition; + return $this; + } + + /** + * Gets the data definition where the options provider is defined. + * + * @return \Drupal\Core\TypedData\DataDefinitionInterface + * The data definition. + */ + public function getDataDefinition() { + return $this->definition; + } + +} diff --git a/core/lib/Drupal/Core/TypedData/Options/DependentOptionsProviderInterface.php b/core/lib/Drupal/Core/TypedData/Options/DependentOptionsProviderInterface.php new file mode 100644 index 0000000000..f152ab8350 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Options/DependentOptionsProviderInterface.php @@ -0,0 +1,29 @@ +classResolver = $class_resolver; + } + + /** + * Returns an options provider for the given definition and arguments. + * + * @param string|null $definition + * The options provider definition; e.g., a class name, or NULL if + * undefined. + * + * @return \Drupal\Core\TypedData\OptionsProviderInterface|null + * The options provider, or NULL if none is defined. + * + * @see \Drupal\Core\TypedData\TypedDataManager::getOptionsProvider() + */ + public function getOptionsProvider($definition) { + if (!isset($definition)) { + return; + } + // If the definition is a callable, wrap it in a suiting options provider. + if ($callable = $this->getCallableFromDefinition($definition)) { + $provider = new CallableOptionsProvider($callable); + } + else { + $provider = $this->classResolver->getInstanceFromDefinition($definition); + } + return $provider; + } + + /** + * Returns a callable for the given definition. + * + * @param string $definition + * The options provider definition. + * + * @return mixed|null + * The callable, or NULL if no callable was defined. + */ + protected function getCallableFromDefinition($definition) { + if (strpos($definition, ':') === FALSE && function_exists($definition)) { + return $definition; + } + // Definition is in the service:method notation. + elseif (substr_count($definition, ':') == 1) { + list($service, $method) = explode(':', $definition, 2); + $instance = $this->classResolver->getInstanceFromDefinition($service); + return [$instance, $method]; + } + // Definition is in the class::method notation for static methods. + elseif (strpos($definition, '::') !== FALSE) { + return explode('::', $definition, 2); + } + } + +} diff --git a/core/lib/Drupal/Core/TypedData/Options/SimpleOptionsProviderBase.php b/core/lib/Drupal/Core/TypedData/Options/SimpleOptionsProviderBase.php new file mode 100644 index 0000000000..d9696f9705 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Options/SimpleOptionsProviderBase.php @@ -0,0 +1,38 @@ +getPossibleOptions($account)); + return array_keys($flatten_options); + } + + /** + * {@inheritdoc} + */ + public function getSettableValues(AccountInterface $account = NULL) { + return $this->getPossibleValues($account); + } + + /** + * {@inheritdoc} + */ + public function getSettableOptions(AccountInterface $account = NULL) { + return $this->getPossibleOptions($account); + } + +} diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php index aab5850ada..e7e096b767 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php @@ -8,6 +8,9 @@ use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\Core\TypedData\Options\DefinitionAwareOptionsProviderInterface; +use Drupal\Core\TypedData\Options\DependentOptionsProviderInterface; +use Drupal\Core\TypedData\Options\OptionsProviderResolver; use Drupal\Core\TypedData\Validation\ExecutionContextFactory; use Drupal\Core\TypedData\Validation\RecursiveValidator; use Drupal\Core\Validation\ConstraintManager; @@ -49,6 +52,13 @@ class TypedDataManager extends DefaultPluginManager implements TypedDataManagerI */ protected $classResolver; + /** + * The options provider resolver. + * + * @var \Drupal\Core\TypedData\Options\OptionsProviderResolver + */ + protected $optionsProviderResolver; + /** * Constructs a new TypedDataManager. * @@ -66,7 +76,7 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac $this->alterInfo('data_type_info'); $this->setCacheBackend($cache_backend, 'typed_data_types_plugins'); $this->classResolver = $class_resolver; - + $this->optionsProviderResolver = new OptionsProviderResolver($this->classResolver); parent::__construct('Plugin/DataType', $namespaces, $module_handler, NULL, 'Drupal\Core\TypedData\Annotation\DataType'); } @@ -210,6 +220,63 @@ public function getPropertyInstance(TypedDataInterface $object, $property_name, return $property; } + /** + * Returns an options provider for the given definition and arguments. + * + * Option providers are usually defined by a class implementing the + * \Drupal\Core\TypedData\OptionsProviderInterface, while in simple cases + * options may be defined by a callable, which returns a single set of options + * for both settable and possible options. + * + * Supported definitions are: + * - The name of a class implementing the OptionsProviderInterface. + * - A callable, either as + * - static method via the class::method notation or as + * - method on a service via the service:method notation or as + * - function callback. + * + * If a class name is given, the class may implement the + * - \Drupal\Core\DependencyInjection\ContainerInjectionInterface, or it is + * instantiated without constructor arguments. + * - \Drupal\Core\TypedData\Options\DefinitionAwareOptionsProviderInterface + * in order to get the data definition passed, or + * - \Drupal\Core\TypedData\Options\DependentOptionsProviderInterface + * in order to provide options in dependence of the current data value. + * Depending on the definition object for which options are provided, some + * additional interfaces may be supported. E.g., Drupal core also supports + * - \Drupal\Core\Field\TypedData\FieldStorageDefinitionAwareOptionsProviderInterface + * on field storage definitions in order to pass the field storage + * definition or + * - \Drupal\Core\Plugin\Context\DependentOptionsProviderInterface + * on context definitions in order to pass the available context objects. + * + * @param \Drupal\Core\TypedData\DataDefinitionInterface $definition + * The definition where the options provider is defined. + * @param \Drupal\Core\TypedData\TypedDataInterface|null $data + * (optional) The data object holding the current value. + * + * @return \Drupal\Core\TypedData\OptionsProviderInterface|null + * The options provider, or NULL if none is defined. + */ + public function getOptionsProvider(DataDefinitionInterface $definition, TypedDataInterface $data = NULL) { + $provider_definition = $definition->getOptionsProviderDefinition(); + $provider = $this->optionsProviderResolver->getOptionsProvider($provider_definition); + + // Forward all options provider context and add default context. + $context = $definition->getOptionsProviderContext(); + $context[DefinitionAwareOptionsProviderInterface::class]['setDataDefinition'] = [$definition]; + $context[DependentOptionsProviderInterface::class]['setData'] = [$data]; + + foreach ($context as $interface => $methods) { + if ($provider instanceof $interface) { + foreach ($methods as $method => $arguments) { + call_user_func_array([$provider, $method], $arguments); + } + } + } + return $provider; + } + /** * Sets the validator for validating typed data. * diff --git a/core/modules/field/src/Entity/FieldStorageConfig.php b/core/modules/field/src/Entity/FieldStorageConfig.php index 250edd6e8d..8de0876960 100644 --- a/core/modules/field/src/Entity/FieldStorageConfig.php +++ b/core/modules/field/src/Entity/FieldStorageConfig.php @@ -4,11 +4,10 @@ use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\FieldableEntityStorageInterface; use Drupal\Core\Field\FieldException; use Drupal\Core\Field\FieldStorageDefinitionInterface; -use Drupal\Core\TypedData\OptionsProviderInterface; +use Drupal\Core\Field\TypedData\FieldDefinitionOptionsProviderTrait; use Drupal\field\FieldStorageConfigInterface; /** @@ -51,6 +50,8 @@ */ class FieldStorageConfig extends ConfigEntityBase implements FieldStorageConfigInterface { + use FieldDefinitionOptionsProviderTrait; + /** * The maximum length of the field name, in characters. * @@ -650,21 +651,6 @@ public function setCardinality($cardinality) { return $this; } - /** - * {@inheritdoc} - */ - public function getOptionsProvider($property_name, FieldableEntityInterface $entity) { - // If the field item class implements the interface, create an orphaned - // runtime item object, so that it can be used as the options provider - // without modifying the entity being worked on. - if (is_subclass_of($this->getFieldItemClass(), OptionsProviderInterface::class)) { - $items = $entity->get($this->getName()); - return \Drupal::service('plugin.manager.field.field_type')->createFieldItem($items, 0); - } - // @todo: Allow setting custom options provider, see - // https://www.drupal.org/node/2002138. - } - /** * {@inheritdoc} */ @@ -753,6 +739,10 @@ public function getPropertyDefinitions() { if (!isset($this->propertyDefinitions)) { $class = $this->getFieldItemClass(); $this->propertyDefinitions = $class::propertyDefinitions($this); + $this->addLegacyOptionsProvider($this->propertyDefinitions); + $this->addFieldStorageDefinitionContext($this->propertyDefinitions); + + // @todo: Allow configurable fields to customize option providers. } return $this->propertyDefinitions; } diff --git a/core/modules/system/src/Tests/TypedData/OptionsProviderTest.php b/core/modules/system/src/Tests/TypedData/OptionsProviderTest.php new file mode 100644 index 0000000000..fbe37fee2c --- /dev/null +++ b/core/modules/system/src/Tests/TypedData/OptionsProviderTest.php @@ -0,0 +1,78 @@ + 1, + 2 => 2, + 3 => 3, + ]; + } + + /** + * Tests using a callable options provider. + */ + public function testCallableOptionsProvider() { + $definition = DataDefinition::create('int') + ->setOptionsProviderDefinition(self::class . '::getOptions'); + + $provider = $definition->getOptionsProvider(); + $this->assertNotNull($provider); + $this->assertEquals($provider->getPossibleOptions(), self::getOptions()); + $this->assertEquals($provider->getPossibleValues(), array_keys(self::getOptions())); + $this->assertEquals($provider->getSettableOptions(), self::getOptions()); + $this->assertEquals($provider->getSettableValues(), array_keys(self::getOptions())); + } + + /** + * Tests using a simple options provider. + */ + public function testSimpleOptionsProviderBase() { + $definition = DataDefinition::create('int') + ->setOptionsProviderDefinition(SimpleOptionsProvider::class); + $expected = [1 => 1]; + + $provider = $definition->getOptionsProvider(); + $this->assertNotNull($provider); + $this->assertEquals($provider->getPossibleOptions(), $expected); + $this->assertEquals($provider->getPossibleValues(), array_keys($expected)); + $this->assertEquals($provider->getSettableOptions(), $expected); + $this->assertEquals($provider->getSettableValues(), array_keys($expected)); + } + + // @todo: Test definition aware and dependent options providers. + +} + +/** + * Helper class for testing simple option providers. + */ +class SimpleOptionsProvider extends SimpleOptionsProviderBase { + + /** + * {@inheritdoc} + */ + public function getPossibleOptions(AccountInterface $account = NULL) { + return [1 => 1]; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php b/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php index 953c90209f..ebadb11f92 100644 --- a/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php +++ b/core/tests/Drupal/Tests/Core/Field/TestBaseFieldDefinitionInterface.php @@ -2,11 +2,14 @@ namespace Drupal\Tests\Core\Field; -use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; /** * Defines a test interface to mock entity base field definitions. + * + * @todo This is not possible anymore. fix by converting to abstract class + * which implements both FDI and FSDI ? */ -interface TestBaseFieldDefinitionInterface extends FieldDefinitionInterface, FieldStorageDefinitionInterface { +interface TestBaseFieldDefinitionInterface extends FieldStorageDefinitionInterface { + } diff --git a/core/tests/Drupal/Tests/Core/Plugin/Context/ContextDefinitionTest.php b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextDefinitionTest.php index d0ddc85b46..e87161595d 100644 --- a/core/tests/Drupal/Tests/Core/Plugin/Context/ContextDefinitionTest.php +++ b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextDefinitionTest.php @@ -37,6 +37,7 @@ public function testGetDataDefinition($is_multiple) { 'setLabel', 'setDescription', 'setRequired', + 'setOptionsProviderDefinition', 'getConstraints', 'setConstraints', ]) @@ -50,6 +51,9 @@ public function testGetDataDefinition($is_multiple) { $mock_data_definition->expects($this->once()) ->method('setRequired') ->willReturnSelf(); + $mock_data_definition->expects($this->once()) + ->method('setOptionsProviderDefinition') + ->willReturnSelf(); $mock_data_definition->expects($this->once()) ->method('getConstraints') ->willReturn([]);