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