trait SchemaCheckTrait

Same name in other branches
  1. 9 core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php \Drupal\Core\Config\Schema\SchemaCheckTrait
  2. 8.9.x core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php \Drupal\Core\Config\Schema\SchemaCheckTrait
  3. 10 core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php \Drupal\Core\Config\Schema\SchemaCheckTrait

Provides a trait for checking configuration schema.

Hierarchy

4 files declare their use of SchemaCheckTrait
CKEditor5PluginDefinition.php in core/modules/ckeditor5/src/Plugin/CKEditor5PluginDefinition.php
ConfigSchemaChecker.php in core/lib/Drupal/Core/Config/Development/ConfigSchemaChecker.php
SchemaCheckTestTrait.php in core/tests/Drupal/Tests/SchemaCheckTestTrait.php
SchemaCheckTraitTest.php in core/tests/Drupal/KernelTests/Core/Config/SchemaCheckTraitTest.php

File

core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php, line 20

Namespace

Drupal\Core\Config\Schema
View source
trait SchemaCheckTrait {
    
    /**
     * The config schema wrapper object for the configuration object under test.
     */
    protected TraversableTypedDataInterface $schema;
    
    /**
     * The configuration object name under test.
     */
    protected string $configName;
    
    /**
     * The ignored property paths.
     *
     * Allow ignoring specific config schema types (top-level keys, require an
     * exact match to one of the top-level entries in *.schema.yml files) by
     * allowing one or more partial property path matches.
     *
     * Keys must be an exact match for a Config object's schema type.
     * Values must be wildcard matches for property paths, where any property
     * path segment can use a wildcard (`*`) to indicate any value for that
     * segment should be accepted for this property path to be ignored.
     *
     * @var \string[][]
     */
    protected static array $ignoredPropertyPaths = [
        'search.page.*' => [
            // @todo Fix config or tweak schema of `type: search.page.*` in
            //   https://drupal.org/i/3380475.
            // @see search.schema.yml
'label' => [
                'This value should not be blank.',
            ],
        ],
        'contact.settings' => [
            // @todo Simple config cannot have dependencies on any other config.
            //   Remove this in https://www.drupal.org/project/drupal/issues/3425992.
'default_form' => [
                "The 'contact.form.feedback' config does not exist.",
            ],
        ],
        'editor.editor.*' => [
            // @todo Fix stream wrappers not being available early enough in
            //   https://www.drupal.org/project/drupal/issues/3416735
'image_upload.scheme' => [
                '^The file storage you selected is not a visible, readable and writable stream wrapper\\. Possible choices: <em class="placeholder"><\\/em>\\.$',
            ],
        ],
        'search.settings' => [
            // @todo Simple config cannot have dependencies on any other config.
            //   Remove this in https://www.drupal.org/project/drupal/issues/3425992.
'default_page' => [
                "The 'search.page.node_search' config does not exist.",
            ],
        ],
    ];
    
    /**
     * Checks the TypedConfigManager has a valid schema for the configuration.
     *
     * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
     *   The TypedConfigManager.
     * @param string $config_name
     *   The configuration name.
     * @param array $config_data
     *   The configuration data, assumed to be data for a top-level config object.
     * @param bool $validate_constraints
     *   Determines if constraints will be validated. If TRUE, constraint
     *   validation errors will be added to the errors found.
     *
     * @return array|bool
     *   FALSE if no schema found. List of errors if any found. TRUE if fully
     *   valid.
     */
    public function checkConfigSchema(TypedConfigManagerInterface $typed_config, $config_name, $config_data, bool $validate_constraints = FALSE) {
        $this->configName = $config_name;
        if (!$typed_config->hasConfigSchema($config_name)) {
            return FALSE;
        }
        $this->schema = $typed_config->createFromNameAndData($config_name, $config_data);
        $errors = [];
        foreach ($config_data as $key => $value) {
            $errors[] = $this->checkValue($key, $value);
        }
        $errors = array_merge(...$errors);
        if ($validate_constraints) {
            // Also perform explicit validation. Note this does NOT require every node
            // in the config schema tree to have validation constraints defined.
            $violations = $this->schema
                ->validate();
            $filtered_violations = array_filter(iterator_to_array($violations), fn(ConstraintViolation $v) => !static::isViolationForIgnoredPropertyPath($v));
            $validation_errors = array_map(fn(ConstraintViolation $v) => sprintf("[%s] %s", $v->getPropertyPath(), (string) $v->getMessage()), $filtered_violations);
            // @todo Decide in https://www.drupal.org/project/drupal/issues/3395099 when/how to trigger deprecation errors or even failures for contrib modules.
            $errors = array_merge($errors, $validation_errors);
        }
        if (empty($errors)) {
            return TRUE;
        }
        return $errors;
    }
    
    /**
     * Determines whether this violation is for an ignored Config property path.
     *
     * @param \Symfony\Component\Validator\ConstraintViolation $v
     *   A validation constraint violation for a Config object.
     *
     * @return bool
     */
    protected static function isViolationForIgnoredPropertyPath(ConstraintViolation $v) : bool {
        // When the validated object is a config entity wrapped in a
        // ConfigEntityAdapter, some work is necessary to map from e.g.
        // `entity:comment_type` to the corresponding `comment.type.*`.
        if ($v->getRoot() instanceof ConfigEntityAdapter) {
            $config_entity = $v->getRoot()
                ->getEntity();
            assert($config_entity instanceof ConfigEntityInterface);
            $config_entity_type = $config_entity->getEntityType();
            assert($config_entity_type instanceof ConfigEntityType);
            $config_prefix = $config_entity_type->getConfigPrefix();
            // Compute the data type of the config object being validated:
            // - the config entity type's config prefix
            // - with as many `.*`-suffixes appended as there are parts in the ID (for
            //   example, for NodeType there's only 1 part, for EntityViewDisplay
            //   there are 3 parts.)
            // TRICKY: in principle it is possible to compute the exact number of
            // suffixes by inspecting ConfigEntity::getConfigDependencyName(), except
            // when the entity ID itself is invalid. Unfortunately that means
            // gradually discovering it is the only available alternative.
            $suffix_count = 1;
            do {
                $config_object_data_type = $config_prefix . str_repeat('.*', $suffix_count);
                $suffix_count++;
            } while ($suffix_count <= 3 && !array_key_exists($config_object_data_type, static::$ignoredPropertyPaths));
        }
        else {
            $config_object_data_type = $v->getRoot()
                ->getDataDefinition()
                ->getDataType();
        }
        if (!array_key_exists($config_object_data_type, static::$ignoredPropertyPaths)) {
            return FALSE;
        }
        foreach (static::$ignoredPropertyPaths[$config_object_data_type] as $ignored_property_path_expression => $ignored_validation_constraint_messages) {
            // Convert the wildcard-based expression to a regex: treat `*` nor in the
            // regex sense nor as something to be escaped: treat it as the wildcard
            // for a segment in a property path (property path segments are separated
            // by periods).
            // That requires first ensuring that preg_quote() does not escape it, and
            // then replacing it with an appropriate regular expression: `[^\.]+`,
            // which means: ">=1 characters that are anything except a period".
            $ignored_property_path_regex = str_replace(' ', '[^\\.]+', preg_quote(str_replace('*', ' ', $ignored_property_path_expression)));
            // To ignore this violation constraint, require a match on both the
            // property path and the message.
            $property_path_match = preg_match('/^' . $ignored_property_path_regex . '$/', $v->getPropertyPath(), $matches) === 1;
            if ($property_path_match) {
                return preg_match(sprintf("/^(%s)\$/", implode('|', $ignored_validation_constraint_messages)), (string) $v->getMessage()) === 1;
            }
        }
        return FALSE;
    }
    
    /**
     * Helper method to check data type.
     *
     * @param string $key
     *   A string of configuration key.
     * @param mixed $value
     *   Value of given key.
     *
     * @return array
     *   List of errors found while checking with the corresponding schema.
     */
    protected function checkValue($key, $value) {
        $error_key = $this->configName . ':' . $key;
        
        /** @var \Drupal\Core\TypedData\TypedDataInterface $element */
        $element = $this->schema
            ->get($key);
        // Check if this type has been deprecated.
        $data_definition = $element->getDataDefinition();
        if (!empty($data_definition['deprecated'])) {
            @trigger_error($data_definition['deprecated'], E_USER_DEPRECATED);
        }
        if ($element instanceof Undefined) {
            return [
                $error_key => 'missing schema',
            ];
        }
        // Do not check value if it is defined to be ignored.
        if ($element && $element instanceof Ignore) {
            return [];
        }
        if ($element && is_scalar($value) || $value === NULL) {
            $success = FALSE;
            $type = gettype($value);
            if ($element instanceof PrimitiveInterface) {
                $success = $type == 'integer' && $element instanceof IntegerInterface || ($type == 'double' || $type == 'integer') && $element instanceof FloatInterface || $type == 'boolean' && $element instanceof BooleanInterface || $type == 'string' && $element instanceof StringInterface || $value === NULL;
            }
            elseif ($element instanceof ArrayElement && $element->isNullable() && $value === NULL) {
                $success = TRUE;
            }
            $class = get_class($element);
            if (!$success) {
                return [
                    $error_key => "variable type is {$type} but applied schema class is {$class}",
                ];
            }
        }
        else {
            $errors = [];
            if (!$element instanceof TraversableTypedDataInterface) {
                $errors[$error_key] = 'non-scalar value but not defined as an array (such as mapping or sequence)';
            }
            // Go on processing so we can get errors on all levels. Any non-scalar
            // value must be an array so cast to an array.
            if (!is_array($value)) {
                $value = (array) $value;
            }
            $nested_errors = [];
            // Recurse into any nested keys.
            foreach ($value as $nested_value_key => $nested_value) {
                $nested_errors[] = $this->checkValue($key . '.' . $nested_value_key, $nested_value);
            }
            return array_merge($errors, ...$nested_errors);
        }
        // No errors found.
        return [];
    }

}

Members

Title Sort descending Modifiers Object type Summary
SchemaCheckTrait::$configName protected property The configuration object name under test.
SchemaCheckTrait::$ignoredPropertyPaths protected static property The ignored property paths.
SchemaCheckTrait::$schema protected property The config schema wrapper object for the configuration object under test.
SchemaCheckTrait::checkConfigSchema public function Checks the TypedConfigManager has a valid schema for the configuration.
SchemaCheckTrait::checkValue protected function Helper method to check data type.
SchemaCheckTrait::isViolationForIgnoredPropertyPath protected static function Determines whether this violation is for an ignored Config property path.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.