Verified Commit 5aa9704c authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3364109 by Wim Leers, effulgentsia, lauriii, phenaproxima, borisson_,...

Issue #3364109 by Wim Leers, effulgentsia, lauriii, phenaproxima, borisson_, bircher, alexpott: Configuration schema & required values: add test coverage for `nullable: true` validation support
parent 502c857b
Loading
Loading
Loading
Loading
Loading
+30 −0
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@
use Drupal\Core\Config\Schema\Undefined;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\TypedData\TypedDataManager;
use Drupal\Core\Validation\Plugin\Validation\Constraint\FullyValidatableConstraint;

/**
 * Manages config schema type plugins.
@@ -120,6 +121,35 @@ public function buildDataDefinition(array $definition, $value, $name = NULL, $pa
        $data_definition[$key] = $value;
      }
    }

    // All values are optional by default (meaning they can be NULL), except for
    // mappings and sequences. A sequence can only be NULL when `nullable: true`
    // is set on the config schema type definition. This is unintuitive and
    // contradicts Drupal core's documentation.
    // @see https://www.drupal.org/node/2264179
    // @see https://www.drupal.org/node/1978714
    // 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 property
    // cannot be NULL unless it specifies `nullable: true`, by adding
    // `FullyValidatable` as a top-level validation constraint.
    // @see https://www.drupal.org/node/3364108
    // @see https://www.drupal.org/node/3364109
    // @see \Drupal\Core\TypedData\TypedDataManager::getDefaultConstraints()
    if ($parent) {
      $root_type_has_opted_in = FALSE;
      foreach ($parent->getRoot()->getConstraints() as $constraint) {
        if ($constraint 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);
      }
    }

    return $data_definition;
  }

+15 −0
Original line number Diff line number Diff line
<?php

declare(strict_types = 1);

namespace Drupal\Core\Validation\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;

/**
 * @Constraint(
 *   id = "FullyValidatable",
 *   label = @Translation("Whether this config schema type is fully validatable", context = "Validation"),
 * )
 */
final class FullyValidatableConstraint extends Constraint {}
+24 −0
Original line number Diff line number Diff line
<?php

declare(strict_types = 1);

namespace Drupal\Core\Validation\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

/**
 * FullyValidatable constraint.
 *
 * @internal
 */
final class FullyValidatableConstraintValidator extends ConstraintValidator {

  /**
   * {@inheritdoc}
   */
  public function validate(mixed $value, Constraint $constraint) {
    // No-op.
  }

}
+6 −1
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@

namespace Drupal\Core\Validation\Plugin\Validation\Constraint;

use Drupal\Core\Config\Schema\ArrayElement;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\ListInterface;
use Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait;
@@ -25,7 +26,11 @@ class NotNullConstraintValidator extends NotNullValidator {
   */
  public function validate($value, Constraint $constraint) {
    $typed_data = $this->getTypedData();
    if (($typed_data instanceof ListInterface || $typed_data instanceof ComplexDataInterface) && $typed_data->isEmpty()) {
    // TRICKY: the Mapping and Sequence data types both extend ArrayElement
    // (which implements ComplexDataInterface), but configuration schema sees a
    // substantial difference between an empty sequence/mapping and NULL. So we
    // want to make sure we don't treat an empty array as NULL.
    if (($typed_data instanceof ListInterface || $typed_data instanceof ComplexDataInterface) && !$typed_data instanceof ArrayElement && $typed_data->isEmpty()) {
      $value = NULL;
    }
    parent::validate($value, $constraint);
+6 −0
Original line number Diff line number Diff line
@@ -20,6 +20,12 @@ public function validate(mixed $value, Constraint $constraint) {
    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');
    }

Loading