diff --git a/core/lib/Drupal/Core/Config/Schema/ArrayElement.php b/core/lib/Drupal/Core/Config/Schema/ArrayElement.php index 8becdd7..86a21f4 100644 --- a/core/lib/Drupal/Core/Config/Schema/ArrayElement.php +++ b/core/lib/Drupal/Core/Config/Schema/ArrayElement.php @@ -6,30 +6,26 @@ */ namespace Drupal\Core\Config\Schema; + use Drupal\Core\TypedData\TraversableTypedDataInterface; +use Drupal\Core\TypedData\TypedData; /** * Defines a generic configuration element that contains multiple properties. */ -abstract class ArrayElement extends Element implements \IteratorAggregate, TraversableTypedDataInterface, \ArrayAccess, \Countable { +abstract class ArrayElement extends TypedData implements \IteratorAggregate, TypedConfigInterface { /** - * Parsed elements. + * The configuration value. + * + * @var mixed */ - protected $elements; + protected $value; /** - * Gets an array of contained elements. - * - * @return \Drupal\Core\TypedData\TypedDataInterface[] - * An array of elements contained in this element. + * Parsed elements. */ - protected function getElements() { - if (!isset($this->elements)) { - $this->elements = $this->parse(); - } - return $this->elements; - } + protected $elements; /** * Gets valid configuration data keys. @@ -47,59 +43,101 @@ protected function getAllKeys() { * @return \Drupal\Core\TypedData\TypedDataInterface[] * An array of elements contained in this element. */ - protected abstract function parse(); - - /** - * Implements TypedDataInterface::validate(). - */ - public function validate() { - foreach ($this->getElements() as $element) { - if (!$element->validate()) { - return FALSE; - } + protected function parse() { + $elements = array(); + foreach ($this->getAllKeys() as $key) { + $value = isset($this->value[$key]) ? $this->value[$key] : NULL; + $definition = $this->getElementDefinition($key); + $elements[$key] = $this->createElement($definition, $value, $key); } - return TRUE; + return $elements; } /** - * Implements ArrayAccess::offsetExists(). + * Gets data definition object for contained element. + * + * @param int|string $key + * Property name or index of the element. + * + * @return \Drupal\Core\TypedData\DataDefinitionInterface */ - public function offsetExists($offset) { - return array_key_exists($offset, $this->getElements()); - } + protected abstract function getElementDefinition($key); /** - * Implements ArrayAccess::offsetGet(). + * {@inheritdoc} */ - public function offsetGet($offset) { + public function get($name) { + $parts = explode('.', $name); + $root_key = array_shift($parts); $elements = $this->getElements(); - return $elements[$offset]; + if (isset($elements[$root_key])) { + $element = $elements[$root_key]; + // If $property_name contained a dot recurse into the keys. + while ($element && ($key = array_shift($parts)) !== NULL) { + if ($element instanceof TypedConfigInterface) { + $element = $element->get($key); + } + else { + $element = NULL; + } + } + } + if (isset($element)) { + return $element; + } + else { + throw new \InvalidArgumentException(String::format("The configuration property @key doesn't exist.", array('@key' => $property_name))); + } } /** - * Implements ArrayAccess::offsetSet(). + * {@inheritdoc} */ - public function offsetSet($offset, $value) { + public function set($key, $value) { + // Support setting values via typed data objects. if ($value instanceof TypedDataInterface) { $value = $value->getValue(); } - $this->value[$offset] = $value; + $this->value[$key] = $value; + // Parsed elements must be rebuilt with new values unset($this->elements); + // Directly notify ourselves. + $this->onChange($key, $value); + return $this; } /** - * Implements ArrayAccess::offsetUnset(). + * {@inheritdoc} */ - public function offsetUnset($offset) { - unset($this->value[$offset]); - unset($this->elements); + public function getElements() { + if (!isset($this->elements)) { + $this->elements = $this->parse(); + } + return $this->elements; } /** - * Implements Countable::count(). + * {@inheritdoc} */ - public function count() { - return count($this->getElements()); + public function isEmpty() { + return empty($this->value); + } + + /** + * {@inheritdoc} + */ + public function toArray() { + return isset($this->value) ? $this->value : array(); + } + + /** + * {@inheritdoc} + */ + public function onChange($name) { + // Notify the parent of changes. + if (isset($this->parent)) { + $this->parent->onChange($this->name); + } } /** @@ -109,4 +147,39 @@ public function getIterator() { return new \ArrayIterator($this->getElements()); } + /** + * Creates a contained typed configuration object. + * + * @param \Drupal\Core\TypedData\DataDefinitionInterface $definition + * The data definition object. + * @param mixed $value + * (optional) The data value. If set, it has to match one of the supported + * data type format as documented for the data type classes. + * @param string $name + * The key of the contained element. + * + * @return \Drupal\Core\TypedData\TypedDataInterface + */ + protected function createElement($definition, $value, $key) { + return \Drupal::service('config.typed')->create($definition, $value, $key, $this); + } + + /** + * Creates a new data definition object from a type definition array and + * actual configuration data. + * + * @param array $definition + * The base type definition array, for which a data definition should be + * created. + * @param $value + * The value of the configuration element. + * @param string $key + * The key of the contained element. + * + * @return \Drupal\Core\TypedData\DataDefinitionInterface + */ + protected function buildDataDefinition($definition, $value, $key) { + return \Drupal::service('config.typed')->buildDataDefinition($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 c492d42..2993f8c 100644 --- a/core/lib/Drupal/Core/Config/Schema/Element.php +++ b/core/lib/Drupal/Core/Config/Schema/Element.php @@ -21,20 +21,4 @@ */ protected $value; - /** - * Create typed config object. - */ - protected function parseElement($key, $data, $definition) { - return \Drupal::service('config.typed')->create($definition, $data, $key, $this); - } - - /** - * Build data definition object for contained elements. - * - * @return \Drupal\Core\TypedData\DataDefinitionInterface - */ - protected function buildDataDefinition($definition, $value, $key) { - return \Drupal::service('config.typed')->buildDataDefinition($definition, $value, $key, $this); - } - } diff --git a/core/lib/Drupal/Core/Config/Schema/Ignore.php b/core/lib/Drupal/Core/Config/Schema/Ignore.php index 20701b8..d005302 100644 --- a/core/lib/Drupal/Core/Config/Schema/Ignore.php +++ b/core/lib/Drupal/Core/Config/Schema/Ignore.php @@ -12,10 +12,4 @@ */ class Ignore extends Element { - /** - * {@inheritdoc}. - */ - public function validate() { - return TRUE; - } } diff --git a/core/lib/Drupal/Core/Config/Schema/Mapping.php b/core/lib/Drupal/Core/Config/Schema/Mapping.php index eed1da8..5adfc20 100644 --- a/core/lib/Drupal/Core/Config/Schema/Mapping.php +++ b/core/lib/Drupal/Core/Config/Schema/Mapping.php @@ -13,145 +13,25 @@ /** * Defines a mapping configuration element. * - * Wraps configuration data and metadata allowing access to configuration data - * using the ComplexDataInterface API. This object may contain any number and - * type of nested properties. + * This object may contain any number and type of nested properties and each + * property key may have its own definition in the 'mapping' property of the + * configuration schema. + * + * Properties in the configuration value that are not defined in the mapping + * will get the 'undefined' data type. + * + * Read https://drupal.org/node/1905070 for more details about configuration + * schema, types and type resolution. */ -class Mapping extends ArrayElement implements ComplexDataInterface { - - /** - * An array of data definitions. - * - * @var \Drupal\Core\TypedData\DataDefinitionInterface[] - */ - protected $propertyDefinitions; - - /** - * {@inheritdoc} - */ - protected function parse() { - $elements = array(); - foreach ($this->getPropertyDefinitions() as $key => $definition) { - $elements[$key] = $this->parseElement($key, $this->value[$key], $definition); - } - return $elements; - } +class Mapping extends ArrayElement { /** * {@inheritdoc} - * - * Since all configuration objects are mappings the function will except a dot - * delimited key to access nested values, for example, 'page.front'. - */ - public function get($property_name) { - $parts = explode('.', $property_name); - $root_key = array_shift($parts); - $elements = $this->getElements(); - if (isset($elements[$root_key])) { - $element = $elements[$root_key]; - // If $property_name contained a dot recurse into the keys. - while ($element && ($key = array_shift($parts)) !== NULL) { - if (method_exists($element, 'get')) { - $element = $element->get($key); - } - else { - $element = NULL; - } - } - } - if (isset($element)) { - return $element; - } - else { - throw new \InvalidArgumentException(String::format("The configuration property @key doesn't exist.", array('@key' => $property_name))); - } - } - - /** - * Implements Drupal\Core\TypedData\ComplexDataInterface::set(). - */ - public function set($property_name, $value, $notify = TRUE) { - // Set the data into the configuration array but behave according to the - // interface specification when we've got a null value. - if (isset($value)) { - $this->value[$property_name] = $value; - $property = $this->get($property_name); - } - else { - // In these objects, when clearing the value, the property is gone. - // As this needs to return a property, we get it before we delete it. - $property = $this->get($property_name); - unset($this->value[$property_name]); - $property->setValue($value); - } - // Notify the parent of any changes. - if ($notify && isset($this->parent)) { - $this->parent->onChange($this->name); - } - return $this; - } - - /** - * Implements Drupal\Core\TypedData\ComplexDataInterface::getProperties(). - */ - public function getProperties($include_computed = FALSE) { - return $this->getElements(); - } - - /** - * {@inheritdoc} - */ - public function toArray() { - return $this->getValue(); - } - - /** - * Gets the definition of a contained property. - * - * @param string $name - * The name of property. - * - * @return \Drupal\Core\TypedData\DataDefinitionInterface|null - * The definition of the property or NULL if the property does not exist. - */ - public function getPropertyDefinition($name) { - $definitions = $this->getPropertyDefinitions(); - return isset($definitions[$name]) ? isset($definitions[$name]) : NULL; - } - - /** - * Gets an array of property definitions of contained properties. - * - * @return \Drupal\Core\TypedData\DataDefinitionInterface[] - * An array of property definitions of contained properties, keyed by - * property name. - */ - public function getPropertyDefinitions() { - if (!isset($this->propertyDefinitions)) { - $this->propertyDefinitions = array(); - foreach ($this->getAllKeys() as $key) { - $definition = isset($this->definition['mapping'][$key]) ? $this->definition['mapping'][$key] : array(); - $this->propertyDefinitions[$key] = $this->buildDataDefinition($definition, $this->value[$key], $key); - } - } - return $this->propertyDefinitions; - } - - /** - * Implements Drupal\Core\TypedData\ComplexDataInterface::isEmpty(). - */ - public function isEmpty() { - return empty($this->value); - } - - /** - * Implements \Drupal\Core\TypedData\ComplexDataInterface::onChange(). */ - public function onChange($property_name) { - // Notify the parent of changes. - if (isset($this->parent)) { - $this->parent->onChange($this->name); - } + protected function getElementDefinition($key) { + $value = isset($this->value[$key]) ? $this->value[$key] : NULL; + $definition = isset($this->definition['mapping'][$key]) ? $this->definition['mapping'][$key] : array(); + return $this->buildDataDefinition($definition, $value, $key); } } diff --git a/core/lib/Drupal/Core/Config/Schema/Sequence.php b/core/lib/Drupal/Core/Config/Schema/Sequence.php index bce81a9..b7c34c2 100644 --- a/core/lib/Drupal/Core/Config/Schema/Sequence.php +++ b/core/lib/Drupal/Core/Config/Schema/Sequence.php @@ -7,95 +7,24 @@ namespace Drupal\Core\Config\Schema; -use Drupal\Core\TypedData\ListInterface; - /** * Defines a configuration element of type Sequence. + * + * This object may contain any number and type of nested elements that share + * a common definition in the 'sequence' property of the configuration schema. + * + * Read https://drupal.org/node/1905070 for more details about configuration + * schema, types and type resolution. */ -class Sequence extends ArrayElement implements ListInterface { - - /** - * Data definition - * - * @var \Drupal\Core\TypedData\DataDefinitionInterface - */ - protected $itemDefinition; +class Sequence extends ArrayElement { /** * {@inheritdoc} */ - protected function parse() { - // Creates a new data definition object for each item from the generic type - // definition array and actual configuration data for that item. Type - // definitions may contain variables to be replaced and those depend on - // each item's data. + protected function getElementDefinition($key) { + $value = isset($this->value[$key]) ? $this->value[$key] : NULL; $definition = isset($this->definition['sequence'][0]) ? $this->definition['sequence'][0] : array(); - $elements = array(); - foreach ($this->value as $key => $value) { - $data_definition = $this->buildDataDefinition($definition, $value, $key); - $elements[$key] = $this->parseElement($key, $value, $data_definition); - } - return $elements; - } - - /** - * Implements Drupal\Core\TypedData\ListInterface::isEmpty(). - */ - public function isEmpty() { - return empty($this->value); - } - - /** - * Implements Drupal\Core\TypedData\ListInterface::getItemDefinition(). - */ - public function getItemDefinition() { - if (!isset($this->itemDefinition)) { - $definition = isset($this->definition['sequence'][0]) ? $this->definition['sequence'][0] : array(); - $this->itemDefinition = $this->buildDataDefinition($definition, NULL); - } - return $this->itemDefinition; - } - - /** - * Implements \Drupal\Core\TypedData\ListInterface::onChange(). - */ - public function onChange($delta) { - // Notify the parent of changes. - if (isset($this->parent)) { - $this->parent->onChange($this->name); - } - } - - /** - * {@inheritdoc} - */ - public function get($key) { - $elements = $this->getElements(); - return $elements[$key]; - } - - /** - * {@inheritdoc} - */ - public function first() { - return $this->get(0); - } - - /** - * {@inheritdoc} - */ - public function set($index, $item) { - $this->offsetSet($index, $item); - return $this; - } - - /** - * {@inheritdoc} - */ - public function filter($callback) { - $this->value = array_filter($this->value, $callback); - unset($this->elements); - return $this; + return $this->buildDataDefinition($definition, $value, $key); } } diff --git a/core/lib/Drupal/Core/Config/Schema/TypedConfigInterface.php b/core/lib/Drupal/Core/Config/Schema/TypedConfigInterface.php new file mode 100644 index 0000000..8ce63df --- /dev/null +++ b/core/lib/Drupal/Core/Config/Schema/TypedConfigInterface.php @@ -0,0 +1,78 @@ +value); - } } diff --git a/core/lib/Drupal/Core/Config/TypedConfigManager.php b/core/lib/Drupal/Core/Config/TypedConfigManager.php index fb1c198..99f7725 100644 --- a/core/lib/Drupal/Core/Config/TypedConfigManager.php +++ b/core/lib/Drupal/Core/Config/TypedConfigManager.php @@ -64,8 +64,8 @@ public function __construct(StorageInterface $configStorage, StorageInterface $s * @param string $name * Configuration object name. * - * @return \Drupal\Core\Config\Schema\Element - * Typed configuration element. + * @return \Drupal\Core\Config\TypedConfigInterface + * Typed configuration data. */ public function get($name) { $data = $this->configStorage->read($name); diff --git a/core/modules/config/src/Tests/ConfigSchemaTest.php b/core/modules/config/src/Tests/ConfigSchemaTest.php index c7a6f55..b2c72ea 100644 --- a/core/modules/config/src/Tests/ConfigSchemaTest.php +++ b/core/modules/config/src/Tests/ConfigSchemaTest.php @@ -68,21 +68,21 @@ function testSchemaMapping() { // Check type detection on elements with undefined types. $config = \Drupal::service('config.typed')->get('config_schema_test.someschema'); - $definition = $config['testitem']->getDataDefinition()->toArray(); + $definition = $config->get('testitem')->getDataDefinition()->toArray(); $expected = array(); $expected['label'] = 'Test item'; $expected['class'] = '\Drupal\Core\Config\Schema\Undefined'; $expected['type'] = 'undefined'; $expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition'; $this->assertEqual($definition, $expected, 'Automatic type detected for a scalar is undefined.'); - $definition = $config['testlist']->getDataDefinition()->toArray(); + $definition = $config->get('testlist')->getDataDefinition()->toArray(); $expected = array(); $expected['label'] = 'Test list'; $expected['class'] = '\Drupal\Core\Config\Schema\Undefined'; $expected['type'] = 'undefined'; $expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition'; $this->assertEqual($definition, $expected, 'Automatic type detected for a list is undefined.'); - $definition = $config['testnoschema']->getDataDefinition()->toArray(); + $definition = $config->get('testnoschema')->getDataDefinition()->toArray(); $expected = array(); $expected['label'] = 'Undefined'; $expected['class'] = '\Drupal\Core\Config\Schema\Undefined'; @@ -196,7 +196,7 @@ function testSchemaMapping() { // Most complex case, get metadata for actual configuration element. $effects = \Drupal::service('config.typed')->get('image.style.medium')->get('effects'); - $definition = $effects['bddf0d06-42f9-4c75-a700-a33cafa25ea0']['data']->getDataDefinition()->toArray(); + $definition = $effects->get('bddf0d06-42f9-4c75-a700-a33cafa25ea0')->get('data')->getDataDefinition()->toArray(); // This should be the schema for image.effect.image_scale, reuse previous one. $expected['type'] = 'image.effect.image_scale'; @@ -230,7 +230,7 @@ function testSchemaMappingWithParents() { // Test fetching parent one level up. $entry = $config_data->get('one_level'); - $definition = $entry['testitem']->getDataDefinition()->toArray(); + $definition = $entry->get('testitem')->getDataDefinition()->toArray(); $expected = array( 'type' => 'config_schema_test.someschema.with_parents.key_1', 'label' => 'Test item nested one level', @@ -241,7 +241,7 @@ function testSchemaMappingWithParents() { // Test fetching parent two levels up. $entry = $config_data->get('two_levels'); - $definition = $entry['wrapper']['testitem']->getDataDefinition()->toArray(); + $definition = $entry->get('wrapper')->get('testitem')->getDataDefinition()->toArray(); $expected = array( 'type' => 'config_schema_test.someschema.with_parents.key_2', 'label' => 'Test item nested two levels', @@ -252,7 +252,7 @@ function testSchemaMappingWithParents() { // Test fetching parent three levels up. $entry = $config_data->get('three_levels'); - $definition = $entry['wrapper_1']['wrapper_2']['testitem']->getDataDefinition()->toArray(); + $definition = $entry->get('wrapper_1')->get('wrapper_2')->get('testitem')->getDataDefinition()->toArray(); $expected = array( 'type' => 'config_schema_test.someschema.with_parents.key_3', 'label' => 'Test item nested three levels', @@ -281,28 +281,26 @@ function testSchemaData() { $this->assertTrue(empty($definition['translatable']), 'Got the right translatability setting for page.front data.'); // Check nested array of properties. - $list = $meta->get('page'); + $list = $meta->get('page')->getElements(); $this->assertEqual(count($list), 3, 'Got a list with the right number of properties for site page data'); $this->assertTrue(isset($list['front']) && isset($list['403']) && isset($list['404']), 'Got a list with the right properties for site page data.'); $this->assertEqual($list['front']->getValue(), 'user/login', 'Got the right value for page.front data from the list.'); - // And test some ComplexDataInterface methods. - $properties = $list->getProperties(); + // And test some TypedConfigInterface methods. + $properties = $list; $this->assertTrue(count($properties) == 3 && $properties['front'] == $list['front'], 'Got the right properties for site page.'); - $values = $list->toArray(); + $values = $meta->get('page')->toArray(); $this->assertTrue(count($values) == 3 && $values['front'] == 'user/login', 'Got the right property values for site page.'); // Now let's try something more complex, with nested objects. $wrapper = \Drupal::service('config.typed')->get('image.style.large'); $effects = $wrapper->get('effects'); - - // The function is_array() doesn't work with ArrayAccess, so we use count(). - $this->assertTrue(count($effects) == 1, 'Got an array with effects for image.style.large data'); + $this->assertTrue(count($effects->toArray()) == 1, 'Got an array with effects for image.style.large data'); $uuid = key($effects->getValue()); - $effect = $effects[$uuid]; - $this->assertTrue(count($effect['data']) && $effect['id']->getValue() == 'image_scale', 'Got data for the image scale effect from metadata.'); - $this->assertTrue($effect['data']['width'] instanceof IntegerInterface, 'Got the right type for the scale effect width.'); - $this->assertEqual($effect['data']['width']->getValue(), 480, 'Got the right value for the scale effect width.' ); + $effect = $effects->get($uuid)->getElements(); + $this->assertTrue(!$effect['data']->isEmpty() && $effect['id']->getValue() == 'image_scale', 'Got data for the image scale effect from metadata.'); + $this->assertTrue($effect['data']->get('width') instanceof IntegerInterface, 'Got the right type for the scale effect width.'); + $this->assertEqual($effect['data']->get('width')->getValue(), 480, 'Got the right value for the scale effect width.' ); // Finally update some object using a configuration wrapper. $new_slogan = 'Site slogan for testing configuration metadata'; @@ -409,7 +407,7 @@ function testSchemaFallback() { * @see \Drupal\Core\Config\TypedConfigManager::getFallbackName() */ function testColonsInSchemaTypeDetermination() { - $tests = \Drupal::service('config.typed')->get('config_schema_test.plugin_types')->get('tests'); + $tests = \Drupal::service('config.typed')->get('config_schema_test.plugin_types')->get('tests')->getElements(); $definition = $tests[0]->getDataDefinition()->toArray(); $this->assertEqual($definition['type'], 'test.plugin_types.boolean'); @@ -422,17 +420,17 @@ function testColonsInSchemaTypeDetermination() { $definition = $tests[3]->getDataDefinition()->toArray(); $this->assertEqual($definition['type'], 'test.plugin_types.*'); - $tests = \Drupal::service('config.typed')->get('config_schema_test.plugin_types')->get('test_with_parents'); - $definition = $tests[0]['settings']->getDataDefinition()->toArray(); + $tests = \Drupal::service('config.typed')->get('config_schema_test.plugin_types')->get('test_with_parents')->getElements(); + $definition = $tests[0]->get('settings')->getDataDefinition()->toArray(); $this->assertEqual($definition['type'], 'test_with_parents.plugin_types.boolean'); - $definition = $tests[1]['settings']->getDataDefinition()->toArray(); + $definition = $tests[1]->get('settings')->getDataDefinition()->toArray(); $this->assertEqual($definition['type'], 'test_with_parents.plugin_types.boolean:*'); - $definition = $tests[2]['settings']->getDataDefinition()->toArray(); + $definition = $tests[2]->get('settings')->getDataDefinition()->toArray(); $this->assertEqual($definition['type'], 'test_with_parents.plugin_types.*'); - $definition = $tests[3]['settings']->getDataDefinition()->toArray(); + $definition = $tests[3]->get('settings')->getDataDefinition()->toArray(); $this->assertEqual($definition['type'], 'test_with_parents.plugin_types.*'); } diff --git a/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php b/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php index 6ab80f7..dbd05fe 100644 --- a/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php +++ b/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php @@ -10,6 +10,7 @@ use Drupal\config_translation\ConfigMapperManagerInterface; use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\TypedData\TypedDataInterface; +use Drupal\Core\TypedData\TraversableTypedDataInterface; use Drupal\Core\Form\BaseFormIdInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; diff --git a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php index ed3104b..27f120d 100644 --- a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php +++ b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php @@ -83,7 +83,7 @@ public function testConfigTranslation() { // Get translation and check we've only got the site name. $translation = $wrapper->getTranslation($langcode); - $properties = $translation->getProperties(); + $properties = $translation->getElements(); $this->assertEqual(count($properties), 1, 'Got the right number of properties after translation'); // Check the translated site name is displayed.