Issue #3364108 by Wim Leers, borisson_, phenaproxima, bircher: Configuration schema & required keys
parent
b7f45944ad
commit
7b49d8700c
|
@ -51,7 +51,8 @@ mapping:
|
|||
definition_class: '\Drupal\Core\TypedData\MapDataDefinition'
|
||||
mapping: {}
|
||||
constraints:
|
||||
# By default, only allow the explicitly listed mapping keys.
|
||||
# By default, allow the explicitly listed mapping keys, and require their
|
||||
# presence unless `requiredKey: false` is specified.
|
||||
ValidKeys: '<infer>'
|
||||
sequence:
|
||||
label: Sequence
|
||||
|
|
|
@ -83,6 +83,9 @@ field_formatter:
|
|||
type: field.formatter.settings.[%parent.type]
|
||||
label: 'Settings'
|
||||
third_party_settings:
|
||||
# Third party settings are always optional: they're an optional extension
|
||||
# point.
|
||||
requiredKey: false
|
||||
type: sequence
|
||||
label: 'Third party settings'
|
||||
sequence:
|
||||
|
@ -138,6 +141,9 @@ core.entity_form_display.*.*.*:
|
|||
type: field.widget.settings.[%parent.type]
|
||||
label: 'Settings'
|
||||
third_party_settings:
|
||||
# Third party settings are always optional: they're an optional extension
|
||||
# point.
|
||||
requiredKey: false
|
||||
type: sequence
|
||||
label: 'Third party settings'
|
||||
sequence:
|
||||
|
|
|
@ -29,7 +29,15 @@ class SequenceDataDefinition extends ListDataDefinition {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDataType() {
|
||||
return 'sequence';
|
||||
// TRICKY: this class extends ListDataDefinition, which always returns a
|
||||
// hardcoded "list". But this is a typed data type used in config schemas,
|
||||
// and hence many subtypes of it exists. The actual concrete subtype must
|
||||
// always be returned.
|
||||
// This effectively means skipping the parent implementation and matching
|
||||
// the grandparent implementation.
|
||||
// @see \Drupal\Core\TypedData\ListDataDefinition::setDataType()
|
||||
// @see \Drupal\Core\TypedData\ListDataDefinition::getDataType()
|
||||
return $this->definition['type'];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,9 +7,12 @@ use Drupal\Component\Utility\NestedArray;
|
|||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Config\Schema\ConfigSchemaAlterException;
|
||||
use Drupal\Core\Config\Schema\ConfigSchemaDiscovery;
|
||||
use Drupal\Core\Config\Schema\SequenceDataDefinition;
|
||||
use Drupal\Core\DependencyInjection\ClassResolverInterface;
|
||||
use Drupal\Core\Config\Schema\Undefined;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\TypedData\MapDataDefinition;
|
||||
use Drupal\Core\TypedData\TraversableTypedDataInterface;
|
||||
use Drupal\Core\TypedData\TypedDataManager;
|
||||
use Drupal\Core\Validation\Plugin\Validation\Constraint\FullyValidatableConstraint;
|
||||
|
||||
|
@ -138,6 +141,7 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI
|
|||
// @see https://www.drupal.org/node/3364109
|
||||
// @see \Drupal\Core\TypedData\TypedDataManager::getDefaultConstraints()
|
||||
if ($parent) {
|
||||
$root_type = $parent->getRoot()->getDataDefinition()->getDataType();
|
||||
$root_type_has_opted_in = FALSE;
|
||||
foreach ($parent->getRoot()->getConstraints() as $constraint) {
|
||||
if ($constraint instanceof FullyValidatableConstraint) {
|
||||
|
@ -145,6 +149,37 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI
|
|||
break;
|
||||
}
|
||||
}
|
||||
// If this is a dynamically typed property path, then not only must the
|
||||
// (absolute) root type be considered, but also the (relative) static root
|
||||
// type: the resolved type.
|
||||
// For example, `block.block.*:settings` has a dynamic type defined:
|
||||
// `block.settings.[%parent.plugin]`, but `block.block.*:plugin` does not.
|
||||
// Consequently, the value at the `plugin` property path depends only on
|
||||
// the `block.block.*` config schema type and hence only that config
|
||||
// schema type must have the `FullyValidatable` constraint, because it
|
||||
// defines which value are required.
|
||||
// In contrast, the `block.block.*:settings` property path depends on
|
||||
// whichever dynamic type `block.settings.[%parent.plugin]` resolved to,
|
||||
// to be able to know which values are required. Therefore that resolved
|
||||
// type determines which values are required and whether it is fully
|
||||
// validatable.
|
||||
// So for example the `block.settings.system_branding_block` config schema
|
||||
// type would also need to have the `FullyValidatable` constraint to
|
||||
// consider its schema-defined keys to require values:
|
||||
// - use_site_logo
|
||||
// - use_site_name
|
||||
// - use_site_slogan
|
||||
$static_type_root = TypedConfigManager::getStaticTypeRoot($parent);
|
||||
$static_type_root_type = $static_type_root->getDataDefinition()->getDataType();
|
||||
if ($root_type !== $static_type_root_type) {
|
||||
$root_type_has_opted_in = FALSE;
|
||||
foreach ($static_type_root->getConstraints() as $c) {
|
||||
if ($c instanceof FullyValidatableConstraint) {
|
||||
$root_type_has_opted_in = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($root_type_has_opted_in) {
|
||||
$data_definition->setRequired(!isset($data_definition['nullable']) || $data_definition['nullable'] === FALSE);
|
||||
}
|
||||
|
@ -153,6 +188,52 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI
|
|||
return $data_definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the static type root for a config schema object.
|
||||
*
|
||||
* @param \Drupal\Core\TypedData\TraversableTypedDataInterface $object
|
||||
* A config schema object to get the static type root for.
|
||||
*
|
||||
* @return \Drupal\Core\TypedData\TraversableTypedDataInterface
|
||||
* The ancestral config schema object at which the static type root lies:
|
||||
* either the first ancestor with a dynamic type (for example:
|
||||
* `block.block.*:settings`, which has the `block.settings.[%parent.plugin]`
|
||||
* type) or the (absolute) root of the config object (in this example:
|
||||
* `block.block.*`).
|
||||
*/
|
||||
public static function getStaticTypeRoot(TraversableTypedDataInterface $object): TraversableTypedDataInterface {
|
||||
$root = $object->getRoot();
|
||||
$static_type_root = NULL;
|
||||
|
||||
while ($static_type_root === NULL && $object !== $root) {
|
||||
// Use the parent data definition to determine the type of this mapping
|
||||
// (including the dynamic placeholders). For example:
|
||||
// - `editor.settings.[%parent.editor]`
|
||||
// - `editor.image_upload_settings.[status]`.
|
||||
$parent_data_def = $object->getParent()->getDataDefinition();
|
||||
$original_mapping_type = match (TRUE) {
|
||||
$parent_data_def instanceof MapDataDefinition => $parent_data_def->toArray()['mapping'][$object->getName()]['type'],
|
||||
$parent_data_def instanceof SequenceDataDefinition => $parent_data_def->toArray()['sequence']['type'],
|
||||
default => throw new \LogicException('Invalid config schema detected.'),
|
||||
};
|
||||
|
||||
// If this mapping's type was dynamically defined, then this is the static
|
||||
// type root inside which all types are statically defined.
|
||||
if (str_contains($original_mapping_type, ']')) {
|
||||
$static_type_root = $object;
|
||||
break;
|
||||
}
|
||||
|
||||
$object = $object->getParent();
|
||||
}
|
||||
|
||||
// Either the discovered static type root is not the actual root, or no
|
||||
// static type root was found and it is the root config object.
|
||||
assert(($static_type_root !== NULL && $static_type_root !== $root) || ($static_type_root === NULL && $object->getParent() === NULL));
|
||||
|
||||
return $static_type_root ?? $root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the typed config type for a plugin ID.
|
||||
*
|
||||
|
|
|
@ -34,6 +34,20 @@ class ValidKeysConstraint extends Constraint {
|
|||
*/
|
||||
public string $dynamicInvalidKeyMessage = "'@key' is an unknown key because @dynamic_type_property_path is @dynamic_type_property_value (see config schema type @resolved_dynamic_type).";
|
||||
|
||||
/**
|
||||
* The error message if a key is missing.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $missingRequiredKeyMessage = "'@key' is a required key.";
|
||||
|
||||
/**
|
||||
* The error message if a dynamically required key is missing.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $dynamicMissingRequiredKeyMessage = "'@key' is a required key because @dynamic_type_property_path is @dynamic_type_property_value (see config schema type @resolved_dynamic_type).";
|
||||
|
||||
/**
|
||||
* The error message if the array being validated is a list.
|
||||
*
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
|
|||
|
||||
use Drupal\Core\Config\Schema\Mapping;
|
||||
use Drupal\Core\Config\Schema\SequenceDataDefinition;
|
||||
use Drupal\Core\Config\TypedConfigManager;
|
||||
use Drupal\Core\TypedData\MapDataDefinition;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
@ -89,6 +90,85 @@ class ValidKeysConstraintValidator extends ConstraintValidator {
|
|||
$this->context->addViolation($constraint->dynamicInvalidKeyMessage, ['@key' => $key] + self::getDynamicMessageParameters($mapping));
|
||||
}
|
||||
}
|
||||
|
||||
// All keys are optional by default (meaning they can be omitted). This is
|
||||
// unintuitive and contradicts Drupal core's documentation.
|
||||
// @see https://www.drupal.org/node/2264179
|
||||
// To gradually evolve configuration schemas in the Drupal ecosystem to be
|
||||
// validatable, this needs to be clarified in a non-disruptive way. Any
|
||||
// config schema type definition — that is, a top-level entry in a
|
||||
// *.schema.yml file — can opt into stricter behavior, whereby a key is
|
||||
// required unless it specifies `requiredKey: false`, by adding
|
||||
// `FullyValidatable` as a top-level validation constraint.
|
||||
// @see https://www.drupal.org/node/3364108
|
||||
// @see https://www.drupal.org/node/3364109
|
||||
$root_type = $this->context->getObject()->getRoot()->getDataDefinition()->getDataType();
|
||||
$root_type_has_opted_in = FALSE;
|
||||
foreach ($this->context->getObject()->getRoot()->getConstraints() as $c) {
|
||||
if ($c instanceof FullyValidatableConstraint) {
|
||||
$root_type_has_opted_in = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Return early: do not generate validation errors for keys that are
|
||||
// required.
|
||||
if (!$root_type_has_opted_in) {
|
||||
return;
|
||||
}
|
||||
// If this is a dynamically typed property path, then not only must the
|
||||
// (absolute) root type be considered, but also the (relative) static root
|
||||
// type: the resolved type.
|
||||
// For example, `block.block.*:settings` has a dynamic type defined:
|
||||
// `block.settings.[%parent.plugin]`, but `block.block.*:plugin` does not.
|
||||
// Consequently, the value at the `plugin` property path depends only on the
|
||||
// `block.block.*` config schema type and hence only that config schema type
|
||||
// must have the `FullyValidatable` constraint, because it defines which
|
||||
// keys are required.
|
||||
// In contrast, the `block.block.*:settings` property path depends on
|
||||
// whichever dynamic type `block.settings.[%parent.plugin]` resolved to, to
|
||||
// be able to know which keys are required. Therefore that resolved type
|
||||
// determines which keys are required and whether it is fully validatable.
|
||||
// So for example the `block.settings.system_branding_block` config schema
|
||||
// type would also need to have the `FullyValidatable` constraint to
|
||||
// consider its schema-defined keys to be required:
|
||||
// - use_site_logo
|
||||
// - use_site_name
|
||||
// - use_site_slogan
|
||||
$static_type_root = TypedConfigManager::getStaticTypeRoot($this->context->getObject());
|
||||
$static_type_root_type = $static_type_root->getDataDefinition()->getDataType();
|
||||
if ($root_type !== $static_type_root_type) {
|
||||
$root_type_has_opted_in = FALSE;
|
||||
foreach ($static_type_root->getConstraints() as $c) {
|
||||
if ($c instanceof FullyValidatableConstraint) {
|
||||
$root_type_has_opted_in = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Return early: do not generate validation errors for keys that are
|
||||
// required.
|
||||
if (!$root_type_has_opted_in) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$required_keys = array_intersect($mapping->getRequiredKeys(), $constraint->getAllowedKeys($this->context));
|
||||
|
||||
// Statically required: same principle as for "statically valid" above, but
|
||||
// this time restricted to the subset of statically valid keys that do not
|
||||
// have `requiredKey: false`.
|
||||
$statically_required_keys = array_diff($required_keys, $all_dynamically_valid_keys);
|
||||
$missing_keys = array_diff($statically_required_keys, array_keys($value));
|
||||
foreach ($missing_keys as $key) {
|
||||
$this->context->addViolation($constraint->missingRequiredKeyMessage, ['@key' => $key]);
|
||||
}
|
||||
// Dynamically required: same principle as for "dynamically valid" above,
|
||||
// but this time restricted to the subset of dynamically valid keys that do
|
||||
// not have `requiredKey: false`.
|
||||
$dynamically_required_keys = array_intersect($required_keys, $all_dynamically_valid_keys);
|
||||
$missing_dynamically_required_keys = array_diff($dynamically_required_keys, array_keys($value));
|
||||
foreach ($missing_dynamically_required_keys as $key) {
|
||||
$this->context->addViolation($constraint->dynamicMissingRequiredKeyMessage, ['@key' => $key] + self::getDynamicMessageParameters($mapping));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,4 +20,20 @@ function config_schema_test_config_schema_info_alter(&$definitions) {
|
|||
if (isset($definitions['config_schema_test.hook'])) {
|
||||
$definitions['config_schema_test.hook']['additional_metadata'] = 'new schema info';
|
||||
}
|
||||
|
||||
// @see \Drupal\KernelTests\Core\TypedData\ValidKeysConstraintValidatorTest
|
||||
if (\Drupal::state()->get('config_schema_test_block_fully_validatable')) {
|
||||
$definitions['block.block.*']['constraints']['FullyValidatable'] = NULL;
|
||||
}
|
||||
else {
|
||||
unset($definitions['block.block.*']['constraints']);
|
||||
}
|
||||
|
||||
// @see \Drupal\Tests\node\Kernel\NodeTypeValidationTest::testThirdPartySettingsMenuUi()
|
||||
if (\Drupal::state()->get('config_schema_test_menu_ui_third_party_settings_fully_validatable')) {
|
||||
$definitions['node.type.*.third_party.menu_ui']['constraints']['FullyValidatable'] = NULL;
|
||||
}
|
||||
else {
|
||||
unset($definitions['node.type.*.third_party.menu_ui']['constraints']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,3 +23,7 @@ nullable_int: null
|
|||
nullable_octal: ~
|
||||
nullable_string: null
|
||||
nullable_string_int: ~
|
||||
# To test required vs optional keys.
|
||||
mapping_with_only_required_keys: {}
|
||||
mapping_with_some_required_keys: {}
|
||||
mapping_with_only_optional_keys: {}
|
||||
|
|
|
@ -90,6 +90,13 @@ config_test.types:
|
|||
type: integer
|
||||
label: 'Integer'
|
||||
octal:
|
||||
# Symfony 5.1's YAML parser issues a deprecation when reading octal with a
|
||||
# leading zero, to comply with YAML 1.2. However PECL YAML is still YAML 1.1
|
||||
# compliant.
|
||||
# @see core/modules/config/tests/config_test/config/install/config_test.types.yml
|
||||
# @see \Drupal\KernelTests\Core\Config\ConfigCRUDTest::testDataTypes()
|
||||
# @todo Mark as required again in https://www.drupal.org/project/drupal/issues/3205480
|
||||
requiredKey: false
|
||||
type: integer
|
||||
label: 'Octal'
|
||||
string:
|
||||
|
@ -142,8 +149,32 @@ config_test.types:
|
|||
type: string
|
||||
label: 'Nullable string integer'
|
||||
nullable: true
|
||||
# To test required vs optional keys.
|
||||
mapping_with_only_required_keys:
|
||||
type: mapping
|
||||
label: 'Mapping with only required keys'
|
||||
mapping:
|
||||
north: { type: string }
|
||||
east: { type: string }
|
||||
south: { type: string }
|
||||
west: { type: string }
|
||||
mapping_with_some_required_keys:
|
||||
type: mapping
|
||||
label: 'Mapping with only required keys'
|
||||
mapping:
|
||||
north: { type: string }
|
||||
east: { type: string, requiredKey: false }
|
||||
south: { type: string }
|
||||
west: { type: string, requiredKey: false }
|
||||
mapping_with_only_optional_keys:
|
||||
type: mapping
|
||||
label: 'Mapping with only optional keys'
|
||||
mapping:
|
||||
north: { type: string, requiredKey: false }
|
||||
east: { type: string, requiredKey: false }
|
||||
south: { type: string, requiredKey: false }
|
||||
west: { type: string, requiredKey: false }
|
||||
|
||||
# cspell:ignore validatable
|
||||
config_test.types.fully_validatable:
|
||||
type: config_test.types
|
||||
constraints:
|
||||
|
|
|
@ -92,4 +92,17 @@ class EditorValidationTest extends ConfigEntityValidationTestBase {
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testRequiredPropertyKeysMissing(?array $additional_expected_validation_errors_when_missing = NULL): void {
|
||||
parent::testRequiredPropertyKeysMissing([
|
||||
'dependencies' => [
|
||||
// @see ::testInvalidDependencies()
|
||||
// @see \Drupal\Core\Config\Plugin\Validation\Constraint\RequiredConfigDependenciesConstraintValidator
|
||||
'' => 'This text editor requires a text format.',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -98,13 +98,27 @@ class FieldConfigValidationTest extends FieldStorageConfigValidationTest {
|
|||
parent::testImmutableProperties($valid_values);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testRequiredPropertyKeysMissing(?array $additional_expected_validation_errors_when_missing = NULL): void {
|
||||
parent::testRequiredPropertyKeysMissing([
|
||||
'dependencies' => [
|
||||
// @see ::testInvalidDependencies()
|
||||
// @see \Drupal\Core\Config\Plugin\Validation\Constraint\RequiredConfigDependenciesConstraintValidator
|
||||
'' => 'This field requires a field storage.',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testRequiredPropertyValuesMissing(?array $additional_expected_validation_errors_when_missing = NULL): void {
|
||||
parent::testRequiredPropertyValuesMissing([
|
||||
'dependencies' => [
|
||||
// @see testInvalidDependencies()
|
||||
// @see ::testInvalidDependencies()
|
||||
// @see \Drupal\Core\Config\Plugin\Validation\Constraint\RequiredConfigDependenciesConstraintValidator
|
||||
'' => 'This field requires a field storage.',
|
||||
],
|
||||
]);
|
||||
|
|
|
@ -76,4 +76,25 @@ class NodeTypeValidationTest extends ConfigEntityValidationTestBase {
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @testWith [true, {"third_party_settings.menu_ui": "'parent' is a required key."}]
|
||||
* [false, {}]
|
||||
*/
|
||||
public function testThirdPartySettingsMenuUi(bool $third_party_settings_menu_ui_fully_validatable, array $expected_validation_errors): void {
|
||||
$this->enableModules(['menu_ui']);
|
||||
|
||||
// Set or unset the `FullyValidatable` constraint on
|
||||
// `node.type.*.third_party.menu_ui`.
|
||||
$this->enableModules(['config_schema_test']);
|
||||
\Drupal::state()->set('config_schema_test_menu_ui_third_party_settings_fully_validatable', $third_party_settings_menu_ui_fully_validatable);
|
||||
$this->container->get('kernel')->rebuildContainer();
|
||||
$this->entity = $this->createContentType();
|
||||
|
||||
// @see system.menu.main.yml
|
||||
$this->installConfig(['system']);
|
||||
$this->entity->setThirdPartySetting('menu_ui', 'available_menus', ['main']);
|
||||
|
||||
$this->assertValidationErrors($expected_validation_errors);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -373,6 +373,8 @@ system.advisories:
|
|||
block.settings.system_branding_block:
|
||||
type: block_settings
|
||||
label: 'Branding block'
|
||||
constraints:
|
||||
FullyValidatable: ~
|
||||
mapping:
|
||||
use_site_logo:
|
||||
type: boolean
|
||||
|
@ -401,6 +403,8 @@ block.settings.system_menu_block:*:
|
|||
block.settings.local_tasks_block:
|
||||
type: block_settings
|
||||
label: 'Tabs block'
|
||||
constraints:
|
||||
FullyValidatable: ~
|
||||
mapping:
|
||||
primary:
|
||||
type: boolean
|
||||
|
|
|
@ -133,7 +133,13 @@ class MappingTest extends KernelTestBase {
|
|||
case 'editor.editor.funky':
|
||||
$this->enableModules(['filter', 'editor', 'ckeditor5']);
|
||||
FilterFormat::create(['format' => 'funky', 'name' => 'Funky'])->save();
|
||||
Editor::create(['format' => 'funky', 'editor' => 'ckeditor5'])->save();
|
||||
Editor::create([
|
||||
'format' => 'funky',
|
||||
'editor' => 'ckeditor5',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
])->save();
|
||||
break;
|
||||
|
||||
case 'field.field.node.forum.comment_forum':
|
||||
|
|
|
@ -294,6 +294,9 @@ class ConfigCRUDTest extends KernelTestBase {
|
|||
'nullable_octal' => NULL,
|
||||
'nullable_string' => NULL,
|
||||
'nullable_string_int' => NULL,
|
||||
'mapping_with_only_required_keys' => [],
|
||||
'mapping_with_some_required_keys' => [],
|
||||
'mapping_with_only_optional_keys' => [],
|
||||
];
|
||||
$data = ['_core' => ['default_config_hash' => Crypt::hashBase64(serialize($data))]] + $data;
|
||||
$this->assertSame($data, $config->get());
|
||||
|
|
|
@ -4,11 +4,14 @@ namespace Drupal\KernelTests\Core\Config;
|
|||
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
use Drupal\Core\Config\Schema\Mapping;
|
||||
use Drupal\Core\Config\TypedConfigManagerInterface;
|
||||
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
|
||||
use Drupal\Core\Entity\Plugin\DataType\ConfigEntityAdapter;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Language\LanguageManager;
|
||||
use Drupal\Core\TypedData\Plugin\DataType\LanguageReference;
|
||||
use Drupal\Core\TypedData\TypedDataInterface;
|
||||
use Drupal\Core\Validation\Plugin\Validation\Constraint\FullyValidatableConstraint;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
@ -47,6 +50,22 @@ abstract class ConfigEntityValidationTestBase extends KernelTestBase {
|
|||
*/
|
||||
protected bool $hasLabel = TRUE;
|
||||
|
||||
/**
|
||||
* The config entity mapping properties with >=1 required keys.
|
||||
*
|
||||
* All top-level properties of a config entity are guaranteed to be defined
|
||||
* (since they are defined as properties on the corresponding PHP class). That
|
||||
* is why they can never trigger "required key" validation errors. Only for
|
||||
* non-top-level properties can such validation errors be triggered, and hence
|
||||
* that is only possible on top-level properties of `type: mapping`.
|
||||
*
|
||||
* @var string[]
|
||||
* @see \Drupal\Core\Config\Entity\ConfigEntityType::getPropertiesToExport()
|
||||
* @see ::testRequiredPropertyKeysMissing()
|
||||
* @see \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraintValidator
|
||||
*/
|
||||
protected static array $propertiesWithRequiredKeys = [];
|
||||
|
||||
/**
|
||||
* The config entity properties whose values are optional (set to NULL).
|
||||
*
|
||||
|
@ -461,6 +480,51 @@ abstract class ConfigEntityValidationTestBase extends KernelTestBase {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A property that is required must have a value (i.e. not NULL).
|
||||
*
|
||||
* @param string[]|null $additional_expected_validation_errors_when_missing
|
||||
* Some required config entity properties have additional validation
|
||||
* constraints that cause additional messages to appear. Keys must be
|
||||
* config entity properties, values must be arrays as expected by
|
||||
* ::assertValidationErrors().
|
||||
*
|
||||
* @todo Remove this optional parameter in https://www.drupal.org/project/drupal/issues/2820364#comment-15333069
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testRequiredPropertyKeysMissing(?array $additional_expected_validation_errors_when_missing = NULL): void {
|
||||
$config_entity_properties = array_keys($this->entity->getEntityType()->getPropertiesToExport());
|
||||
|
||||
if (!empty(array_diff(array_keys($additional_expected_validation_errors_when_missing ?? []), $config_entity_properties))) {
|
||||
throw new \LogicException(sprintf('The test %s lists `%s` in $additional_expected_validation_errors_when_missing but it is not a property of the `%s` config entity type.',
|
||||
get_called_class(),
|
||||
implode(', ', array_diff(array_keys($additional_expected_validation_errors_when_missing), $config_entity_properties)),
|
||||
$this->entity->getEntityTypeId(),
|
||||
));
|
||||
}
|
||||
|
||||
$mapping_properties = array_keys(array_filter(
|
||||
ConfigEntityAdapter::createFromEntity($this->entity)->getProperties(FALSE),
|
||||
fn (TypedDataInterface $v) => $v instanceof Mapping
|
||||
));
|
||||
|
||||
$required_property_keys = $this->getRequiredPropertyKeys();
|
||||
if (!$this->isFullyValidatable()) {
|
||||
$this->assertEmpty($required_property_keys, 'No keys can be required when a config entity type is not fully validatable.');
|
||||
}
|
||||
|
||||
$original_entity = clone $this->entity;
|
||||
foreach ($mapping_properties as $property) {
|
||||
$this->entity = clone $original_entity;
|
||||
$this->entity->set($property, []);
|
||||
$expected_validation_errors = array_key_exists($property, $required_property_keys)
|
||||
? [$property => $required_property_keys[$property]]
|
||||
: [];
|
||||
$this->assertValidationErrors(($additional_expected_validation_errors_when_missing[$property] ?? []) + $expected_validation_errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A property that is required must have a value (i.e. not NULL).
|
||||
*
|
||||
|
@ -560,6 +624,54 @@ abstract class ConfigEntityValidationTestBase extends KernelTestBase {
|
|||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the config entity mapping properties with required keys.
|
||||
*
|
||||
* This refers only to the top-level properties of the config entity which are expected to be mappings, and of those mappings, only the ones which have required keys.
|
||||
*
|
||||
* @return string[]
|
||||
* An array of key-value pairs, with:
|
||||
* - keys: names of the config entity properties which are mappings that
|
||||
* contain required keys.
|
||||
* - values: the corresponding expected validation error message.
|
||||
*/
|
||||
protected function getRequiredPropertyKeys(): array {
|
||||
// If a config entity type is not fully validatable, no mapping property
|
||||
// keys are required.
|
||||
if (!$this->isFullyValidatable()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$config_entity_properties = array_keys($this->entity->getEntityType()
|
||||
->getPropertiesToExport());
|
||||
|
||||
// Otherwise, all mapping property keys are required except for those marked
|
||||
// optional. Rather than inspecting config schema, require authors of tests
|
||||
// to explicitly list optional properties in a `propertiesWithRequiredKeys`
|
||||
// property on this class.
|
||||
// @see \Drupal\KernelTests\Config\Schema\MappingTest::testMappingInterpretation()
|
||||
$class = static::class;
|
||||
$properties_with_required_keys = [];
|
||||
while ($class) {
|
||||
if (property_exists($class, 'propertiesWithRequiredKeys')) {
|
||||
$properties_with_required_keys += $class::$propertiesWithRequiredKeys;
|
||||
}
|
||||
$class = get_parent_class($class);
|
||||
}
|
||||
|
||||
// Guide developers when $propertiesWithRequiredKeys does not contain
|
||||
// sensible values.
|
||||
if (!empty(array_diff(array_keys($properties_with_required_keys), $config_entity_properties))) {
|
||||
throw new \LogicException(sprintf('The %s test class lists %s in $propertiesWithRequiredKeys but it is not a property of the %s config entity type.',
|
||||
get_called_class(),
|
||||
implode(', ', array_diff(array_keys($properties_with_required_keys), $config_entity_properties)),
|
||||
$this->entity->getEntityTypeId()
|
||||
));
|
||||
}
|
||||
|
||||
return $properties_with_required_keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the config entity properties with optional values.
|
||||
*
|
||||
|
|
|
@ -42,7 +42,7 @@ class SchemaCheckTraitTest extends KernelTestBase {
|
|||
*
|
||||
* @dataProvider providerCheckConfigSchema
|
||||
*/
|
||||
public function testCheckConfigSchema(string $type_to_validate_against, bool $validate_constraints, array|bool $nulled_expectations, array $expectations) {
|
||||
public function testCheckConfigSchema(string $type_to_validate_against, bool $validate_constraints, array|bool $nulled_expectations, array|bool $no_data_expectations, array $expectations) {
|
||||
// Test a non existing schema.
|
||||
$ret = $this->checkConfigSchema($this->typedConfig, 'config_schema_test.no_schema', $this->config('config_schema_test.no_schema')->get());
|
||||
$this->assertFalse($ret);
|
||||
|
@ -64,6 +64,12 @@ class SchemaCheckTraitTest extends KernelTestBase {
|
|||
|
||||
$ret = $this->checkConfigSchema($this->typedConfig, $type_to_validate_against, $config_data, $validate_constraints);
|
||||
$this->assertEquals($expectations, $ret);
|
||||
|
||||
// Omit all data, this should trigger validation errors for required keys
|
||||
// missing.
|
||||
$config_data = [];
|
||||
$ret = $this->checkConfigSchema($this->typedConfig, $type_to_validate_against, $config_data, $validate_constraints);
|
||||
$this->assertEquals($no_data_expectations, $ret);
|
||||
}
|
||||
|
||||
public function providerCheckConfigSchema(): array {
|
||||
|
@ -75,6 +81,9 @@ class SchemaCheckTraitTest extends KernelTestBase {
|
|||
// @see \Drupal\Core\Config\ConfigInstaller::createConfiguration()
|
||||
'config_test.types:_core' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
|
||||
'config_test.types:array' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Sequence',
|
||||
'config_test.types:mapping_with_only_required_keys' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
|
||||
'config_test.types:mapping_with_some_required_keys' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
|
||||
'config_test.types:mapping_with_only_optional_keys' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
|
||||
];
|
||||
$expected_storage_type_check_errors = [
|
||||
'config_test.types:new_key' => 'missing schema',
|
||||
|
@ -93,19 +102,24 @@ class SchemaCheckTraitTest extends KernelTestBase {
|
|||
'config_test.types',
|
||||
FALSE,
|
||||
$expected_storage_null_check_errors,
|
||||
TRUE,
|
||||
$expected_storage_type_check_errors,
|
||||
],
|
||||
'config_test.types, with validation' => [
|
||||
'config_test.types',
|
||||
TRUE,
|
||||
$expected_storage_null_check_errors,
|
||||
TRUE,
|
||||
$expected_storage_type_check_errors + $expected_validation_errors,
|
||||
],
|
||||
];
|
||||
|
||||
// Test that if the exact same schema is reused but now has the constraint
|
||||
// "FullyValidatable" specified at the top level, that `NULL` values are now
|
||||
// trigger validation errors, except when `nullable: true` is set.
|
||||
// "FullyValidatable" specified at the top level, that:
|
||||
// 1. `NULL` values now trigger validation errors, except when
|
||||
// `nullable: true` is set.
|
||||
// 2. missing required keys now trigger validation errors, except when
|
||||
// `requiredKey: false` is set.
|
||||
// @see `type: config_test.types.fully_validatable`
|
||||
// @see core/modules/config/tests/config_test/config/schema/config_test.schema.yml
|
||||
$expected_storage_null_check_errors = [
|
||||
|
@ -114,6 +128,9 @@ class SchemaCheckTraitTest extends KernelTestBase {
|
|||
// @see \Drupal\Core\Config\ConfigInstaller::createConfiguration()
|
||||
'config_test.types.fully_validatable:_core' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
|
||||
'config_test.types.fully_validatable:array' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Sequence',
|
||||
'config_test.types.fully_validatable:mapping_with_only_required_keys' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
|
||||
'config_test.types.fully_validatable:mapping_with_some_required_keys' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
|
||||
'config_test.types.fully_validatable:mapping_with_only_optional_keys' => 'variable type is NULL but applied schema class is Drupal\Core\Config\Schema\Mapping',
|
||||
];
|
||||
$expected_storage_type_check_errors = [
|
||||
'config_test.types.fully_validatable:new_key' => 'missing schema',
|
||||
|
@ -125,6 +142,7 @@ class SchemaCheckTraitTest extends KernelTestBase {
|
|||
'config_test.types.fully_validatable',
|
||||
FALSE,
|
||||
$expected_storage_null_check_errors,
|
||||
TRUE,
|
||||
$expected_storage_type_check_errors,
|
||||
],
|
||||
'config_test.types.fully_validatable, with validation' => [
|
||||
|
@ -141,8 +159,44 @@ class SchemaCheckTraitTest extends KernelTestBase {
|
|||
'[int] This value should not be null.',
|
||||
'[string] This value should not be null.',
|
||||
'[string_int] This value should not be null.',
|
||||
'[mapping_with_only_required_keys] This value should not be null.',
|
||||
'[mapping_with_some_required_keys] This value should not be null.',
|
||||
'[mapping_with_only_optional_keys] This value should not be null.',
|
||||
],
|
||||
[
|
||||
"[] 'array' is a required key.",
|
||||
"[] 'boolean' is a required key.",
|
||||
"[] 'exp' is a required key.",
|
||||
"[] 'float' is a required key.",
|
||||
"[] 'float_as_integer' is a required key.",
|
||||
"[] 'hex' is a required key.",
|
||||
"[] 'int' is a required key.",
|
||||
"[] 'string' is a required key.",
|
||||
"[] 'string_int' is a required key.",
|
||||
"[] 'nullable_array' is a required key.",
|
||||
"[] 'nullable_boolean' is a required key.",
|
||||
"[] 'nullable_exp' is a required key.",
|
||||
"[] 'nullable_float' is a required key.",
|
||||
"[] 'nullable_float_as_integer' is a required key.",
|
||||
"[] 'nullable_hex' is a required key.",
|
||||
"[] 'nullable_int' is a required key.",
|
||||
"[] 'nullable_octal' is a required key.",
|
||||
"[] 'nullable_string' is a required key.",
|
||||
"[] 'nullable_string_int' is a required key.",
|
||||
"[] 'mapping_with_only_required_keys' is a required key.",
|
||||
"[] 'mapping_with_some_required_keys' is a required key.",
|
||||
"[] 'mapping_with_only_optional_keys' is a required key.",
|
||||
],
|
||||
$expected_storage_type_check_errors + $expected_validation_errors + [
|
||||
// For `mapping_with_only_required_keys`: errors for all 4 keys.
|
||||
3 => "[mapping_with_only_required_keys] 'north' is a required key.",
|
||||
4 => "[mapping_with_only_required_keys] 'east' is a required key.",
|
||||
5 => "[mapping_with_only_required_keys] 'south' is a required key.",
|
||||
6 => "[mapping_with_only_required_keys] 'west' is a required key.",
|
||||
// For `mapping_with_some_required_keys`: errors for 2 required keys.
|
||||
7 => "[mapping_with_some_required_keys] 'north' is a required key.",
|
||||
8 => "[mapping_with_some_required_keys] 'south' is a required key.",
|
||||
],
|
||||
$expected_storage_type_check_errors + $expected_validation_errors,
|
||||
],
|
||||
];
|
||||
|
||||
|
|
|
@ -33,8 +33,13 @@ class ValidKeysConstraintValidatorTest extends KernelTestBase {
|
|||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Install the Block module and create a Block config entity, so that we can
|
||||
// test that the validator infers keys from a defined schema.
|
||||
// test that the validator infers the required keys from a defined schema.
|
||||
$this->enableModules(['system', 'block']);
|
||||
// Also install the config_schema_test module, to enable testing with
|
||||
// config entities as the example in the test cases below, simulating both
|
||||
// possible schema states: fully validatable and not fully validatable.
|
||||
// @see \Drupal\KernelTests\Config\Schema\MappingTest::testMappingInterpretations()
|
||||
$this->enableModules(['config_schema_test']);
|
||||
/** @var \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer */
|
||||
$theme_installer = $this->container->get('theme_installer');
|
||||
$theme_installer->install(['stark']);
|
||||
|
@ -43,6 +48,20 @@ class ValidKeysConstraintValidatorTest extends KernelTestBase {
|
|||
'id' => 'branding',
|
||||
'plugin' => 'system_branding_block',
|
||||
'theme' => 'stark',
|
||||
'status' => TRUE,
|
||||
'weight' => 0,
|
||||
'provider' => 'system',
|
||||
'settings' => [
|
||||
'use_site_logo' => TRUE,
|
||||
'use_site_name' => TRUE,
|
||||
'use_site_slogan' => TRUE,
|
||||
'label_display' => FALSE,
|
||||
// TRICKY: these 4 are inherited from `type: block_settings`.
|
||||
'status' => TRUE,
|
||||
'info' => '',
|
||||
'view_mode' => 'full',
|
||||
'context_mapping' => [],
|
||||
],
|
||||
]);
|
||||
$block->save();
|
||||
|
||||
|
@ -100,6 +119,111 @@ class ValidKeysConstraintValidatorTest extends KernelTestBase {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests detecting missing required keys.
|
||||
*
|
||||
* @testWith [true, {"settings": "'label_display' is a required key."}]
|
||||
* [false, {}]
|
||||
*
|
||||
* @see \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraint::$missingRequiredKeyMessage
|
||||
*/
|
||||
public function testRequiredKeys(bool $block_is_fully_validatable, array $expected_validation_errors): void {
|
||||
// Set or unset the `FullyValidatable` constraint on `block.block.*`.
|
||||
\Drupal::state()->set('config_schema_test_block_fully_validatable', $block_is_fully_validatable);
|
||||
$this->container->get('kernel')->rebuildContainer();
|
||||
$this->config = $this->container->get('config.typed')
|
||||
->get('block.block.branding');
|
||||
|
||||
// Start from the valid config.
|
||||
$this->assertEmpty($this->config->validate());
|
||||
|
||||
// Then modify only one thing: remove the `label_display` setting.
|
||||
$data = $this->config->toArray();
|
||||
unset($data['settings']['label_display']);
|
||||
$this->config = $this->container->get('config.typed')
|
||||
->createFromNameAndData('block.block.branding', $data);
|
||||
|
||||
// Now 1 validation error should be triggered: one for the missing
|
||||
// (statically) required key. It is only required because all block plugins
|
||||
// are required to set it: see `type: block_settings`.
|
||||
// @see \Drupal\system\Plugin\Block\SystemBrandingBlock::defaultConfiguration()
|
||||
// @see \Drupal\system\Plugin\Block\SystemPoweredByBlock::defaultConfiguration()
|
||||
$this->assertValidationErrors('block.block.branding', $data, $expected_validation_errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests detecting missing dynamically required keys.
|
||||
*
|
||||
* @testWith [true, {"settings": "'use_site_name' is a required key because plugin is system_branding_block (see config schema type block.settings.system_branding_block)."}]
|
||||
* [false, {}]
|
||||
*
|
||||
* @see \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraint::$dynamicMissingRequiredKeyMessage
|
||||
*/
|
||||
public function testDynamicallyRequiredKeys(bool $block_is_fully_validatable, array $expected_validation_errors): void {
|
||||
// Set or unset the `FullyValidatable` constraint on `block.block.*`.
|
||||
\Drupal::state()->set('config_schema_test_block_fully_validatable', $block_is_fully_validatable);
|
||||
$this->container->get('kernel')->rebuildContainer();
|
||||
$this->config = $this->container->get('config.typed')
|
||||
->get('block.block.branding');
|
||||
|
||||
// Start from the valid config.
|
||||
$this->assertEmpty($this->config->validate());
|
||||
|
||||
// Then modify only one thing: remove the `use_site_name` setting.
|
||||
$data = $this->config->toArray();
|
||||
unset($data['settings']['use_site_name']);
|
||||
$this->config = $this->container->get('config.typed')
|
||||
->createFromNameAndData('block.block.branding', $data);
|
||||
|
||||
// Now 1 validation error should be triggered: one for the missing
|
||||
// required key. It is only dynamically required because not
|
||||
// all block plugins support this key in their configuration.
|
||||
// @see \Drupal\system\Plugin\Block\SystemBrandingBlock::defaultConfiguration()
|
||||
// @see \Drupal\system\Plugin\Block\SystemPoweredByBlock::defaultConfiguration()
|
||||
$this->assertValidationErrors('block.block.branding', $data, $expected_validation_errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests detecting both unknown and required keys.
|
||||
*
|
||||
* @testWith [true, ["'primary' is a required key because plugin is local_tasks_block (see config schema type block.settings.local_tasks_block).", "'secondary' is a required key because plugin is local_tasks_block (see config schema type block.settings.local_tasks_block)."]]
|
||||
* [false, []]
|
||||
*
|
||||
* @see \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraint::$dynamicInvalidKeyMessage
|
||||
* @see \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraint::$dynamicMissingRequiredKeyMessage
|
||||
*/
|
||||
public function testBothUnknownAndDynamicallyRequiredKeys(bool $block_is_fully_validatable, array $additional_expected_validation_errors): void {
|
||||
// Set or unset the `FullyValidatable` constraint on `block.block.*`.
|
||||
\Drupal::state()->set('config_schema_test_block_fully_validatable', $block_is_fully_validatable);
|
||||
$this->container->get('kernel')->rebuildContainer();
|
||||
$this->config = $this->container->get('config.typed')
|
||||
->get('block.block.branding');
|
||||
|
||||
// Start from the valid config.
|
||||
$this->assertEmpty($this->config->validate());
|
||||
|
||||
// Then modify only one thing: the block plugin that is being used.
|
||||
$data = $this->config->toArray();
|
||||
$data['plugin'] = 'local_tasks_block';
|
||||
$this->config = $this->container->get('config.typed')
|
||||
->createFromNameAndData('block.block.branding', $data);
|
||||
|
||||
// Now 3 validation errors should be triggered: one for each of the settings
|
||||
// that exist in the "branding" block but not the "powered by" block.
|
||||
// @see \Drupal\system\Plugin\Block\SystemBrandingBlock::defaultConfiguration()
|
||||
// @see \Drupal\system\Plugin\Block\SystemPoweredByBlock::defaultConfiguration()
|
||||
$this->assertValidationErrors('block.block.branding', $data,
|
||||
[
|
||||
'settings' => [
|
||||
"'use_site_logo' is an unknown key because plugin is local_tasks_block (see config schema type block.settings.local_tasks_block).",
|
||||
"'use_site_name' is an unknown key because plugin is local_tasks_block (see config schema type block.settings.local_tasks_block).",
|
||||
"'use_site_slogan' is an unknown key because plugin is local_tasks_block (see config schema type block.settings.local_tasks_block).",
|
||||
...$additional_expected_validation_errors,
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the ValidKeys constraint validator.
|
||||
*/
|
||||
|
@ -172,6 +296,24 @@ class ValidKeysConstraintValidatorTest extends KernelTestBase {
|
|||
unset($definition['mapping']['east']['requiredKey']);
|
||||
$violations = $typed_config->create(clone $definition, $value)->validate();
|
||||
$this->assertCount(0, $violations);
|
||||
|
||||
// If in the mapping definition some keys that do NOT have
|
||||
// `requiredKey: false` set, then they MUST be set.
|
||||
// First test without changing the value: no error should occur because all
|
||||
// keys passed to the ValidKeys constraint have a value.
|
||||
unset($definition['mapping']['south']['requiredKey']);
|
||||
unset($definition['mapping']['east']['requiredKey']);
|
||||
$violations = $typed_config->create(clone $definition, $value)->validate();
|
||||
$this->assertCount(0, $violations);
|
||||
// Then remove the required key-value pair: this must trigger an error, but
|
||||
// only if the root type has opted in.
|
||||
unset($value['south']);
|
||||
$violations = $typed_config->create(clone $definition, $value)->validate();
|
||||
$this->assertCount(0, $violations);
|
||||
$definition->addConstraint('FullyValidatable', NULL);
|
||||
$violations = $typed_config->create(clone $definition, $value)->validate();
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame("'south' is a required key.", (string) $violations->get(0)->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -203,6 +345,39 @@ class ValidKeysConstraintValidatorTest extends KernelTestBase {
|
|||
$config->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests ValidKeys constraint validator detecting optional keys.
|
||||
*/
|
||||
public function testMarkedAsOptional(): void {
|
||||
\Drupal::state()->set('config_schema_test_block_fully_validatable', TRUE);
|
||||
$this->container->get('kernel')->rebuildContainer();
|
||||
$this->config = $this->container->get('config.typed')
|
||||
->get('block.block.branding');
|
||||
|
||||
$violations = $this->config->validate();
|
||||
$this->assertCount(0, $violations);
|
||||
|
||||
// Reference to the mapping in the schema, to allow adjusting it for testing
|
||||
// purposes.
|
||||
assert($this->config->getDataDefinition() instanceof MapDataDefinition);
|
||||
$mapping = $this->config->getDataDefinition()['mapping'];
|
||||
|
||||
// Removing a key-value pair should trigger a validation error.
|
||||
$data = $this->config->getValue();
|
||||
unset($data['status']);
|
||||
$this->config->setValue($data);
|
||||
$violations = $this->config->validate();
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame("'status' is a required key.", (string) $violations->get(0)
|
||||
->getMessage());
|
||||
|
||||
// Unless a key is explicitly marked as optional.
|
||||
$mapping['status']['requiredKey'] = FALSE;
|
||||
$this->config->getDataDefinition()['mapping'] = $mapping;
|
||||
$violations = $this->config->validate();
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a set of validation errors is raised when the config is validated.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue