diff --git a/core/core.services.yml b/core/core.services.yml index 1ee07fe..e7d818b 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -318,9 +318,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/Schema/Mapping.php b/core/lib/Drupal/Core/Config/Schema/Mapping.php index 2f4b58f..44af54d 100644 --- a/core/lib/Drupal/Core/Config/Schema/Mapping.php +++ b/core/lib/Drupal/Core/Config/Schema/Mapping.php @@ -2,6 +2,8 @@ namespace Drupal\Core\Config\Schema; +use Drupal\Core\TypedData\ComplexDataInterface; + /** * Defines a mapping configuration element. * @@ -15,7 +17,7 @@ * Read https://www.drupal.org/node/1905070 for more details about configuration * schema, types and type resolution. */ -class Mapping extends ArrayElement { +class Mapping extends ArrayElement implements ComplexDataInterface { /** * {@inheritdoc} @@ -26,4 +28,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 e03427e..476f8aa 100644 --- a/core/lib/Drupal/Core/Config/Schema/Sequence.php +++ b/core/lib/Drupal/Core/Config/Schema/Sequence.php @@ -2,6 +2,8 @@ namespace Drupal\Core\Config\Schema; +use Drupal\Core\TypedData\TraversableTypedDataInterface; + /** * Defines a configuration element of type Sequence. * @@ -11,7 +13,7 @@ * Read https://www.drupal.org/node/1905070 for more details about configuration * schema, types and type resolution. */ -class Sequence extends ArrayElement { +class Sequence extends ArrayElement implements TraversableTypedDataInterface { /** * {@inheritdoc} diff --git a/core/lib/Drupal/Core/Config/TypedConfigManager.php b/core/lib/Drupal/Core/Config/TypedConfigManager.php index 8e3ea45..c932904 100644 --- a/core/lib/Drupal/Core/Config/TypedConfigManager.php +++ b/core/lib/Drupal/Core/Config/TypedConfigManager.php @@ -6,6 +6,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; @@ -44,13 +45,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; } /** @@ -179,6 +185,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; } diff --git a/core/lib/Drupal/Core/TypedData/DataDefinition.php b/core/lib/Drupal/Core/TypedData/DataDefinition.php index d0878ed..07b78bf 100644 --- a/core/lib/Drupal/Core/TypedData/DataDefinition.php +++ b/core/lib/Drupal/Core/TypedData/DataDefinition.php @@ -2,11 +2,16 @@ namespace Drupal\Core\TypedData; +use Drupal\Core\DependencyInjection\DependencySerializationTrait; + /** * A typed data definition class for defining data based on defined data types. */ class DataDefinition implements DataDefinitionInterface, \ArrayAccess { + use DependencySerializationTrait; + use TypedDataTrait; + /** * The array holding values for all definition keys. * @@ -258,7 +263,7 @@ public function setSetting($setting_name, $value) { */ public function getConstraints() { $constraints = isset($this->definition['constraints']) ? $this->definition['constraints'] : array(); - $constraints += \Drupal::typedDataManager()->getDefaultConstraints($this); + $constraints += $this->getTypedDataManager()->getDefaultConstraints($this); return $constraints; } diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php index 4904150..9f59a54 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php @@ -117,7 +117,10 @@ public function createDataDefinition($data_type) { throw new \InvalidArgumentException("Invalid data type '$data_type' has been given"); } $class = $type_definition['definition_class']; - return $class::createFromDataType($data_type); + $data_definition = $class::createFromDataType($data_type); + $data_definition->setTypedDataManager($this); + + return $data_definition; } /** diff --git a/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php b/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php index b765b05..cf4b3fc 100644 --- a/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php +++ b/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php @@ -4,6 +4,7 @@ use Drupal\Core\TypedData\ComplexDataInterface; use Drupal\Core\TypedData\ListInterface; +use Drupal\Core\TypedData\TraversableTypedDataInterface; use Drupal\Core\TypedData\TypedDataInterface; use Drupal\Core\TypedData\TypedDataManagerInterface; use Symfony\Component\Validator\Constraint; @@ -142,7 +143,9 @@ protected function validateNode(TypedDataInterface $data, $constraints = NULL, $ // If the data is a list or complex data, validate the contained list items // or properties. However, do not recurse if the data is empty. - if (($data instanceof ListInterface || $data instanceof ComplexDataInterface) && !$data->isEmpty()) { + if ((($data instanceof ComplexDataInterface || $data instanceof ListInterface) && !$data->isEmpty()) || + ($data instanceof TraversableTypedDataInterface) + ) { foreach ($data as $name => $property) { $this->validateNode($property); } 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..e63c68c 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,33 @@ 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 + constraints: + Callback: + callback: [\Drupal\config_test\ConfigValidation, validateGiraffes] 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..7bf5f70 --- /dev/null +++ b/core/modules/config/tests/config_test/src/ConfigValidation.php @@ -0,0 +1,68 @@ +addViolation('no valid llama'); + } + } + + /** + * Validates cats. + * + * @param string $object + * The string to validate. + * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context + * The validation execution context. + */ + public static function validateCats($object, ExecutionContextInterface $context) { + if (!in_array($object, ['kitten', 'cats', 'nyans'])) { + $context->addViolation('no valid cat'); + } + } + + /** + * Validates a number. + * + * @param string $object + * The string to validate. + * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context + * The validation execution context. + */ + public static function validateCatCount($object, ExecutionContextInterface $context) { + if ($object <= 1) { + $context->addViolation('no enough cats'); + } + } + + /** + * Validates giraffes. + * + * @param string $object + * The string to validate. + * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context + * The validation execution context. + */ + public static function validateGiraffes($object, ExecutionContextInterface $context) { + if (strpos($object, 'hum') !== 0) { + $context->addViolation('Giraffes just hum'); + } + } + +} 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/ConfigValidationTest.php b/core/tests/Drupal/KernelTests/Config/ConfigValidationTest.php new file mode 100644 index 0000000..9af5496 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Config/ConfigValidationTest.php @@ -0,0 +1,77 @@ +installConfig('config_test'); + } + + 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(); + // Its not a valid llama anymore. + $this->assertCount(1, $result); + $this->assertEquals('no valid llama', $result->get(0)->getMessage()); + + // Test constraints on mapping. + $config->set('llama', 'meh'); + $config->set('cat.type', 'nyans'); + $config->save(); + + $typed_config = $typed_config_manager->get('config_test.validation'); + $result = $typed_config->validate(); + $this->assertEmpty($result); + + // Test constrains on nested mapping. + $config->set('cat.type', 'miaus'); + $config->save(); + + $typed_config = $typed_config_manager->get('config_test.validation'); + $result = $typed_config->validate(); + $this->assertCount(1, $result); + $this->assertEquals('no valid cat', $result->get(0)->getMessage()); + + // Test constrains on sequences. + $config->set('cat.type', 'nyans'); + $config->set('giraffe', ['muh', 'hum2']); + $config->save(); + + $typed_config = $typed_config_manager->get('config_test.validation'); + $result = $typed_config->validate(); + $this->assertCount(1, $result); + $this->assertEquals('Giraffes just hum', $result->get(0)->getMessage()); + } + +}