From f08607f5cf3bb2650820b0d0127e046fcf6abbdb Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Tue, 29 Nov 2022 19:53:30 +0100
Subject: [PATCH 01/73] =?UTF-8?q?Analysis/the=20plan=20=F0=9F=A4=93?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 core/config/schema/core.data_types.schema.yml | 61 +++++++++++++++++++
 .../field/config/schema/field.schema.yml      | 51 ++++++++++++++++
 2 files changed, 112 insertions(+)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index d866c01b1ed1..9a6e6e13f54c 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -414,48 +414,109 @@ base_entity_reference_field_settings:
 field_config_base:
   type: config_entity
   mapping:
+    constraints:
+      FieldStorageExistsOnFieldableContentEntityType:
+        entityType: entity_type
+        fieldName: field_name
     id:
       type: string
       label: 'ID'
+      constraints:
+        NotNull: []
+        StringParts:
+          separator: .
+          parts:
+            - entity_type
+            - bundle
+            - field_name
     field_name:
       type: string
       label: 'Field name'
+      constraints:
+        NotNull: []
+        MachineName: []
+        UniqueField: []
+        ImmutableAfterCreation: []
     entity_type:
       type: string
       label: 'Entity type'
+      constraints:
+        NotNull: []
+        PluginExists:
+          pluginType: content_entity_type
+          pluginClassImplementsInterface: \Drupal\Core\Entity\FieldableEntityInterface
+        ImmutableAfterCreation: []
     bundle:
       type: string
       label: 'Bundle'
+      constraints:
+        NotNull: []
+        BundleExists: []
+        ImmutableAfterCreation: []
     label:
       type: label
       label: 'Label'
+      constraints:
+        NotNull: []
     description:
       type: text
       label: 'Help text'
+      constraints:
+        # Optional, so at minimum must be set to the empty string.
+        NotNull: []
     required:
       type: boolean
       label: 'Required field'
+      constraints:
+        NotNull: []
     translatable:
       type: boolean
       label: 'Translatable'
+      constraints:
+        NotNull: []
     default_value:
       type: sequence
       label: 'Default values'
+      constraints:
+        # Optional, so at minimum must be set to the empty array.
+        NotNull: []
       sequence:
         type: field.value.[%parent.%parent.field_type]
         label: 'Default value'
+        constraint:
+          # TBD, looks like \Symfony\Component\Validator\Constraints\ValidValidator *could* work here…
+          Valid: []
     default_value_callback:
       type: string
       label: 'Default value callback'
+      # @todo Constraints — validate that this callback A) returns an entity, B) a *valid* entity?
     settings:
       type: field.field_settings.[%parent.field_type]
+      # @todo Constraints on each of the field type-specific settings config schemas BUT extra special challenge: until Drupal 11, at best only core field types will have validation constraints set!
     field_type:
       type: string
       label: 'Field type'
+      constraints:
+        NotNull: []
+        Matches:
+          # This field_storage_config entity must exist.
+          config:
+            type: field_storage_config
+            id:
+              - entity_type
+              - field_name
+          # The value found at this place in that config entity must match.
+          selector:
+            - type
+        ImmutableAfterCreation: []
 
 core.base_field_override.*.*.*:
   type: field_config_base
   label: 'Base field bundle override'
+  constraints:
+    FieldIsBaseField:
+      entityType: entity_type
+      fieldName: field_name
 
 core.date_format.*:
   type: config_entity
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index ca182c42abf9..d55c3c981bed 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -12,35 +12,77 @@ field.storage.*.*:
   type: config_entity
   label: 'Field'
   mapping:
+    constraints:
+      IsUnchanged:
+        selector: locked
+      IsFalse:
+        selector: locked
     id:
       type: string
       label: 'ID'
+      constraints:
+        NotNull: []
+        StringParts:
+          separator: .
+          parts:
+            - entity_type
+            - field_name
     field_name:
       type: string
       label: 'Field name'
+      constraints:
+        NotNull: []
+        # Note: typically but optionally, this would use `field_ui.settings`' `field_prefix` value as the first part.
+        MachineName: []
+        UniqueField: []
+        ImmutableAfterCreation: []
     entity_type:
       type: string
       label: 'Entity type'
+      constraints:
+        NotNull: []
+        PluginExists:
+          pluginType: content_entity_type
+          pluginClassImplementsInterface: \Drupal\Core\Entity\FieldableEntityInterface
+        ImmutableAfterCreation: []
     type:
       type: string
       label: 'Type'
+      constraints:
+        NotNull: []
+        # @todo In principle, we should validate that this plugin was indeed provided by `module`.
+        PluginExists:
+          pluginType: field_type
+        ImmutableAfterCreation: []
     settings:
       type: field.storage_settings.[%parent.type]
     module:
       type: string
       label: 'Module'
+      constraints:
+        NotNull: []
+        ModuleExists: []
+        ImmutableAfterCreation: []
     locked:
       type: boolean
       label: 'Locked'
+      constraints:
+        NotNull: []
     cardinality:
       type: integer
       label: 'Maximum number of values users can enter'
+      constraints:
+        NotNull: []
+        NoEntitiesExistYetWithHigherCardinality: []
     translatable:
       type: boolean
       label: 'Translatable'
+      constraints:
+        NotNull: []
     indexes:
       type: sequence
       label: 'Indexes'
+      # @todo Constraints — but shockingly this A) uses `type: ignore`, B) has zero validation logic in core?!
       sequence:
         type: sequence
         label: 'Indexes'
@@ -50,10 +92,19 @@ field.storage.*.*:
     persist_with_no_fields:
       type: boolean
       label: 'Persist field storage with no fields'
+      constraints:
+        NotNull: []
     custom_storage:
       type: boolean
       label: 'Enable custom storage'
+      constraints:
+        NotNull: []
+        ImmutableAfterCreation: []
 
 field.field.*.*.*:
   type: field_config_base
   label: 'Field'
+  constraints:
+    FieldIsConfigurableField:
+      entityType: entity_type
+      fieldName: field_name
-- 
GitLab


From 5cd3d14239cf00c5a7231ce5d67bb0759c1d43f8 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 30 Nov 2022 15:30:52 +0100
Subject: [PATCH 02/73] `StringParts` constraint.

---
 .../Constraint/StringPartsConstraint.php      | 47 ++++++++++++
 .../StringPartsConstraintValidator.php        | 72 +++++++++++++++++++
 .../Constraint/TreeAwareConstraintTrait.php   | 66 +++++++++++++++++
 3 files changed, 185 insertions(+)
 create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraint.php
 create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php
 create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php

diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraint.php
new file mode 100644
index 000000000000..bdde9e678359
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraint.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks a string consists of specific parts found in the parent mapping.
+ *
+ * @Constraint(
+ *   id = "StringParts",
+ *   label = @Translation("String consists of specific parts", context = "Validation")
+ * )
+ */
+class StringPartsConstraint extends Constraint {
+
+  /**
+   * The error message if the string does not match.
+   *
+   * @var string
+   */
+  public string $message = "Expected '@expected_string', not '@value'. Format: '@expected_format'.";
+
+  /**
+   * The separator separating the parts.
+   *
+   * @var string
+   */
+  public string $separator;
+
+  /**
+   * The parent mapping's elements string values that should be used as parts.
+   *
+   * @var array
+   */
+  public array $parts;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRequiredOptions() {
+    return ['separator', 'parts'];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php
new file mode 100644
index 000000000000..e41b34d7bbd5
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php
@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Drupal\Core\Config\Schema\Mapping;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Validates the StringParts constraint.
+ */
+class StringPartsConstraintValidator extends ConstraintValidator {
+
+  use TreeAwareConstraintTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(mixed $value, Constraint $constraint) {
+    if (!is_string($value)) {
+      throw new UnexpectedTypeException($value, 'string');
+    }
+
+    // Find the parent mapping.
+    $mapping = $this->getParentProperty();
+    // If it's not a Mapping, it's a logical error in the config schema, not the
+    // concrete config.
+    if (!$mapping instanceof Mapping) {
+      throw new \LogicException('This constraint can only be set on `type: mapping`.');
+    }
+
+    // Verify the required parts are present; if not, that's a logical error in
+    // the config schema, not in concrete config.
+    $elements = $mapping->getElements();
+    $missing_elements = array_diff($constraint->parts, array_keys($elements));
+    if (!empty($missing_elements)) {
+      throw new \LogicException(sprintf('This validation constraint is configured to inspect the mapping elements %s, but some do not exist: %s.',
+        implode(', ', $constraint->parts),
+        implode(', ', $missing_elements)
+      ));
+    }
+
+    // Retrieve the parts of the expected string.
+    $expected_string_parts = [];
+    foreach ($constraint->parts as $part) {
+      $part_value = $elements[$part]->getValue();
+      if (!is_string($part_value)) {
+        throw new \LogicException();
+      }
+      $expected_string_parts[] = $part_value;
+    }
+    $expected_string = implode($constraint->separator, $expected_string_parts);
+
+    if ($expected_string !== $value) {
+      $expected_format = implode(
+        $constraint->separator,
+        array_map(function (string $v) {
+          return sprintf('<%s>', $v);
+        }, $constraint->parts)
+      );
+      $this->context->addViolation($constraint->message, [
+        '@value' => $value,
+        '@expected_string' => $expected_string,
+        '@expected_format' => $expected_format,
+      ]);
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php
new file mode 100644
index 000000000000..8b9a32f8c3db
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php
@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Drupal\Core\Config\Schema\ArrayElement;
+use Drupal\Core\Config\Schema\Element;
+
+/**
+ * Helper methods for tree-aware validation constraints.
+ */
+trait TreeAwareConstraintTrait {
+
+  /**
+   * Find the parent property.
+   *
+   * @return \Drupal\Core\Config\Schema\Element
+   *   The parent property.
+   */
+  private function getParentProperty(): Element {
+    $parent_property_path = array_slice(explode('.', $this->context->getPropertyPath()), 0, -1);
+    return self::findPropertyForPath($this->context->getRoot(), $parent_property_path);
+  }
+
+  /**
+   * Finds the specified property path in the given tree.
+   *
+   * @todo consider adopting Symfony's PropertyAccess component.
+   *
+   * @param \Drupal\Core\Config\Schema\ArrayElement $tree
+   *   A config schema (sub)tree.
+   * @param string[] $property_path
+   *   A property path, in array form.
+   *
+   * @return \Drupal\Core\Config\Schema\Element
+   *   The element found at the specified property path.
+   *
+   * @throws \OutOfRangeException
+   *   When requesting a non-existent property path.
+   */
+  private static function findPropertyForPath(ArrayElement $tree, array $property_path): Element {
+    // Edge case: root is requested.
+    if (empty($property_path)) {
+      return $tree;
+    }
+
+    $elements = $tree->getElements();
+    $name = array_shift($property_path);
+
+    // Check if it exists.
+    if (!isset($elements[$name])) {
+      throw new \OutOfRangeException();
+    }
+
+    // If it does, and there are no more parts of the property path, we're done.
+    if (empty($parts)) {
+      return $elements[$name];
+    }
+    // Otherwise, recurse.
+    else {
+      return self::findPropertyForPath($elements[$name], $parts);
+    }
+  }
+
+}
-- 
GitLab


From 882da803d64bfebcbd81aeab79a9c8e5dd299dae Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 30 Nov 2022 15:46:56 +0100
Subject: [PATCH 03/73] Fix `TreeAwareConstraintTrait::findPropertyForPath()`.

---
 .../Validation/Constraint/TreeAwareConstraintTrait.php   | 9 +--------
 1 file changed, 1 insertion(+), 8 deletions(-)

diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php
index 8b9a32f8c3db..5b676ae1775e 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php
@@ -53,14 +53,7 @@ private static function findPropertyForPath(ArrayElement $tree, array $property_
       throw new \OutOfRangeException();
     }
 
-    // If it does, and there are no more parts of the property path, we're done.
-    if (empty($parts)) {
-      return $elements[$name];
-    }
-    // Otherwise, recurse.
-    else {
-      return self::findPropertyForPath($elements[$name], $parts);
-    }
+    return self::findPropertyForPath($elements[$name], $property_path);
   }
 
 }
-- 
GitLab


From f756506e68a3e0b4a2bc9e5d922aefc6608c91dd Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 30 Nov 2022 17:30:28 +0100
Subject: [PATCH 04/73] `MachineName` constraint.

---
 .../Constraint/MachineNameConstraint.php      | 43 +++++++++++++++++++
 1 file changed, 43 insertions(+)
 create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MachineNameConstraint.php

diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MachineNameConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MachineNameConstraint.php
new file mode 100644
index 000000000000..28bdaabade4b
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MachineNameConstraint.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+/**
+ * @Constraint(
+ *   id = "MachineName",
+ *   label = @Translation("A valid machine name", context = "Validation")
+ * )
+ *
+ * Note: this does not check uniqueness, only the format. Add an additional
+ * constraint to ensure unique machine names; how to do that depends on the
+ * configuration. For config entities, the `UniqueField` is usually possible.
+ *
+ * @see \Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldConstraint
+ */
+class MachineNameConstraint extends RegexConstraint {
+
+  /**
+   * {@inheritdoc}
+   */
+  public $message = 'Invalid machine name.';
+
+  /**
+   * The pattern to use.
+   *
+   * @var string
+   *
+   * @see \Drupal\Core\Render\Element\MachineName::validateMachineName
+   */
+  const PATTERN = '/^[a-z0-9_]+$/';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array|string|null $pattern, string $message = NULL, string $htmlPattern = NULL, bool $match = NULL, callable $normalizer = NULL, array $groups = NULL, mixed $payload = NULL, array $options = []) {
+    parent::__construct(self::PATTERN, $message, $htmlPattern, $match, $normalizer, $groups, $payload, $options);
+  }
+
+
+}
-- 
GitLab


From 03e772427ee16cdaf7ed89a891dafd226b0adbe1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Thu, 1 Dec 2022 15:46:55 -0500
Subject: [PATCH 05/73] Implement immutable field checks for field storages

---
 .../Constraint/ImmutableFieldsConstraint.php  | 32 ++++++++
 .../ImmutableFieldsConstraintValidator.php    | 80 +++++++++++++++++++
 .../field/config/schema/field.schema.yml      | 31 +++----
 .../field/src/Entity/FieldStorageConfig.php   |  9 +++
 .../FieldStorageConfigValidationTest.php      | 34 +++++---
 5 files changed, 159 insertions(+), 27 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraint.php
 create mode 100644 core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php

diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraint.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraint.php
new file mode 100644
index 000000000000..b10c7e4016ed
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraint.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Config\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks if config entity properties have been changed.
+ *
+ * @Constraint(
+ *   id = "ImmutableFields",
+ *   label = @Translation("Fields are unchanged", context = "Validation"),
+ *   type = { "entity" }
+ * )
+ */
+class ImmutableFieldsConstraint extends Constraint {
+
+  public string $message = "The '@name' property cannot be changed.";
+
+  public array $fields = [];
+
+  public function getDefaultOption() {
+    return 'fields';
+  }
+
+  public function getRequiredOptions() {
+    return ['fields'];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php
new file mode 100644
index 000000000000..d8f912bb02ab
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Config\Plugin\Validation\Constraint;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\LogicException;
+use Symfony\Component\Validator\Exception\RuntimeException;
+use Symfony\Component\Validator\Exception\UnexpectedValueException;
+
+/**
+ * Validates the ImmutableFields constraint.
+ */
+class ImmutableFieldsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+  /**
+   * The entity type manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected EntityTypeManagerInterface $entityTypeManager;
+
+  /**
+   * Constructs an ImmutableFieldsConstraintValidator object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager service.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(mixed $value, Constraint $constraint) {
+    assert($constraint instanceof ImmutableFieldsConstraint);
+
+    if (!$value instanceof ConfigEntityInterface) {
+      throw new UnexpectedValueException($value, ConfigEntityInterface::class);
+    }
+    // This validation is irrelevant on new entities.
+    if ($value->isNew()) {
+      return;
+    }
+
+    $id = $value->getOriginalId() ?: $value->id();
+    if (empty($id)) {
+      throw new LogicException('The entity does not have an ID.');
+    }
+
+    $original = $this->entityTypeManager->getStorage($value->getEntityTypeId())
+      ->loadUnchanged($id);
+    if (empty($original)) {
+      throw new RuntimeException('The original entity could not be loaded.');
+    }
+
+    foreach ($constraint->fields as $name) {
+      if ($original->get($name) !== $value->get($name)) {
+        $this->context->addViolation($constraint->message, ['@name' => $name]);
+      }
+    }
+  }
+
+}
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index d55c3c981bed..a247bbd7154f 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -22,11 +22,11 @@ field.storage.*.*:
       label: 'ID'
       constraints:
         NotNull: []
-        StringParts:
-          separator: .
-          parts:
-            - entity_type
-            - field_name
+#        StringParts:
+#          separator: .
+#          parts:
+#            - entity_type
+#            - field_name
     field_name:
       type: string
       label: 'Field name'
@@ -34,26 +34,23 @@ field.storage.*.*:
         NotNull: []
         # Note: typically but optionally, this would use `field_ui.settings`' `field_prefix` value as the first part.
         MachineName: []
-        UniqueField: []
-        ImmutableAfterCreation: []
+#        UniqueField: []
     entity_type:
       type: string
       label: 'Entity type'
       constraints:
         NotNull: []
-        PluginExists:
-          pluginType: content_entity_type
-          pluginClassImplementsInterface: \Drupal\Core\Entity\FieldableEntityInterface
-        ImmutableAfterCreation: []
+#        PluginExists:
+#          pluginType: content_entity_type
+#          pluginClassImplementsInterface: \Drupal\Core\Entity\FieldableEntityInterface
     type:
       type: string
       label: 'Type'
       constraints:
         NotNull: []
         # @todo In principle, we should validate that this plugin was indeed provided by `module`.
-        PluginExists:
-          pluginType: field_type
-        ImmutableAfterCreation: []
+#        PluginExists:
+#          pluginType: field_type
     settings:
       type: field.storage_settings.[%parent.type]
     module:
@@ -61,8 +58,7 @@ field.storage.*.*:
       label: 'Module'
       constraints:
         NotNull: []
-        ModuleExists: []
-        ImmutableAfterCreation: []
+#        ModuleExists: []
     locked:
       type: boolean
       label: 'Locked'
@@ -73,7 +69,7 @@ field.storage.*.*:
       label: 'Maximum number of values users can enter'
       constraints:
         NotNull: []
-        NoEntitiesExistYetWithHigherCardinality: []
+#        NoEntitiesExistYetWithHigherCardinality: []
     translatable:
       type: boolean
       label: 'Translatable'
@@ -99,7 +95,6 @@ field.storage.*.*:
       label: 'Enable custom storage'
       constraints:
         NotNull: []
-        ImmutableAfterCreation: []
 
 field.field.*.*.*:
   type: field_config_base
diff --git a/core/modules/field/src/Entity/FieldStorageConfig.php b/core/modules/field/src/Entity/FieldStorageConfig.php
index 0aa8dca37860..7ab9861e1760 100644
--- a/core/modules/field/src/Entity/FieldStorageConfig.php
+++ b/core/modules/field/src/Entity/FieldStorageConfig.php
@@ -46,6 +46,15 @@
  *     "indexes",
  *     "persist_with_no_fields",
  *     "custom_storage",
+ *   },
+ *   constraints = {
+ *     "ImmutableFields" = {
+ *       "field_name",
+ *       "entity_type",
+ *       "type",
+ *       "module",
+ *       "custom_storage"
+ *     }
  *   }
  * )
  */
diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
index 95ba5ec42bd8..b7965268aea1 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
@@ -15,20 +15,36 @@ class FieldStorageConfigValidationTest extends ConfigEntityValidationTestBase {
   /**
    * {@inheritdoc}
    */
-  protected static $modules = ['field', 'user'];
+  protected static $modules = ['field', 'entity_test'];
 
   /**
-   * {@inheritdoc}
+   * Tests that immutable fields cannot be changed.
    */
-  protected function setUp(): void {
-    parent::setUp();
-
-    $this->entity = FieldStorageConfig::create([
-      'type' => 'boolean',
+  public function testImmutableFields(): void {
+    /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
+    $field_storage = FieldStorageConfig::create([
       'field_name' => 'test',
-      'entity_type' => 'user',
+      'entity_type' => 'entity_test',
+      'type' => 'boolean',
     ]);
-    $this->entity->save();
+    $field_storage->save();
+
+    $field_storage->set('field_name', 'broken');
+    $field_storage->set('entity_type', 'entity_test_mul');
+    $field_storage->set('type', 'email');
+    $field_storage->set('module', 'entity_test');
+    $field_storage->set('custom_storage', !$field_storage->hasCustomStorage());
+
+    $typed_data = $this->container->get('typed_data_manager');
+    $definition = $typed_data->createDataDefinition('entity:field_storage_config');
+    $violations = $typed_data->create($definition, $field_storage)->validate();
+    $this->assertCount(5, $violations);
+
+    $this->assertSame("The 'field_name' property cannot be changed.", (string) $violations->get(0)->getMessage());
+    $this->assertSame("The 'entity_type' property cannot be changed.", (string) $violations->get(1)->getMessage());
+    $this->assertSame("The 'type' property cannot be changed.", (string) $violations->get(2)->getMessage());
+    $this->assertSame("The 'module' property cannot be changed.", (string) $violations->get(3)->getMessage());
+    $this->assertSame("The 'custom_storage' property cannot be changed.", (string) $violations->get(4)->getMessage());
   }
 
 }
-- 
GitLab


From dc09097f1c661d23f595f6972a57dcbdbd03c970 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 2 Dec 2022 10:15:52 -0500
Subject: [PATCH 06/73] try to fix ye olde coding standards

---
 .../Constraint/MachineNameConstraint.php      |  6 ++---
 .../field/config/schema/field.schema.yml      | 26 +++++++++----------
 2 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MachineNameConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MachineNameConstraint.php
index 28bdaabade4b..f19adf3132ad 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MachineNameConstraint.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MachineNameConstraint.php
@@ -35,9 +35,9 @@ class MachineNameConstraint extends RegexConstraint {
   /**
    * {@inheritdoc}
    */
-  public function __construct(array|string|null $pattern, string $message = NULL, string $htmlPattern = NULL, bool $match = NULL, callable $normalizer = NULL, array $groups = NULL, mixed $payload = NULL, array $options = []) {
-    parent::__construct(self::PATTERN, $message, $htmlPattern, $match, $normalizer, $groups, $payload, $options);
+  public function __construct(array|string|null $pattern, ...$arguments) {
+    $pattern = self::PATTERN;
+    parent::__construct($pattern, ...$arguments);
   }
 
-
 }
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index a247bbd7154f..5268415ae0b3 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -22,11 +22,11 @@ field.storage.*.*:
       label: 'ID'
       constraints:
         NotNull: []
-#        StringParts:
-#          separator: .
-#          parts:
-#            - entity_type
-#            - field_name
+        # StringParts:
+          # separator: .
+          # parts:
+            # - entity_type
+            # - field_name
     field_name:
       type: string
       label: 'Field name'
@@ -34,23 +34,23 @@ field.storage.*.*:
         NotNull: []
         # Note: typically but optionally, this would use `field_ui.settings`' `field_prefix` value as the first part.
         MachineName: []
-#        UniqueField: []
+        # UniqueField: []
     entity_type:
       type: string
       label: 'Entity type'
       constraints:
         NotNull: []
-#        PluginExists:
-#          pluginType: content_entity_type
-#          pluginClassImplementsInterface: \Drupal\Core\Entity\FieldableEntityInterface
+        # PluginExists:
+          # pluginType: content_entity_type
+          # pluginClassImplementsInterface: \Drupal\Core\Entity\FieldableEntityInterface
     type:
       type: string
       label: 'Type'
       constraints:
         NotNull: []
         # @todo In principle, we should validate that this plugin was indeed provided by `module`.
-#        PluginExists:
-#          pluginType: field_type
+        # PluginExists:
+          # pluginType: field_type
     settings:
       type: field.storage_settings.[%parent.type]
     module:
@@ -58,7 +58,7 @@ field.storage.*.*:
       label: 'Module'
       constraints:
         NotNull: []
-#        ModuleExists: []
+        # ModuleExists: []
     locked:
       type: boolean
       label: 'Locked'
@@ -69,7 +69,7 @@ field.storage.*.*:
       label: 'Maximum number of values users can enter'
       constraints:
         NotNull: []
-#        NoEntitiesExistYetWithHigherCardinality: []
+        # NoEntitiesExistYetWithHigherCardinality: []
     translatable:
       type: boolean
       label: 'Translatable'
-- 
GitLab


From 355691e45dded7e88d99dd6350e8733e8a30fc35 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 2 Dec 2022 10:34:30 -0500
Subject: [PATCH 07/73] Validate plugin exists

---
 .../Constraint/ImmutableFieldsConstraint.php  | 16 +++++++
 .../Constraint/PluginExistsConstraint.php     | 48 +++++++++++++++++++
 .../PluginExistsConstraintValidator.php       | 47 ++++++++++++++++++
 .../field/config/schema/field.schema.yml      |  5 +-
 .../FieldStorageConfigValidationTest.php      | 19 ++++++++
 5 files changed, 132 insertions(+), 3 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraint.php
 create mode 100644 core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php

diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraint.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraint.php
index b10c7e4016ed..138c36495eb8 100644
--- a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraint.php
+++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraint.php
@@ -17,14 +17,30 @@
  */
 class ImmutableFieldsConstraint extends Constraint {
 
+  /**
+   * The error message if an immutable property has been changed.
+   *
+   * @var string
+   */
   public string $message = "The '@name' property cannot be changed.";
 
+  /**
+   * The names of the immutable fields.
+   *
+   * @var string[]
+   */
   public array $fields = [];
 
+  /**
+   * {@inheritdoc}
+   */
   public function getDefaultOption() {
     return 'fields';
   }
 
+  /**
+   * {@inheritdoc}
+   */
   public function getRequiredOptions() {
     return ['fields'];
   }
diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraint.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraint.php
new file mode 100644
index 000000000000..6da32bbc9e84
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraint.php
@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Config\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks if a plugin exists.
+ *
+ * @Constraint(
+ *   id = "PluginExists",
+ *   label = @Translation("Plugin exists", context = "Validation"),
+ *   type = { "entity" }
+ * )
+ */
+class PluginExistsConstraint extends Constraint {
+
+  /**
+   * The error message if a plugin does not exist.
+   *
+   * @var string
+   */
+  public string $message = "The '@plugin_id' plugin does not exist.";
+
+  /**
+   * The name of the plugin manager service.
+   *
+   * @var string
+   */
+  public string $manager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultOption() {
+    return 'manager';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRequiredOptions() {
+    return ['manager'];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php
new file mode 100644
index 000000000000..4dde258fa82d
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Config\Plugin\Validation\Constraint;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Symfony\Component\DependencyInjection\ContainerAwareInterface;
+use Symfony\Component\DependencyInjection\ContainerAwareTrait;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Validates the PluginExists constraint.
+ */
+class PluginExistsConstraintValidator extends ConstraintValidator implements ContainerAwareInterface, ContainerInjectionInterface {
+
+  use ContainerAwareTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    $validator = new static();
+    $validator->setContainer($container);
+    return $validator;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(mixed $plugin_id, Constraint $constraint) {
+    assert($constraint instanceof PluginExistsConstraint);
+
+    $manager = $this->container->get($constraint->manager);
+    assert($manager instanceof PluginManagerInterface);
+
+    if (!$manager->hasDefinition($plugin_id)) {
+      $this->context->addViolation($constraint->message, [
+        '@plugin_id' => $plugin_id,
+      ]);
+    }
+  }
+
+}
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index 5268415ae0b3..1feb9f7d87b2 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -47,10 +47,9 @@ field.storage.*.*:
       type: string
       label: 'Type'
       constraints:
-        NotNull: []
+        NotBlank: []
         # @todo In principle, we should validate that this plugin was indeed provided by `module`.
-        # PluginExists:
-          # pluginType: field_type
+        PluginExists: plugin.manager.field.field_type
     settings:
       type: field.storage_settings.[%parent.type]
     module:
diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
index b7965268aea1..c0d21a9c1f7a 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
@@ -47,4 +47,23 @@ public function testImmutableFields(): void {
     $this->assertSame("The 'custom_storage' property cannot be changed.", (string) $violations->get(4)->getMessage());
   }
 
+  /**
+   * Tests that the field type plugin is validated.
+   */
+  public function testFieldTypePlugin(): void {
+    /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => 'test',
+      'entity_type' => 'entity_test',
+      'type' => 'non_existent',
+      'module' => 'core',
+    ]);
+
+    $typed_data = $this->container->get('typed_data_manager');
+    $definition = $typed_data->createDataDefinition('entity:field_storage_config');
+    $violations = $typed_data->create($definition, $field_storage)->validate();
+    $this->assertCount(1, $violations);
+    $this->assertSame("The 'non_existent' plugin does not exist.", (string) $violations->get(0)->getMessage());
+  }
+
 }
-- 
GitLab


From c70e0b81186fd1f18ea3115061932f0b1de7fd68 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 2 Dec 2022 10:48:44 -0500
Subject: [PATCH 08/73] Validate the entity type exists and has the correct
 interface

---
 .../Constraint/PluginExistsConstraint.php     | 18 ++++++++++--
 .../PluginExistsConstraintValidator.php       | 27 ++++++++++++++++--
 .../field/config/schema/field.schema.yml      |  6 ++--
 .../FieldStorageConfigValidationTest.php      | 28 +++++++++++++++++++
 4 files changed, 72 insertions(+), 7 deletions(-)

diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraint.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraint.php
index 6da32bbc9e84..fe54cd91f760 100644
--- a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraint.php
+++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraint.php
@@ -7,7 +7,7 @@
 use Symfony\Component\Validator\Constraint;
 
 /**
- * Checks if a plugin exists.
+ * Checks if a plugin exists and optionally implements a particular interface.
  *
  * @Constraint(
  *   id = "PluginExists",
@@ -22,7 +22,14 @@ class PluginExistsConstraint extends Constraint {
    *
    * @var string
    */
-  public string $message = "The '@plugin_id' plugin does not exist.";
+  public string $unknownPluginMessage = "The '@plugin_id' plugin does not exist.";
+
+  /**
+   * The error message if a plugin does not implement the expected interface.
+   *
+   * @var string
+   */
+  public string $invalidInterfaceMessage = "The '@plugin_id' plugin must implement or extend @interface.";
 
   /**
    * The name of the plugin manager service.
@@ -31,6 +38,13 @@ class PluginExistsConstraint extends Constraint {
    */
   public string $manager;
 
+  /**
+   * Optional name of the interface that the plugin must implement.
+   *
+   * @var string|null
+   */
+  public ?string $interface = NULL;
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php
index 4dde258fa82d..49b600e904ca 100644
--- a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php
+++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php
@@ -4,6 +4,7 @@
 
 namespace Drupal\Core\Config\Plugin\Validation\Constraint;
 
+use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
 use Drupal\Component\Plugin\PluginManagerInterface;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Symfony\Component\DependencyInjection\ContainerAwareInterface;
@@ -11,6 +12,7 @@
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
 
 /**
  * Validates the PluginExists constraint.
@@ -37,10 +39,31 @@ public function validate(mixed $plugin_id, Constraint $constraint) {
     $manager = $this->container->get($constraint->manager);
     assert($manager instanceof PluginManagerInterface);
 
-    if (!$manager->hasDefinition($plugin_id)) {
-      $this->context->addViolation($constraint->message, [
+    $definition = $manager->getDefinition($plugin_id, FALSE);
+    if (empty($definition)) {
+      $this->context->addViolation($constraint->unknownPluginMessage, [
         '@plugin_id' => $plugin_id,
       ]);
+      return;
+    }
+
+    if ($constraint->interface) {
+      if ($definition instanceof PluginDefinitionInterface) {
+        $class = $definition->getClass();
+      }
+      elseif (is_array($definition)) {
+        $class = $definition['class'];
+      }
+      else {
+        throw new UnexpectedTypeException($definition, 'array|' . PluginDefinitionInterface::class);
+      }
+
+      if (!is_a($class, $constraint->interface, TRUE)) {
+        $this->context->addViolation($constraint->invalidInterfaceMessage, [
+          '@plugin_id' => $plugin_id,
+          '@interface' => $constraint->interface,
+        ]);
+      }
     }
   }
 
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index 1feb9f7d87b2..b6e66029f147 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -40,9 +40,9 @@ field.storage.*.*:
       label: 'Entity type'
       constraints:
         NotNull: []
-        # PluginExists:
-          # pluginType: content_entity_type
-          # pluginClassImplementsInterface: \Drupal\Core\Entity\FieldableEntityInterface
+        PluginExists:
+          manager: entity_type.manager
+          interface: '\Drupal\Core\Entity\FieldableEntityInterface'
     type:
       type: string
       label: 'Type'
diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
index c0d21a9c1f7a..642959187f38 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
@@ -66,4 +66,32 @@ public function testFieldTypePlugin(): void {
     $this->assertSame("The 'non_existent' plugin does not exist.", (string) $violations->get(0)->getMessage());
   }
 
+  /**
+   * Tests that the target entity type is validated.
+   */
+  public function testEntityType(): void {
+    /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => 'test',
+      'entity_type' => 'entity_test',
+      'type' => 'boolean',
+      'module' => 'core',
+    ]);
+
+    $typed_data = $this->container->get('typed_data_manager');
+    $definition = $typed_data->createDataDefinition('entity:field_storage_config');
+    $violations = $typed_data->create($definition, $field_storage)->validate();
+    $this->assertCount(0, $violations);
+
+    $field_storage->set('entity_type', 'strange_entity');
+    $violations = $typed_data->create($definition, $field_storage)->validate();
+    $this->assertCount(1, $violations);
+    $this->assertSame("The 'strange_entity' plugin does not exist.", (string) $violations->get(0)->getMessage());
+
+    $field_storage->set('entity_type', 'field_config');
+    $violations = $typed_data->create($definition, $field_storage)->validate();
+    $this->assertCount(1, $violations);
+    $this->assertSame("The 'field_config' plugin must implement or extend \Drupal\Core\Entity\FieldableEntityInterface.", (string) $violations->get(0)->getMessage());
+  }
+
 }
-- 
GitLab


From 3acfcebe88696116637f08112bad1eb1af0dc0ad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 2 Dec 2022 10:49:30 -0500
Subject: [PATCH 09/73] Change constraints for moduel

---
 core/modules/field/config/schema/field.schema.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index b6e66029f147..abae66d9f455 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -56,8 +56,8 @@ field.storage.*.*:
       type: string
       label: 'Module'
       constraints:
-        NotNull: []
-        # ModuleExists: []
+        NotBlank: []
+        # ExtensionExists: 'module'
     locked:
       type: boolean
       label: 'Locked'
-- 
GitLab


From 51f98a48d7e58c5dd49ff3e19936b8a261404be3 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Mon, 5 Dec 2022 13:29:15 +0100
Subject: [PATCH 10/73] `phpcs`

---
 .../FieldStorageConfigValidationTest.php      | 26 ++++++++++++-------
 1 file changed, 16 insertions(+), 10 deletions(-)

diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
index 642959187f38..ee050122788a 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
@@ -35,16 +35,22 @@ public function testImmutableFields(): void {
     $field_storage->set('module', 'entity_test');
     $field_storage->set('custom_storage', !$field_storage->hasCustomStorage());
 
-    $typed_data = $this->container->get('typed_data_manager');
-    $definition = $typed_data->createDataDefinition('entity:field_storage_config');
-    $violations = $typed_data->create($definition, $field_storage)->validate();
-    $this->assertCount(5, $violations);
-
-    $this->assertSame("The 'field_name' property cannot be changed.", (string) $violations->get(0)->getMessage());
-    $this->assertSame("The 'entity_type' property cannot be changed.", (string) $violations->get(1)->getMessage());
-    $this->assertSame("The 'type' property cannot be changed.", (string) $violations->get(2)->getMessage());
-    $this->assertSame("The 'module' property cannot be changed.", (string) $violations->get(3)->getMessage());
-    $this->assertSame("The 'custom_storage' property cannot be changed.", (string) $violations->get(4)->getMessage());
+  /**
+   * Tests that immutable fields cannot be changed.
+   *
+   * @param array $fields_to_change
+   *   An array of key-value pairs with field names as keys and the value to set
+   *   on the field as values.
+   *
+   * @dataProvider providerImmutableFields
+   */
+  public function testImmutableFields(array $fields_to_change): void {
+    $expected_messages = [];
+    foreach ($fields_to_change as $field_name => $new_value) {
+      $this->entity->set($field_name, $new_value);
+      $expected_messages[] = "The '$field_name' property cannot be changed.";
+    }
+    $this->assertValidationErrors($expected_messages);
   }
 
   /**
-- 
GitLab


From 5ebc48f36558bfd74bae50fbe3067ef670f27e95 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Mon, 5 Dec 2022 14:57:41 +0100
Subject: [PATCH 11/73] =?UTF-8?q?Make=20`StringPartsConstraintValidator`?=
 =?UTF-8?q?=20work=20again=20=E2=80=94=20thanks=20to=20@phenaproxima=20a?=
 =?UTF-8?q?=20proper=20`entity:field=5Fstorage=5Fconfig`=20data=20definiti?=
 =?UTF-8?q?on=20is=20used=20during=20validation,=20and=20the=20validation?=
 =?UTF-8?q?=20logic=20was=20assuming=20pure=20config=20schema=20until=20no?=
 =?UTF-8?q?w.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../StringPartsConstraintValidator.php        | 19 ++--
 .../Constraint/TreeAwareConstraintTrait.php   | 21 +++--
 .../field/config/schema/field.schema.yml      | 10 +--
 .../FieldStorageConfigValidationTest.php      | 88 ++++++++++---------
 4 files changed, 72 insertions(+), 66 deletions(-)

diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php
index e41b34d7bbd5..39f6601d8e22 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php
@@ -5,6 +5,7 @@
 namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
 
 use Drupal\Core\Config\Schema\Mapping;
+use Drupal\Core\Entity\Plugin\DataType\ConfigEntityAdapter;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidator;
 use Symfony\Component\Validator\Exception\UnexpectedTypeException;
@@ -26,27 +27,27 @@ public function validate(mixed $value, Constraint $constraint) {
 
     // Find the parent mapping.
     $mapping = $this->getParentProperty();
-    // If it's not a Mapping, it's a logical error in the config schema, not the
-    // concrete config.
-    if (!$mapping instanceof Mapping) {
+    // If it's not a Mapping nor a Config Entity, it's a logical error in the
+    // config schema, not the concrete config.
+    if (!$mapping instanceof Mapping && !$mapping instanceof ConfigEntityAdapter) {
       throw new \LogicException('This constraint can only be set on `type: mapping`.');
     }
 
     // Verify the required parts are present; if not, that's a logical error in
     // the config schema, not in concrete config.
-    $elements = $mapping->getElements();
-    $missing_elements = array_diff($constraint->parts, array_keys($elements));
-    if (!empty($missing_elements)) {
-      throw new \LogicException(sprintf('This validation constraint is configured to inspect the mapping elements %s, but some do not exist: %s.',
+    $properties = $mapping->getProperties();
+    $missing_properties = array_diff($constraint->parts, array_keys($properties));
+    if (!empty($missing_properties)) {
+      throw new \LogicException(sprintf('This validation constraint is configured to inspect the properties %s, but some do not exist: %s.',
         implode(', ', $constraint->parts),
-        implode(', ', $missing_elements)
+        implode(', ', $missing_properties)
       ));
     }
 
     // Retrieve the parts of the expected string.
     $expected_string_parts = [];
     foreach ($constraint->parts as $part) {
-      $part_value = $elements[$part]->getValue();
+      $part_value = $properties[$part]->getValue();
       if (!is_string($part_value)) {
         throw new \LogicException();
       }
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php
index 5b676ae1775e..dfc3c67ec120 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php
@@ -4,21 +4,24 @@
 
 namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
 
-use Drupal\Core\Config\Schema\ArrayElement;
-use Drupal\Core\Config\Schema\Element;
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
 
 /**
  * Helper methods for tree-aware validation constraints.
+ *
+ * @todo Figure out whether to deal with TraversableTypedDataInterface, ComplexDataInterface, Mapping, or something else 😬
+ * @todo Similarly, figure out whether to return TypedDataInterface or \Drupal\Core\Config\Schema\Element 😬
  */
 trait TreeAwareConstraintTrait {
 
   /**
-   * Find the parent property.
+   * Finds the parent property.
    *
-   * @return \Drupal\Core\Config\Schema\Element
+   * @return \Drupal\Core\TypedData\TypedDataInterface
    *   The parent property.
    */
-  private function getParentProperty(): Element {
+  private function getParentProperty(): TypedDataInterface {
     $parent_property_path = array_slice(explode('.', $this->context->getPropertyPath()), 0, -1);
     return self::findPropertyForPath($this->context->getRoot(), $parent_property_path);
   }
@@ -28,18 +31,18 @@ private function getParentProperty(): Element {
    *
    * @todo consider adopting Symfony's PropertyAccess component.
    *
-   * @param \Drupal\Core\Config\Schema\ArrayElement $tree
+   * @param \Drupal\Core\TypedData\ComplexDataInterface $tree
    *   A config schema (sub)tree.
    * @param string[] $property_path
    *   A property path, in array form.
    *
-   * @return \Drupal\Core\Config\Schema\Element
-   *   The element found at the specified property path.
+   * @return \Drupal\Core\TypedData\TypedDataInterface
+   *   The property found at the specified property path.
    *
    * @throws \OutOfRangeException
    *   When requesting a non-existent property path.
    */
-  private static function findPropertyForPath(ArrayElement $tree, array $property_path): Element {
+  private static function findPropertyForPath(ComplexDataInterface $tree, array $property_path): TypedDataInterface {
     // Edge case: root is requested.
     if (empty($property_path)) {
       return $tree;
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index abae66d9f455..08d74811d249 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -22,11 +22,11 @@ field.storage.*.*:
       label: 'ID'
       constraints:
         NotNull: []
-        # StringParts:
-          # separator: .
-          # parts:
-            # - entity_type
-            # - field_name
+        StringParts:
+          separator: .
+          parts:
+            - entity_type
+            - field_name
     field_name:
       type: string
       label: 'Field name'
diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
index ee050122788a..07821574ae8c 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
@@ -18,22 +18,39 @@ class FieldStorageConfigValidationTest extends ConfigEntityValidationTestBase {
   protected static $modules = ['field', 'entity_test'];
 
   /**
-   * Tests that immutable fields cannot be changed.
+   * {@inheritdoc}
    */
-  public function testImmutableFields(): void {
-    /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
-    $field_storage = FieldStorageConfig::create([
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->entity = FieldStorageConfig::create([
+      'type' => 'boolean',
       'field_name' => 'test',
       'entity_type' => 'entity_test',
-      'type' => 'boolean',
+      'custom_storage' => FALSE,
     ]);
-    $field_storage->save();
+    $this->entity->save();
+  }
 
-    $field_storage->set('field_name', 'broken');
-    $field_storage->set('entity_type', 'entity_test_mul');
-    $field_storage->set('type', 'email');
-    $field_storage->set('module', 'entity_test');
-    $field_storage->set('custom_storage', !$field_storage->hasCustomStorage());
+  public function providerImmutableFields(): array {
+    return [
+      'field_name' => [
+        ['field_name' => 'broken'],
+      ],
+      'entity_type' => [
+        ['entity_type' => 'entity_test_mul'],
+      ],
+      'type' => [
+        ['type' => 'email'],
+      ],
+      'module' => [
+        ['module' => 'entity_test'],
+      ],
+      'custom_storage' => [
+        ['custom_storage' => TRUE],
+      ],
+    ];
+  }
 
   /**
    * Tests that immutable fields cannot be changed.
@@ -57,47 +74,32 @@ public function testImmutableFields(array $fields_to_change): void {
    * Tests that the field type plugin is validated.
    */
   public function testFieldTypePlugin(): void {
-    /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
-    $field_storage = FieldStorageConfig::create([
-      'field_name' => 'test',
-      'entity_type' => 'entity_test',
-      'type' => 'non_existent',
-      'module' => 'core',
+    $this->entity->set('type', 'non_existent');
+    $this->assertValidationErrors([
+      "The 'type' property cannot be changed.",
+      "The 'non_existent' plugin does not exist.",
     ]);
-
-    $typed_data = $this->container->get('typed_data_manager');
-    $definition = $typed_data->createDataDefinition('entity:field_storage_config');
-    $violations = $typed_data->create($definition, $field_storage)->validate();
-    $this->assertCount(1, $violations);
-    $this->assertSame("The 'non_existent' plugin does not exist.", (string) $violations->get(0)->getMessage());
   }
 
   /**
    * Tests that the target entity type is validated.
    */
   public function testEntityType(): void {
-    /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
-    $field_storage = FieldStorageConfig::create([
-      'field_name' => 'test',
-      'entity_type' => 'entity_test',
-      'type' => 'boolean',
-      'module' => 'core',
-    ]);
-
-    $typed_data = $this->container->get('typed_data_manager');
-    $definition = $typed_data->createDataDefinition('entity:field_storage_config');
-    $violations = $typed_data->create($definition, $field_storage)->validate();
-    $this->assertCount(0, $violations);
+    // Ensure the target entity type is valid to begin with.
+    $this->assertValidationErrors([]);
 
-    $field_storage->set('entity_type', 'strange_entity');
-    $violations = $typed_data->create($definition, $field_storage)->validate();
-    $this->assertCount(1, $violations);
-    $this->assertSame("The 'strange_entity' plugin does not exist.", (string) $violations->get(0)->getMessage());
+    $this->entity->set('entity_type', 'strange_entity');
+    $this->assertValidationErrors([
+      "The 'entity_type' property cannot be changed.",
+      "The 'strange_entity' plugin does not exist.",
+    ]);
 
-    $field_storage->set('entity_type', 'field_config');
-    $violations = $typed_data->create($definition, $field_storage)->validate();
-    $this->assertCount(1, $violations);
-    $this->assertSame("The 'field_config' plugin must implement or extend \Drupal\Core\Entity\FieldableEntityInterface.", (string) $violations->get(0)->getMessage());
+    // A valid, but non-fieldable, entity type should raise an error.
+    $this->entity->set('entity_type', 'field_config');
+    $this->assertValidationErrors([
+      "The 'entity_type' property cannot be changed.",
+      "The 'field_config' plugin must implement or extend \Drupal\Core\Entity\FieldableEntityInterface.",
+    ]);
   }
 
 }
-- 
GitLab


From 605101a5c35fc0de3690f873bd292dc2838d5589 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Sat, 31 Dec 2022 12:05:40 +0100
Subject: [PATCH 12/73] Move top-level constraints for `field.storage.*.*` to
 the right place.

---
 core/modules/field/config/schema/field.schema.yml | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index 08d74811d249..7e72fb0f0278 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -11,12 +11,12 @@ field.settings:
 field.storage.*.*:
   type: config_entity
   label: 'Field'
+  constraints:
+    IsUnchanged:
+      selector: locked
+    IsFalse:
+      selector: locked
   mapping:
-    constraints:
-      IsUnchanged:
-        selector: locked
-      IsFalse:
-        selector: locked
     id:
       type: string
       label: 'ID'
-- 
GitLab


From 2ae18a0300fc57bb48670b065db7c6982946717d Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Sat, 31 Dec 2022 12:15:10 +0100
Subject: [PATCH 13/73] Remove `ImmutableAfterCreation` constraint uses from
 `field_config_base` in favor of the `ImmutableFields` constraint that
 @phenaproxima wrote.

---
 core/config/schema/core.data_types.schema.yml           | 4 ----
 core/lib/Drupal/Core/Field/Entity/BaseFieldOverride.php | 8 ++++++++
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 9a6e6e13f54c..5036e6f37883 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -436,7 +436,6 @@ field_config_base:
         NotNull: []
         MachineName: []
         UniqueField: []
-        ImmutableAfterCreation: []
     entity_type:
       type: string
       label: 'Entity type'
@@ -445,14 +444,12 @@ field_config_base:
         PluginExists:
           pluginType: content_entity_type
           pluginClassImplementsInterface: \Drupal\Core\Entity\FieldableEntityInterface
-        ImmutableAfterCreation: []
     bundle:
       type: string
       label: 'Bundle'
       constraints:
         NotNull: []
         BundleExists: []
-        ImmutableAfterCreation: []
     label:
       type: label
       label: 'Label'
@@ -508,7 +505,6 @@ field_config_base:
           # The value found at this place in that config entity must match.
           selector:
             - type
-        ImmutableAfterCreation: []
 
 core.base_field_override.*.*.*:
   type: field_config_base
diff --git a/core/lib/Drupal/Core/Field/Entity/BaseFieldOverride.php b/core/lib/Drupal/Core/Field/Entity/BaseFieldOverride.php
index 54c08284f4e1..7df5df3ca6c1 100644
--- a/core/lib/Drupal/Core/Field/Entity/BaseFieldOverride.php
+++ b/core/lib/Drupal/Core/Field/Entity/BaseFieldOverride.php
@@ -37,6 +37,14 @@
  *     "default_value_callback",
  *     "settings",
  *     "field_type",
+ *   },
+ *   constraints = {
+ *     "ImmutableFields" = {
+ *       "field_name",
+ *       "entity_type",
+ *       "bundle",
+ *       "field_type",
+ *     }
  *   }
  * )
  */
-- 
GitLab


From 9ab60e635e19363818dd18b7489ebd766dc29559 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Sat, 31 Dec 2022 12:48:28 +0100
Subject: [PATCH 14/73] Generalize `UniqueFieldValueValidator` to work for both
 content and config entity types.

---
 .../Constraint/UniqueFieldValueValidator.php  | 20 ++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
index e965310861a3..24562bcd2eea 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
@@ -7,19 +7,18 @@
 
 /**
  * Validates that a field is unique for the given entity type.
+ *
+ * Supports top-level properties on both content and config entity types.
  */
 class UniqueFieldValueValidator extends ConstraintValidator {
 
   /**
    * {@inheritdoc}
    */
-  public function validate($items, Constraint $constraint) {
-    if (!$item = $items->first()) {
-      return;
-    }
-    $field_name = $items->getFieldDefinition()->getName();
+  public function validate($items_or_value, Constraint $constraint) {
+    $field_name = $this->context->getPropertyPath();
     /** @var \Drupal\Core\Entity\EntityInterface $entity */
-    $entity = $items->getEntity();
+    $entity = $this->context->getRoot()->getValue();
     $entity_type_id = $entity->getEntityTypeId();
     $id_key = $entity->getEntityType()->getKey('id');
 
@@ -33,17 +32,20 @@ public function validate($items, Constraint $constraint) {
       $query->condition($id_key, $entity_id, '<>');
     }
 
+    $value = is_array($items_or_value) ? $items_or_value->first()->value : $items_or_value;
     $value_taken = (bool) $query
-      ->condition($field_name, $item->value)
+      ->condition($field_name, $value)
       ->range(0, 1)
       ->count()
       ->execute();
 
     if ($value_taken) {
       $this->context->addViolation($constraint->message, [
-        '%value' => $item->value,
+        '%value' => $value,
         '@entity_type' => $entity->getEntityType()->getSingularLabel(),
-        '@field_name' => $items->getFieldDefinition()->getLabel(),
+        '@field_name' => is_array($items_or_value)
+          ? $items_or_value->getFieldDefinition()->getLabel()
+          : $field_name,
       ]);
     }
   }
-- 
GitLab


From 99ff31d41afd8346164bbb2b9564a00bff903122 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Sat, 31 Dec 2022 12:48:42 +0100
Subject: [PATCH 15/73] Move top-level constraints for `field_config_base` to
 the right place.

---
 core/config/schema/core.data_types.schema.yml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 5036e6f37883..ad06b3ad4d80 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -413,11 +413,11 @@ base_entity_reference_field_settings:
 
 field_config_base:
   type: config_entity
+  constraints:
+    FieldStorageExistsOnFieldableContentEntityType:
+      entityType: entity_type
+      fieldName: field_name
   mapping:
-    constraints:
-      FieldStorageExistsOnFieldableContentEntityType:
-        entityType: entity_type
-        fieldName: field_name
     id:
       type: string
       label: 'ID'
-- 
GitLab


From 5907d5f727c052a6cafc9509d6ccbd4129e8e616 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 11 Jan 2023 10:43:04 +0100
Subject: [PATCH 16/73] #2920682 landed => remove
 `Drupal\Core\Config\Plugin\Validation\Constraint\PluginExists*`.

---
 .../Constraint/PluginExistsConstraint.php     | 62 ----------------
 .../PluginExistsConstraintValidator.php       | 70 -------------------
 2 files changed, 132 deletions(-)
 delete mode 100644 core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraint.php
 delete mode 100644 core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php

diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraint.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraint.php
deleted file mode 100644
index fe54cd91f760..000000000000
--- a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraint.php
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-
-declare(strict_types = 1);
-
-namespace Drupal\Core\Config\Plugin\Validation\Constraint;
-
-use Symfony\Component\Validator\Constraint;
-
-/**
- * Checks if a plugin exists and optionally implements a particular interface.
- *
- * @Constraint(
- *   id = "PluginExists",
- *   label = @Translation("Plugin exists", context = "Validation"),
- *   type = { "entity" }
- * )
- */
-class PluginExistsConstraint extends Constraint {
-
-  /**
-   * The error message if a plugin does not exist.
-   *
-   * @var string
-   */
-  public string $unknownPluginMessage = "The '@plugin_id' plugin does not exist.";
-
-  /**
-   * The error message if a plugin does not implement the expected interface.
-   *
-   * @var string
-   */
-  public string $invalidInterfaceMessage = "The '@plugin_id' plugin must implement or extend @interface.";
-
-  /**
-   * The name of the plugin manager service.
-   *
-   * @var string
-   */
-  public string $manager;
-
-  /**
-   * Optional name of the interface that the plugin must implement.
-   *
-   * @var string|null
-   */
-  public ?string $interface = NULL;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getDefaultOption() {
-    return 'manager';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getRequiredOptions() {
-    return ['manager'];
-  }
-
-}
diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php
deleted file mode 100644
index 49b600e904ca..000000000000
--- a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php
+++ /dev/null
@@ -1,70 +0,0 @@
-<?php
-
-declare(strict_types = 1);
-
-namespace Drupal\Core\Config\Plugin\Validation\Constraint;
-
-use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
-use Drupal\Component\Plugin\PluginManagerInterface;
-use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
-use Symfony\Component\DependencyInjection\ContainerAwareInterface;
-use Symfony\Component\DependencyInjection\ContainerAwareTrait;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\Validator\Constraint;
-use Symfony\Component\Validator\ConstraintValidator;
-use Symfony\Component\Validator\Exception\UnexpectedTypeException;
-
-/**
- * Validates the PluginExists constraint.
- */
-class PluginExistsConstraintValidator extends ConstraintValidator implements ContainerAwareInterface, ContainerInjectionInterface {
-
-  use ContainerAwareTrait;
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    $validator = new static();
-    $validator->setContainer($container);
-    return $validator;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validate(mixed $plugin_id, Constraint $constraint) {
-    assert($constraint instanceof PluginExistsConstraint);
-
-    $manager = $this->container->get($constraint->manager);
-    assert($manager instanceof PluginManagerInterface);
-
-    $definition = $manager->getDefinition($plugin_id, FALSE);
-    if (empty($definition)) {
-      $this->context->addViolation($constraint->unknownPluginMessage, [
-        '@plugin_id' => $plugin_id,
-      ]);
-      return;
-    }
-
-    if ($constraint->interface) {
-      if ($definition instanceof PluginDefinitionInterface) {
-        $class = $definition->getClass();
-      }
-      elseif (is_array($definition)) {
-        $class = $definition['class'];
-      }
-      else {
-        throw new UnexpectedTypeException($definition, 'array|' . PluginDefinitionInterface::class);
-      }
-
-      if (!is_a($class, $constraint->interface, TRUE)) {
-        $this->context->addViolation($constraint->invalidInterfaceMessage, [
-          '@plugin_id' => $plugin_id,
-          '@interface' => $constraint->interface,
-        ]);
-      }
-    }
-  }
-
-}
-- 
GitLab


From e87967a867d2bf5d2ce222ed7614444f570789f5 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 11 Jan 2023 10:46:46 +0100
Subject: [PATCH 17/73] Fix constraint options for
 `field.storage.*.*`.`entity_type`.

---
 core/config/schema/core.data_types.schema.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index ad06b3ad4d80..39db3d1b69b8 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -442,8 +442,8 @@ field_config_base:
       constraints:
         NotNull: []
         PluginExists:
-          pluginType: content_entity_type
-          pluginClassImplementsInterface: \Drupal\Core\Entity\FieldableEntityInterface
+          manager: dentity_type.manager
+          interface: \Drupal\Core\Entity\FieldableEntityInterface
     bundle:
       type: string
       label: 'Bundle'
-- 
GitLab


From 0c47622e7283a2d29202c5e75a749480cf0c6711 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 11 Jan 2023 10:48:42 +0100
Subject: [PATCH 18/73] @larowlan review: use constructor promotion.

---
 .../ImmutableFieldsConstraintValidator.php           | 12 ++----------
 1 file changed, 2 insertions(+), 10 deletions(-)

diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php
index d8f912bb02ab..2805a895ebea 100644
--- a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php
+++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php
@@ -19,21 +19,13 @@
  */
 class ImmutableFieldsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
 
-  /**
-   * The entity type manager service.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected EntityTypeManagerInterface $entityTypeManager;
-
   /**
    * Constructs an ImmutableFieldsConstraintValidator object.
    *
-   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
    *   The entity type manager service.
    */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
-    $this->entityTypeManager = $entity_type_manager;
+  public function __construct(protected EntityTypeManagerInterface $entityTypeManager) {
   }
 
   /**
-- 
GitLab


From b4816791b76f5fddb8492eee6505aab374334ff0 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 11 Jan 2023 10:49:10 +0100
Subject: [PATCH 19/73] @larowlan review: `static` return type.

---
 .../Constraint/ImmutableFieldsConstraintValidator.php           | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php
index 2805a895ebea..a5926d8251e6 100644
--- a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php
+++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php
@@ -31,7 +31,7 @@ public function __construct(protected EntityTypeManagerInterface $entityTypeMana
   /**
    * {@inheritdoc}
    */
-  public static function create(ContainerInterface $container) {
+  public static function create(ContainerInterface $container): static {
     return new static(
       $container->get('entity_type.manager')
     );
-- 
GitLab


From ff583dd41769ef0cd1f099c1a647c896bf890ef2 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 11 Jan 2023 11:01:36 +0100
Subject: [PATCH 20/73] @larowlan review: better exception in
 `StringPartsConstraintValidator`.

---
 .../Validation/Constraint/StringPartsConstraintValidator.php    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php
index 39f6601d8e22..91fc187a5697 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php
@@ -49,7 +49,7 @@ public function validate(mixed $value, Constraint $constraint) {
     foreach ($constraint->parts as $part) {
       $part_value = $properties[$part]->getValue();
       if (!is_string($part_value)) {
-        throw new \LogicException();
+        throw new \LogicException(sprintf('The "%s" property does not contain a string, but a %s: "%s".', $part, gettype($part_value), (string) $part_value));
       }
       $expected_string_parts[] = $part_value;
     }
-- 
GitLab


From ac1e06b2ae34ec2314515d9eacf2bca0ed83b02a Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 11 Jan 2023 11:27:52 +0100
Subject: [PATCH 21/73] Make `UniqueFieldValueValidator` work for non-new
 entities too.

---
 .../Constraint/UniqueFieldValueValidator.php  | 22 +++++++++++++++----
 .../field/config/schema/field.schema.yml      |  2 +-
 2 files changed, 19 insertions(+), 5 deletions(-)

diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
index 24562bcd2eea..47441f250a42 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
@@ -33,13 +33,15 @@ public function validate($items_or_value, Constraint $constraint) {
     }
 
     $value = is_array($items_or_value) ? $items_or_value->first()->value : $items_or_value;
-    $value_taken = (bool) $query
+    $matching_entity_ids = $query
       ->condition($field_name, $value)
-      ->range(0, 1)
-      ->count()
       ->execute();
 
-    if ($value_taken) {
+    if (empty($matching_entity_ids)) {
+      return;
+    }
+
+    if ($entity->isNew()) {
       $this->context->addViolation($constraint->message, [
         '%value' => $value,
         '@entity_type' => $entity->getEntityType()->getSingularLabel(),
@@ -48,6 +50,18 @@ public function validate($items_or_value, Constraint $constraint) {
           : $field_name,
       ]);
     }
+    // If not new, the only match must be *this* entity.
+    else if ($entity_id != reset($matching_entity_ids)) {
+      // @todo decide how to handle this without breaking BC — I think logging? For now, just an exception to allow making it relaible.
+      throw new \Exception(sprintf(
+        'The existing %s entity (ID: %s) is being validated and it violates a uniqueness constraint: %s have the same value for the "%s" field: "%s".',
+        $entity_type_id,
+        $entity_id,
+        implode($matching_entity_ids),
+        $field_name,
+        (string) $value
+      ));
+    }
   }
 
 }
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index 7e72fb0f0278..3b3afa18e39e 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -34,7 +34,7 @@ field.storage.*.*:
         NotNull: []
         # Note: typically but optionally, this would use `field_ui.settings`' `field_prefix` value as the first part.
         MachineName: []
-        # UniqueField: []
+        UniqueField: []
     entity_type:
       type: string
       label: 'Entity type'
-- 
GitLab


From 8edd31f0c574c193fb65b829e0753985b15f5a30 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Mon, 10 Jul 2023 10:32:14 +0200
Subject: [PATCH 22/73] Remove `MachineName` constraint now that #2920678 is
 in.

---
 core/config/schema/core.data_types.schema.yml |  1 -
 .../Constraint/MachineNameConstraint.php      | 43 -------------------
 .../field/config/schema/field.schema.yml      |  2 -
 3 files changed, 46 deletions(-)
 delete mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MachineNameConstraint.php

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 39db3d1b69b8..ba78b9efedd4 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -434,7 +434,6 @@ field_config_base:
       label: 'Field name'
       constraints:
         NotNull: []
-        MachineName: []
         UniqueField: []
     entity_type:
       type: string
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MachineNameConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MachineNameConstraint.php
deleted file mode 100644
index f19adf3132ad..000000000000
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MachineNameConstraint.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-declare(strict_types = 1);
-
-namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
-
-/**
- * @Constraint(
- *   id = "MachineName",
- *   label = @Translation("A valid machine name", context = "Validation")
- * )
- *
- * Note: this does not check uniqueness, only the format. Add an additional
- * constraint to ensure unique machine names; how to do that depends on the
- * configuration. For config entities, the `UniqueField` is usually possible.
- *
- * @see \Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldConstraint
- */
-class MachineNameConstraint extends RegexConstraint {
-
-  /**
-   * {@inheritdoc}
-   */
-  public $message = 'Invalid machine name.';
-
-  /**
-   * The pattern to use.
-   *
-   * @var string
-   *
-   * @see \Drupal\Core\Render\Element\MachineName::validateMachineName
-   */
-  const PATTERN = '/^[a-z0-9_]+$/';
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct(array|string|null $pattern, ...$arguments) {
-    $pattern = self::PATTERN;
-    parent::__construct($pattern, ...$arguments);
-  }
-
-}
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index 3b3afa18e39e..6a5a3413a129 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -32,8 +32,6 @@ field.storage.*.*:
       label: 'Field name'
       constraints:
         NotNull: []
-        # Note: typically but optionally, this would use `field_ui.settings`' `field_prefix` value as the first part.
-        MachineName: []
         UniqueField: []
     entity_type:
       type: string
-- 
GitLab


From 6e1765fc28b0e6581ad9e7b912c5d365d86ef480 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Mon, 10 Jul 2023 10:44:36 +0200
Subject: [PATCH 23/73] Fix `cspell` + `phpcs`.

---
 .../Validation/Constraint/UniqueFieldValueValidator.php       | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
index 47441f250a42..4a8430b5fae9 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
@@ -51,8 +51,8 @@ public function validate($items_or_value, Constraint $constraint) {
       ]);
     }
     // If not new, the only match must be *this* entity.
-    else if ($entity_id != reset($matching_entity_ids)) {
-      // @todo decide how to handle this without breaking BC — I think logging? For now, just an exception to allow making it relaible.
+    elseif ($entity_id != reset($matching_entity_ids)) {
+      // @todo decide how to handle this without breaking BC — I think logging? For now, just an exception to allow making it reliable.
       throw new \Exception(sprintf(
         'The existing %s entity (ID: %s) is being validated and it violates a uniqueness constraint: %s have the same value for the "%s" field: "%s".',
         $entity_type_id,
-- 
GitLab


From e17135e614fed180ed5a1efc04ca9c17538fe4ef Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Mon, 10 Jul 2023 11:07:52 +0200
Subject: [PATCH 24/73] =?UTF-8?q?=F0=9F=A4=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 core/config/schema/core.data_types.schema.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index ba78b9efedd4..47296652bde8 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -441,7 +441,7 @@ field_config_base:
       constraints:
         NotNull: []
         PluginExists:
-          manager: dentity_type.manager
+          manager: entity_type.manager
           interface: \Drupal\Core\Entity\FieldableEntityInterface
     bundle:
       type: string
-- 
GitLab


From 10658f719b894180e0c8e0a190dcfed948626299 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Mon, 10 Jul 2023 11:29:27 +0200
Subject: [PATCH 25/73] Update `::assertValidationErrors()` calls for #3349293.

---
 .../Kernel/Entity/FieldStorageConfigValidationTest.php    | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
index 07821574ae8c..a2ff27e557f8 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
@@ -65,7 +65,7 @@ public function testImmutableFields(array $fields_to_change): void {
     $expected_messages = [];
     foreach ($fields_to_change as $field_name => $new_value) {
       $this->entity->set($field_name, $new_value);
-      $expected_messages[] = "The '$field_name' property cannot be changed.";
+      $expected_messages[''] = "The '$field_name' property cannot be changed.";
     }
     $this->assertValidationErrors($expected_messages);
   }
@@ -76,8 +76,8 @@ public function testImmutableFields(array $fields_to_change): void {
   public function testFieldTypePlugin(): void {
     $this->entity->set('type', 'non_existent');
     $this->assertValidationErrors([
-      "The 'type' property cannot be changed.",
-      "The 'non_existent' plugin does not exist.",
+      '' => "The 'type' property cannot be changed.",
+      'type' => "The 'non_existent' plugin does not exist.",
     ]);
   }
 
@@ -90,7 +90,7 @@ public function testEntityType(): void {
 
     $this->entity->set('entity_type', 'strange_entity');
     $this->assertValidationErrors([
-      "The 'entity_type' property cannot be changed.",
+      '' => "The 'entity_type' property cannot be changed.",
       "The 'strange_entity' plugin does not exist.",
     ]);
 
-- 
GitLab


From 060175290c0abc96b95d13a63422892b5f4b4fd9 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Mon, 10 Jul 2023 14:23:14 +0200
Subject: [PATCH 26/73] Borrowing
 https://git.drupalcode.org/project/drupal/-/merge_requests/4092/diffs?commit_id=7cd01152ed9150d2feea9a6e9ac4aeca4c717113
 from #3364109.

---
 .../Validation/Constraint/NotNullConstraintValidator.php | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NotNullConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NotNullConstraintValidator.php
index 1cbf5d9600b8..3dd1ca4ec25f 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NotNullConstraintValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NotNullConstraintValidator.php
@@ -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,13 @@ 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: configuration schema's Mapping and Sequence types both extend
+    // ArrayElement which in turn implements ComplexDataInterface. But
+    // configuration schema distinguishes between empty sequences/mappings on
+    // the one hand and NULL on the other. Therefore do not cast empty arrays
+    // to NULL for configuration schema ArrayElement implementations.
+    if (($typed_data instanceof ListInterface || $typed_data instanceof ComplexDataInterface) && !$typed_data instanceof ArrayElement && $typed_data->isEmpty()) {
       $value = NULL;
     }
     parent::validate($value, $constraint);
-- 
GitLab


From 2557c24153c8a29631958b7ec64c872d760a9b68 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Mon, 10 Jul 2023 14:32:22 +0200
Subject: [PATCH 27/73] Revert changes to `UniqueFieldValueValidator`.

---
 .../Constraint/UniqueFieldValueValidator.php  | 42 ++++++-------------
 1 file changed, 13 insertions(+), 29 deletions(-)

diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
index 4a8430b5fae9..e965310861a3 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
@@ -7,18 +7,19 @@
 
 /**
  * Validates that a field is unique for the given entity type.
- *
- * Supports top-level properties on both content and config entity types.
  */
 class UniqueFieldValueValidator extends ConstraintValidator {
 
   /**
    * {@inheritdoc}
    */
-  public function validate($items_or_value, Constraint $constraint) {
-    $field_name = $this->context->getPropertyPath();
+  public function validate($items, Constraint $constraint) {
+    if (!$item = $items->first()) {
+      return;
+    }
+    $field_name = $items->getFieldDefinition()->getName();
     /** @var \Drupal\Core\Entity\EntityInterface $entity */
-    $entity = $this->context->getRoot()->getValue();
+    $entity = $items->getEntity();
     $entity_type_id = $entity->getEntityTypeId();
     $id_key = $entity->getEntityType()->getKey('id');
 
@@ -32,36 +33,19 @@ public function validate($items_or_value, Constraint $constraint) {
       $query->condition($id_key, $entity_id, '<>');
     }
 
-    $value = is_array($items_or_value) ? $items_or_value->first()->value : $items_or_value;
-    $matching_entity_ids = $query
-      ->condition($field_name, $value)
+    $value_taken = (bool) $query
+      ->condition($field_name, $item->value)
+      ->range(0, 1)
+      ->count()
       ->execute();
 
-    if (empty($matching_entity_ids)) {
-      return;
-    }
-
-    if ($entity->isNew()) {
+    if ($value_taken) {
       $this->context->addViolation($constraint->message, [
-        '%value' => $value,
+        '%value' => $item->value,
         '@entity_type' => $entity->getEntityType()->getSingularLabel(),
-        '@field_name' => is_array($items_or_value)
-          ? $items_or_value->getFieldDefinition()->getLabel()
-          : $field_name,
+        '@field_name' => $items->getFieldDefinition()->getLabel(),
       ]);
     }
-    // If not new, the only match must be *this* entity.
-    elseif ($entity_id != reset($matching_entity_ids)) {
-      // @todo decide how to handle this without breaking BC — I think logging? For now, just an exception to allow making it reliable.
-      throw new \Exception(sprintf(
-        'The existing %s entity (ID: %s) is being validated and it violates a uniqueness constraint: %s have the same value for the "%s" field: "%s".',
-        $entity_type_id,
-        $entity_id,
-        implode($matching_entity_ids),
-        $field_name,
-        (string) $value
-      ));
-    }
   }
 
 }
-- 
GitLab


From 97b3b8be6a016295ac30ce93b365b946d4d64997 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Mon, 10 Jul 2023 21:20:39 +0200
Subject: [PATCH 28/73] Introduce `UniqueFieldCombination` constraint.

---
 core/config/schema/core.data_types.schema.yml |  2 +-
 .../UniqueFieldCombinationConstraint.php      | 42 +++++++++++
 ...queFieldCombinationConstraintValidator.php | 70 +++++++++++++++++++
 .../field/config/schema/field.schema.yml      |  2 +-
 4 files changed, 114 insertions(+), 2 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraint.php
 create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraintValidator.php

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 47296652bde8..7f318310cbe7 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -414,6 +414,7 @@ base_entity_reference_field_settings:
 field_config_base:
   type: config_entity
   constraints:
+    UniqueFieldCombination: [entity_type, bundle, field_name]
     FieldStorageExistsOnFieldableContentEntityType:
       entityType: entity_type
       fieldName: field_name
@@ -434,7 +435,6 @@ field_config_base:
       label: 'Field name'
       constraints:
         NotNull: []
-        UniqueField: []
     entity_type:
       type: string
       label: 'Entity type'
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraint.php
new file mode 100644
index 000000000000..053649477a00
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraint.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks if a set of config entity fields has a unique combination.
+ *
+ * @Constraint(
+ *   id = "UniqueFieldCombination",
+ *   label = @Translation("Unique field combination constraint", context = "Validation"),
+ * )
+ */
+class UniqueFieldCombinationConstraint extends Constraint {
+
+  public $message = 'A @entity_type with this combination of values for the fields @field_name_list (%field_value_list) already exists.';
+
+  /**
+   * Fields which must have a unique combination among all entities of a type.
+   *
+   * @var string[]
+   */
+  public array $fields;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultOption() {
+    return 'fields';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRequiredOptions() {
+    return ['fields'];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraintValidator.php
new file mode 100644
index 000000000000..9b3d6162a2cb
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraintValidator.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Validates that a field is unique for the given config entity type.
+ */
+class UniqueFieldCombinationConstraintValidator extends ConstraintValidator {
+
+  use TreeAwareConstraintTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, Constraint $constraint) {
+    assert($constraint instanceof UniqueFieldCombinationConstraint);
+
+    if (!is_array($value)) {
+      throw new UnexpectedTypeException($value, 'array');
+    }
+
+    // Find the parent mapping.
+    $mapping = $this->getParentProperty();
+
+    // Verify the required fields are present; if not, that's a logical error in
+    // the config schema, not in concrete config.
+    $properties = $mapping->getProperties();
+    $missing_fields = array_diff($constraint->fields, array_keys($properties));
+    if (!empty($missing_fields)) {
+      throw new \LogicException(sprintf('This validation constraint is configured to inspect the fields %s, but some do not exist: %s.',
+        implode(', ', $constraint->fields),
+        implode(', ', $missing_fields)
+      ));
+    }
+
+    // Construct an entity query.
+    // @see \Drupal\Core\Config\ConfigImporter::checkOp()
+    // @see \Drupal\Core\Config\ConfigInstaller::createConfiguration()
+    $config_manager = \Drupal::service('config.manager');
+    $entity_type_id = $config_manager->getEntityTypeIdByName($mapping->getRoot()->getName());
+    $entity_storage = $config_manager->getEntityTypeManager()->getStorage($entity_type_id);
+    $query = $entity_storage->getQuery()
+      ->accessCheck(FALSE);
+    foreach ($constraint->fields as $field_name) {
+      $query->condition($field_name, $mapping->get($field_name)->getValue());
+    }
+
+    $uuid = $mapping->get('uuid')->getValue();
+    if (!empty($uuid)) {
+      $query->condition('uuid', $uuid, '<>');
+    }
+
+    $value_taken = (bool) $query
+      ->range(0, 1)
+      ->execute();
+
+    if ($value_taken) {
+      $this->context->addViolation($constraint->message, [
+        '@entity_type' => $entity_type_id,
+        '@field_name_list' => implode(', ', $constraint->fields),
+        '%field_value_list' => implode(', ', array_map(fn (string $field_name) => $mapping->get($field_name)->getValue(), $constraint->fields)),
+      ]);
+    }
+  }
+
+}
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index 6a5a3413a129..1c5db39e9fde 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -12,6 +12,7 @@ field.storage.*.*:
   type: config_entity
   label: 'Field'
   constraints:
+    UniqueFieldCombination: [entity_type, field_name]
     IsUnchanged:
       selector: locked
     IsFalse:
@@ -32,7 +33,6 @@ field.storage.*.*:
       label: 'Field name'
       constraints:
         NotNull: []
-        UniqueField: []
     entity_type:
       type: string
       label: 'Entity type'
-- 
GitLab


From 36bd30035a2b10dffbb0a1f3b0547ecfc33ac286 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Mon, 10 Jul 2023 21:22:49 +0200
Subject: [PATCH 29/73] More `::assertValidationErrors()` call updates for
 #3349293.

---
 .../src/Kernel/Entity/FieldStorageConfigValidationTest.php  | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
index a2ff27e557f8..ea21d2a9fbec 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
@@ -91,14 +91,14 @@ public function testEntityType(): void {
     $this->entity->set('entity_type', 'strange_entity');
     $this->assertValidationErrors([
       '' => "The 'entity_type' property cannot be changed.",
-      "The 'strange_entity' plugin does not exist.",
+      'entity_type' => "The 'strange_entity' plugin does not exist.",
     ]);
 
     // A valid, but non-fieldable, entity type should raise an error.
     $this->entity->set('entity_type', 'field_config');
     $this->assertValidationErrors([
-      "The 'entity_type' property cannot be changed.",
-      "The 'field_config' plugin must implement or extend \Drupal\Core\Entity\FieldableEntityInterface.",
+      '' => "The 'entity_type' property cannot be changed.",
+      'entity_type' => "The 'field_config' plugin must implement or extend \Drupal\Core\Entity\FieldableEntityInterface.",
     ]);
   }
 
-- 
GitLab


From b37c400e6abee05e6699b5884c4913985d9db14f Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Mon, 10 Jul 2023 21:28:01 +0200
Subject: [PATCH 30/73] Adopt `ImmutableFields` and `PluginExists` constraints
 for validating `FieldConfig` too, just like for `FieldStorageConfig`.

---
 core/modules/field/src/Entity/FieldConfig.php |  6 ++++
 .../Entity/FieldConfigValidationTest.php      | 28 +++++++++++++++++++
 2 files changed, 34 insertions(+)

diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php
index f43a7987487a..24b54d1438e6 100644
--- a/core/modules/field/src/Entity/FieldConfig.php
+++ b/core/modules/field/src/Entity/FieldConfig.php
@@ -48,6 +48,12 @@
  *   constraints = {
  *     "RequiredConfigDependencies" = {
  *       "field_storage_config"
+ *     },
+ *     "ImmutableFields" = {
+ *       "field_name",
+ *       "entity_type",
+ *       "bundle",
+ *       "field_type"
  *     }
  *   }
  * )
diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
index fde68011cea0..6cfbe64d271c 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
@@ -53,4 +53,32 @@ public function testInvalidDependencies(): void {
     ]);
   }
 
+  public function providerImmutableFields(): array {
+    return [
+      'field_name' => [
+        ['field_name' => 'broken'],
+      ],
+      'entity_type' => [
+        ['entity_type' => 'entity_test_mul'],
+      ],
+      'field_type' => [
+        ['field_type' => 'email'],
+      ],
+      'bundle' => [
+        ['bundle' => 'foo'],
+      ],
+    ];
+  }
+
+  /**
+   * Tests that the field type plugin is validated.
+   */
+  public function testFieldTypePlugin(): void {
+    $this->entity->set('field_type', 'non_existent');
+    $this->assertValidationErrors([
+      '' => "The 'field_type' property cannot be changed.",
+      'field_type' => "The 'non_existent' plugin does not exist.",
+    ]);
+  }
+
 }
-- 
GitLab


From 0906a0a57c3d08019ca8553720e4ea084b5c6bf1 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Tue, 11 Jul 2023 17:39:58 +0200
Subject: [PATCH 31/73] Introduce `EntityBundleExists` constraint.

---
 core/config/schema/core.data_types.schema.yml |  3 +-
 core/modules/field/src/Entity/FieldConfig.php |  4 +++
 .../Entity/FieldConfigValidationTest.php      | 30 +++++++++++++++++++
 .../BaseFieldOverrideValidationTest.php       | 19 ++++++++++++
 4 files changed, 55 insertions(+), 1 deletion(-)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 7f318310cbe7..6dcbd4239ceb 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -448,7 +448,8 @@ field_config_base:
       label: 'Bundle'
       constraints:
         NotNull: []
-        BundleExists: []
+        EntityBundleExists:
+          entityTypeId: '%parent.entity_type'
     label:
       type: label
       label: 'Label'
diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php
index 24b54d1438e6..6065607d4d2d 100644
--- a/core/modules/field/src/Entity/FieldConfig.php
+++ b/core/modules/field/src/Entity/FieldConfig.php
@@ -138,6 +138,10 @@ public function __construct(array $values, $entity_type = 'field_config') {
     }
 
     parent::__construct($values, $entity_type);
+
+    // For this to be a valid config entity, at the very least a dependency on a
+    // FieldStorageConfig entity must be present.
+    $this->addDependency('config', $this->getFieldStorageDefinition()->getConfigDependencyName());
   }
 
   /**
diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
index 6cfbe64d271c..10f8d5332538 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
@@ -3,6 +3,8 @@
 namespace Drupal\Tests\field\Kernel\Entity;
 
 use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\field\FieldStorageConfigInterface;
 
 /**
  * Tests validation of field_config entities.
@@ -81,4 +83,32 @@ public function testFieldTypePlugin(): void {
     ]);
   }
 
+  /**
+   * Tests that the bundle is validated.
+   */
+  public function testBundle(): void {
+    $entity_type_id = 'entity_test';
+    $field_name = 'test';
+
+    // Assert that the FieldStorageConfig which this FieldConfig will depend on,
+    // already exists.
+    $field_storage_config = FieldStorageConfig::loadByName($entity_type_id, $field_name);
+    $this->assertInstanceOf(FieldStorageConfigInterface::class, $field_storage_config);
+
+    // Try to create an instance of this field on a bundle that does not exist.
+    $this->entity = FieldConfig::create([
+      'entity_type' => $entity_type_id,
+      'field_name' => $field_name,
+      'field_type' => $field_storage_config->getType(),
+      'bundle' => 'non_existent',
+    ]);
+    $this->assertValidationErrors([
+      'bundle' => "The 'non_existent' bundle does not exist on the 'entity_test' entity type.",
+    ]);
+
+    // Next, try to create it on a bundle that does exist.
+    $this->entity->set('bundle', 'entity_test');
+    $this->assertValidationErrors([]);
+  }
+
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
index e13c50d0eeaf..d4ef24b07e6a 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
@@ -31,4 +31,23 @@ protected function setUp(): void {
     $this->entity->save();
   }
 
+  /**
+   * Tests that the bundle is validated.
+   */
+  public function testBundle(): void {
+    $fields = $this->container->get('entity_field.manager')
+      ->getBaseFieldDefinitions('user');
+
+    // Try to create an instance of this base field override on a bundle that
+    // does not exist.
+    $this->entity = BaseFieldOverride::createFromBaseFieldDefinition(reset($fields), 'non_existent');
+    $this->assertValidationErrors([
+      'bundle' => "The 'non_existent' bundle does not exist on the 'user' entity type.",
+    ]);
+
+    // Next, try to create it on a bundle that does exist.
+    $this->entity = BaseFieldOverride::createFromBaseFieldDefinition(reset($fields), 'user');
+    $this->assertValidationErrors([]);
+  }
+
 }
-- 
GitLab


From 15025764af4e45b76e56faa46880183855cc5731 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Tue, 11 Jul 2023 17:41:59 +0200
Subject: [PATCH 32/73] Temporarily disable the `Matches` constraint, because
 it is yet to be written. This should allow the full test suite to run.

---
 core/config/schema/core.data_types.schema.yml | 20 +++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 6dcbd4239ceb..8d8d8d031e82 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -495,16 +495,16 @@ field_config_base:
       label: 'Field type'
       constraints:
         NotNull: []
-        Matches:
-          # This field_storage_config entity must exist.
-          config:
-            type: field_storage_config
-            id:
-              - entity_type
-              - field_name
-          # The value found at this place in that config entity must match.
-          selector:
-            - type
+#        Matches:
+#          # This field_storage_config entity must exist.
+#          config:
+#            type: field_storage_config
+#            id:
+#              - entity_type
+#              - field_name
+#          # The value found at this place in that config entity must match.
+#          selector:
+#            - type
 
 core.base_field_override.*.*.*:
   type: field_config_base
-- 
GitLab


From c812ea495b980b35d552df95bb517db04274248f Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 12 Jul 2023 10:24:54 +0200
Subject: [PATCH 33/73] =?UTF-8?q?*Actually*=20add=20the=20`EntityBundleExi?=
 =?UTF-8?q?sts`=20constraint=E2=80=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../EntityBundleExistsConstraint.php          | 43 +++++++++++++++++
 .../EntityBundleExistsConstraintValidator.php | 46 +++++++++++++++++++
 2 files changed, 89 insertions(+)
 create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/EntityBundleExistsConstraint.php
 create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/EntityBundleExistsConstraintValidator.php

diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/EntityBundleExistsConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/EntityBundleExistsConstraint.php
new file mode 100644
index 000000000000..ccf7d83b7d3b
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/EntityBundleExistsConstraint.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks if a bundle exists on a certain content entity type.
+ *
+ * @Constraint(
+ *   id = "EntityBundleExists",
+ *   label = @Translation("Entity bundle exists", context = "Validation"),
+ *   type = "entity",
+ * )
+ */
+class EntityBundleExistsConstraint extends Constraint {
+
+  public $message = "The '@bundle' bundle does not exist on the '@entity_type_id' entity type.";
+
+  /**
+   * Fields which must have a unique combination among all entities of a type.
+   *
+   * @var string[]
+   */
+  public string $entityTypeId;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultOption() {
+    return 'entityTypeId';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRequiredOptions() {
+    return ['entityTypeId'];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/EntityBundleExistsConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/EntityBundleExistsConstraintValidator.php
new file mode 100644
index 000000000000..f356b2bd4073
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/EntityBundleExistsConstraintValidator.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Validates that a bundle exists on a certain content entity type.
+ */
+class EntityBundleExistsConstraintValidator extends ConstraintValidator {
+
+  use TreeAwareConstraintTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, Constraint $constraint) {
+    assert($constraint instanceof EntityBundleExistsConstraint);
+
+    if (!is_string($value)) {
+      throw new UnexpectedTypeException($value, 'string');
+    }
+
+    // @see \Drupal\Core\Config\TypedConfigManager::buildDataDefinition()
+    // @todo generalize: support multiple `%parent` occurrences, support hardcoded value
+    assert(str_starts_with($constraint->entityTypeId, '%parent.'));
+    $mapping = $this->getParentProperty();
+    $entity_type_id_property_path = str_replace('%parent.', '', $constraint->entityTypeId);
+    $entity_type_id = $mapping->get($entity_type_id_property_path)->getValue();
+
+    $entity_type_bundle_info = \Drupal::service('entity_type.bundle.info');
+    assert($entity_type_bundle_info instanceof EntityTypeBundleInfoInterface);
+    $bundles = $entity_type_bundle_info->getBundleInfo($entity_type_id);
+
+    if (!array_key_exists($value, $bundles)) {
+      $this->context->addViolation($constraint->message, [
+        '@bundle' => $value,
+        '@entity_type_id' => $entity_type_id,
+      ]);
+    }
+  }
+
+}
-- 
GitLab


From fe4589f6424a30e74a4451127d0414869598ed66 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 12 Jul 2023 13:46:45 +0200
Subject: [PATCH 34/73] Fix pre-existing bug in
 `FieldConfigValidationTest::setUp()`. This fixes most failures in
 `FieldConfigValidationTest`.

---
 .../field/tests/src/Kernel/Entity/FieldConfigValidationTest.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
index 10f8d5332538..a4319ab203db 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
@@ -24,7 +24,7 @@ protected function setUp(): void {
 
     $this->entity = FieldConfig::create([
       'field_storage' => $field_storage,
-      'bundle' => 'user',
+      'bundle' => 'entity_test',
     ]);
     $this->entity->save();
   }
-- 
GitLab


From 32b08f39c92602456e35a9eb6ddd605ade1ebb3f Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 12 Jul 2023 14:12:20 +0200
Subject: [PATCH 35/73] Switch `Field(Storage)Config` tests to use
 `entity_test_mul_with_bundle` config entities instead of `entity_test`.
 Otherwise bundle validation of `FieldConfig` is not testable.

---
 core/config/schema/core.data_types.schema.yml |  2 +
 .../Entity/FieldConfigValidationTest.php      | 54 +++++++++++++++++--
 .../FieldStorageConfigValidationTest.php      |  4 +-
 3 files changed, 55 insertions(+), 5 deletions(-)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 8d8d8d031e82..48aa78e8ae94 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -495,6 +495,8 @@ field_config_base:
       label: 'Field type'
       constraints:
         NotNull: []
+        # @todo In principle, we should validate that this plugin was indeed provided by `module`.
+        PluginExists: plugin.manager.field.field_type
 #        Matches:
 #          # This field_storage_config entity must exist.
 #          config:
diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
index a4319ab203db..260a311c2db5 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\field\Kernel\Entity;
 
+use Drupal\entity_test\Entity\EntityTestMulBundle;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\field\FieldStorageConfigInterface;
@@ -19,6 +20,25 @@ class FieldConfigValidationTest extends FieldStorageConfigValidationTest {
   protected function setUp(): void {
     parent::setUp();
 
+    // Specifically create a bundle for `entity_test_mul_with_bundle` content
+    // entities confusingly named `entity_test`, to allow testing the modifying
+    // of the `entity_type` field on FieldConfig entities without triggering
+    // additional validation errors.
+    // @see ::providerImmutableFields()
+    EntityTestMulBundle::create([
+      'id' => 'entity_test',
+      'label' => $this->randomString(),
+    ])->save();
+
+    // Similar to the above, but now for testing the immutability of the bundle:
+    // the bundle should exist to only get a validation error for immutability
+    // violation.
+    // @see ::providerImmutableFields()
+    EntityTestMulBundle::create([
+      'id' => 'foo',
+      'label' => $this->randomString(),
+    ])->save();
+
     // The field storage was created in the parent method.
     $field_storage = $this->entity;
 
@@ -61,12 +81,14 @@ public function providerImmutableFields(): array {
         ['field_name' => 'broken'],
       ],
       'entity_type' => [
-        ['entity_type' => 'entity_test_mul'],
+        // @see ::setUp()
+        ['entity_type' => 'entity_test'],
       ],
       'field_type' => [
         ['field_type' => 'email'],
       ],
       'bundle' => [
+        // @see ::setUp()
         ['bundle' => 'foo'],
       ],
     ];
@@ -83,11 +105,37 @@ public function testFieldTypePlugin(): void {
     ]);
   }
 
+  /**
+   * Tests that the target entity type is validated.
+   *
+   * 90% identical to the parent: an additional validation error is triggered
+   * due to bundle validation.
+   */
+  public function testEntityType(): void {
+    // Ensure the target entity type is valid to begin with.
+    $this->assertValidationErrors([]);
+
+    $this->entity->set('entity_type', 'strange_entity');
+    $this->assertValidationErrors([
+      '' => "The 'entity_type' property cannot be changed.",
+      'entity_type' => "The 'strange_entity' plugin does not exist.",
+      'bundle' => "The 'entity_test' bundle does not exist on the 'strange_entity' entity type.",
+    ]);
+
+    // A valid, but non-fieldable, entity type should raise an error.
+    $this->entity->set('entity_type', 'field_config');
+    $this->assertValidationErrors([
+      '' => "The 'entity_type' property cannot be changed.",
+      'entity_type' => "The 'field_config' plugin must implement or extend \Drupal\Core\Entity\FieldableEntityInterface.",
+      'bundle' => "The 'entity_test' bundle does not exist on the 'field_config' entity type.",
+    ]);
+  }
+
   /**
    * Tests that the bundle is validated.
    */
   public function testBundle(): void {
-    $entity_type_id = 'entity_test';
+    $entity_type_id = 'entity_test_mul_with_bundle';
     $field_name = 'test';
 
     // Assert that the FieldStorageConfig which this FieldConfig will depend on,
@@ -103,7 +151,7 @@ public function testBundle(): void {
       'bundle' => 'non_existent',
     ]);
     $this->assertValidationErrors([
-      'bundle' => "The 'non_existent' bundle does not exist on the 'entity_test' entity type.",
+      'bundle' => "The 'non_existent' bundle does not exist on the 'entity_test_mul_with_bundle' entity type.",
     ]);
 
     // Next, try to create it on a bundle that does exist.
diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
index ea21d2a9fbec..7733a4e6c810 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
@@ -26,7 +26,7 @@ protected function setUp(): void {
     $this->entity = FieldStorageConfig::create([
       'type' => 'boolean',
       'field_name' => 'test',
-      'entity_type' => 'entity_test',
+      'entity_type' => 'entity_test_mul_with_bundle',
       'custom_storage' => FALSE,
     ]);
     $this->entity->save();
@@ -38,7 +38,7 @@ public function providerImmutableFields(): array {
         ['field_name' => 'broken'],
       ],
       'entity_type' => [
-        ['entity_type' => 'entity_test_mul'],
+        ['entity_type' => 'entity_test'],
       ],
       'type' => [
         ['type' => 'email'],
-- 
GitLab


From a0d6ea6a8b46d39dc9ebaf1e0759199bb5a9d990 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Thu, 13 Jul 2023 13:04:39 -0400
Subject: [PATCH 36/73] Fix one test case

---
 core/modules/field/tests/src/Unit/FieldConfigEntityUnitTest.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/modules/field/tests/src/Unit/FieldConfigEntityUnitTest.php b/core/modules/field/tests/src/Unit/FieldConfigEntityUnitTest.php
index 3cab469051f5..cf2c2ed5a3d8 100644
--- a/core/modules/field/tests/src/Unit/FieldConfigEntityUnitTest.php
+++ b/core/modules/field/tests/src/Unit/FieldConfigEntityUnitTest.php
@@ -135,7 +135,7 @@ public function testCalculateDependencies() {
       ->with('test_field')
       ->willReturn(['provider' => 'test_module', 'config_dependencies' => ['module' => ['test_module2']], 'class' => '\Drupal\Tests\field\Unit\DependencyFieldItem']);
 
-    $this->fieldStorage->expects($this->once())
+    $this->fieldStorage->expects($this->atLeastOnce())
       ->method('getConfigDependencyName')
       ->willReturn('field.storage.test_entity_type.test_field');
 
-- 
GitLab


From 81b05c5570659ae4c190ef4f995b1e022deafe36 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Fri, 14 Jul 2023 14:01:49 +0200
Subject: [PATCH 37/73] Fix failure in
 `FieldSettingsTest::testConfigurableFieldSettings()`: `FieldStorageConfig`
 did not exist yet!

---
 core/tests/Drupal/KernelTests/Core/Field/FieldSettingsTest.php | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/core/tests/Drupal/KernelTests/Core/Field/FieldSettingsTest.php b/core/tests/Drupal/KernelTests/Core/Field/FieldSettingsTest.php
index 1ba196ce583f..0d03b3dbb734 100644
--- a/core/tests/Drupal/KernelTests/Core/Field/FieldSettingsTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Field/FieldSettingsTest.php
@@ -111,6 +111,7 @@ public function testConfigurableFieldSettings() {
       'entity_type' => 'entity_test',
       'type' => 'test_field',
     ]);
+    $field_storage->save();
     $field = FieldConfig::create([
       'field_storage' => $field_storage,
       'bundle' => 'entity_test',
@@ -130,6 +131,7 @@ public function testConfigurableFieldSettings() {
       'test_field_setting' => 'dummy test string',
       'translatable_field_setting' => 'a translatable field setting',
       'field_setting_from_config_data' => 'TRUE',
+      'storage_setting_from_config_data' => 'TRUE',
     ];
     $this->assertEquals($expected_settings, $field->getSettings());
 
-- 
GitLab


From e241bba21f5ea9bc101edd408f43814ac9d1a531 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Mon, 17 Jul 2023 11:04:28 -0400
Subject: [PATCH 38/73] Fix FieldConfigEntityUnitTest

---
 .../field/tests/src/Unit/FieldConfigEntityUnitTest.php      | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/core/modules/field/tests/src/Unit/FieldConfigEntityUnitTest.php b/core/modules/field/tests/src/Unit/FieldConfigEntityUnitTest.php
index 544ee2f59c33..47507659625d 100644
--- a/core/modules/field/tests/src/Unit/FieldConfigEntityUnitTest.php
+++ b/core/modules/field/tests/src/Unit/FieldConfigEntityUnitTest.php
@@ -245,7 +245,9 @@ public function testToArray() {
       'default_value' => [],
       'default_value_callback' => '',
       'settings' => [],
-      'dependencies' => [],
+      'dependencies' => [
+        'config' => [NULL],
+      ],
       'field_type' => 'test_field',
     ];
     $this->entityTypeManager->expects($this->any())
@@ -271,7 +273,7 @@ public function testToArray() {
   public function testGetType() {
     // Ensure that FieldConfig::getType() is not delegated to
     // FieldStorage.
-    $this->entityFieldManager->expects($this->never())
+    $this->entityFieldManager->expects($this->atLeastOnce())
       ->method('getFieldStorageDefinitions');
     $this->fieldStorage->expects($this->never())
       ->method('getType');
-- 
GitLab


From b239338f071012d2c6dbc0baa4290ce6c080b3d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Tue, 25 Jul 2023 10:42:20 -0400
Subject: [PATCH 39/73] Cache clear in MediaSourceAudioVideoTest

---
 .../src/FunctionalJavascript/MediaSourceAudioVideoTest.php  | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php
index 05ff322ecb39..4e8a8f70f9b1 100644
--- a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceAudioVideoTest.php
@@ -32,6 +32,9 @@ public function testAudioTypeCreation() {
     $type_name = 'audio_type';
     $field_name = 'field_media_' . $source_id;
     $this->doTestCreateMediaType($type_name, $source_id);
+    // @todo This line can be removed when the entity field manager's cache
+    //   is automatically reset when a field is created.
+    $this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
 
     // Check that the source field was created with the correct settings.
     $storage = FieldStorageConfig::load("media.$field_name");
@@ -81,6 +84,9 @@ public function testVideoTypeCreation() {
     $type_name = 'video_type';
     $field_name = 'field_media_' . $source_id;
     $this->doTestCreateMediaType($type_name, $source_id);
+    // @todo This line can be removed when the entity field manager's cache
+    //   is automatically reset when a field is created.
+    $this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
 
     // Check that the source field was created with the correct settings.
     $storage = FieldStorageConfig::load("media.$field_name");
-- 
GitLab


From 70460407c98f5370eb8d8ed03c90861f0914c58a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Tue, 25 Jul 2023 20:40:43 -0400
Subject: [PATCH 40/73] Remove the troublesome line and change the test to
 compensate for the fact that fields are weird

---
 core/modules/field/src/Entity/FieldConfig.php |  4 ----
 .../Entity/FieldConfigValidationTest.php      | 19 ++++++++++++++-----
 2 files changed, 14 insertions(+), 9 deletions(-)

diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php
index 8bd508a25a7f..ca59414c3306 100644
--- a/core/modules/field/src/Entity/FieldConfig.php
+++ b/core/modules/field/src/Entity/FieldConfig.php
@@ -133,10 +133,6 @@ public function __construct(array $values, $entity_type = 'field_config') {
     }
 
     parent::__construct($values, $entity_type);
-
-    // For this to be a valid config entity, at the very least a dependency on a
-    // FieldStorageConfig entity must be present.
-    $this->addDependency('config', $this->getFieldStorageDefinition()->getConfigDependencyName());
   }
 
   /**
diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
index 01a787ed504b..434519fb88ce 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
@@ -128,17 +128,26 @@ public function testBundle(): void {
 
     // Try to create an instance of this field on a bundle that does not exist.
     $this->entity = FieldConfig::create([
-      'entity_type' => $entity_type_id,
-      'field_name' => $field_name,
-      'field_type' => $field_storage_config->getType(),
       'bundle' => 'non_existent',
+      'field_storage' => $field_storage_config,
     ]);
+    // The field storage is not listed in the entity's config dependencies,
+    // because the dependencies have not been recalculated yet. They can't be
+    // recalculated until the target bundle exists, or we'll get an exception.
+    // So, for testing purposes, use reflection to access the protected
+    // addDependency() method and add the field storage as a dependency.
+    // @see \Drupal\field\Entity\FieldConfig::calculateDependencies() and
+    //   ::getFieldStorageDefinition()
+    (new \ReflectionMethod($this->entity, 'addDependency'))
+      ->invoke($this->entity, 'config', $field_storage_config->getConfigDependencyName());
     $this->assertValidationErrors([
       'bundle' => "The 'non_existent' bundle does not exist on the 'entity_test_mul_with_bundle' entity type.",
     ]);
 
-    // Next, try to create it on a bundle that does exist.
-    $this->entity->set('bundle', 'one');
+    // Next, try to create it on a bundle that *does* exist. We need to
+    // recalculate dependencies because that's how we check whether or not the
+    // bundle exists.
+    $this->entity->set('bundle', 'one')->calculateDependencies();
     $this->assertValidationErrors([]);
   }
 
-- 
GitLab


From 4f3f44abe21a2c50ece3308b0a4efa966d5979d9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Tue, 25 Jul 2023 21:01:10 -0400
Subject: [PATCH 41/73] cs

---
 .../tests/src/Kernel/Entity/FieldConfigValidationTest.php      | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
index 434519fb88ce..f1ff61e2f019 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
@@ -136,8 +136,7 @@ public function testBundle(): void {
     // recalculated until the target bundle exists, or we'll get an exception.
     // So, for testing purposes, use reflection to access the protected
     // addDependency() method and add the field storage as a dependency.
-    // @see \Drupal\field\Entity\FieldConfig::calculateDependencies() and
-    //   ::getFieldStorageDefinition()
+    // @see \Drupal\Core\Field\FieldConfigBase::calculateDependencies()
     (new \ReflectionMethod($this->entity, 'addDependency'))
       ->invoke($this->entity, 'config', $field_storage_config->getConfigDependencyName());
     $this->assertValidationErrors([
-- 
GitLab


From c123be8fadbeb216dc93244791ddc84bbfbfe6b0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Tue, 25 Jul 2023 22:12:31 -0400
Subject: [PATCH 42/73] Revert FieldConfigEntityUnitTest

---
 .../field/tests/src/Unit/FieldConfigEntityUnitTest.php    | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/core/modules/field/tests/src/Unit/FieldConfigEntityUnitTest.php b/core/modules/field/tests/src/Unit/FieldConfigEntityUnitTest.php
index 47507659625d..5797c8a35205 100644
--- a/core/modules/field/tests/src/Unit/FieldConfigEntityUnitTest.php
+++ b/core/modules/field/tests/src/Unit/FieldConfigEntityUnitTest.php
@@ -132,7 +132,7 @@ public function testCalculateDependencies() {
       ->with('test_field')
       ->willReturn(['provider' => 'test_module', 'config_dependencies' => ['module' => ['test_module2']], 'class' => '\Drupal\Tests\field\Unit\DependencyFieldItem']);
 
-    $this->fieldStorage->expects($this->atLeastOnce())
+    $this->fieldStorage->expects($this->once())
       ->method('getConfigDependencyName')
       ->willReturn('field.storage.test_entity_type.test_field');
 
@@ -245,9 +245,7 @@ public function testToArray() {
       'default_value' => [],
       'default_value_callback' => '',
       'settings' => [],
-      'dependencies' => [
-        'config' => [NULL],
-      ],
+      'dependencies' => [],
       'field_type' => 'test_field',
     ];
     $this->entityTypeManager->expects($this->any())
@@ -273,7 +271,7 @@ public function testToArray() {
   public function testGetType() {
     // Ensure that FieldConfig::getType() is not delegated to
     // FieldStorage.
-    $this->entityFieldManager->expects($this->atLeastOnce())
+    $this->entityFieldManager->expects($this->never())
       ->method('getFieldStorageDefinitions');
     $this->fieldStorage->expects($this->never())
       ->method('getType');
-- 
GitLab


From 9eea5b24d28dcc7120149e1bbf3628b5d1193ef1 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 31 Jan 2024 11:27:59 +0100
Subject: [PATCH 43/73] Remove the obsolete `ImmutableFields` constraint thanks
 to #3382580.

---
 .../Constraint/ImmutableFieldsConstraint.php  | 48 -------------
 .../ImmutableFieldsConstraintValidator.php    | 72 -------------------
 2 files changed, 120 deletions(-)
 delete mode 100644 core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraint.php
 delete mode 100644 core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php

diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraint.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraint.php
deleted file mode 100644
index 138c36495eb8..000000000000
--- a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraint.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-
-declare(strict_types = 1);
-
-namespace Drupal\Core\Config\Plugin\Validation\Constraint;
-
-use Symfony\Component\Validator\Constraint;
-
-/**
- * Checks if config entity properties have been changed.
- *
- * @Constraint(
- *   id = "ImmutableFields",
- *   label = @Translation("Fields are unchanged", context = "Validation"),
- *   type = { "entity" }
- * )
- */
-class ImmutableFieldsConstraint extends Constraint {
-
-  /**
-   * The error message if an immutable property has been changed.
-   *
-   * @var string
-   */
-  public string $message = "The '@name' property cannot be changed.";
-
-  /**
-   * The names of the immutable fields.
-   *
-   * @var string[]
-   */
-  public array $fields = [];
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getDefaultOption() {
-    return 'fields';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getRequiredOptions() {
-    return ['fields'];
-  }
-
-}
diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php
deleted file mode 100644
index a5926d8251e6..000000000000
--- a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/ImmutableFieldsConstraintValidator.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-
-declare(strict_types = 1);
-
-namespace Drupal\Core\Config\Plugin\Validation\Constraint;
-
-use Drupal\Core\Config\Entity\ConfigEntityInterface;
-use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\Validator\Constraint;
-use Symfony\Component\Validator\ConstraintValidator;
-use Symfony\Component\Validator\Exception\LogicException;
-use Symfony\Component\Validator\Exception\RuntimeException;
-use Symfony\Component\Validator\Exception\UnexpectedValueException;
-
-/**
- * Validates the ImmutableFields constraint.
- */
-class ImmutableFieldsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
-
-  /**
-   * Constructs an ImmutableFieldsConstraintValidator object.
-   *
-   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
-   *   The entity type manager service.
-   */
-  public function __construct(protected EntityTypeManagerInterface $entityTypeManager) {
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container): static {
-    return new static(
-      $container->get('entity_type.manager')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validate(mixed $value, Constraint $constraint) {
-    assert($constraint instanceof ImmutableFieldsConstraint);
-
-    if (!$value instanceof ConfigEntityInterface) {
-      throw new UnexpectedValueException($value, ConfigEntityInterface::class);
-    }
-    // This validation is irrelevant on new entities.
-    if ($value->isNew()) {
-      return;
-    }
-
-    $id = $value->getOriginalId() ?: $value->id();
-    if (empty($id)) {
-      throw new LogicException('The entity does not have an ID.');
-    }
-
-    $original = $this->entityTypeManager->getStorage($value->getEntityTypeId())
-      ->loadUnchanged($id);
-    if (empty($original)) {
-      throw new RuntimeException('The original entity could not be loaded.');
-    }
-
-    foreach ($constraint->fields as $name) {
-      if ($original->get($name) !== $value->get($name)) {
-        $this->context->addViolation($constraint->message, ['@name' => $name]);
-      }
-    }
-  }
-
-}
-- 
GitLab


From 1d6c5d0400de1f1b9d10e59be8e6b4cffed6ddd3 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 31 Jan 2024 11:20:56 +0100
Subject: [PATCH 44/73] Use `FullyValidatable` on `FieldConfig`,
 `FieldStorageConfig` and `BaseFieldOverride`. And comment out constraints
 that do not yet exist.

---
 core/config/schema/core.data_types.schema.yml | 35 ++++++-------------
 .../field/config/schema/field.schema.yml      | 18 ++++++----
 .../Entity/FieldConfigValidationTest.php      |  8 +++++
 .../BaseFieldOverrideValidationTest.php       |  8 +++++
 4 files changed, 38 insertions(+), 31 deletions(-)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index ffcd2021ae1d..330dcbb7093b 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -488,15 +488,15 @@ field_config_base:
   type: config_entity
   constraints:
     UniqueFieldCombination: [entity_type, bundle, field_name]
-    FieldStorageExistsOnFieldableContentEntityType:
-      entityType: entity_type
-      fieldName: field_name
+    # @todo
+    #FieldStorageExistsOnFieldableContentEntityType:
+    #  entityType: entity_type
+    #  fieldName: field_name
   mapping:
     id:
       type: string
       label: 'ID'
       constraints:
-        NotNull: []
         StringParts:
           separator: .
           parts:
@@ -506,13 +506,10 @@ field_config_base:
     field_name:
       type: string
       label: 'Field name'
-      constraints:
-        NotNull: []
     entity_type:
       type: string
       label: 'Entity type'
       constraints:
-        NotNull: []
         PluginExists:
           manager: entity_type.manager
           interface: \Drupal\Core\Entity\FieldableEntityInterface
@@ -520,35 +517,24 @@ field_config_base:
       type: string
       label: 'Bundle'
       constraints:
-        NotNull: []
         EntityBundleExists: '%parent.entity_type'
     label:
       type: required_label
       label: 'Label'
-      constraints:
-        NotNull: []
     description:
       type: text
       label: 'Help text'
-      constraints:
-        # Optional, so at minimum must be set to the empty string.
-        NotNull: []
+      nullable: true
     required:
       type: boolean
       label: 'Required field'
-      constraints:
-        NotNull: []
     translatable:
       type: boolean
       label: 'Translatable'
-      constraints:
-        NotNull: []
     default_value:
       type: sequence
       label: 'Default values'
-      constraints:
-        # Optional, so at minimum must be set to the empty array.
-        NotNull: []
+      nullable: true
       sequence:
         type: field.value.[%parent.%parent.field_type]
         label: 'Default value'
@@ -566,7 +552,6 @@ field_config_base:
       type: string
       label: 'Field type'
       constraints:
-        NotNull: []
         # @todo In principle, we should validate that this plugin was indeed provided by `module`.
         PluginExists: plugin.manager.field.field_type
 #        Matches:
@@ -584,9 +569,11 @@ core.base_field_override.*.*.*:
   type: field_config_base
   label: 'Base field bundle override'
   constraints:
-    FieldIsBaseField:
-      entityType: entity_type
-      fieldName: field_name
+    FullyValidatable: ~
+    # @todo
+    #FieldIsBaseField:
+    #  entityType: entity_type
+    #  fieldName: field_name
 
 core.date_format.*:
   type: config_entity
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index 1c5db39e9fde..cbcf24076323 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -12,11 +12,13 @@ field.storage.*.*:
   type: config_entity
   label: 'Field'
   constraints:
+    FullyValidatable: ~
     UniqueFieldCombination: [entity_type, field_name]
-    IsUnchanged:
-      selector: locked
-    IsFalse:
-      selector: locked
+    # @todo
+    #IsUnchanged:
+    #  selector: locked
+    #IsFalse:
+    #  selector: locked
   mapping:
     id:
       type: string
@@ -97,6 +99,8 @@ field.field.*.*.*:
   type: field_config_base
   label: 'Field'
   constraints:
-    FieldIsConfigurableField:
-      entityType: entity_type
-      fieldName: field_name
+    FullyValidatable: ~
+    # @todo
+    #FieldIsConfigurableField:
+    #  entityType: entity_type
+    #  fieldName: field_name
diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
index f1ff61e2f019..3b058daa6838 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
@@ -16,6 +16,14 @@
  */
 class FieldConfigValidationTest extends FieldStorageConfigValidationTest {
 
+  /**
+   * {@inheritdoc}
+   */
+  protected static array $propertiesWithOptionalValues = [
+    'default_value',
+    'description',
+  ];
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
index a156b7d252e3..d4c95ece4284 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
@@ -17,6 +17,14 @@ class BaseFieldOverrideValidationTest extends ConfigEntityValidationTestBase {
 
   use ContentTypeCreationTrait;
 
+  /**
+   * {@inheritdoc}
+   */
+  protected static array $propertiesWithOptionalValues = [
+    'default_value',
+    'description',
+  ];
+
   /**
    * {@inheritdoc}
    */
-- 
GitLab


From 3d666b9de5a4f528bdb16795c3e11ba5848d09c2 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 7 Feb 2024 14:15:08 +0100
Subject: [PATCH 45/73] Add `MatchesOtherConfigValue` validation constraint and
 use it for the `type` property in `type: field_config_base`.

---
 core/config/schema/core.data_types.schema.yml |  17 +--
 .../MatchesOtherConfigValueConstraint.php     |  55 ++++++++
 ...hesOtherConfigValueConstraintValidator.php | 121 ++++++++++++++++++
 .../Constraint/TreeAwareConstraintTrait.php   |   5 +-
 .../field/config/schema/field.schema.yml      |   7 +-
 5 files changed, 189 insertions(+), 16 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraint.php
 create mode 100644 core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraintValidator.php

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 330dcbb7093b..c011d7264ab2 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -554,16 +554,13 @@ field_config_base:
       constraints:
         # @todo In principle, we should validate that this plugin was indeed provided by `module`.
         PluginExists: plugin.manager.field.field_type
-#        Matches:
-#          # This field_storage_config entity must exist.
-#          config:
-#            type: field_storage_config
-#            id:
-#              - entity_type
-#              - field_name
-#          # The value found at this place in that config entity must match.
-#          selector:
-#            - type
+        MatchesOtherConfigValue:
+          # This field_storage_config entity must exist. This is already guaranteed.
+          # @see \Drupal\field\Entity\FieldConfig
+          # @see \Drupal\Core\Config\Plugin\Validation\Constraint\RequiredConfigDependenciesConstraint
+          prefix: field.storage.
+          id: ['%parent.entity_type', '%parent.field_name']
+          propertyPath: type
 
 core.base_field_override.*.*.*:
   type: field_config_base
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraint.php
new file mode 100644
index 000000000000..f4f40b65fc82
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraint.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks that a value in this config matches a value in other config.
+ *
+ * @Constraint(
+ *   id = "MatchesOtherConfigValue",
+ *   label = @Translation("Value must match value in other config", context = "Validation")
+ * )
+ */
+class MatchesOtherConfigValueConstraint extends Constraint {
+
+  /**
+   * The error message if the string does not match.
+   *
+   * @var string
+   */
+  public string $message = "Expected this to match the value in the '@other_config_name' config at the '@other_config_property_path' property: '@expected_value' was expected, not '@actual_value'.";
+
+  /**
+   * The config prefix to use.
+   *
+   * @var string
+   */
+  public string $prefix;
+
+  /**
+   * One or more expressions that resolve to the config entity ID parts.
+   *
+   * @var string|string[]
+   * @see \Drupal\Core\Config\Schema\TypeResolver::resolveExpression()
+   */
+  public string|array $id;
+
+  /**
+   * The property path.
+   *
+   * @var string
+   */
+  public string $propertyPath;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRequiredOptions() {
+    return ['prefix', 'id', 'propertyPath'];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraintValidator.php
new file mode 100644
index 000000000000..5b70d43fde4b
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraintValidator.php
@@ -0,0 +1,121 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\Schema\TypeResolver;
+use Drupal\Core\Config\TypedConfigManagerInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\TypedData\PrimitiveInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Validates the MatchesOtherConfigValue constraint.
+ */
+class MatchesOtherConfigValueConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+  /**
+   * Constructs a MatchesOtherConfigValueConstraintValidator object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
+   *   The config factory service.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
+   */
+  public function __construct(
+    protected readonly ConfigFactoryInterface $configFactory,
+    protected readonly TypedConfigManagerInterface $typedConfigManager,
+  ) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get(ConfigFactoryInterface::class),
+      $container->get(TypedConfigManagerInterface::class)
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(mixed $value, Constraint $constraint) {
+    if (!$constraint instanceof MatchesOtherConfigValueConstraint) {
+      throw new UnexpectedTypeException($constraint, MatchesOtherConfigValueConstraint::class);
+    }
+
+    $this_property = $this->context->getObject();
+    assert($this_property instanceof TypedDataInterface);
+    $this_data_definition = $this_property->getDataDefinition();
+
+    // This validation constraint should only be used for scalar values
+    // (primitives()), not for comparisons across entire arrays (sequences or
+    // mappings). That is possible in theory but not in practice yet.
+    // @todo Re-evaluate this after https://www.drupal.org/project/drupal/issues/3230826
+    if (!is_subclass_of($this_data_definition->getClass(), PrimitiveInterface::class)) {
+      throw new \LogicException(sprintf(
+        'The MatchesOtherConfigValue constraint used at %s uses the %s config schema type. Only config schema types implementing %s are supported.',
+        $this_property->getPropertyPath(),
+        $this_data_definition->getDataType(),
+        PrimitiveInterface::class,
+      ));
+    }
+
+    // Determine the name of the other config object.
+    $id_parts = array_map(
+      fn (string $expression): mixed => TypeResolver::resolveExpression($expression, $this_property),
+      (array) $constraint->id
+    );
+    $other_config_name = $constraint->prefix . implode('.', $id_parts);
+
+    // When a developer uses this constraint inappropriately, guide them.
+    if ($other_config_name === $this->context->getRoot()->getName()) {
+      throw new \LogicException(sprintf('The MatchesOtherConfigValue constraint used in %s is configured to not look at another config object but at itself.', $this_property->getPropertyPath()));
+    }
+    if (!in_array($other_config_name, $this->configFactory->listAll(), TRUE)) {
+      throw new \LogicException(sprintf('The config %s does not exist, and is assumed to exist by this constraint. This means a RequiredConfigDependencies is absent from the config entity type.', $other_config_name));
+    }
+
+    $other_config_object = $this->typedConfigManager->get($other_config_name);
+    $other_property_to_match = $other_config_object->get($constraint->propertyPath);
+
+    // When a developer uses this constraint inappropriately, guide them.
+    $other_data_definition = $other_property_to_match->getDataDefinition();
+    if ($this_data_definition->getDataType() !== $other_data_definition->getDataType()) {
+      throw new \LogicException(sprintf(
+        'The config schema type of this value (%s) does not match the config schema type (%s) of the %s property in the %s config object.',
+        $this_data_definition->getDataType(),
+        $other_data_definition->getDataType(),
+        $constraint->propertyPath,
+        $other_config_name,
+      ));
+    }
+    $missing_constraints = array_diff_key($other_data_definition->getConstraints(), $this_data_definition->getConstraints());
+    if (!empty($missing_constraints)) {
+      throw new \LogicException(sprintf(
+        'Fewer constraints are applied to this value than to the %s property in the %s config object. The following are missing: %s.',
+        $constraint->propertyPath,
+        $other_config_name,
+        implode(', ', array_keys($missing_constraints)),
+      ));
+    }
+
+    assert($value === $this_property->getValue());
+    if ($value !== $other_property_to_match->getValue()) {
+      $this->context->addViolation($constraint->message, [
+        '@other_config_name' => $other_config_name,
+        '@other_config_property_path' => $constraint->propertyPath,
+        '@expected_value' => $other_property_to_match->getString(),
+        '@actual_value' => $this_property->getString(),
+      ]);
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php
index dfc3c67ec120..b0b9dab8604d 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/TreeAwareConstraintTrait.php
@@ -51,11 +51,12 @@ private static function findPropertyForPath(ComplexDataInterface $tree, array $p
     $elements = $tree->getElements();
     $name = array_shift($property_path);
 
-    // Check if it exists.
     if (!isset($elements[$name])) {
       throw new \OutOfRangeException();
     }
-
+    if (empty($property_path)) {
+      return $elements[$name];
+    }
     return self::findPropertyForPath($elements[$name], $property_path);
   }
 
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index cbcf24076323..dee6ce9f358f 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -47,7 +47,6 @@ field.storage.*.*:
       type: string
       label: 'Type'
       constraints:
-        NotBlank: []
         # @todo In principle, we should validate that this plugin was indeed provided by `module`.
         PluginExists: plugin.manager.field.field_type
     settings:
@@ -55,9 +54,9 @@ field.storage.*.*:
     module:
       type: string
       label: 'Module'
-      constraints:
-        NotBlank: []
-        # ExtensionExists: 'module'
+      constraints: {}
+      # @todo Make ExtensionExistsConstraintValidator aware that `core` is ALWAYS installed 🙄, then enable this constraint again.
+      # ExtensionExists: 'module'
     locked:
       type: boolean
       label: 'Locked'
-- 
GitLab


From 1ef827143f47fdb86e8bf8bcce0dd51aff674e49 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 7 Feb 2024 14:39:41 +0100
Subject: [PATCH 46/73] Adjust some of the existing tests in
 `FieldConfigValidationTest` now that validation has become more complete.

---
 .../Entity/FieldConfigValidationTest.php      | 49 ++++++++++++++-----
 .../FieldStorageConfigValidationTest.php      |  4 +-
 .../ContentLanguageSettingsValidationTest.php |  2 +-
 ...BuilderEntityViewDisplayValidationTest.php |  2 +-
 .../src/Kernel/MediaTypeValidationTest.php    |  2 +-
 .../Config/ConfigEntityValidationTestBase.php |  9 +++-
 .../BaseFieldOverrideValidationTest.php       |  2 +-
 .../EntityFormDisplayValidationTest.php       |  2 +-
 .../Entity/EntityFormModeValidationTest.php   |  2 +-
 .../EntityViewDisplayValidationTest.php       |  2 +-
 .../Entity/EntityViewModeValidationTest.php   |  2 +-
 11 files changed, 53 insertions(+), 25 deletions(-)

diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
index 3b058daa6838..3459f16d8a2f 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
@@ -92,7 +92,10 @@ public function testFieldTypePlugin(): void {
     $this->entity->set('settings', []);
     $this->assertValidationErrors([
       '' => "The 'field_type' property cannot be changed.",
-      'field_type' => "The 'non_existent' plugin does not exist.",
+      'field_type' => [
+        "The 'non_existent' plugin does not exist.",
+        "Expected this to match the value in the 'field.storage.entity_test_mul_with_bundle.test' config at the 'type' property: 'boolean' was expected, not 'non_existent'.",
+      ],
     ]);
   }
 
@@ -106,19 +109,18 @@ public function testEntityType(): void {
     // Ensure the target entity type is valid to begin with.
     $this->assertValidationErrors([]);
 
-    $this->entity->set('entity_type', 'strange_entity');
-    $this->assertValidationErrors([
-      '' => "The 'entity_type' property cannot be changed.",
-      'entity_type' => "The 'strange_entity' plugin does not exist.",
-      'bundle' => "The 'one' bundle does not exist on the 'strange_entity' entity type.",
-    ]);
-
-    // A valid, but non-fieldable, entity type should raise an error.
-    $this->entity->set('entity_type', 'field_config');
+    // Ensure that it is at least plausible that the `entity_type` is modified:
+    // a corresponding FieldStorageConfig must exist due to the entity-level
+    // `RequiredConfigDependencies` constraint.
+    FieldStorageConfig::create([
+      'type' => 'boolean',
+      'field_name' => 'test',
+      'entity_type' => 'entity_test',
+    ])->save();
+    $this->entity->set('entity_type', 'entity_test');
     $this->assertValidationErrors([
       '' => "The 'entity_type' property cannot be changed.",
-      'entity_type' => "The 'field_config' plugin must implement or extend \Drupal\Core\Entity\FieldableEntityInterface.",
-      'bundle' => "The 'one' bundle does not exist on the 'field_config' entity type.",
+      'bundle' => "The 'one' bundle does not exist on the 'entity_test' entity type.",
     ]);
   }
 
@@ -204,15 +206,36 @@ public function testTargetBundleMustExist(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     // If we don't clear the previous settings here, we will get unrelated
     // validation errors (in addition to the one we're expecting), because the
     // settings from the *old* field_type won't match the config schema for the
     // settings of the *new* field_type.
     $this->entity->set('settings', []);
+
+    // Ensure that it is at least plausible that the `entity_type` is modified:
+    // a corresponding FieldStorageConfig must exist due to the entity-level
+    // `RequiredConfigDependencies` constraint.
+    FieldStorageConfig::create([
+      'type' => 'boolean',
+      'field_name' => 'test',
+      'entity_type' => 'entity_test_with_bundle',
+    ])->save();
+    // Same thing for `field_name`.
+    FieldStorageConfig::create([
+      'type' => 'boolean',
+      'field_name' => 'foobar',
+      'entity_type' => 'entity_test_mul_with_bundle',
+    ])->save();
+
     parent::testImmutableProperties([
       'bundle' => 'another',
       'field_type' => 'email',
+      'field_name' => 'foobar',
+    ], [
+      'field_type' => [
+        'field_type' => "Expected this to match the value in the 'field.storage.entity_test_mul_with_bundle.test' config at the 'type' property: 'boolean' was expected, not 'email'.",
+      ],
     ]);
   }
 
diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
index 60be02100130..eded27d95ff4 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php
@@ -36,11 +36,11 @@ protected function setUp(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     parent::testImmutableProperties($valid_values + [
       'entity_type' => 'entity_test_with_bundle',
       'type' => 'email',
-    ]);
+    ], $additional_expected_validation_errors_when_modified);
   }
 
   /**
diff --git a/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php b/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php
index 93f05ac59d21..dedfc156e08a 100644
--- a/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php
+++ b/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php
@@ -68,7 +68,7 @@ public function testTargetBundleMustExist(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     parent::testImmutableProperties([
       'target_entity_type_id' => 'entity_test_with_bundle',
       'target_bundle' => 'bravo',
diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php
index a09145a94d49..3b6ffaea23e9 100644
--- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php
+++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php
@@ -68,7 +68,7 @@ public function testLabelValidation(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     parent::testImmutableProperties([
       'targetEntityType' => 'entity_test_with_bundle',
       'bundle' => 'two',
diff --git a/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php b/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php
index 4f8ba575a529..5c018827c00f 100644
--- a/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php
+++ b/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php
@@ -30,7 +30,7 @@ protected function setUp(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     // If we don't clear the previous settings here, we will get unrelated
     // validation errors (in addition to the one we're expecting), because the
     // settings from the *old* source won't match the config schema for the
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
index 8b48cdd6ee23..ef7080f80d6e 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
@@ -465,15 +465,20 @@ public function testLangcode(): void {
    *   (optional) The values to set for the immutable properties, keyed by name.
    *   This should be used if the immutable properties can only accept certain
    *   values, e.g. valid plugin IDs.
+   * @param string[]|null $additional_expected_validation_errors_when_modified
+   *   Some immutable 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().
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     $constraints = $this->entity->getEntityType()->getConstraints();
     $this->assertNotEmpty($constraints['ImmutableProperties'], 'All config entities should have at least one immutable ID property.');
 
     foreach ($constraints['ImmutableProperties'] as $property_name) {
       $original_value = $this->entity->get($property_name);
       $this->entity->set($property_name, $valid_values[$property_name] ?? $this->randomMachineName());
-      $this->assertValidationErrors([
+      $this->assertValidationErrors(($additional_expected_validation_errors_when_modified[$property_name] ?? []) + [
         '' => "The '$property_name' property cannot be changed.",
       ]);
       $this->entity->set($property_name, $original_value);
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
index d4c95ece4284..351df0810c9c 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
@@ -83,7 +83,7 @@ public function testTargetBundleMustExist(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     // If we don't clear the previous settings here, we will get unrelated
     // validation errors (in addition to the one we're expecting), because the
     // settings from the *old* field_type won't match the config schema for the
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php
index dca5203e3824..8651468e84e3 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php
@@ -109,7 +109,7 @@ public function testTargetBundleMustExist(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     parent::testImmutableProperties([
       'targetEntityType' => 'entity_test_with_bundle',
       'bundle' => 'two',
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php
index 3725ffec67c6..a783ff6ef263 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormModeValidationTest.php
@@ -37,7 +37,7 @@ protected function setUp(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     $valid_values['id'] = 'user.test_changed';
     parent::testImmutableProperties($valid_values);
   }
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php
index 8596a96365fe..28d96ec9122b 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php
@@ -66,7 +66,7 @@ public function testTargetBundleMustExist(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     parent::testImmutableProperties([
       'targetEntityType' => 'entity_test_with_bundle',
       'bundle' => 'two',
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php
index 53f759121ae6..8701f35bd7d9 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewModeValidationTest.php
@@ -37,7 +37,7 @@ protected function setUp(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     $valid_values['id'] = 'user.test_changed';
     parent::testImmutableProperties($valid_values);
   }
-- 
GitLab


From 8fc28aff0078d67691ad3259629fef014a686bf4 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 7 Feb 2024 14:50:54 +0100
Subject: [PATCH 47/73] Move the `MatchesOtherConfigValue` constraint out of
 `type: field_config_base` because it affects `BaseFieldOverride` too and it
 should only affect `FieldConfig`.

---
 core/config/schema/core.data_types.schema.yml     |  7 -------
 core/modules/field/config/schema/field.schema.yml | 12 ++++++++++++
 2 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index c011d7264ab2..42c64a46a0a0 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -554,13 +554,6 @@ field_config_base:
       constraints:
         # @todo In principle, we should validate that this plugin was indeed provided by `module`.
         PluginExists: plugin.manager.field.field_type
-        MatchesOtherConfigValue:
-          # This field_storage_config entity must exist. This is already guaranteed.
-          # @see \Drupal\field\Entity\FieldConfig
-          # @see \Drupal\Core\Config\Plugin\Validation\Constraint\RequiredConfigDependenciesConstraint
-          prefix: field.storage.
-          id: ['%parent.entity_type', '%parent.field_name']
-          propertyPath: type
 
 core.base_field_override.*.*.*:
   type: field_config_base
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index dee6ce9f358f..b0272d8619a8 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -97,6 +97,18 @@ field.storage.*.*:
 field.field.*.*.*:
   type: field_config_base
   label: 'Field'
+  mapping:
+    # Extend `field_config_base:field_type` with a FieldConfig entity-specific
+    # constraint. (This does not apply to BaseFieldOverride entities.)
+    field_type:
+      constraints:
+        MatchesOtherConfigValue:
+          # This field_storage_config entity must exist. This is already guaranteed.
+          # @see \Drupal\field\Entity\FieldConfig
+          # @see \Drupal\Core\Config\Plugin\Validation\Constraint\RequiredConfigDependenciesConstraint
+          prefix: field.storage.
+          id: ['%parent.entity_type', '%parent.field_name']
+          propertyPath: type
   constraints:
     FullyValidatable: ~
     # @todo
-- 
GitLab


From 8522420e78f4c551ed27a9cfa897f86b228b0a74 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 7 Feb 2024 15:04:42 +0100
Subject: [PATCH 48/73] Expand `ExtensionExists` constraint test coverage.

---
 .../ExtensionExistsConstraintValidatorTest.php     | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php
index f698279a56d6..7cc412bb268b 100644
--- a/core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionExistsConstraintValidatorTest.php
@@ -40,6 +40,15 @@ public function testValidation(): void {
     $this->enableModules(['user']);
     $this->assertCount(0, $data->validate());
 
+    // Special case: the `core` module — this is not a real module but is the
+    // official module-like extension that provides many plugins.
+    $data = $typed_data->create($definition, 'core');
+    $this->assertCount(0, $data->validate());
+    // Special case: `NULL` — validation constraints should be compatible with
+    // optional values.
+    $data = $typed_data->create($definition, NULL);
+    $this->assertCount(0, $data->validate());
+
     $definition->setConstraints(['ExtensionExists' => 'theme']);
     $data = $typed_data->create($definition, 'stark');
 
@@ -56,6 +65,11 @@ public function testValidation(): void {
       ->create($definition, 'stark');
     $this->assertCount(0, $data->validate());
 
+    // Special case: `NULL` — validation constraints should be compatible with
+    // optional values.
+    $data = $typed_data->create($definition, NULL);
+    $this->assertCount(0, $data->validate());
+
     // Anything but a module or theme should raise an exception.
     $definition->setConstraints(['ExtensionExists' => 'profile']);
     $this->expectExceptionMessage("Unknown extension type: 'profile'");
-- 
GitLab


From be945ac4b0d2a8dee45daab38ab230e2a99400ae Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 7 Feb 2024 15:05:19 +0100
Subject: [PATCH 49/73] Fix `ExtensionExists` constraint and use it again for
 `field_storage.*.*:module`.

---
 .../Constraint/ExtensionExistsConstraintValidator.php     | 8 ++++++--
 core/modules/field/config/schema/field.schema.yml         | 5 ++---
 2 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraintValidator.php b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraintValidator.php
index 404d5dcf38c2..524db7475219 100644
--- a/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraintValidator.php
+++ b/core/lib/Drupal/Core/Extension/Plugin/Validation/Constraint/ExtensionExistsConstraintValidator.php
@@ -61,13 +61,17 @@ public function validate(mixed $extension_name, Constraint $constraint) {
 
     switch ($constraint->type) {
       case 'module':
-        if (!$this->moduleHandler->moduleExists($extension_name)) {
+        // Special case: `core` — for core-provided plugins.
+        if ($extension_name === 'core') {
+          break;
+        }
+        if ($extension_name !== NULL && !$this->moduleHandler->moduleExists($extension_name)) {
           $this->context->addViolation($constraint->moduleMessage, $variables);
         }
         break;
 
       case 'theme':
-        if (!$this->themeHandler->themeExists($extension_name)) {
+        if ($extension_name !== NULL && !$this->themeHandler->themeExists($extension_name)) {
           $this->context->addViolation($constraint->themeMessage, $variables);
         }
         break;
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index b0272d8619a8..4bded7d66d21 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -54,9 +54,8 @@ field.storage.*.*:
     module:
       type: string
       label: 'Module'
-      constraints: {}
-      # @todo Make ExtensionExistsConstraintValidator aware that `core` is ALWAYS installed 🙄, then enable this constraint again.
-      # ExtensionExists: 'module'
+      constraints:
+        ExtensionExists: 'module'
     locked:
       type: boolean
       label: 'Locked'
-- 
GitLab


From 5f9bfa3f459981c159044ed552299b8509707675 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 7 Feb 2024 15:27:23 +0100
Subject: [PATCH 50/73] =?UTF-8?q?Refactor=20`StringPartsConstraintValidato?=
 =?UTF-8?q?r`=20to=20not=20use=20the=20new=20`TreeAwareConstraintTrait`=20?=
 =?UTF-8?q?but=20the=20pre-existing=20`TypeResolver`=20instead=20?=
 =?UTF-8?q?=F0=9F=91=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 core/config/schema/core.data_types.schema.yml |  6 ++---
 .../StringPartsConstraintValidator.php        | 23 +++++++------------
 .../field/config/schema/field.schema.yml      |  4 ++--
 3 files changed, 13 insertions(+), 20 deletions(-)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 42c64a46a0a0..bb9ac0cd8f33 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -500,9 +500,9 @@ field_config_base:
         StringParts:
           separator: .
           parts:
-            - entity_type
-            - bundle
-            - field_name
+            - '%parent.entity_type'
+            - '%parent.bundle'
+            - '%parent.field_name'
     field_name:
       type: string
       label: 'Field name'
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php
index 91fc187a5697..c27c0711cfa8 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraintValidator.php
@@ -4,8 +4,7 @@
 
 namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
 
-use Drupal\Core\Config\Schema\Mapping;
-use Drupal\Core\Entity\Plugin\DataType\ConfigEntityAdapter;
+use Drupal\Core\Config\Schema\TypeResolver;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidator;
 use Symfony\Component\Validator\Exception\UnexpectedTypeException;
@@ -15,8 +14,6 @@
  */
 class StringPartsConstraintValidator extends ConstraintValidator {
 
-  use TreeAwareConstraintTrait;
-
   /**
    * {@inheritdoc}
    */
@@ -25,18 +22,14 @@ public function validate(mixed $value, Constraint $constraint) {
       throw new UnexpectedTypeException($value, 'string');
     }
 
-    // Find the parent mapping.
-    $mapping = $this->getParentProperty();
-    // If it's not a Mapping nor a Config Entity, it's a logical error in the
-    // config schema, not the concrete config.
-    if (!$mapping instanceof Mapping && !$mapping instanceof ConfigEntityAdapter) {
-      throw new \LogicException('This constraint can only be set on `type: mapping`.');
-    }
+    $resolved_parts = array_map(
+      fn (string $expression): mixed => TypeResolver::resolveExpression($expression, $this->context->getObject()),
+      $constraint->parts
+    );
 
     // Verify the required parts are present; if not, that's a logical error in
     // the config schema, not in concrete config.
-    $properties = $mapping->getProperties();
-    $missing_properties = array_diff($constraint->parts, array_keys($properties));
+    $missing_properties = array_intersect($constraint->parts, $resolved_parts);
     if (!empty($missing_properties)) {
       throw new \LogicException(sprintf('This validation constraint is configured to inspect the properties %s, but some do not exist: %s.',
         implode(', ', $constraint->parts),
@@ -46,8 +39,8 @@ public function validate(mixed $value, Constraint $constraint) {
 
     // Retrieve the parts of the expected string.
     $expected_string_parts = [];
-    foreach ($constraint->parts as $part) {
-      $part_value = $properties[$part]->getValue();
+    foreach ($constraint->parts as $index => $part) {
+      $part_value = $resolved_parts[$index];
       if (!is_string($part_value)) {
         throw new \LogicException(sprintf('The "%s" property does not contain a string, but a %s: "%s".', $part, gettype($part_value), (string) $part_value));
       }
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index 4bded7d66d21..186c1a58f33c 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -28,8 +28,8 @@ field.storage.*.*:
         StringParts:
           separator: .
           parts:
-            - entity_type
-            - field_name
+            - '%parent.entity_type'
+            - '%parent.field_name'
     field_name:
       type: string
       label: 'Field name'
-- 
GitLab


From ddb5bd9f8ded351ce207ecfa05baab0ee449c1b5 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 7 Feb 2024 15:31:13 +0100
Subject: [PATCH 51/73] Remove all obsolete `@todo`s and refine one.

---
 core/config/schema/core.data_types.schema.yml     | 7 +------
 core/modules/field/config/schema/field.schema.yml | 9 ---------
 2 files changed, 1 insertion(+), 15 deletions(-)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index bb9ac0cd8f33..888c78378316 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -488,10 +488,6 @@ field_config_base:
   type: config_entity
   constraints:
     UniqueFieldCombination: [entity_type, bundle, field_name]
-    # @todo
-    #FieldStorageExistsOnFieldableContentEntityType:
-    #  entityType: entity_type
-    #  fieldName: field_name
   mapping:
     id:
       type: string
@@ -552,7 +548,6 @@ field_config_base:
       type: string
       label: 'Field type'
       constraints:
-        # @todo In principle, we should validate that this plugin was indeed provided by `module`.
         PluginExists: plugin.manager.field.field_type
 
 core.base_field_override.*.*.*:
@@ -560,7 +555,7 @@ core.base_field_override.*.*.*:
   label: 'Base field bundle override'
   constraints:
     FullyValidatable: ~
-    # @todo
+    # @todo Implement this using \Drupal\Core\Entity\EntityFieldManagerInterface::getBaseFieldDefinitions()
     #FieldIsBaseField:
     #  entityType: entity_type
     #  fieldName: field_name
diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index 186c1a58f33c..370335a9f452 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -14,11 +14,6 @@ field.storage.*.*:
   constraints:
     FullyValidatable: ~
     UniqueFieldCombination: [entity_type, field_name]
-    # @todo
-    #IsUnchanged:
-    #  selector: locked
-    #IsFalse:
-    #  selector: locked
   mapping:
     id:
       type: string
@@ -110,7 +105,3 @@ field.field.*.*.*:
           propertyPath: type
   constraints:
     FullyValidatable: ~
-    # @todo
-    #FieldIsConfigurableField:
-    #  entityType: entity_type
-    #  fieldName: field_name
-- 
GitLab


From 074443a7fba0599e199d78f3fa42c5d14cf7d937 Mon Sep 17 00:00:00 2001
From: Wim Leers <wim.leers@acquia.com>
Date: Wed, 7 Feb 2024 15:35:04 +0100
Subject: [PATCH 52/73] Removed all `NotNull` occurrences, which are obsolete
 thanks to #3364109 and adopting `FullyValidatable: ~`.

---
 .../field/config/schema/field.schema.yml      | 22 +++++++------------
 1 file changed, 8 insertions(+), 14 deletions(-)

diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml
index 370335a9f452..3fba42077bda 100644
--- a/core/modules/field/config/schema/field.schema.yml
+++ b/core/modules/field/config/schema/field.schema.yml
@@ -19,7 +19,6 @@ field.storage.*.*:
       type: string
       label: 'ID'
       constraints:
-        NotNull: []
         StringParts:
           separator: .
           parts:
@@ -28,13 +27,16 @@ field.storage.*.*:
     field_name:
       type: string
       label: 'Field name'
+
       constraints:
-        NotNull: []
+        NotBlank: {}
+        Length:
+          # @see \Drupal\field\Entity\FieldStorageConfig::NAME_MAX_LENGTH
+          max: 32
     entity_type:
       type: string
       label: 'Entity type'
       constraints:
-        NotNull: []
         PluginExists:
           manager: entity_type.manager
           interface: '\Drupal\Core\Entity\FieldableEntityInterface'
@@ -54,19 +56,15 @@ field.storage.*.*:
     locked:
       type: boolean
       label: 'Locked'
-      constraints:
-        NotNull: []
     cardinality:
       type: integer
       label: 'Maximum number of values users can enter'
-      constraints:
-        NotNull: []
-        # NoEntitiesExistYetWithHigherCardinality: []
+      constraints: {}
+      # @todo Create this constraint.
+      # NoEntitiesExistYetWithHigherCardinality: []
     translatable:
       type: boolean
       label: 'Translatable'
-      constraints:
-        NotNull: []
     indexes:
       type: sequence
       label: 'Indexes'
@@ -80,13 +78,9 @@ field.storage.*.*:
     persist_with_no_fields:
       type: boolean
       label: 'Persist field storage with no fields'
-      constraints:
-        NotNull: []
     custom_storage:
       type: boolean
       label: 'Enable custom storage'
-      constraints:
-        NotNull: []
 
 field.field.*.*.*:
   type: field_config_base
-- 
GitLab


From c77ca464ee734247490f4f1ada91d4e1bb01fec8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 9 Feb 2024 10:54:58 -0500
Subject: [PATCH 53/73] Removed those todos, the errors are legit

---
 .../field/tests/src/Kernel/Entity/FieldConfigValidationTest.php | 2 --
 1 file changed, 2 deletions(-)

diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
index aba0685eb321..b014cc2103e4 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
@@ -280,7 +280,6 @@ public function testFieldTypePluginIsValidated(): void {
     $this->assertValidationErrors([
       'field_type' => [
         "The 'invalid' plugin does not exist.",
-        // @todo I'm not sure this error should be here.
         "Expected this to match the value in the 'field.storage.entity_test_mul_with_bundle.test' config at the 'type' property: 'boolean' was expected, not 'invalid'.",
       ],
     ]);
@@ -301,7 +300,6 @@ public function testEntityReferenceSelectionHandlerIsValidated(): void {
       ->set('settings', ['handler' => 'non_existent']);
 
     $this->assertValidationErrors([
-      // @todo I'm not sure this field_type error should be here.
       'field_type' => "Expected this to match the value in the 'field.storage.entity_test_mul_with_bundle.test' config at the 'type' property: 'boolean' was expected, not 'entity_reference'.",
       'settings.handler' => "The 'non_existent' plugin does not exist.",
     ]);
-- 
GitLab


From 77d72d32226d7016a9e905bece4212a55d0e16c7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 9 Feb 2024 13:11:59 -0500
Subject: [PATCH 54/73] Add an update path for the field description

---
 .../lib/Drupal/Core/Field/FieldConfigBase.php |  6 +++---
 core/modules/field/field.post_update.php      | 20 +++++++++++++++++++
 2 files changed, 23 insertions(+), 3 deletions(-)

diff --git a/core/lib/Drupal/Core/Field/FieldConfigBase.php b/core/lib/Drupal/Core/Field/FieldConfigBase.php
index ca9a5b5285ea..e788fccaef8e 100644
--- a/core/lib/Drupal/Core/Field/FieldConfigBase.php
+++ b/core/lib/Drupal/Core/Field/FieldConfigBase.php
@@ -79,9 +79,9 @@ abstract class FieldConfigBase extends ConfigEntityBase implements FieldConfigIn
    * For example, the description will be the help text of Form API elements for
    * this field in entity edit forms.
    *
-   * @var string
+   * @var string|null
    */
-  protected $description = '';
+  protected $description = NULL;
 
   /**
    * Field-type specific settings.
@@ -336,7 +336,7 @@ public function setLabel($label) {
    * {@inheritdoc}
    */
   public function getDescription() {
-    return $this->description;
+    return $this->description ?? '';
   }
 
   /**
diff --git a/core/modules/field/field.post_update.php b/core/modules/field/field.post_update.php
index 1535c477e256..03a05d805442 100644
--- a/core/modules/field/field.post_update.php
+++ b/core/modules/field/field.post_update.php
@@ -5,6 +5,26 @@
  * Post update functions for Field module.
  */
 
+use Drupal\Core\Config\Entity\ConfigEntityUpdater;
+use Drupal\Core\Field\FieldConfigBase;
+
+/**
+ * Converts empty `description` on fields to NULL.
+ */
+function field_post_update_set_empty_description_to_null(array &$sandbox): void {
+  $callback = function (FieldConfigBase $entity): bool {
+    if (trim($entity->getDescription()) === '') {
+      $entity->set('description', NULL);
+      return TRUE;
+    }
+    return FALSE;
+  };
+  /** @var \Drupal\Core\Config\Entity\ConfigEntityUpdater $updater */
+  $updater = \Drupal::classResolver(ConfigEntityUpdater::class);
+  $updater->update($sandbox, 'field_config', $callback);
+  $updater->update($sandbox, 'base_field_override', $callback);
+}
+
 /**
  * Implements hook_removed_post_updates().
  */
-- 
GitLab


From 1d7a50c279d4f3bd7d75ec655c93899d49cf0151 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 9 Feb 2024 13:46:24 -0500
Subject: [PATCH 55/73] Add IsBaseField constraint and indirect test coverage

---
 core/config/schema/core.data_types.schema.yml |  4 --
 .../Core/Field/Entity/BaseFieldOverride.php   |  1 +
 .../Constraint/IsBaseFieldConstraint.php      | 24 ++++++++
 .../IsBaseFieldConstraintValidator.php        | 55 +++++++++++++++++++
 .../BaseFieldOverrideValidationTest.php       | 28 ++++++++++
 5 files changed, 108 insertions(+), 4 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraint.php
 create mode 100644 core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 5252be95347e..85a2fdb93af3 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -565,10 +565,6 @@ core.base_field_override.*.*.*:
   label: 'Base field bundle override'
   constraints:
     FullyValidatable: ~
-    # @todo Implement this using \Drupal\Core\Entity\EntityFieldManagerInterface::getBaseFieldDefinitions()
-    #FieldIsBaseField:
-    #  entityType: entity_type
-    #  fieldName: field_name
 
 core.date_format.*:
   type: config_entity
diff --git a/core/lib/Drupal/Core/Field/Entity/BaseFieldOverride.php b/core/lib/Drupal/Core/Field/Entity/BaseFieldOverride.php
index 3f16fdad9891..35bebc19eb50 100644
--- a/core/lib/Drupal/Core/Field/Entity/BaseFieldOverride.php
+++ b/core/lib/Drupal/Core/Field/Entity/BaseFieldOverride.php
@@ -40,6 +40,7 @@
  *   },
  *   constraints = {
  *     "ImmutableProperties" = {"id", "entity_type", "bundle", "field_name", "field_type"},
+ *     "IsBaseField" = {}
  *   }
  * )
  */
diff --git a/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraint.php b/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraint.php
new file mode 100644
index 000000000000..c4a1e1cc5e42
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraint.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Field\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Validates that the value overrides a base field that actually exists.
+ *
+ * @Constraint(
+ *   id = "IsBaseField",
+ *   label = @Translation("Is a base field", context = "Validation")
+ * )
+ */
+class IsBaseFieldConstraint extends Constraint {
+
+  /**
+   * Tbe error message if validation fails.
+   *
+   * @var string
+   */
+  public string $message = "'@field_name' is not a base field of the @entity_type entity type.";
+
+}
diff --git a/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php b/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php
new file mode 100644
index 000000000000..243922e2cb37
--- /dev/null
+++ b/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\Core\Field\Plugin\Validation\Constraint;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Field\Entity\BaseFieldOverride;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Validates the IsBaseField constraint.
+ */
+class IsBaseFieldConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+  /**
+   * Constructs an IsBaseFieldConstraintValidator object.
+   *
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
+   *   The entity field manager service.
+   */
+  public function __construct(protected readonly EntityFieldManagerInterface $entityFieldManager) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get(EntityFieldManagerInterface::class),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate(mixed $value, Constraint $constraint): void {
+    assert($constraint instanceof IsBaseFieldConstraint);
+
+    if (!$value instanceof BaseFieldOverride) {
+      throw new UnexpectedTypeException($value, BaseFieldOverride::class);
+    }
+    $field_name = $value->getName();
+    $entity_type_id = $value->getTargetEntityTypeId();
+    $base_fields = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id);
+    if (!array_key_exists($field_name, $base_fields)) {
+      $this->context->addViolation($constraint->message, [
+        '@field_name' => $field_name,
+        '@entity_type' => $entity_type_id,
+      ]);
+    }
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
index d4a0123f52ce..8cae65d9149c 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
@@ -4,6 +4,8 @@
 
 use Drupal\Core\Field\Entity\BaseFieldOverride;
 use Drupal\entity_test\Entity\EntityTestBundle;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
 use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
 
@@ -109,4 +111,30 @@ public function testFieldTypePluginIsValidated(): void {
     ]);
   }
 
+  /**
+   * Tests that base field overrides must be overriding, well, base fields.
+   */
+  public function testOverriddenFieldMustBeABaseField(): void {
+    $storage = FieldStorageConfig::create([
+      'entity_type' => 'node',
+      'field_name' => 'field_mail',
+      'type' => 'email',
+    ]);
+    $storage->save();
+
+    $field = FieldConfig::create([
+      'field_storage' => $storage,
+      'bundle' => 'one',
+    ]);
+    $field->save();
+
+    // The `field_name` property is immutable, so we need to clone the entity
+    // in order to change it.
+    $this->entity = $this->entity->createDuplicate()
+      ->set('field_name', 'field_mail');
+    $this->assertValidationErrors([
+      '' => "'field_mail' is not a base field of the node entity type.",
+    ]);
+  }
+
 }
-- 
GitLab


From 77fceefea292905d69c98dab5e2a820643bad9e1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 9 Feb 2024 13:56:56 -0500
Subject: [PATCH 56/73] IsBaseFieldConstraint can use FieldDefinitionInterface

---
 .../Constraint/IsBaseFieldConstraintValidator.php        | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php b/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php
index 243922e2cb37..0ed781576f60 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php
@@ -4,7 +4,7 @@
 
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
-use Drupal\Core\Field\Entity\BaseFieldOverride;
+use Drupal\Core\Field\FieldDefinitionInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidator;
@@ -38,13 +38,12 @@ public static function create(ContainerInterface $container) {
   public function validate(mixed $value, Constraint $constraint): void {
     assert($constraint instanceof IsBaseFieldConstraint);
 
-    if (!$value instanceof BaseFieldOverride) {
-      throw new UnexpectedTypeException($value, BaseFieldOverride::class);
+    if (!$value instanceof FieldDefinitionInterface::class) {
+      throw new UnexpectedTypeException($value, FieldDefinitionInterface::class);
     }
     $field_name = $value->getName();
     $entity_type_id = $value->getTargetEntityTypeId();
-    $base_fields = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id);
-    if (!array_key_exists($field_name, $base_fields)) {
+    if (!array_key_exists($field_name, $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id))) {
       $this->context->addViolation($constraint->message, [
         '@field_name' => $field_name,
         '@entity_type' => $entity_type_id,
-- 
GitLab


From ca6de6d08b62d5fa95f3c8b78ebfba52eed386b1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 9 Feb 2024 13:58:21 -0500
Subject: [PATCH 57/73] Fix bad syntax

---
 .../Validation/Constraint/IsBaseFieldConstraintValidator.php    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php b/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php
index 0ed781576f60..b03c7a06c2c4 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php
@@ -38,7 +38,7 @@ public static function create(ContainerInterface $container) {
   public function validate(mixed $value, Constraint $constraint): void {
     assert($constraint instanceof IsBaseFieldConstraint);
 
-    if (!$value instanceof FieldDefinitionInterface::class) {
+    if (!$value instanceof FieldDefinitionInterface) {
       throw new UnexpectedTypeException($value, FieldDefinitionInterface::class);
     }
     $field_name = $value->getName();
-- 
GitLab


From 4d74dea33bd964f7f5fda66afded08e547d31bc4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 9 Feb 2024 13:59:07 -0500
Subject: [PATCH 58/73] Fix FieldCrudTest

---
 core/modules/field/tests/src/Kernel/FieldCrudTest.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/modules/field/tests/src/Kernel/FieldCrudTest.php b/core/modules/field/tests/src/Kernel/FieldCrudTest.php
index 7ba79f70aae9..134363c95a20 100644
--- a/core/modules/field/tests/src/Kernel/FieldCrudTest.php
+++ b/core/modules/field/tests/src/Kernel/FieldCrudTest.php
@@ -92,7 +92,7 @@ public function testCreateField() {
     // Check that default values are set.
     $this->assertFalse($config['required'], 'Required defaults to false.');
     $this->assertSame($config['label'], $this->fieldDefinition['field_name'], 'Label defaults to field name.');
-    $this->assertSame('', $config['description'], 'Description defaults to empty string.');
+    $this->assertNull($config['description'], 'Description defaults to NULL.');
 
     // Check that default settings are set.
     $this->assertEquals($config['settings'], $field_type_manager->getDefaultFieldSettings($this->fieldStorageDefinition['type']), 'Default field settings have been written.');
-- 
GitLab


From 05ad96a9f838fbfd3ec48ee4a296a8278537e63b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 9 Feb 2024 14:30:18 -0500
Subject: [PATCH 59/73] Fix BaseFieldOverrideValidationTest

---
 .../KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php  | 1 +
 1 file changed, 1 insertion(+)

diff --git a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
index 8cae65d9149c..08f4fcf36a15 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php
@@ -95,6 +95,7 @@ public function testImmutableProperties(array $valid_values = [], ?array $additi
       'entity_type' => 'entity_test_with_bundle',
       'bundle' => 'another',
       'field_type' => 'email',
+      'field_name' => 'title',
     ]);
   }
 
-- 
GitLab


From 08b82fa5692ed660d90eeff1510630a5d303f371 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 9 Feb 2024 14:33:48 -0500
Subject: [PATCH 60/73] Fix update path tests, hopefully

---
 core/modules/field/field.post_update.php | 38 ++++++++++++++++--------
 1 file changed, 25 insertions(+), 13 deletions(-)

diff --git a/core/modules/field/field.post_update.php b/core/modules/field/field.post_update.php
index 03a05d805442..22df9e8b9554 100644
--- a/core/modules/field/field.post_update.php
+++ b/core/modules/field/field.post_update.php
@@ -6,23 +6,35 @@
  */
 
 use Drupal\Core\Config\Entity\ConfigEntityUpdater;
-use Drupal\Core\Field\FieldConfigBase;
+use Drupal\Core\Field\Entity\BaseFieldOverride;
+use Drupal\field\FieldConfigInterface;
 
 /**
  * Converts empty `description` on fields to NULL.
  */
-function field_post_update_set_empty_description_to_null(array &$sandbox): void {
-  $callback = function (FieldConfigBase $entity): bool {
-    if (trim($entity->getDescription()) === '') {
-      $entity->set('description', NULL);
-      return TRUE;
-    }
-    return FALSE;
-  };
-  /** @var \Drupal\Core\Config\Entity\ConfigEntityUpdater $updater */
-  $updater = \Drupal::classResolver(ConfigEntityUpdater::class);
-  $updater->update($sandbox, 'field_config', $callback);
-  $updater->update($sandbox, 'base_field_override', $callback);
+function field_post_update_set_field_config_empty_description_to_null(array &$sandbox): void {
+  \Drupal::classResolver(ConfigEntityUpdater::class)
+    ->update($sandbox, 'field_config', function (FieldConfigInterface $entity): bool {
+      if (trim($entity->getDescription()) === '') {
+        $entity->set('description', NULL);
+        return TRUE;
+      }
+      return FALSE;
+  });
+}
+
+/**
+ * Converts empty `description` on base field overrides to NULL.
+ */
+function field_post_update_set_base_field_override_empty_description_to_null(array &$sandbox): void {
+  \Drupal::classResolver(ConfigEntityUpdater::class)
+    ->update($sandbox, 'base_field_override', function (BaseFieldOverride $entity): bool {
+      if (trim($entity->getDescription()) === '') {
+        $entity->set('description', NULL);
+        return TRUE;
+      }
+      return FALSE;
+    });
 }
 
 /**
-- 
GitLab


From dbbe80db223e385679dba4a0b63c369bead8850d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Fri, 9 Feb 2024 14:52:20 -0500
Subject: [PATCH 61/73] My kingdom for a couple of stupid spaces

---
 core/modules/field/field.post_update.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/modules/field/field.post_update.php b/core/modules/field/field.post_update.php
index 22df9e8b9554..812ef6e71976 100644
--- a/core/modules/field/field.post_update.php
+++ b/core/modules/field/field.post_update.php
@@ -20,7 +20,7 @@ function field_post_update_set_field_config_empty_description_to_null(array &$sa
         return TRUE;
       }
       return FALSE;
-  });
+    });
 }
 
 /**
-- 
GitLab


From 2cf87372fd3226d88ed1efaf33823111bd3ecabc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Mon, 12 Feb 2024 09:04:45 -0500
Subject: [PATCH 62/73] Fix BaseFieldOverrideTest

---
 .../jsonapi/tests/src/Functional/BaseFieldOverrideTest.php      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/modules/jsonapi/tests/src/Functional/BaseFieldOverrideTest.php b/core/modules/jsonapi/tests/src/Functional/BaseFieldOverrideTest.php
index 9bd4a18994d8..6475f0900576 100644
--- a/core/modules/jsonapi/tests/src/Functional/BaseFieldOverrideTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/BaseFieldOverrideTest.php
@@ -101,7 +101,7 @@ protected function getExpectedDocument() {
               'node.type.camelids',
             ],
           ],
-          'description' => '',
+          'description' => NULL,
           'entity_type' => 'node',
           'field_name' => 'promote',
           'field_type' => 'boolean',
-- 
GitLab


From dc75c01e72ba53641ea7a574b40fe864f1a2b7a9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Mon, 12 Feb 2024 09:05:14 -0500
Subject: [PATCH 63/73] Fix FieldConfigTest

---
 core/modules/jsonapi/tests/src/Functional/FieldConfigTest.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/modules/jsonapi/tests/src/Functional/FieldConfigTest.php b/core/modules/jsonapi/tests/src/Functional/FieldConfigTest.php
index c5d94575104d..dcd65d84098c 100644
--- a/core/modules/jsonapi/tests/src/Functional/FieldConfigTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/FieldConfigTest.php
@@ -113,7 +113,7 @@ protected function getExpectedDocument() {
               'text',
             ],
           ],
-          'description' => '',
+          'description' => NULL,
           'entity_type' => 'node',
           'field_name' => 'field_llama',
           'field_type' => 'text',
-- 
GitLab


From 01cd4846f63f0f424311c426875d93f9a8ab6cbe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Mon, 12 Feb 2024 09:06:27 -0500
Subject: [PATCH 64/73] Fix  broken REST tests

---
 .../tests/src/Functional/Rest/FieldConfigResourceTestBase.php   | 2 +-
 .../FunctionalTests/Rest/BaseFieldOverrideResourceTestBase.php  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/core/modules/field/tests/src/Functional/Rest/FieldConfigResourceTestBase.php b/core/modules/field/tests/src/Functional/Rest/FieldConfigResourceTestBase.php
index f13eb9865dce..88dae30e542c 100644
--- a/core/modules/field/tests/src/Functional/Rest/FieldConfigResourceTestBase.php
+++ b/core/modules/field/tests/src/Functional/Rest/FieldConfigResourceTestBase.php
@@ -74,7 +74,7 @@ protected function getExpectedNormalizedEntity() {
           'text',
         ],
       ],
-      'description' => '',
+      'description' => NULL,
       'entity_type' => 'node',
       'field_name' => 'field_llama',
       'field_type' => 'text',
diff --git a/core/tests/Drupal/FunctionalTests/Rest/BaseFieldOverrideResourceTestBase.php b/core/tests/Drupal/FunctionalTests/Rest/BaseFieldOverrideResourceTestBase.php
index b0225ad4b632..9f8f342a8dfd 100644
--- a/core/tests/Drupal/FunctionalTests/Rest/BaseFieldOverrideResourceTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Rest/BaseFieldOverrideResourceTestBase.php
@@ -64,7 +64,7 @@ protected function getExpectedNormalizedEntity() {
           'node.type.camelids',
         ],
       ],
-      'description' => '',
+      'description' => NULL,
       'entity_type' => 'node',
       'field_name' => 'promote',
       'field_type' => 'boolean',
-- 
GitLab


From 59304cae79237a4ac2adf12dfd49e8d80c8ddd26 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Mon, 12 Feb 2024 09:34:30 -0500
Subject: [PATCH 65/73] Mark settings for string and string_long fields as
 fully validatable because they have no settings

---
 core/config/schema/core.data_types.schema.yml | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 0f4a776de998..c052d3484967 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -647,6 +647,8 @@ field.storage_settings.string:
 field.field_settings.string:
   type: mapping
   label: 'String settings'
+  constraints:
+    FullyValidatable: ~
 
 field.value.string:
   type: mapping
@@ -669,6 +671,8 @@ field.storage_settings.string_long:
 field.field_settings.string_long:
   type: mapping
   label: 'String (long) settings'
+  constraints:
+    FullyValidatable: ~
 
 field.value.string_long:
   type: mapping
-- 
GitLab


From 94c100b94c463f4192dd8343dda7546b81cc43f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Mon, 12 Feb 2024 09:48:37 -0500
Subject: [PATCH 66/73] Mark a few more field settings config schema types as
 fully validatable, including boolean, which actually has settings.

---
 core/config/schema/core.data_types.schema.yml | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index c052d3484967..6b6b467ced2c 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -691,6 +691,8 @@ field.storage_settings.password:
 field.field_settings.password:
   type: mapping
   label: 'Password settings'
+  constraints:
+    FullyValidatable: ~
 
 # Schema for the configuration of the URI field type.
 
@@ -708,6 +710,8 @@ field.storage_settings.uri:
 field.field_settings.uri:
   type: mapping
   label: 'URI settings'
+  constraints:
+    FullyValidatable: ~
 
 field.value.uri:
   type: mapping
@@ -726,6 +730,8 @@ field.storage_settings.created:
 field.field_settings.created:
   type: mapping
   label: 'Created timestamp settings'
+  constraints:
+    FullyValidatable: ~
 
 field.value.created:
   type: mapping
@@ -744,6 +750,8 @@ field.storage_settings.changed:
 field.field_settings.changed:
   type: mapping
   label: 'Changed timestamp settings'
+  constraints:
+    FullyValidatable: ~
 
 field.value.changed:
   type: mapping
@@ -803,6 +811,8 @@ field.field_settings.boolean:
     off_label:
       type: required_label
       label: 'Off label'
+  constraints:
+    FullyValidatable: ~
 
 field.value.boolean:
   type: mapping
-- 
GitLab


From de22a4f2dd34bacc705a0b50970825bbf08ed0ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Mon, 12 Feb 2024 10:46:34 -0500
Subject: [PATCH 67/73] Partially fix FieldConfigValidationTest.

---
 .../Kernel/Entity/FieldConfigValidationTest.php    | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
index b014cc2103e4..d63bc9e3dc67 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
@@ -50,6 +50,10 @@ protected function setUp(): void {
     $this->entity = FieldConfig::create([
       'field_storage' => $field_storage,
       'bundle' => 'one',
+      'settings' => [
+        'on_label' => 'Hello!',
+        'off_label' => 'Goodbye.',
+      ],
     ]);
     $this->entity->save();
   }
@@ -207,12 +211,6 @@ public function testTargetBundleMustExist(): void {
    * {@inheritdoc}
    */
   public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
-    // If we don't clear the previous settings here, we will get unrelated
-    // validation errors (in addition to the one we're expecting), because the
-    // settings from the *old* field_type won't match the config schema for the
-    // settings of the *new* field_type.
-    $this->entity->set('settings', []);
-
     // Ensure that it is at least plausible that the `entity_type` is modified:
     // a corresponding FieldStorageConfig must exist due to the entity-level
     // `RequiredConfigDependencies` constraint.
@@ -235,6 +233,10 @@ public function testImmutableProperties(array $valid_values = [], ?array $additi
     ], [
       'field_type' => [
         'field_type' => "Expected this to match the value in the 'field.storage.entity_test_mul_with_bundle.test' config at the 'type' property: 'boolean' was expected, not 'email'.",
+        'settings' => [
+          "'on_label' is an unknown key because field_type is email (see config schema type field.field_settings.email).",
+          "'off_label' is an unknown key because field_type is email (see config schema type field.field_settings.email).",
+        ],
       ],
     ]);
   }
-- 
GitLab


From b1b7eeb8ac1bc9c2ecc3e137ab2b2b44ca8d4e5c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Mon, 12 Feb 2024 10:53:09 -0500
Subject: [PATCH 68/73] Fully fix it

---
 .../tests/src/Kernel/Entity/FieldConfigValidationTest.php  | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
index d63bc9e3dc67..3c4a14dbe630 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
@@ -251,6 +251,13 @@ public function testRequiredPropertyKeysMissing(?array $additional_expected_vali
         // @see \Drupal\Core\Config\Plugin\Validation\Constraint\RequiredConfigDependenciesConstraintValidator
         '' => 'This field requires a field storage.',
       ],
+      // If the settings are removed, we should see errors about them missing.
+      'settings' => [
+        'settings' => [
+          "'on_label' is a required key because field_type is boolean (see config schema type field.field_settings.boolean).",
+          "'off_label' is a required key because field_type is boolean (see config schema type field.field_settings.boolean).",
+        ],
+      ],
     ]);
   }
 
-- 
GitLab


From ee54c4eef4ef0c58739c0d7d4f85eb90df51203f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Mon, 12 Feb 2024 10:54:33 -0500
Subject: [PATCH 69/73] Mark field settings for integer, float, and decimal as
 fully validatable

---
 core/config/schema/core.data_types.schema.yml | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 6b6b467ced2c..49cefa965d66 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -871,6 +871,8 @@ field.field_settings.integer:
     suffix:
       type: label
       label: 'Suffix'
+  constraints:
+    FullyValidatable: ~
 
 field.value.integer:
   type: mapping
@@ -909,6 +911,8 @@ field.field_settings.decimal:
     suffix:
       type: label
       label: 'Suffix'
+  constraints:
+    FullyValidatable: ~
 
 field.value.decimal:
   type: mapping
@@ -940,6 +944,8 @@ field.field_settings.float:
     suffix:
       type: label
       label: 'Suffix'
+  constraints:
+    FullyValidatable: ~
 
 field.value.float:
   type: mapping
-- 
GitLab


From 69442f9d88d4390dbead05a2b38e8fa6bb93409a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Mon, 12 Feb 2024 12:42:40 -0500
Subject: [PATCH 70/73] Revert "Mark field settings for integer, float, and
 decimal as fully validatable"

This reverts commit ee54c4eef4ef0c58739c0d7d4f85eb90df51203f.
---
 core/config/schema/core.data_types.schema.yml | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 49cefa965d66..6b6b467ced2c 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -871,8 +871,6 @@ field.field_settings.integer:
     suffix:
       type: label
       label: 'Suffix'
-  constraints:
-    FullyValidatable: ~
 
 field.value.integer:
   type: mapping
@@ -911,8 +909,6 @@ field.field_settings.decimal:
     suffix:
       type: label
       label: 'Suffix'
-  constraints:
-    FullyValidatable: ~
 
 field.value.decimal:
   type: mapping
@@ -944,8 +940,6 @@ field.field_settings.float:
     suffix:
       type: label
       label: 'Suffix'
-  constraints:
-    FullyValidatable: ~
 
 field.value.float:
   type: mapping
-- 
GitLab


From db2f5271b281fc395e45d9c16c91012d4513c000 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Mon, 12 Feb 2024 13:56:53 -0500
Subject: [PATCH 71/73] Mark some field storage settings config types as fully
 validatable

---
 core/config/schema/core.data_types.schema.yml | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 6b6b467ced2c..2bc7f81ef04c 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -667,6 +667,8 @@ field.storage_settings.string_long:
     case_sensitive:
       type: boolean
       label: 'Case sensitive'
+  constraints:
+    FullyValidatable: ~
 
 field.field_settings.string_long:
   type: mapping
@@ -687,6 +689,8 @@ field.value.string_long:
 field.storage_settings.password:
   type: field.storage_settings.string
   label: 'Password settings'
+  constraints:
+    FullyValidatable: ~
 
 field.field_settings.password:
   type: mapping
@@ -726,6 +730,8 @@ field.value.uri:
 field.storage_settings.created:
   type: mapping
   label: 'Created timestamp settings'
+  constraints:
+    FullyValidatable: ~
 
 field.field_settings.created:
   type: mapping
@@ -746,6 +752,8 @@ field.value.created:
 field.storage_settings.changed:
   type: mapping
   label: 'Changed timestamp settings'
+  constraints:
+    FullyValidatable: ~
 
 field.field_settings.changed:
   type: mapping
@@ -770,6 +778,10 @@ field.storage_settings.entity_reference:
     target_type:
       type: string
       label: 'Type of item to reference'
+      constraints:
+        PluginExists:
+          manager: entity_type.manager
+          interface: Drupal\Core\Entity\EntityTypeInterface
 
 field.field_settings.entity_reference:
   type: mapping
@@ -826,6 +838,8 @@ field.value.boolean:
 field.storage_settings.email:
   type: mapping
   label: 'Email settings'
+  constraints:
+    FullyValidatable: ~
 
 field.field_settings.email:
   type: mapping
@@ -923,6 +937,8 @@ field.value.decimal:
 field.storage_settings.float:
   type: mapping
   label: 'Float settings'
+  constraints:
+    FullyValidatable: ~
 
 field.field_settings.float:
   type: mapping
-- 
GitLab


From 9fcaa37fef693f9cdded6940eed3503c7eb91c7f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Mon, 12 Feb 2024 16:00:24 -0500
Subject: [PATCH 72/73] Fix wrong interface check for
 field.storage_settings.entity_reference

---
 core/config/schema/core.data_types.schema.yml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 2bc7f81ef04c..b51dfdef2d04 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -781,7 +781,6 @@ field.storage_settings.entity_reference:
       constraints:
         PluginExists:
           manager: entity_type.manager
-          interface: Drupal\Core\Entity\EntityTypeInterface
 
 field.field_settings.entity_reference:
   type: mapping
-- 
GitLab


From 5838d63c70cf86b2434a6ca5371164f2808918e2 Mon Sep 17 00:00:00 2001
From: bjorn <bjorn@swis.nl>
Date: Sat, 22 Feb 2025 16:58:35 +0100
Subject: [PATCH 73/73] build: fix linting errors

---
 .../Constraint/IsBaseFieldConstraintValidator.php          | 2 +-
 .../Constraint/MatchesOtherConfigValueConstraint.php       | 2 +-
 .../MatchesOtherConfigValueConstraintValidator.php         | 2 +-
 .../Plugin/Validation/Constraint/StringPartsConstraint.php | 2 +-
 .../Constraint/UniqueFieldCombinationConstraint.php        | 7 +++++--
 .../tests/src/Kernel/Entity/FieldConfigValidationTest.php  | 2 --
 .../tests/src/Kernel/Entity/ActionValidationTest.php       | 2 +-
 7 files changed, 10 insertions(+), 9 deletions(-)

diff --git a/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php b/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php
index b03c7a06c2c4..be309120e5f9 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Validation/Constraint/IsBaseFieldConstraintValidator.php
@@ -26,7 +26,7 @@ public function __construct(protected readonly EntityFieldManagerInterface $enti
   /**
    * {@inheritdoc}
    */
-  public static function create(ContainerInterface $container) {
+  public static function create(ContainerInterface $container): static {
     return new static(
       $container->get(EntityFieldManagerInterface::class),
     );
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraint.php
index f4f40b65fc82..d3007bf07b9b 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraint.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraint.php
@@ -48,7 +48,7 @@ class MatchesOtherConfigValueConstraint extends Constraint {
   /**
    * {@inheritdoc}
    */
-  public function getRequiredOptions() {
+  public function getRequiredOptions(): array {
     return ['prefix', 'id', 'propertyPath'];
   }
 
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraintValidator.php
index 5b70d43fde4b..fc1efcb38e25 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraintValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/MatchesOtherConfigValueConstraintValidator.php
@@ -36,7 +36,7 @@ public function __construct(
   /**
    * {@inheritdoc}
    */
-  public static function create(ContainerInterface $container) {
+  public static function create(ContainerInterface $container): static {
     return new static(
       $container->get(ConfigFactoryInterface::class),
       $container->get(TypedConfigManagerInterface::class)
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraint.php
index bdde9e678359..59cda8e8f50e 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraint.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/StringPartsConstraint.php
@@ -40,7 +40,7 @@ class StringPartsConstraint extends Constraint {
   /**
    * {@inheritdoc}
    */
-  public function getRequiredOptions() {
+  public function getRequiredOptions(): array {
     return ['separator', 'parts'];
   }
 
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraint.php
index 053649477a00..7a4464a319ce 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraint.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldCombinationConstraint.php
@@ -16,6 +16,9 @@
  */
 class UniqueFieldCombinationConstraint extends Constraint {
 
+  /**
+   * @var string $message The error message.
+   */
   public $message = 'A @entity_type with this combination of values for the fields @field_name_list (%field_value_list) already exists.';
 
   /**
@@ -28,14 +31,14 @@ class UniqueFieldCombinationConstraint extends Constraint {
   /**
    * {@inheritdoc}
    */
-  public function getDefaultOption() {
+  public function getDefaultOption(): string {
     return 'fields';
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getRequiredOptions() {
+  public function getRequiredOptions(): array {
     return ['fields'];
   }
 
diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
index 74f9c6eafa9e..84ecf06874d5 100644
--- a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
+++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php
@@ -33,8 +33,6 @@ protected function setUp(): void {
 
     $this->installEntitySchema('node');
     $this->installConfig('node');
-    $this->createContentType(['type' => 'one']);
-    $this->createContentType(['type' => 'another']);
 
     EntityTestBundle::create(['id' => 'one'])->save();
     EntityTestMulBundle::create(['id' => 'one'])->save();
diff --git a/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php b/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php
index 1e3efa75252b..3e259eaec9e7 100644
--- a/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php
+++ b/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php
@@ -62,7 +62,7 @@ public function testInvalidPluginId(): void {
   /**
    * {@inheritdoc}
    */
-  public function testImmutableProperties(array $valid_values = []): void {
+  public function testImmutableProperties(array $valid_values = [], ?array $additional_expected_validation_errors_when_modified = NULL): void {
     $valid_values['id'] = 'test_changed';
     parent::testImmutableProperties($valid_values);
   }
-- 
GitLab