function ValidKeysConstraintValidator::validate

Same name and namespace in other branches
  1. 10 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraintValidator.php \Drupal\Core\Validation\Plugin\Validation\Constraint\ValidKeysConstraintValidator::validate()

File

core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ValidKeysConstraintValidator.php, line 23

Class

ValidKeysConstraintValidator
Validates the ValidKeys constraint.

Namespace

Drupal\Core\Validation\Plugin\Validation\Constraint

Code

public function validate(mixed $value, Constraint $constraint) : void {
    assert($constraint instanceof ValidKeysConstraint);
    if (!is_array($value)) {
        // If the value is NULL, then the `NotNull` constraint validator will
        // set the appropriate validation error message.
        // @see \Drupal\Core\Validation\Plugin\Validation\Constraint\NotNullConstraintValidator
        if ($value === NULL) {
            return;
        }
        throw new UnexpectedTypeException($value, 'array');
    }
    // Indexed arrays are invalid by definition. array_is_list() returns TRUE
    // for empty arrays, so only do this check if $value is not empty.
    if ($value && array_is_list($value)) {
        $this->context
            ->addViolation($constraint->indexedArrayMessage);
        return;
    }
    $mapping = $this->context
        ->getObject();
    assert($mapping instanceof Mapping);
    $resolved_type = $mapping->getDataDefinition()
        ->getDataType();
    $valid_keys = $constraint->getAllowedKeys($this->context);
    $dynamically_valid_keys = $mapping->getDynamicallyValidKeys();
    $all_dynamically_valid_keys = array_merge(...array_values($dynamically_valid_keys));
    // Statically valid: keys that are valid for all possible types matching the
    // type definition of this mapping.
    // For example, `block.block.*:settings` has the following statically valid
    // keys: id, label, label_display, provider, status, info, view_mode and
    // context_mapping.
    // @see \Drupal\KernelTests\Config\Schema\MappingTest::providerMappingInterpretation()
    $invalid_keys = array_diff(array_keys($value), $valid_keys, $all_dynamically_valid_keys);
    foreach ($invalid_keys as $key) {
        $this->context
            ->buildViolation($constraint->invalidKeyMessage)
            ->setParameter('@key', $key)
            ->atPath((string) $key)
            ->setInvalidValue($key)
            ->addViolation();
    }
    // Dynamically valid: keys that are valid not for all possible types, but
    // for the actually resolved type definition of this mapping (in addition to
    // the statically valid keys).
    // @see \Drupal\Core\Config\Schema\Mapping::getDynamicallyValidKeys()
    if (!empty($all_dynamically_valid_keys)) {
        // For example, `block.block.*:settings` has the following dynamically valid
        // keys when the block plugin is `system_branding_block`:
        // - use_site_logo
        // - use_site_name
        // - use_site_slogan
        // @see \Drupal\KernelTests\Config\Schema\MappingTest::providerMappingInterpretation()
        $resolved_type_dynamically_valid_keys = $dynamically_valid_keys[$resolved_type] ?? [];
        // But if the `local_tasks_block` plugin is being used, then the
        // dynamically valid keys are:
        // - primary
        // - secondary
        // And for the `block.settings.search_form_block` plugin the dynamically
        // valid keys are:
        // - page_id
        // To help determine which keys are dynamically invalid, gather all keys
        // except for those for the actual resolved type of this mapping.
        // @see \Drupal\Core\Config\Schema\Mapping::getPossibleTypes()
        $other_types_valid_keys = array_diff($all_dynamically_valid_keys, $resolved_type_dynamically_valid_keys);
        $dynamically_invalid_keys = array_intersect(array_keys($value), $other_types_valid_keys);
        foreach ($dynamically_invalid_keys as $key) {
            $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));
    }
}

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