Issue #2264179 by Gábor Hojtsy, vijaycs85, alexpott, benjy: Clarify use of property/undefined and add an ignore type in configuration schema.

8.0.x
Nathaniel Catchpole 2014-05-20 17:38:14 +01:00
parent 51edc990be
commit 4451ee6a80
10 changed files with 195 additions and 97 deletions

View File

@ -1,3 +1,23 @@
# Base types provided by Drupal core.
# Read https://drupal.org/node/1905070 for more details about configuration
# schema, types and type resolution.
# Undefined type used by the system to assign to elements at any level where
# configuration schema is not defined. Using explicitly has the same effect as
# not defining schema, so there is no point in doing that.
undefined:
label: 'Undefined'
class: '\Drupal\Core\Config\Schema\Undefined'
# Explicit type to use when no data typing is possible. Instead of using this
# type, we strongly suggest you use configuration structures that can be
# described with other structural elements of schema, and describe your schema
# with those elements.
ignore:
label: 'Ignore'
class: '\Drupal\Core\Config\Schema\Ignore'
# Basic scalar data types from typed data. # Basic scalar data types from typed data.
boolean: boolean:
label: 'Boolean' label: 'Boolean'
@ -18,10 +38,7 @@ uri:
label: 'Uri' label: 'Uri'
class: '\Drupal\Core\TypedData\Plugin\DataType\Uri' class: '\Drupal\Core\TypedData\Plugin\DataType\Uri'
# Basic data types for configuration. # Container data types for lists with known and unknown keys.
undefined:
label: 'Undefined'
class: '\Drupal\Core\Config\Schema\Property'
mapping: mapping:
label: Mapping label: Mapping
class: '\Drupal\Core\Config\Schema\Mapping' class: '\Drupal\Core\Config\Schema\Mapping'
@ -29,11 +46,6 @@ sequence:
label: Sequence label: Sequence
class: '\Drupal\Core\Config\Schema\Sequence' class: '\Drupal\Core\Config\Schema\Sequence'
# Default mapping for unknown types or types not found.
default:
type: undefined
label: 'Unknown'
# Simple extended data types: # Simple extended data types:
# Human readable string that must be plain text and editable with a text field. # Human readable string that must be plain text and editable with a text field.
@ -59,6 +71,11 @@ date_format:
label: 'PHP date format' label: 'PHP date format'
translatable: true translatable: true
# HTML color value.
color_hex:
type: string
label: 'Color'
# Complex extended data types: # Complex extended data types:
# Mail text with subject and body parts. # Mail text with subject and body parts.
@ -194,11 +211,6 @@ route:
- type: string - type: string
label: 'Param' label: 'Param'
# HTML color value.
color_hex:
type: string
label: 'Color'
# Config dependencies. # Config dependencies.
config_dependencies: config_dependencies:
type: mapping type: mapping

View File

@ -0,0 +1,21 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\Ignore.
*/
namespace Drupal\Core\Config\Schema;
/**
* Configuration property to ignore.
*/
class Ignore extends Element {
/**
* {@inheritdoc}.
*/
public function validate() {
return TRUE;
}
}

View File

@ -1,22 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\Sequence.
*/
namespace Drupal\Core\Config\Schema;
/**
* Generic configuration property.
*/
class Property extends Element {
/**
* Implements TypedDataInterface::validate().
*/
public function validate() {
return isset($this->value);
}
}

View File

@ -0,0 +1,21 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Schema\Undefined.
*/
namespace Drupal\Core\Config\Schema;
/**
* Undefined configuration element.
*/
class Undefined extends Element {
/**
* {@inheritdoc}.
*/
public function validate() {
return isset($this->value);
}
}

View File

@ -8,10 +8,12 @@
namespace Drupal\Core\Config; namespace Drupal\Core\Config;
use Drupal\Component\Utility\String; use Drupal\Component\Utility\String;
use Drupal\Core\Config\Schema\Ignore;
use Drupal\Core\Config\Schema\SchemaIncompleteException; use Drupal\Core\Config\Schema\SchemaIncompleteException;
use Drupal\Core\TypedData\PrimitiveInterface; use Drupal\Core\TypedData\PrimitiveInterface;
use Drupal\Core\TypedData\Type\FloatInterface; use Drupal\Core\TypedData\Type\FloatInterface;
use Drupal\Core\TypedData\Type\IntegerInterface; use Drupal\Core\TypedData\Type\IntegerInterface;
use Drupal\Core\Config\Schema\Undefined;
/** /**
* Provides a base class for configuration objects with storage support. * Provides a base class for configuration objects with storage support.
@ -167,37 +169,36 @@ abstract class StorableConfigBase extends ConfigBase {
* Exception on unsupported/undefined data type deducted. * Exception on unsupported/undefined data type deducted.
*/ */
protected function castValue($key, $value) { protected function castValue($key, $value) {
if ($value === NULL) { $element = FALSE;
$value = NULL; try {
$element = $this->getSchemaWrapper()->get($key);
} }
elseif (is_scalar($value)) { catch (SchemaIncompleteException $e) {
try { // @todo Consider making schema handling more strict by throwing
$element = $this->getSchemaWrapper()->get($key); // SchemaIncompleteException for all incomplete schema conditions *and*
if ($element instanceof PrimitiveInterface) { // throwing it forward. See https://drupal.org/node/2183983.
// Special handling for integers and floats since the configuration // Until then, we need to handle the Undefined case below.
// system is primarily concerned with saving values from the Form API }
// we have to special case the meaning of an empty string for numeric // Do not cast value if it is unknown or defined to be ignored.
// types. In PHP this would be casted to a 0 but for the purposes of if ($element && ($element instanceof Undefined || $element instanceof Ignore)) {
// configuration we need to treat this as a NULL. return $value;
if ($value === '' && ($element instanceof IntegerInterface || $element instanceof FloatInterface)) { }
$value = NULL; if ((is_scalar($value) || $value === NULL)) {
} if ($element && $element instanceof PrimitiveInterface) {
else { // Special handling for integers and floats since the configuration
$value = $element->getCastedValue(); // system is primarily concerned with saving values from the Form API
} // we have to special case the meaning of an empty string for numeric
// types. In PHP this would be casted to a 0 but for the purposes of
// configuration we need to treat this as a NULL.
$empty_value = $value === '' && ($element instanceof IntegerInterface || $element instanceof FloatInterface);
if ($value === NULL || $empty_value) {
$value = NULL;
} }
else { else {
// Config only supports primitive data types. If the config schema $value = $element->getCastedValue();
// does define a type $element will be an instance of
// \Drupal\Core\Config\Schema\Property. Convert it to string since it
// is the safest possible type.
$value = $element->getString();
} }
} }
catch (SchemaIncompleteException $e) {
// @todo throw an exception due to an incomplete schema.
// Fix as part of https://drupal.org/node/2183983.
}
} }
else { else {
// Throw exception on any non-scalar or non-array value. // Throw exception on any non-scalar or non-array value.

View File

@ -152,9 +152,8 @@ class TypedConfigManager extends PluginManagerBase implements TypedConfigManager
$type = $name; $type = $name;
} }
else { else {
// If we don't have definition, return the 'default' element. // If we don't have definition, return the 'undefined' element.
// This should map to 'undefined' type by default, unless overridden. $type = 'undefined';
$type = 'default';
} }
$definition = $definitions[$type]; $definition = $definitions[$type];
// Check whether this type is an extension of another one and compile it. // Check whether this type is an extension of another one and compile it.
@ -326,10 +325,9 @@ class TypedConfigManager extends PluginManagerBase implements TypedConfigManager
* {@inheritdoc} * {@inheritdoc}
*/ */
public function hasConfigSchema($name) { public function hasConfigSchema($name) {
// The schema system falls back on the Property class for unknown types. // The schema system falls back on the Undefined class for unknown types.
// See http://drupal.org/node/1905230
$definition = $this->getDefinition($name); $definition = $this->getDefinition($name);
return is_array($definition) && ($definition['class'] != '\Drupal\Core\Config\Schema\Property'); return is_array($definition) && ($definition['class'] != '\Drupal\Core\Config\Schema\Undefined');
} }
} }

