diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 4ab0fd7..8ddb55a 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -46,7 +46,7 @@ mapping: sequence: label: Sequence class: '\Drupal\Core\Config\Schema\Sequence' - definition_class: '\Drupal\Core\TypedData\ListDataDefinition' + definition_class: '\Drupal\Core\TypedData\MapDataDefinition' # Simple extended data types: diff --git a/core/core.services.yml b/core/core.services.yml index c5a13a4..7e474e2 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -313,9 +313,11 @@ services: arguments: ['@config.storage', 'config/schema'] config.typed: class: Drupal\Core\Config\TypedConfigManager - arguments: ['@config.storage', '@config.storage.schema', '@cache.discovery', '@module_handler'] + arguments: ['@config.storage', '@config.storage.schema', '@cache.discovery', '@module_handler', '@class_resolver'] tags: - { name: plugin_manager_cache_clear } + calls: + - [setValidationConstraintManager, ['@validation.constraint']] context.handler: class: Drupal\Core\Plugin\Context\ContextHandler arguments: ['@typed_data_manager'] diff --git a/core/lib/Drupal/Core/Config/Plugin/DataType/Config.php b/core/lib/Drupal/Core/Config/Plugin/DataType/Config.php new file mode 100644 index 0000000..3377fbf --- /dev/null +++ b/core/lib/Drupal/Core/Config/Plugin/DataType/Config.php @@ -0,0 +1,30 @@ +getTypedDataManager()->create($definition, $value, $key, $this); diff --git a/core/lib/Drupal/Core/Config/Schema/Element.php b/core/lib/Drupal/Core/Config/Schema/Element.php index 86cf4de..8f7d453 100644 --- a/core/lib/Drupal/Core/Config/Schema/Element.php +++ b/core/lib/Drupal/Core/Config/Schema/Element.php @@ -8,13 +8,13 @@ namespace Drupal\Core\Config\Schema; use Drupal\Core\Config\TypedConfigManagerInterface; -use Drupal\Core\TypedData\TypedData; +use Drupal\Core\TypedData\Plugin\DataType\Config; use Drupal\Core\TypedData\TypedDataManagerInterface; /** * Defines a generic configuration element. */ -abstract class Element extends TypedData { +abstract class Element extends Config { /** * The configuration value. diff --git a/core/lib/Drupal/Core/Config/Schema/Mapping.php b/core/lib/Drupal/Core/Config/Schema/Mapping.php index cf02dd4..450ee97 100644 --- a/core/lib/Drupal/Core/Config/Schema/Mapping.php +++ b/core/lib/Drupal/Core/Config/Schema/Mapping.php @@ -7,6 +7,8 @@ namespace Drupal\Core\Config\Schema; +use Drupal\Core\TypedData\MapDataDefinition; + /** * Defines a mapping configuration element. * @@ -31,4 +33,24 @@ protected function getElementDefinition($key) { return $this->buildDataDefinition($definition, $value, $key); } + /** + * {@inheritdoc} + */ + public function set($property_name, $value, $notify = TRUE) { + $this->value[$property_name] = $value; + // @todo notify? + return $this; + } + + public function getProperties($include_computed = FALSE) { + $properties = array(); + foreach (array_keys($this->value) as $name) { + $definition = $this->getElementDefinition($name); + if ($include_computed || !$definition->isComputed()) { + $properties[$name] = $this->get($name); + } + } + return $properties; + } + } diff --git a/core/lib/Drupal/Core/Config/Schema/Sequence.php b/core/lib/Drupal/Core/Config/Schema/Sequence.php index 0c0900a..048b1b6 100644 --- a/core/lib/Drupal/Core/Config/Schema/Sequence.php +++ b/core/lib/Drupal/Core/Config/Schema/Sequence.php @@ -7,6 +7,8 @@ namespace Drupal\Core\Config\Schema; +use Drupal\Core\TypedData\ListInterface; + /** * Defines a configuration element of type Sequence. * @@ -15,8 +17,11 @@ * * Read https://www.drupal.org/node/1905070 for more details about configuration * schema, types and type resolution. + * + * @todo for Drupal 9: Do not implement ListInterface any more as lists are + * just ordered sets without keys, while Sequences have (unknown) keys. */ -class Sequence extends ArrayElement { +class Sequence extends ArrayElement implements ListInterface { /** * {@inheritdoc} @@ -34,4 +39,90 @@ protected function getElementDefinition($key) { return $this->buildDataDefinition($definition, $value, $key); } + /** + * @todo for Drupal 9: Do not implement Listinterface and remove this method. + */ + public function getItemDefinition() { + // @todo. + } + + /** + * {@inheritdoc} + */ + public function set($index, $value, $notify = TRUE) { + $this->value[$index] = $value; + return TRUE; + } + + /** + * @todo for Drupal 9: Do not implement Listinterface and remove this method. + */ + public function first() { + reset($this->value); + return current($this->value); + } + + /** + * @todo for Drupal 9: Do not implement Listinterface and remove this method. + */ + public function appendItem($value = NULL) { + $this->value[] = $value; + } + + /** + * @todo for Drupal 9: Do not implement Listinterface and remove this method. + */ + public function removeItem($index) { + unset($this->value[$index]); + } + + /** + * @todo for Drupal 9: Do not implement Listinterface and remove this method. + */ + public function filter($callback) { + return array_filter($this->value, $callback); + } + + /** + * {@inheritdoc} + */ + public function count() { + return count($this->value); + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) { + return isset($this->value[$offset]); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) { + return $this->get($offset); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value) { + return $this->set($offset, $value); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) { + unset($this->elements[$offset]); + } + + /** + * {@inheritdoc} + */ + public function getProperties($include_computed = FALSE) { + return $this->getElements(); + } + } diff --git a/core/lib/Drupal/Core/Config/Schema/TypedConfigInterface.php b/core/lib/Drupal/Core/Config/Schema/TypedConfigInterface.php index f8cda30..850721b 100644 --- a/core/lib/Drupal/Core/Config/Schema/TypedConfigInterface.php +++ b/core/lib/Drupal/Core/Config/Schema/TypedConfigInterface.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Config\Schema; +use Drupal\Core\TypedData\ComplexDataInterface; use Drupal\Core\TypedData\TraversableTypedDataInterface; /** @@ -19,15 +20,7 @@ * When implementing this interface which extends Traversable, make sure to list * IteratorAggregate or Iterator before this interface in the implements clause. */ -interface TypedConfigInterface extends TraversableTypedDataInterface { - - /** - * Determines whether the data structure is empty. - * - * @return bool - * TRUE if the data structure is empty, FALSE otherwise. - */ - public function isEmpty(); +interface TypedConfigInterface extends TraversableTypedDataInterface, ComplexDataInterface { /** * Gets an array of contained elements. @@ -37,28 +30,4 @@ public function isEmpty(); */ public function getElements(); - /** - * Gets a contained typed configuration element. - * - * @param $name - * The name of the property to get; e.g., 'title' or 'name'. Nested - * elements can be get using multiple dot delimited names, for example, - * 'page.front'. - * - * @return \Drupal\Core\TypedData\TypedDataInterface - * The property object. - * - * @throws \InvalidArgumentException - * If an invalid property name is given. - */ - public function get($name); - - /** - * Returns an array of all property values. - * - * @return array - * An array of property values, keyed by property name. - */ - public function toArray(); - } diff --git a/core/lib/Drupal/Core/Config/TypedConfigManager.php b/core/lib/Drupal/Core/Config/TypedConfigManager.php index 1181953..ebd4d2a 100644 --- a/core/lib/Drupal/Core/Config/TypedConfigManager.php +++ b/core/lib/Drupal/Core/Config/TypedConfigManager.php @@ -11,6 +11,7 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Config\Schema\ConfigSchemaAlterException; use Drupal\Core\Config\Schema\ConfigSchemaDiscovery; +use Drupal\Core\DependencyInjection\ClassResolverInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\TypedData\TypedDataManager; @@ -49,13 +50,18 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI * The storage object to use for reading schema data * @param \Drupal\Core\Cache\CacheBackendInterface $cache * The cache backend to use for caching the definitions. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver + * The class resolver. */ - public function __construct(StorageInterface $configStorage, StorageInterface $schemaStorage, CacheBackendInterface $cache, ModuleHandlerInterface $module_handler) { + public function __construct(StorageInterface $configStorage, StorageInterface $schemaStorage, CacheBackendInterface $cache, ModuleHandlerInterface $module_handler, ClassResolverInterface $class_resolver) { $this->configStorage = $configStorage; $this->schemaStorage = $schemaStorage; $this->setCacheBackend($cache, 'typed_config_definitions'); $this->alterInfo('config_schema_info'); $this->moduleHandler = $module_handler; + $this->classResolver = $class_resolver; } /** @@ -104,8 +110,9 @@ public function buildDataDefinition(array $definition, $value, $name = NULL, $pa } // Add default values from type definition. $definition += $this->getDefinitionWithReplacements($type, $replace); - - $data_definition = $this->createDataDefinition($definition['type']); + $class = $definition['definition_class']; + $data_definition = $class::create('config'); + $data_definition->setSettings($definition); // Pass remaining values from definition array to data definition. foreach ($definition as $key => $value) { @@ -113,6 +120,10 @@ public function buildDataDefinition(array $definition, $value, $name = NULL, $pa $data_definition[$key] = $value; } } + + // @todo: Add property definitions for all known properties from the mapping + // and sequence keys. + return $data_definition; } @@ -184,6 +195,7 @@ protected function getDefinitionWithReplacements($base_plugin_id, array $replace $definition += array( 'definition_class' => '\Drupal\Core\TypedData\DataDefinition', 'type' => $type, + 'unwrap_for_canonical_representation' => TRUE, ); return $definition; } @@ -334,7 +346,7 @@ protected function replaceVariable($value, $data) { // Switch replacement values with values from the parent. $parent = $data['%parent']; $data = $parent->getValue(); - $data['%type'] = $parent->getDataDefinition()->getDataType(); + $data['%type'] = $parent->getDataDefinition()->getSetting('type'); // The special %parent and %key values now need to point one level up. if ($new_parent = $parent->getParent()) { $data['%parent'] = $new_parent; diff --git a/core/lib/Drupal/Core/Validation/ConstraintManager.php b/core/lib/Drupal/Core/Validation/ConstraintManager.php index 631a3d1..47d291a 100644 --- a/core/lib/Drupal/Core/Validation/ConstraintManager.php +++ b/core/lib/Drupal/Core/Validation/ConstraintManager.php @@ -12,6 +12,9 @@ use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\DefaultPluginManager; use Drupal\Core\StringTranslation\TranslatableMarkup; +use Symfony\Component\Validator\Constraints\Choice; +use Symfony\Component\Validator\Constraints\EqualTo; +use Symfony\Component\Validator\Constraints\GreaterThan; /** * Constraint plugin manager. diff --git a/core/modules/config/tests/config_test/config/install/config_test.validation.yml b/core/modules/config/tests/config_test/config/install/config_test.validation.yml new file mode 100644 index 0000000..47b397d --- /dev/null +++ b/core/modules/config/tests/config_test/config/install/config_test.validation.yml @@ -0,0 +1,7 @@ +llama: meh +cat: + type: kitten + count: 2 +giraffe: + hum1: hum1 + hum2: hum2 diff --git a/core/modules/config/tests/config_test/config/schema/config_test.schema.yml b/core/modules/config/tests/config_test/config/schema/config_test.schema.yml index 6cad91b..91f4fd6 100644 --- a/core/modules/config/tests/config_test/config/schema/config_test.schema.yml +++ b/core/modules/config/tests/config_test/config/schema/config_test.schema.yml @@ -152,3 +152,30 @@ config_test.foo: config_test.bar: type: config_test.foo + +config_test.validation: + type: config_object + label: 'Configuration type' + mapping: + llama: + type: string + constraints: + Callback: + callback: [\Drupal\config_test\ConfigValidation, validateLlama] + cat: + type: mapping + mapping: + type: + type: string + constraints: + Callback: + callback: [\Drupal\config_test\ConfigValidation, validateCats] + count: + type: integer + constraints: + Callback: + callback: [\Drupal\config_test\ConfigValidation, validateCatCount] + giraffe: + type: sequence + sequence: + type: string diff --git a/core/modules/config/tests/config_test/src/ConfigValidation.php b/core/modules/config/tests/config_test/src/ConfigValidation.php new file mode 100644 index 0000000..19729e2 --- /dev/null +++ b/core/modules/config/tests/config_test/src/ConfigValidation.php @@ -0,0 +1,25 @@ + 1; + } + +} diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index e34d375..15b3a08 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -7,6 +7,8 @@ system.site: uuid: type: string label: 'Site UUID' + constraints: + NotNull: [] name: type: label label: 'Site name' diff --git a/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php b/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php new file mode 100644 index 0000000..ff36258 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php @@ -0,0 +1,119 @@ +installConfig('config_test'); + } + + /** + * Verifies that the Typed Data API is implemented correctly. + */ + public function testTypedDataAPI() { + + /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager */ + $typed_config_manager = \Drupal::service('config.typed'); + /** @var \Drupal\Core\Config\Schema\TypedConfigInterface $typed_config */ + $typed_config = $typed_config_manager->get('config_test.validation'); + + // Test a primitive. + $string_data = $typed_config->get('llama'); + $this->assertInstanceOf(StringInterface::class, $string_data); + $this->assertEquals('meh', $string_data->getValue()); + + // Test complex data. + $mapping = $typed_config->get('cat'); + /** @var \Drupal\Core\TypedData\ComplexDataInterface $mapping */ + $this->assertInstanceOf(ComplexDataInterface::class, $mapping); + $this->assertInstanceOf(StringInterface::class, $mapping->get('type')); + $this->assertEquals('kitten', $mapping->get('type')->getValue()); + $this->assertInstanceOf(IntegerInterface::class, $mapping->get('count')); + $this->assertEquals(2, $mapping->get('count')->getValue()); + // Verify the metadata is available. + $definitions = $mapping->getDataDefinition()->getPropertyDefinitions(); + $this->assertArrayHasKey('type', $definitions); + $this->assertArrayHasKey('count', $definitions); + $this->assertEquals('string', $definitions['type']->getDataType()); + $this->assertEquals('integer', $definitions['count']->getDataType()); + + // Test accessing sequences. As they are keyed, they are complex data as + // well. + $sequence = $typed_config->get('giraffe'); + /** @var \Drupal\Core\TypedData\ComplexDataInterface $sequence */ + $this->assertInstanceOf(ComplexDataInterface::class, $sequence); + $this->assertInstanceOf(StringInterface::class, $sequence->get(0)); + $this->assertEquals('hum1', $sequence->get('hum1')->getValue()); + $this->assertEquals('hum2', $sequence->get('hum2')->getValue()); + $this->assertEquals(2, $sequence->count()); + // Verify the item metadata is available. + $definition = $sequence->getDataDefinition()->getItemDefinition(); + $this->assertEquals('string', $definition->getDataType()); + } + + + public function testSimpleConfigValidation() { + $config = \Drupal::configFactory()->getEditable('config_test.validation'); + /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager */ + $typed_config_manager = \Drupal::service('config.typed'); + /** @var \Drupal\Core\Config\Schema\TypedConfigInterface $typed_config */ + $typed_config = $typed_config_manager->get('config_test.validation'); + + $result = $typed_config->validate(); + $this->assertInstanceOf(ConstraintViolationListInterface::class, $result); + $this->assertEmpty($result); + + // Test constraints on primitive types. + $config->set('llama', 'muh'); + $config->save(); + + $typed_config = $typed_config_manager->get('config_test.validation'); + $result = $typed_config->validate(); + // 1. llama is no longer a string and 2. its not meh anymore. + $this->assertCount(2, $result); + + // Test constraints on mapping + $config->set('cat.type', 'nyans'); + $config->save(); + + $typed_config = $typed_config_manager->get('config_test.validation'); + $result = $typed_config->validate(); + $this->assertEmpty($result); + + $config->set('cat.type', 'miaus'); + $config->save(); + + $typed_config = $typed_config_manager->get('config_test.validation'); + $result = $typed_config->validate(); + $this->assertCount(2, $result); + } + +}