diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 73db361..99954c4 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/lib/Drupal/Core/Config/Schema/ArrayElement.php b/core/lib/Drupal/Core/Config/Schema/ArrayElement.php index 7fc6cc5..4c11305 100644 --- a/core/lib/Drupal/Core/Config/Schema/ArrayElement.php +++ b/core/lib/Drupal/Core/Config/Schema/ArrayElement.php @@ -2,10 +2,12 @@ namespace Drupal\Core\Config\Schema; +use Drupal\Core\TypedData\ComplexDataInterface; + /** * Defines a generic configuration element that contains multiple properties. */ -abstract class ArrayElement extends Element implements \IteratorAggregate, TypedConfigInterface { +abstract class ArrayElement extends Element implements \IteratorAggregate, TypedConfigInterface, ComplexDataInterface { /** * Parsed elements. @@ -161,4 +163,28 @@ public function isNullable() { return isset($this->definition['nullable']) && $this->definition['nullable'] == TRUE; } + /** + * {@inheritdoc} + */ + public function set($property_name, $value, $notify = TRUE) { + $this->value[$property_name] = $value; + // Config schema elements to not make use of notifications. Thus, we skip + // notifying parents. + return $this; + } + + /** + * {@inheritdoc} + */ + 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/Mapping.php b/core/lib/Drupal/Core/Config/Schema/Mapping.php index 44af54d..2f4b58f 100644 --- a/core/lib/Drupal/Core/Config/Schema/Mapping.php +++ b/core/lib/Drupal/Core/Config/Schema/Mapping.php @@ -2,8 +2,6 @@ namespace Drupal\Core\Config\Schema; -use Drupal\Core\TypedData\ComplexDataInterface; - /** * Defines a mapping configuration element. * @@ -17,7 +15,7 @@ * Read https://www.drupal.org/node/1905070 for more details about configuration * schema, types and type resolution. */ -class Mapping extends ArrayElement implements ComplexDataInterface { +class Mapping extends ArrayElement { /** * {@inheritdoc} @@ -28,24 +26,4 @@ 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 476f8aa..79e6f9f 100644 --- a/core/lib/Drupal/Core/Config/Schema/Sequence.php +++ b/core/lib/Drupal/Core/Config/Schema/Sequence.php @@ -2,8 +2,6 @@ namespace Drupal\Core\Config\Schema; -use Drupal\Core\TypedData\TraversableTypedDataInterface; - /** * Defines a configuration element of type Sequence. * @@ -12,8 +10,14 @@ * * Read https://www.drupal.org/node/1905070 for more details about configuration * schema, types and type resolution. + * + * Note that sequences implement the typed data ComplexDataInterface (via the + * parent ArrayElement) rather than the ListInterface. This is, as sequences + * have named keys, what is not covered by lists. From the typed data API + * perspective sequences are handled as ordered mappings without metadata about + * existing properties. */ -class Sequence extends ArrayElement implements TraversableTypedDataInterface { +class Sequence extends ArrayElement { /** * {@inheritdoc} diff --git a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php index f2600c7..594f31d 100644 --- a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php +++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php @@ -576,6 +576,16 @@ protected function getFieldItemClass() { /** * {@inheritdoc} */ + public function __sleep() { + // Do not serialize the statically cached property definitions. + $vars = get_object_vars($this); + unset($vars['propertyDefinitions']); + return array_keys($vars); + } + + /** + * {@inheritdoc} + */ public function getTargetEntityTypeId() { return isset($this->definition['entity_type']) ? $this->definition['entity_type'] : NULL; } diff --git a/core/lib/Drupal/Core/TypedData/ComplexDataDefinitionBase.php b/core/lib/Drupal/Core/TypedData/ComplexDataDefinitionBase.php index 95ba817..0acdba4 100644 --- a/core/lib/Drupal/Core/TypedData/ComplexDataDefinitionBase.php +++ b/core/lib/Drupal/Core/TypedData/ComplexDataDefinitionBase.php @@ -36,4 +36,14 @@ public function getMainPropertyName() { return NULL; } + /** + * {@inheritdoc} + */ + public function __sleep() { + // Do not serialize the cached property definitions. + $vars = get_object_vars($this); + unset($vars['propertyDefinitions']); + return array_keys($vars); + } + } diff --git a/core/lib/Drupal/Core/TypedData/DataDefinition.php b/core/lib/Drupal/Core/TypedData/DataDefinition.php index 866beb3..07b78bf 100644 --- a/core/lib/Drupal/Core/TypedData/DataDefinition.php +++ b/core/lib/Drupal/Core/TypedData/DataDefinition.php @@ -345,14 +345,4 @@ public function toArray() { return $this->definition; } - /** - * {@inheritdoc} - */ - public function __sleep() { - // Do not serialize the statically cached property definitions. - $vars = get_object_vars($this); - unset($vars['propertyDefinitions'], $vars['typedDataManager']); - return array_keys($vars); - } - } diff --git a/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php b/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php index c52c9db..b765b05 100644 --- a/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php +++ b/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php @@ -4,7 +4,6 @@ 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; @@ -143,10 +142,7 @@ 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. - // Note: Most traversable, doesn't have a concept of empty/not empty, but - // most implementations, like ListInterface/TypedConfigInterface have. - // Therefor first check, that the method exists. - if ($data instanceof TraversableTypedDataInterface && (!(method_exists($data, 'isEmpty') && $data->isEmpty()))) { + if (($data instanceof ListInterface || $data instanceof ComplexDataInterface) && !$data->isEmpty()) { foreach ($data as $name => $property) { $this->validateNode($property); } diff --git a/core/tests/Drupal/KernelTests/Config/ConfigValidationTest.php b/core/tests/Drupal/KernelTests/Config/ConfigValidationTest.php deleted file mode 100644 index af6a4c5..0000000 --- a/core/tests/Drupal/KernelTests/Config/ConfigValidationTest.php +++ /dev/null @@ -1,79 +0,0 @@ -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()); - } - -} diff --git a/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php b/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php new file mode 100644 index 0000000..4b906b4 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php @@ -0,0 +1,126 @@ +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()); + $this->assertInstanceOf(ComplexDataDefinitionInterface::class, $mapping->getDataDefinition()); + // @todo: Make metadata about contained properties available and add test + // coverage here. + + // Test accessing sequences. + $sequence = $typed_config->get('giraffe'); + /** @var \Drupal\Core\TypedData\ListInterface $sequence */ + $this->assertInstanceOf(ComplexDataInterface::class, $sequence); + $this->assertInstanceOf(StringInterface::class, $sequence->get('hum1')); + $this->assertEquals('hum1', $sequence->get('hum1')->getValue()); + $this->assertEquals('hum2', $sequence->get('hum2')->getValue()); + $this->assertEquals(2, count($sequence->getIterator())); + // Verify the item metadata is available. + $this->assertInstanceOf(ComplexDataDefinitionInterface::class, $sequence->getDataDefinition()); + // @todo: Make metadata about contained properties available and add test + // coverage here. + } + + /** + * Tests config validation via the typed data api. + */ + 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()); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php index dcc7610..d9a7100 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php @@ -65,7 +65,6 @@ function testSchemaMapping() { $expected['type'] = 'config_schema_test.someschema'; $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; $expected['unwrap_for_canonical_representation'] = TRUE; - $this->assertEqual($definition, $expected, 'Retrieved the right metadata for nonexistent configuration.'); $this->assertEqual($definition, $expected, 'Retrieved the right metadata for configuration with only some schema.'); // Check type detection on elements with undefined types. @@ -93,7 +92,6 @@ function testSchemaMapping() { $expected['type'] = 'undefined'; $expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition'; $expected['unwrap_for_canonical_representation'] = TRUE; - $this->assertEqual($definition, $expected, 'Retrieved the right metadata for nonexistent configuration.'); $this->assertEqual($definition, $expected, 'Automatic type detected for an undefined integer is undefined.'); // Simple case, straight metadata. @@ -113,7 +111,6 @@ function testSchemaMapping() { $expected['type'] = 'system.maintenance'; $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; $expected['unwrap_for_canonical_representation'] = TRUE; - $this->assertEqual($definition, $expected, 'Retrieved the right metadata for nonexistent configuration.'); $this->assertEqual($definition, $expected, 'Retrieved the right metadata for system.maintenance'); // Mixed schema with ignore elements. @@ -145,7 +142,6 @@ function testSchemaMapping() { ); $expected['type'] = 'config_schema_test.ignore'; $expected['unwrap_for_canonical_representation'] = TRUE; - $this->assertEqual($definition, $expected, 'Retrieved the right metadata for nonexistent configuration.'); $this->assertEqual($definition, $expected); @@ -157,7 +153,6 @@ function testSchemaMapping() { $expected['class'] = '\Drupal\Core\Config\Schema\Ignore'; $expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition'; $expected['unwrap_for_canonical_representation'] = TRUE; - $this->assertEqual($definition, $expected, 'Retrieved the right metadata for nonexistent configuration.'); $this->assertEqual($definition, $expected); $definition = \Drupal::service('config.typed')->get('config_schema_test.ignore')->get('indescribable')->getDataDefinition()->toArray(); $expected['label'] = 'Indescribable'; @@ -193,7 +188,6 @@ function testSchemaMapping() { $expected['mapping']['third_party_settings']['sequence']['type'] = '[%parent.%parent.%type].third_party.[%key]'; $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['type'] = 'image.style.*'; - $this->assertEqual($definition, $expected, 'Retrieved the right metadata for nonexistent configuration.'); $this->assertEqual($definition, $expected);