View File

@ -7,6 +7,8 @@
namespace Drupal\config\Tests; namespace Drupal\config\Tests;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\TypedData\Type\IntegerInterface; use Drupal\Core\TypedData\Type\IntegerInterface;
use Drupal\Core\TypedData\Type\StringInterface; use Drupal\Core\TypedData\Type\StringInterface;
use Drupal\simpletest\DrupalUnitTestBase; use Drupal\simpletest\DrupalUnitTestBase;
@ -40,15 +42,15 @@ class ConfigSchemaTest extends DrupalUnitTestBase {
* Tests the basic metadata retrieval layer. * Tests the basic metadata retrieval layer.
*/ */
function testSchemaMapping() { function testSchemaMapping() {
// Nonexistent configuration key will have Unknown as metadata. // Nonexistent configuration key will have Undefined as metadata.
$this->assertIdentical(FALSE, \Drupal::service('config.typed')->hasConfigSchema('config_schema_test.no_such_key')); $this->assertIdentical(FALSE, \Drupal::service('config.typed')->hasConfigSchema('config_schema_test.no_such_key'));
$definition = \Drupal::service('config.typed')->getDefinition('config_schema_test.no_such_key'); $definition = \Drupal::service('config.typed')->getDefinition('config_schema_test.no_such_key');
$expected = array(); $expected = array();
$expected['label'] = 'Unknown'; $expected['label'] = 'Undefined';
$expected['class'] = '\Drupal\Core\Config\Schema\Property'; $expected['class'] = '\Drupal\Core\Config\Schema\Undefined';
$this->assertEqual($definition, $expected, 'Retrieved the right metadata for nonexistent configuration.'); $this->assertEqual($definition, $expected, 'Retrieved the right metadata for nonexistent configuration.');
// Configuration file without schema will return Unknown as well. // Configuration file without schema will return Undefined as well.
$this->assertIdentical(FALSE, \Drupal::service('config.typed')->hasConfigSchema('config_schema_test.noschema')); $this->assertIdentical(FALSE, \Drupal::service('config.typed')->hasConfigSchema('config_schema_test.noschema'));
$definition = \Drupal::service('config.typed')->getDefinition('config_schema_test.noschema'); $definition = \Drupal::service('config.typed')->getDefinition('config_schema_test.noschema');
$this->assertEqual($definition, $expected, 'Retrieved the right metadata for configuration with no schema.'); $this->assertEqual($definition, $expected, 'Retrieved the right metadata for configuration with no schema.');
@ -68,13 +70,13 @@ class ConfigSchemaTest extends DrupalUnitTestBase {
$definition = $config['testitem']->getDataDefinition(); $definition = $config['testitem']->getDataDefinition();
$expected = array(); $expected = array();
$expected['label'] = 'Test item'; $expected['label'] = 'Test item';
$expected['class'] = '\Drupal\Core\Config\Schema\Property'; $expected['class'] = '\Drupal\Core\Config\Schema\Undefined';
$expected['type'] = 'undefined'; $expected['type'] = 'undefined';
$this->assertEqual($definition, $expected, 'Automatic type detected for a scalar is undefined.'); $this->assertEqual($definition, $expected, 'Automatic type detected for a scalar is undefined.');
$definition = $config['testlist']->getDataDefinition(); $definition = $config['testlist']->getDataDefinition();
$expected = array(); $expected = array();
$expected['label'] = 'Test list'; $expected['label'] = 'Test list';
$expected['class'] = '\Drupal\Core\Config\Schema\Property'; $expected['class'] = '\Drupal\Core\Config\Schema\Undefined';
$expected['type'] = 'undefined'; $expected['type'] = 'undefined';
$this->assertEqual($definition, $expected, 'Automatic type detected for a list is undefined.'); $this->assertEqual($definition, $expected, 'Automatic type detected for a list is undefined.');
@ -93,6 +95,40 @@ class ConfigSchemaTest extends DrupalUnitTestBase {
); );
$this->assertEqual($definition, $expected, 'Retrieved the right metadata for system.maintenance'); $this->assertEqual($definition, $expected, 'Retrieved the right metadata for system.maintenance');
// Mixed schema with ignore elements.
$definition = \Drupal::service('config.typed')->getDefinition('config_schema_test.ignore');
$expected = array();
$expected['label'] = 'Ignore test';
$expected['class'] = '\Drupal\Core\Config\Schema\Mapping';
$expected['mapping']['label'] = array(
'label' => 'Label',
'type' => 'label',
);
$expected['mapping']['irrelevant'] = array(
'label' => 'Irrelevant',
'type' => 'ignore',
);
$expected['mapping']['indescribable'] = array(
'label' => 'Indescribable',
'type' => 'ignore',
);
$expected['mapping']['weight'] = array(
'label' => 'Weight',
'type' => 'integer',
);
$this->assertEqual($definition, $expected);
// The ignore elements themselves.
$definition = \Drupal::service('config.typed')->get('config_schema_test.ignore')->get('irrelevant')->getDataDefinition();
$expected = array();
$expected['type'] = 'ignore';
$expected['label'] = 'Irrelevant';
$expected['class'] = '\Drupal\Core\Config\Schema\Ignore';
$this->assertEqual($definition, $expected);
$definition = \Drupal::service('config.typed')->get('config_schema_test.ignore')->get('indescribable')->getDataDefinition();
$expected['label'] = 'Indescribable';
$this->assertEqual($definition, $expected);
// More complex case, generic type. Metadata for image style. // More complex case, generic type. Metadata for image style.
$definition = \Drupal::service('config.typed')->getDefinition('image.style.large'); $definition = \Drupal::service('config.typed')->getDefinition('image.style.large');
$expected = array(); $expected = array();
@ -138,7 +174,7 @@ class ConfigSchemaTest extends DrupalUnitTestBase {
$this->assertEqual($definition, $expected, 'Retrieved the right metadata for the first effect of image.style.medium'); $this->assertEqual($definition, $expected, 'Retrieved the right metadata for the first effect of image.style.medium');
// More complex, multiple filesystem marker test. // More complex, several level deep test.
$definition = \Drupal::service('config.typed')->getDefinition('config_schema_test.someschema.somemodule.section_one.subsection'); $definition = \Drupal::service('config.typed')->getDefinition('config_schema_test.someschema.somemodule.section_one.subsection');
// This should be the schema of config_schema_test.someschema.somemodule.*.*. // This should be the schema of config_schema_test.someschema.somemodule.*.*.
$expected = array(); $expected = array();
@ -254,7 +290,7 @@ class ConfigSchemaTest extends DrupalUnitTestBase {
'integer' => '100', 'integer' => '100',
'null_integer' => '', 'null_integer' => '',
'boolean' => 1, 'boolean' => 1,
// If the config schema doesn't have a type it should be casted to string. // If the config schema doesn't have a type it shouldn't be casted.
'no_type' => 1, 'no_type' => 1,
'mapping' => array( 'mapping' => array(
'string' => 1 'string' => 1
@ -277,7 +313,7 @@ class ConfigSchemaTest extends DrupalUnitTestBase {
'integer' => 100, 'integer' => 100,
'null_integer' => NULL, 'null_integer' => NULL,
'boolean' => TRUE, 'boolean' => TRUE,
'no_type' => '1', 'no_type' => 1,
'mapping' => array( 'mapping' => array(
'string' => '1' 'string' => '1'
), ),
@ -300,6 +336,14 @@ class ConfigSchemaTest extends DrupalUnitTestBase {
->setData($untyped_values) ->setData($untyped_values)
->save(); ->save();
$this->assertIdentical(\Drupal::config('config_schema_test.no_schema_data_types')->get(), $untyped_values); $this->assertIdentical(\Drupal::config('config_schema_test.no_schema_data_types')->get(), $untyped_values);
// Ensure that configuration objects with keys marked as ignored are not
// changed when saved. The 'config_schema_test.ignore' will have been saved
// during the installation of configuration in the setUp method.
$extension_path = drupal_get_path('module', 'config_schema_test');
$install_storage = new FileStorage($extension_path . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY);
$original_data = $install_storage->read('config_schema_test.ignore');
$this->assertIdentical(\Drupal::config('config_schema_test.ignore')->get(), $original_data);
} }
/** /**

View File

@ -8,7 +8,6 @@
namespace Drupal\config\Tests; namespace Drupal\config\Tests;
use Drupal\Core\Config\Schema\ArrayElement; use Drupal\Core\Config\Schema\ArrayElement;
use Drupal\Core\Config\Schema\Property;
use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\TypedData\Type\BooleanInterface; use Drupal\Core\TypedData\Type\BooleanInterface;
use Drupal\Core\TypedData\Type\StringInterface; use Drupal\Core\TypedData\Type\StringInterface;
@ -84,36 +83,34 @@ abstract class ConfigSchemaTestBase extends WebTestBase {
* Returns mixed value. * Returns mixed value.
*/ */
protected function checkValue($key, $value) { protected function checkValue($key, $value) {
$element = FALSE;
try { try {
$element = $this->schema->get($key); $element = $this->schema->get($key);
} }
catch (SchemaIncompleteException $e) { catch (SchemaIncompleteException $e) {
$this->fail("{$this->configName}:$key has no schema."); if (is_scalar($value) || $value === NULL) {
$this->fail("{$this->configName}:$key has no schema.");
}
} }
// Do not check value if it is defined to be ignored.
if ($element && $element instanceof Ignore) {
return $value;
}
if (is_scalar($value) || $value === NULL) { if (is_scalar($value) || $value === NULL) {
$success = FALSE; $success = FALSE;
$type = gettype($value); $type = gettype($value);
if ($element instanceof PrimitiveInterface) { if ($element instanceof PrimitiveInterface) {
if ($type == 'integer' && $element instanceof IntegerInterface) { $success =
$success = TRUE; ($type == 'integer' && $element instanceof IntegerInterface) ||
} ($type == 'double' && $element instanceof FloatInterface) ||
if ($type == 'double' && $element instanceof FloatInterface) { ($type == 'boolean' && $element instanceof BooleanInterface) ||
$success = TRUE; ($type == 'string' && $element instanceof StringInterface) ||
} // Null values are allowed for all types.
if ($type == 'boolean' && $element instanceof BooleanInterface) { ($value === NULL);
$success = TRUE;
}
if ($type == 'string' && ($element instanceof StringInterface || $element instanceof Property)) {
$success = TRUE;
}
// Null values are allowed for all scalar types.
if ($value === NULL) {
$success = TRUE;
}
} }
$class = get_class($element);
if (!$success) { if (!$success) {
$class = get_class($element);
$this->fail("{$this->configName}:$key has the wrong schema. Variable type is $type and schema class is $class."); $this->fail("{$this->configName}:$key has the wrong schema. Variable type is $type and schema class is $class.");
} }
} }

View File

@ -0,0 +1,9 @@
label: 'Label string'
irrelevant: 123
indescribable:
abc: 789
def:
- 456
- 'abc'
xyz: 13.4
weight: 27

View File

@ -132,3 +132,20 @@ config_schema_test.schema_in_install:
config_schema_test_integer: config_schema_test_integer:
type: integer type: integer
label: 'Config test integer' label: 'Config test integer'
config_schema_test.ignore:
type: mapping
label: 'Ignore test'
mapping:
label:
type: label
label: 'Label'
irrelevant:
type: ignore
label: 'Irrelevant'
indescribable:
type: ignore
label: 'Indescribable'
weight:
type: integer
label: 'Weight'