Commit d31759af authored by alexpott's avatar alexpott

Issue #1928868 by dawehner, fago, Gábor Hojtsy, Jose Reyero, tstoeckler, Wim...

Issue #1928868 by dawehner, fago, Gábor Hojtsy, Jose Reyero, tstoeckler, Wim Leers, Berdir: Typed config incorrectly implements Typed Data interfaces
parent 98bf9603
......@@ -331,9 +331,11 @@ services:
arguments: ['@config.storage', 'config/schema', '', true, '%install_profile%']
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']
......
......@@ -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,25 @@ 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 do not make use of notifications. Thus, we skip
// notifying parents.
return $this;
}
/**
* {@inheritdoc}
*/
public function getProperties($include_computed = FALSE) {
$properties = [];
foreach (array_keys($this->value) as $name) {
$properties[$name] = $this->get($name);
}
return $properties;
}
}
......@@ -10,6 +10,12 @@
*
* 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 because sequences
* may have named keys, which is not supported by ListInterface. From the typed
* data API perspective sequences are handled as ordered mappings without
* metadata about existing properties.
*/
class Sequence extends ArrayElement {
......
......@@ -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\Config\Schema\Undefined;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\TypedData\TypedDataManager;
......@@ -45,13 +46,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
* (optional) 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 = NULL) {
$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 ?: \Drupal::service('class_resolver');
}
/**
......@@ -184,6 +190,7 @@ protected function getDefinitionWithReplacements($base_plugin_id, array $replace
$definition += [
'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
'type' => $type,
'unwrap_for_canonical_representation' => TRUE,
];
return $definition;
}
......
......@@ -585,7 +585,7 @@ protected function getFieldItemClass() {
public function __sleep() {
// Do not serialize the statically cached property definitions.
$vars = get_object_vars($this);
unset($vars['propertyDefinitions']);
unset($vars['propertyDefinitions'], $vars['typedDataManager']);
return array_keys($vars);
}
......
......@@ -42,7 +42,7 @@ public function getMainPropertyName() {
public function __sleep() {
// Do not serialize the cached property definitions.
$vars = get_object_vars($this);
unset($vars['propertyDefinitions']);
unset($vars['propertyDefinitions'], $vars['typedDataManager']);
return array_keys($vars);
}
......
......@@ -7,6 +7,8 @@
*/
class DataDefinition implements DataDefinitionInterface, \ArrayAccess {
use TypedDataTrait;
/**
* The array holding values for all definition keys.
*
......@@ -258,7 +260,7 @@ public function setSetting($setting_name, $value) {
*/
public function getConstraints() {
$constraints = isset($this->definition['constraints']) ? $this->definition['constraints'] : [];
$constraints += \Drupal::typedDataManager()->getDefaultConstraints($this);
$constraints += $this->getTypedDataManager()->getDefaultConstraints($this);
return $constraints;
}
......@@ -340,4 +342,14 @@ public function toArray() {
return $this->definition;
}
/**
* {@inheritdoc}
*/
public function __sleep() {
// Never serialize the typed data manager.
$vars = get_object_vars($this);
unset($vars['typedDataManager']);
return array_keys($vars);
}
}
......@@ -117,7 +117,13 @@ 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);
if (method_exists($data_definition, 'setTypedDataManager')) {
$data_definition->setTypedDataManager($this);
}
return $data_definition;
}
/**
......
llama: llama
cat:
type: kitten
count: 2
giraffe:
hum1: hum1
hum2: hum2
......@@ -158,3 +158,39 @@ config_test.foo:
config_test.bar:
type: config_test.foo
config_test.validation:
type: config_object
label: 'Configuration type'
constraints:
Callback:
callback: [\Drupal\config_test\ConfigValidation, validateMapping]
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
constraints:
Callback:
callback: [\Drupal\config_test\ConfigValidation, validateSequence]
sequence:
type: string
constraints:
Callback:
callback: [\Drupal\config_test\ConfigValidation, validateGiraffes]
<?php
namespace Drupal\config_test;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* Provides a collection of validation callbacks for testing purposes.
*/
class ConfigValidation {
/**
* Validates a llama.
*
* @param string $string
* The string to validate.
* @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
* The validation execution context.
*/
public static function validateLlama($string, ExecutionContextInterface $context) {
if (!in_array($string, ['llama', 'alpaca', 'guanaco', 'vicuña'], TRUE)) {
$context->addViolation('no valid llama');
}
}
/**
* Validates cats.
*
* @param string $string
* The string to validate.
* @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
* The validation execution context.
*/
public static function validateCats($string, ExecutionContextInterface $context) {
if (!in_array($string, ['kitten', 'cats', 'nyans'])) {
$context->addViolation('no valid cat');
}
}
/**
* Validates a number.
*
* @param int $count
* The integer to validate.
* @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
* The validation execution context.
*/
public static function validateCatCount($count, ExecutionContextInterface $context) {
if ($count <= 1) {
$context->addViolation('no enough cats');
}
}
/**
* Validates giraffes.
*
* @param string $string
* The string to validate.
* @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
* The validation execution context.
*/
public static function validateGiraffes($string, ExecutionContextInterface $context) {
if (strpos($string, 'hum') !== 0) {
$context->addViolation('Giraffes just hum');
}
}
/**
* Validates a mapping.
*
* @param array $mapping
* The data to validate.
* @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
* The validation execution context.
*/
public static function validateMapping($mapping, ExecutionContextInterface $context) {
if ($diff = array_diff(array_keys($mapping), ['llama', 'cat', 'giraffe', '_core'])) {
$context->addViolation('Missing giraffe.');
}
}
/**
* Validates a sequence.
*
* @param array $sequence
* The data to validate.
* @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
* The validation execution context.
*/
public static function validateSequence($sequence, ExecutionContextInterface $context) {
if (isset($sequence['invalid-key'])) {
$context->addViolation('Invalid giraffe key.');
}
}
}
......@@ -7,6 +7,8 @@ system.site:
uuid:
type: string
label: 'Site UUID'
constraints:
NotNull: []
name:
type: label
label: 'Site name'
......
......@@ -34,7 +34,8 @@ public function testDefaultConfig() {
\Drupal::service('config.storage'),
new TestInstallStorage(InstallStorage::CONFIG_SCHEMA_DIRECTORY),
\Drupal::service('cache.discovery'),
\Drupal::service('module_handler')
\Drupal::service('module_handler'),
\Drupal::service('class_resolver')
);
// Create a configuration storage with access to default configuration in
......
<?php
namespace Drupal\KernelTests\Config;
use Drupal\Core\Config\Schema\SequenceDataDefinition;
use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\Type\IntegerInterface;
use Drupal\Core\TypedData\Type\StringInterface;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
* Tests config validation mechanism.
*
* @group Config
*/
class TypedConfigTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['config_test'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->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('llama', $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 item metadata is available.
$this->assertInstanceOf(ComplexDataDefinitionInterface::class, $mapping->getDataDefinition());
$this->assertArrayHasKey('type', $mapping->getProperties());
$this->assertArrayHasKey('count', $mapping->getProperties());
// 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(SequenceDataDefinition::class, $sequence->getDataDefinition());
}
/**
* 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', 'elephant');
$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', 'llama');
$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 elements.
$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());
// Test constrains on the sequence itself.
$config->set('giraffe', ['hum', 'hum2', 'invalid-key' => 'hum']);
$config->save();
$typed_config = $typed_config_manager->get('config_test.validation');
$result = $typed_config->validate();
$this->assertCount(1, $result);
$this->assertEquals('giraffe', $result->get(0)->getPropertyPath());
$this->assertEquals('Invalid giraffe key.', $result->get(0)->getMessage());
// Validates mapping.
$typed_config = $typed_config_manager->get('config_test.validation');
$value = $typed_config->getValue();
unset($value['giraffe']);
$value['elephant'] = 'foo';
$typed_config->setValue($value);
$result = $typed_config->validate();
$this->assertCount(1, $result);
$this->assertEquals('', $result->get(0)->getPropertyPath());
$this->assertEquals('Missing giraffe.', $result->get(0)->getMessage());
}
}
......@@ -47,6 +47,7 @@ public function testSchemaMapping() {
$expected['class'] = Undefined::class;
$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.');
// Configuration file without schema will return Undefined as well.
......@@ -67,6 +68,7 @@ public function testSchemaMapping() {
$expected['mapping']['testlist'] = ['label' => 'Test list'];
$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 configuration with only some schema.');
// Check type detection on elements with undefined types.
......@@ -77,6 +79,7 @@ public function testSchemaMapping() {
$expected['class'] = Undefined::class;
$expected['type'] = 'undefined';
$expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$this->assertEqual($definition, $expected, 'Automatic type detected for a scalar is undefined.');
$definition = $config->get('testlist')->getDataDefinition()->toArray();
$expected = [];
......@@ -84,6 +87,7 @@ public function testSchemaMapping() {
$expected['class'] = Undefined::class;
$expected['type'] = 'undefined';
$expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$this->assertEqual($definition, $expected, 'Automatic type detected for a list is undefined.');
$definition = $config->get('testnoschema')->getDataDefinition()->toArray();
$expected = [];
......@@ -91,6 +95,7 @@ public function testSchemaMapping() {
$expected['class'] = Undefined::class;
$expected['type'] = 'undefined';
$expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$this->assertEqual($definition, $expected, 'Automatic type detected for an undefined integer is undefined.');
// Simple case, straight metadata.
......@@ -109,6 +114,7 @@ public function testSchemaMapping() {
$expected['mapping']['_core']['type'] = '_core_config_info';
$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 system.maintenance');
// Mixed schema with ignore elements.
......@@ -139,6 +145,7 @@ public function testSchemaMapping() {
'type' => 'integer',
];
$expected['type'] = 'config_schema_test.ignore';
$expected['unwrap_for_canonical_representation'] = TRUE;
$this->assertEqual($definition, $expected);
......@@ -149,6 +156,7 @@ public function testSchemaMapping() {
$expected['label'] = 'Irrelevant';
$expected['class'] = Ignore::class;
$expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$this->assertEqual($definition, $expected);
$definition = \Drupal::service('config.typed')->get('config_schema_test.ignore')->get('indescribable')->getDataDefinition()->toArray();
$expected['label'] = 'Indescribable';
......@@ -160,6 +168,7 @@ public function testSchemaMapping() {
$expected['label'] = 'Image style';
$expected['class'] = Mapping::class;
$expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['mapping']['name']['type'] = 'string';
$expected['mapping']['uuid']['type'] = 'string';
$expected['mapping']['uuid']['label'] = 'UUID';
......@@ -193,6 +202,7 @@ public function testSchemaMapping() {
$expected['label'] = 'Image scale';
$expected['class'] = Mapping::class;
$expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['mapping']['width']['type'] = 'integer';
$expected['mapping']['width']['label'] = 'Width';
$expected['mapping']['height']['type'] = 'integer';
......@@ -220,6 +230,7 @@ public function testSchemaMapping() {
$expected['label'] = 'Mapping';
$expected['class'] = Mapping::class;
$expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['mapping'] = [
'integer' => ['type' => 'integer'],
'string' => ['type' => 'string'],
......@@ -241,6 +252,7 @@ public function testSchemaMapping() {
$expected['mapping']['testdescription']['label'] = 'Description';
$expected['type'] = 'config_schema_test.someschema.somemodule.*.*';
$expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$this->assertEqual($definition, $expected, 'Retrieved the right metadata for config_schema_test.someschema.somemodule.section_one.subsection');
......@@ -263,6 +275,7 @@ public function testSchemaMappingWithParents() {
'label' => 'Test item nested one level',
'class' => StringData::class,
'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
'unwrap_for_canonical_representation' => TRUE,
];
$this->assertEqual($definition, $expected);
......@@ -274,6 +287,7 @@ public function testSchemaMappingWithParents() {
'label' => 'Test item nested two levels',
'class' => StringData::class,
'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
'unwrap_for_canonical_representation' => TRUE,
];
$this->assertEqual($definition, $expected);
......@@ -285,6 +299,7 @@ public function testSchemaMappingWithParents() {
'label' => 'Test item nested three levels',
'class' => StringData::class,
'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
'unwrap_for_canonical_representation' => TRUE,
];
$this->assertEqual($definition, $expected);
}
......@@ -475,6 +490,7 @@ public function testSchemaFallback() {
$expected['label'] = 'Schema wildcard fallback test';
$expected['class'] = Mapping::class;
$expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
$expected['unwrap_for_canonical_representation'] = TRUE;
$expected['mapping']['langcode']['type'] = 'string';
$expected['mapping']['langcode']['label'] = 'Language code';
$expected['mapping']['_core']['type'] = '_core_config_info';
......
......@@ -101,8 +101,8 @@ public function testEntities() {
// Test that the definition factory creates the right definitions for all
// entity data types variants.
$this->assertEqual($this->typedDataManager->createDataDefinition('entity'), EntityDataDefinition::create());
$this->assertEqual($this->typedDataManager->createDataDefinition('entity:node'), EntityDataDefinition::create('node'));
$this->assertEqual(serialize($this->typedDataManager->createDataDefinition('entity')), serialize(EntityDataDefinition::create()));
$this->assertEqual(serialize($this->typedDataManager->createDataDefinition('entity:node')), serialize(EntityDataDefinition::create('node')));
// Config entities don't support typed data.
$entity_definition = EntityDataDefinition::create('node_type');
......@@ -123,7 +123,7 @@ public function testEntityReferences() {
// Test that the definition factory creates the right definition object.
$reference_definition2 = $this->typedDataManager->createDataDefinition('entity_reference');
$this->assertTrue($reference_definition2 instanceof DataReferenceDefinitionInterface);
$this->assertEqual($reference_definition2, $reference_definition);
$this->assertEqual(serialize($reference_definition2), serialize($reference_definition));
}
}
......@@ -77,7 +77,7 @@ public function testMaps() {
$map_definition2->setPropertyDefinition('one', DataDefinition::create('string'))
->setPropertyDefinition('two', DataDefinition::create('string'))
->setPropertyDefinition('three', DataDefinition::create('string'));
$this->assertEqual($map_definition, $map_definition2);
$this->assertEqual(serialize($map_definition), serialize($map_definition2));
}
/**
......@@ -93,7 +93,7 @@ public function testDataReferences() {
// Test using the definition factory.
$language_reference_definition2 = $this->typedDataManager->createDataDefinition('language_reference');
$this->assertTrue($language_reference_definition2 instanceof DataReferenceDefinitionInterface);
$this->assertEqual($language_reference_definition, $language_reference_definition2);
$this->assertEqual(serialize($language_reference_definition), serialize($language_reference_definition2));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment