diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index dec91072294..fd68d501ae8 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -499,6 +499,8 @@ field_config_base: bundle: type: string label: 'Bundle' + constraints: + EntityBundleExists: '%parent.entity_type' label: type: required_label label: 'Label' diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml index 5163e67f0d2..fa7114d3049 100644 --- a/core/config/schema/core.entity.schema.yml +++ b/core/config/schema/core.entity.schema.yml @@ -54,6 +54,8 @@ core.entity_view_display.*.*.*: bundle: type: string label: 'Bundle' + constraints: + EntityBundleExists: '%parent.targetEntityType' mode: type: string label: 'View or form mode machine name' @@ -115,6 +117,8 @@ core.entity_form_display.*.*.*: bundle: type: string label: 'Bundle' + constraints: + EntityBundleExists: '%parent.targetEntityType' mode: type: string label: 'View or form mode machine name' diff --git a/core/lib/Drupal/Core/Config/Schema/TypeResolver.php b/core/lib/Drupal/Core/Config/Schema/TypeResolver.php new file mode 100644 index 00000000000..cd00567cf9c --- /dev/null +++ b/core/lib/Drupal/Core/Config/Schema/TypeResolver.php @@ -0,0 +1,124 @@ + replacement. + $replace = []; + foreach (array_combine($matches[0], $matches[1]) as $key => $value) { + $replace[$key] = self::resolveExpression($value, $data); + } + return strtr($name, $replace); + } + return $name; + } + + /** + * Resolves a dynamic type expression using configuration data. + * + * Dynamic type names are nested configuration keys containing expressions to + * be replaced by the value at the property path that the expression is + * pointing at. The expression may contain the following special strings: + * - '%key', will be replaced by the element's key. + * - '%parent', to reference the parent element. + * - '%type', to reference the schema definition type. Can only be used in + * combination with %parent. + * + * There may be nested configuration keys separated by dots or more complex + * patterns like '%parent.name' which references the 'name' value of the + * parent element. + * + * Example expressions: + * - 'name.subkey', indicates a nested value of the current element. + * - '%parent.name', will be replaced by the 'name' value of the parent. + * - '%parent.%key', will be replaced by the parent element's key. + * - '%parent.%type', will be replaced by the schema type of the parent. + * - '%parent.%parent.%type', will be replaced by the schema type of the + * parent's parent. + * + * @param string $expression + * Expression to be resolved. + * @param array|\Drupal\Core\TypedData\TypedDataInterface $data + * Configuration data for the element. + * + * @return string + * The value the expression resolves to, or the given expression if it + * cannot be resolved. + * + * @todo Validate the expression in https://www.drupal.org/project/drupal/issues/3392903 + */ + public static function resolveExpression(string $expression, array|TypedDataInterface $data): string { + if ($data instanceof TypedDataInterface) { + $data = [ + '%parent' => $data->getParent(), + '%key' => $data->getName(), + '%type' => $data->getDataDefinition()->getDataType(), + ]; + } + + $parts = explode('.', $expression); + // Process each value part, one at a time. + while ($name = array_shift($parts)) { + if (!is_array($data) || !isset($data[$name])) { + // Key not found, return original value + return $expression; + } + if (!$parts) { + $expression = $data[$name]; + if (is_bool($expression)) { + $expression = (int) $expression; + } + // If no more parts left, this is the final property. + return (string) $expression; + } + // Get nested value and continue processing. + if ($name == '%parent') { + /** @var \Drupal\Core\Config\Schema\ArrayElement $parent */ + // Switch replacement values with values from the parent. + $parent = $data['%parent']; + $data = $parent->getValue(); + $data['%type'] = $parent->getDataDefinition()->getDataType(); + // The special %parent and %key values now need to point one level up. + if ($new_parent = $parent->getParent()) { + $data['%parent'] = $new_parent; + $data['%key'] = $new_parent->getName(); + } + continue; + } + $data = $data[$name]; + } + // Return the original value + return $expression; + } + +} diff --git a/core/lib/Drupal/Core/Config/TypedConfigManager.php b/core/lib/Drupal/Core/Config/TypedConfigManager.php index c6775ffd2a1..fec847d3312 100644 --- a/core/lib/Drupal/Core/Config/TypedConfigManager.php +++ b/core/lib/Drupal/Core/Config/TypedConfigManager.php @@ -7,6 +7,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Config\Schema\ConfigSchemaAlterException; use Drupal\Core\Config\Schema\ConfigSchemaDiscovery; +use Drupal\Core\Config\Schema\TypeResolver; use Drupal\Core\Config\Schema\SequenceDataDefinition; use Drupal\Core\DependencyInjection\ClassResolverInterface; use Drupal\Core\Config\Schema\Undefined; @@ -108,7 +109,7 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI if (isset($name)) { $replace['%key'] = $name; } - $type = $this->resolveDynamicTypeName($type, $replace); + $type = TypeResolver::resolveDynamicTypeName($type, $replace); // Remove the type from the definition so that it is replaced with the // concrete type from schema definitions. unset($definition['type']); @@ -288,7 +289,7 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI // Replace dynamic portions of the definition type. if (!empty($replacements) && strpos($definition['type'], ']')) { - $sub_type = $this->determineType($this->resolveDynamicTypeName($definition['type'], $replacements), $definitions); + $sub_type = $this->determineType(TypeResolver::resolveDynamicTypeName($definition['type'], $replacements), $definitions); $sub_definition = $definitions[$sub_type]; if (isset($definitions[$sub_type]['type'])) { $sub_merge = $this->getDefinition($definitions[$sub_type]['type'], $exception_on_invalid); @@ -395,45 +396,8 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI /** * Replaces dynamic type expressions in configuration type. * - * The configuration type name may contain one or more expressions to be - * replaced, enclosed in square brackets like '[name]' or '[%parent.id]' and - * will follow the replacement rules defined by the resolveExpression() - * method. - * - * @param string $type - * Configuration type, potentially with expressions in square brackets. - * @param array $data - * Configuration data for the element. - * - * @return string - * Configuration type name with all expressions resolved. - */ - protected function resolveDynamicTypeName(string $type, array $data): string { - // Parse the expressions in the dynamic type, if any. - if (preg_match_all("/\[(.*)\]/U", $type, $matches)) { - // Build our list of '[value]' => replacement. - $replace = []; - foreach (array_combine($matches[0], $matches[1]) as $key => $value) { - $replace[$key] = $this->resolveExpression($value, $data); - } - return strtr($type, $replace); - } - else { - // No expressions: nothing to resolve. - return $type; - } - } - - /** - * Replaces dynamic type expressions in configuration type. - * - * The configuration type name may contain one or more expressions to be - * replaced, enclosed in square brackets like '[name]' or '[%parent.id]' and - * will follow the replacement rules defined by the resolveExpression() - * method. - * * @param string $name - * Configuration type, potentially with expressions in square brackets. + * Configuration type, potentially with expressions in square brackets.f * @param array $data * Configuration data for the element. * @@ -441,113 +405,19 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI * Configuration type name with all expressions resolved. * * @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use - * ::resolveDynamicTypeName() instead. + * \Drupal\Core\Config\Schema\TypeResolver::resolveDynamicTypeName::resolveDynamicTypeName() + * instead. * * @see https://www.drupal.org/node/3408266 */ protected function replaceName($name, $data) { - @trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use ::resolveDynamicTypeName() instead. See https://www.drupal.org/node/3408266', E_USER_DEPRECATED); - return $this->resolveDynamicTypeName($name, $data); + @trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\Core\Config\Schema\TypeResolver::resolveDynamicTypeName() instead. See https://www.drupal.org/node/3408266', E_USER_DEPRECATED); + return TypeResolver::resolveDynamicTypeName($name, $data); } /** * Resolves a dynamic type expression using configuration data. * - * Dynamic type names are nested configuration keys containing expressions to - * be replaced by the value at the property path that the expression is - * pointing at. The expression may contain the following special strings: - * - '%key', will be replaced by the element's key. - * - '%parent', to reference the parent element. - * - '%type', to reference the schema definition type. Can only be used in - * combination with %parent. - * - * There may be nested configuration keys separated by dots or more complex - * patterns like '%parent.name' which references the 'name' value of the - * parent element. - * - * Example expressions: - * - 'name.subkey', indicates a nested value of the current element. - * - '%parent.name', will be replaced by the 'name' value of the parent. - * - '%parent.%key', will be replaced by the parent element's key. - * - '%parent.%type', will be replaced by the schema type of the parent. - * - '%parent.%parent.%type', will be replaced by the schema type of the - * parent's parent. - * - * @param string $expression - * Expression to be resolved. - * @param array $data - * Configuration data for the element. - * - * @return string - * The value the expression resolves to, or the given expression if it - * cannot be resolved. - * - * @todo Validate the expression in https://www.drupal.org/project/drupal/issues/3392903 - */ - protected function resolveExpression(string $expression, array $data): string { - assert(!str_contains($expression, '[') && !str_contains($expression, ']')); - $parts = explode('.', $expression); - // Process each value part, one at a time. - while ($name = array_shift($parts)) { - if (!is_array($data) || !isset($data[$name])) { - // Key not found, return original value - return $expression; - } - elseif (!$parts) { - $expression = $data[$name]; - if (is_bool($expression)) { - $expression = (int) $expression; - } - // If no more parts left, this is the final property. - return (string) $expression; - } - else { - // Get nested value and continue processing. - if ($name == '%parent') { - /** @var \Drupal\Core\Config\Schema\ArrayElement $parent */ - // Switch replacement values with values from the parent. - $parent = $data['%parent']; - $data = $parent->getValue(); - $data['%type'] = $parent->getDataDefinition()->getDataType(); - // The special %parent and %key values now need to point one level up. - if ($new_parent = $parent->getParent()) { - $data['%parent'] = $new_parent; - $data['%key'] = $new_parent->getName(); - } - } - else { - $data = $data[$name]; - } - } - } - - // Satisfy PHPStan, which cannot interpret the loop. - return $expression; - } - - /** - * Resolves a dynamic type expression using configuration data. - * - * Dynamic type names are nested configuration keys containing expressions to - * be replaced by the value at the property path that the expression is - * pointing at. The expression may contain the following special strings: - * - '%key', will be replaced by the element's key. - * - '%parent', to reference the parent element. - * - '%type', to reference the schema definition type. Can only be used in - * combination with %parent. - * - * There may be nested configuration keys separated by dots or more complex - * patterns like '%parent.name' which references the 'name' value of the - * parent element. - * - * Example expressions: - * - 'name.subkey', indicates a nested value of the current element. - * - '%parent.name', will be replaced by the 'name' value of the parent. - * - '%parent.%key', will be replaced by the parent element's key. - * - '%parent.%type', will be replaced by the schema type of the parent. - * - '%parent.%parent.%type', will be replaced by the schema type of the - * parent's parent. - * * @param string $value * Expression to be resolved. * @param array $data @@ -558,13 +428,14 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI * cannot be resolved. * * @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use - * ::resolveExpression() instead. + * \Drupal\Core\Config\Schema\TypeResolver::resolveDynamicTypeName::resolveExpression() + * instead. * * @see https://www.drupal.org/node/3408266 */ protected function replaceVariable($value, $data) { - @trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use ::resolveExpression() instead. See https://www.drupal.org/node/3408266', E_USER_DEPRECATED); - return $this->resolveExpression($value, $data); + @trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\Core\Config\Schema\TypeResolver::resolveExpression() instead. See https://www.drupal.org/node/3408266', E_USER_DEPRECATED); + return TypeResolver::resolveExpression($value, $data); } /** @@ -608,4 +479,48 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI return $this->create($data_definition, $config_data, $config_name); } + /** + * Resolves a dynamic type name. + * + * @param string $type + * Configuration type, potentially with expressions in square brackets. + * @param array $data + * Configuration data for the element. + * + * @return string + * Configuration type name with all expressions resolved. + * + * @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use + * \Drupal\Core\Config\Schema\TypeResolver::resolveDynamicTypeName() + * instead. + * + * @see https://www.drupal.org/node/3413264 + */ + protected function resolveDynamicTypeName(string $type, array $data): string { + @trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\Core\Config\Schema\TypeResolver::' . __FUNCTION__ . '() instead. See https://www.drupal.org/node/3413264', E_USER_DEPRECATED); + return TypeResolver::resolveDynamicTypeName($type, $data); + } + + /** + * Resolves a dynamic expression. + * + * @param string $expression + * Expression to be resolved. + * @param array|\Drupal\Core\TypedData\TypedDataInterface $data + * Configuration data for the element. + * + * @return string + * The value the expression resolves to, or the given expression if it + * cannot be resolved. + * + * @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use + * \Drupal\Core\Config\Schema\TypeResolver::resolveExpression() instead. + * + * @see https://www.drupal.org/node/3413264 + */ + protected function resolveExpression(string $expression, array $data): string { + @trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\Core\Config\Schema\TypeResolver::' . __FUNCTION__ . '() instead. See https://www.drupal.org/node/3413264', E_USER_DEPRECATED); + return TypeResolver::resolveExpression($expression, $data); + } + } diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/BundleConstraint.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/BundleConstraint.php index c2eaca6f80c..408171ab029 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/BundleConstraint.php +++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/BundleConstraint.php @@ -7,6 +7,9 @@ use Symfony\Component\Validator\Constraint; /** * Checks if a value is a valid entity type. * + * This differs from the `EntityBundleExists` constraint in that checks that the + * validated value is an *entity* of a particular bundle. + * * @Constraint( * id = "Bundle", * label = @Translation("Bundle", context = "Validation"), 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 00000000000..aaf70af66bf --- /dev/null +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/EntityBundleExistsConstraint.php @@ -0,0 +1,55 @@ +get(EntityTypeBundleInfoInterface::class), + ); + } + + /** + * {@inheritdoc} + */ + public function validate($value, Constraint $constraint) { + assert($constraint instanceof EntityBundleExistsConstraint); + + if (!is_string($value)) { + throw new UnexpectedTypeException($value, 'string'); + } + // Resolve any dynamic tokens, like %parent, in the entity type ID. + $entity_type_id = TypeResolver::resolveDynamicTypeName("[$constraint->entityTypeId]", $this->context->getObject()); + + if (!array_key_exists($value, $this->bundleInfo->getBundleInfo($entity_type_id))) { + $this->context->addViolation($constraint->message, [ + '@bundle' => $value, + '@entity_type_id' => $entity_type_id, + ]); + } + } + +} diff --git a/core/modules/contact/tests/src/Functional/ContactSitewideTest.php b/core/modules/contact/tests/src/Functional/ContactSitewideTest.php index 45cffdc9d2e..93555b0bc09 100644 --- a/core/modules/contact/tests/src/Functional/ContactSitewideTest.php +++ b/core/modules/contact/tests/src/Functional/ContactSitewideTest.php @@ -532,6 +532,10 @@ class ContactSitewideTest extends BrowserTestBase { $edit += $third_party_settings; $this->drupalGet('admin/structure/contact/add'); $this->submitForm($edit, 'Save'); + + // Ensure the statically cached bundle info is aware of the contact form + // that was just created in the UI. + $this->container->get('entity_type.bundle.info')->clearCachedBundles(); } /** diff --git a/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php b/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php index 4b2fdab08c2..fe4fe4bee68 100644 --- a/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php +++ b/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php @@ -54,6 +54,9 @@ class NodeAccessTest extends ModerationStateTestBase { parent::setUp(); $this->drupalLogin($this->adminUser); $this->createContentTypeFromUi('Moderated content', 'moderated_content', FALSE); + // Ensure the statically cached entity bundle info is aware of the content + // type that was just created in the UI. + $this->container->get('entity_type.bundle.info')->clearCachedBundles(); $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content'); // Add the private field to the node type. diff --git a/core/modules/field/tests/src/Functional/EntityReference/Views/SelectionTest.php b/core/modules/field/tests/src/Functional/EntityReference/Views/SelectionTest.php index 0981e3c2f20..928d4eb4255 100644 --- a/core/modules/field/tests/src/Functional/EntityReference/Views/SelectionTest.php +++ b/core/modules/field/tests/src/Functional/EntityReference/Views/SelectionTest.php @@ -66,6 +66,10 @@ class SelectionTest extends BrowserTestBase { $this->nodes[$node->id()] = $node; } + // Ensure the bundle to which the field is attached actually exists, or we + // will get config validation errors. + entity_test_create_bundle('test_bundle'); + // Create an entity reference field. $handler_settings = [ 'view' => [ diff --git a/core/modules/field/tests/src/Functional/FormTest.php b/core/modules/field/tests/src/Functional/FormTest.php index 45eddc53cf0..ebdcf56a43b 100644 --- a/core/modules/field/tests/src/Functional/FormTest.php +++ b/core/modules/field/tests/src/Functional/FormTest.php @@ -669,6 +669,9 @@ class FormTest extends FieldTestBase { $user = $this->drupalCreateUser(['administer entity_test content']); $this->drupalLogin($user); + // Ensure that the 'bar' bundle exists, to avoid config validation errors. + entity_test_create_bundle('bar', entity_type: 'entity_test_base_field_display'); + FieldStorageConfig::create([ 'entity_type' => 'entity_test_base_field_display', 'field_name' => 'foo', diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php index 305f04f83f3..2378e130856 100644 --- a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php +++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php @@ -2,8 +2,11 @@ namespace Drupal\Tests\field\Kernel\Entity; +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; /** * Tests validation of field_config entities. @@ -11,7 +14,14 @@ use Drupal\field\Entity\FieldStorageConfig; * @group field * @group #slow */ -class FieldConfigValidationTest extends FieldStorageConfigValidationTest { +class FieldConfigValidationTest extends ConfigEntityValidationTestBase { + + use ContentTypeCreationTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = ['field', 'node', 'entity_test', 'text', 'user']; /** * {@inheritdoc} @@ -19,14 +29,14 @@ class FieldConfigValidationTest extends FieldStorageConfigValidationTest { protected function setUp(): void { parent::setUp(); - // The field storage was created in the parent method. - $field_storage = $this->entity; + $this->installConfig('node'); + $this->createContentType(['type' => 'one']); + $this->createContentType(['type' => 'another']); - $this->entity = FieldConfig::create([ - 'field_storage' => $field_storage, - 'bundle' => 'user', - ]); - $this->entity->save(); + EntityTestBundle::create(['id' => 'one'])->save(); + EntityTestBundle::create(['id' => 'another'])->save(); + + $this->entity = FieldConfig::loadByName('node', 'one', 'body'); } /** @@ -87,6 +97,17 @@ class FieldConfigValidationTest extends FieldStorageConfigValidationTest { $this->assertValidationErrors([]); } + /** + * Tests that the target bundle of the field is checked. + */ + public function testTargetBundleMustExist(): void { + $this->entity->set('bundle', 'nope'); + $this->assertValidationErrors([ + '' => "The 'bundle' property cannot be changed.", + 'bundle' => "The 'nope' bundle does not exist on the 'node' entity type.", + ]); + } + /** * {@inheritdoc} */ @@ -96,7 +117,10 @@ class FieldConfigValidationTest extends FieldStorageConfigValidationTest { // settings from the *old* field_type won't match the config schema for the // settings of the *new* field_type. $this->entity->set('settings', []); - parent::testImmutableProperties($valid_values); + parent::testImmutableProperties([ + 'entity_type' => 'entity_test_with_bundle', + 'bundle' => 'another', + ]); } /** diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldEntitySettingsTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldEntitySettingsTest.php index da442c604ba..74f76547183 100644 --- a/core/modules/field/tests/src/Kernel/Entity/FieldEntitySettingsTest.php +++ b/core/modules/field/tests/src/Kernel/Entity/FieldEntitySettingsTest.php @@ -36,7 +36,7 @@ class FieldEntitySettingsTest extends KernelTestBase { /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */ $field_storage = FieldStorageConfig::create([ 'type' => 'integer', - 'entity_type' => 'entity_test', + 'entity_type' => 'entity_test_with_bundle', 'field_name' => 'test', ]); $field = FieldConfig::create([ @@ -95,10 +95,10 @@ class FieldEntitySettingsTest extends KernelTestBase { $field_storage = FieldStorageConfig::create([ 'field_name' => 'test_reference', 'type' => 'entity_reference', - 'entity_type' => 'entity_test', + 'entity_type' => 'entity_test_with_bundle', 'cardinality' => 1, 'settings' => [ - 'target_type' => 'entity_test', + 'target_type' => 'entity_test_with_bundle', ], ]); $field_storage->save(); @@ -111,10 +111,10 @@ class FieldEntitySettingsTest extends KernelTestBase { 'handler' => 'default', ], ]); - $this->assertSame('default:entity_test', $field->getSetting('handler')); + $this->assertSame('default:entity_test_with_bundle', $field->getSetting('handler')); // If the handler is changed, it should be normalized again on pre-save. $field->setSetting('handler', 'default')->save(); - $this->assertSame('default:entity_test', $field->getSetting('handler')); + $this->assertSame('default:entity_test_with_bundle', $field->getSetting('handler')); } } diff --git a/core/modules/field/tests/src/Kernel/EntityReference/Views/SelectionTest.php b/core/modules/field/tests/src/Kernel/EntityReference/Views/SelectionTest.php index 8110e0c525e..aa6303514ee 100644 --- a/core/modules/field/tests/src/Kernel/EntityReference/Views/SelectionTest.php +++ b/core/modules/field/tests/src/Kernel/EntityReference/Views/SelectionTest.php @@ -73,6 +73,10 @@ class SelectionTest extends KernelTestBase { $this->nodes[$node->id()] = $node; } + // Ensure the bundle to which the field is attached actually exists, or we + // will get config validation errors. + entity_test_create_bundle('test_bundle'); + // Create an entity reference field. $handler_settings = [ 'view' => [ diff --git a/core/modules/field/tests/src/Kernel/FieldAttachStorageTest.php b/core/modules/field/tests/src/Kernel/FieldAttachStorageTest.php index 16c43b86281..3a339575072 100644 --- a/core/modules/field/tests/src/Kernel/FieldAttachStorageTest.php +++ b/core/modules/field/tests/src/Kernel/FieldAttachStorageTest.php @@ -84,8 +84,8 @@ class FieldAttachStorageTest extends FieldKernelTestBase { 1 => 'test_bundle_1', 2 => 'test_bundle_2', ]; - entity_test_create_bundle($bundles[1]); - entity_test_create_bundle($bundles[2]); + entity_test_create_bundle($bundles[1], entity_type: $entity_type); + entity_test_create_bundle($bundles[2], entity_type: $entity_type); // Define 3 fields: // - field_1 is in bundle_1 and bundle_2, // - field_2 is in bundle_1, @@ -361,7 +361,12 @@ class FieldAttachStorageTest extends FieldKernelTestBase { $this->assertCount(4, $entity->{$this->fieldTestData->field_name}, 'First field got loaded'); $this->assertCount(1, $entity->{$field_name}, 'Second field got loaded'); - // Delete the bundle. + // Delete the bundle. The form display has to be deleted first to prevent + // schema errors when fields attached to the deleted bundle are themselves + // deleted, which triggers an update of the form display. + $this->container->get('entity_display.repository') + ->getFormDisplay($entity_type, $this->fieldTestData->field->getTargetBundle()) + ->delete(); entity_test_delete_bundle($this->fieldTestData->field->getTargetBundle(), $entity_type); // Verify no data gets loaded diff --git a/core/modules/field/tests/src/Kernel/FieldImportChangeTest.php b/core/modules/field/tests/src/Kernel/FieldImportChangeTest.php index c3c863ffa4f..a2821c920ff 100644 --- a/core/modules/field/tests/src/Kernel/FieldImportChangeTest.php +++ b/core/modules/field/tests/src/Kernel/FieldImportChangeTest.php @@ -26,6 +26,8 @@ class FieldImportChangeTest extends FieldKernelTestBase { * Tests importing an updated field. */ public function testImportChange() { + entity_test_create_bundle('test_bundle'); + $this->installConfig(['field_test_config']); $field_storage_id = 'field_test_import'; $field_id = "entity_test.entity_test.$field_storage_id"; diff --git a/core/modules/field/tests/src/Kernel/FieldImportDeleteTest.php b/core/modules/field/tests/src/Kernel/FieldImportDeleteTest.php index fa756f535e9..590e424ff0d 100644 --- a/core/modules/field/tests/src/Kernel/FieldImportDeleteTest.php +++ b/core/modules/field/tests/src/Kernel/FieldImportDeleteTest.php @@ -28,6 +28,8 @@ class FieldImportDeleteTest extends FieldKernelTestBase { * Tests deleting field storages and fields as part of config import. */ public function testImportDelete() { + entity_test_create_bundle('test_bundle'); + $this->installConfig(['field_test_config']); // At this point there are 5 field configuration objects in the active // storage. diff --git a/core/modules/language/config/schema/language.schema.yml b/core/modules/language/config/schema/language.schema.yml index 2df46a5e4e2..06259081d8c 100644 --- a/core/modules/language/config/schema/language.schema.yml +++ b/core/modules/language/config/schema/language.schema.yml @@ -117,6 +117,8 @@ language.content_settings.*.*: target_bundle: type: string label: 'Bundle' + constraints: + EntityBundleExists: '%parent.target_entity_type_id' default_langcode: type: langcode label: 'Default language' diff --git a/core/modules/language/tests/src/Functional/LanguageConfigurationElementTest.php b/core/modules/language/tests/src/Functional/LanguageConfigurationElementTest.php index 6f396cf96c1..1b025afd6dd 100644 --- a/core/modules/language/tests/src/Functional/LanguageConfigurationElementTest.php +++ b/core/modules/language/tests/src/Functional/LanguageConfigurationElementTest.php @@ -106,6 +106,10 @@ class LanguageConfigurationElementTest extends BrowserTestBase { ])->save(); } + // Ensure the bundles under test exist, to avoid config validation errors. + entity_test_create_bundle('custom_bundle'); + entity_test_create_bundle('some_bundle'); + // Fixed language. ContentLanguageSettings::loadByEntityTypeBundle('entity_test', 'custom_bundle') ->setLanguageAlterable(TRUE) diff --git a/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php b/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php index ba245ad0b59..93f05ac59d2 100644 --- a/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php +++ b/core/modules/language/tests/src/Kernel/ContentLanguageSettingsValidationTest.php @@ -2,8 +2,10 @@ namespace Drupal\Tests\language\Kernel; +use Drupal\entity_test\Entity\EntityTestBundle; use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase; use Drupal\language\Entity\ContentLanguageSettings; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; /** * Tests validation of content_language_settings entities. @@ -13,10 +15,19 @@ use Drupal\language\Entity\ContentLanguageSettings; */ class ContentLanguageSettingsValidationTest extends ConfigEntityValidationTestBase { + use ContentTypeCreationTrait; + /** * {@inheritdoc} */ - protected static $modules = ['language', 'user']; + protected static $modules = [ + 'entity_test', + 'field', + 'language', + 'node', + 'text', + 'user', + ]; /** * {@inheritdoc} @@ -28,12 +39,40 @@ class ContentLanguageSettingsValidationTest extends ConfigEntityValidationTestBa */ protected function setUp(): void { parent::setUp(); + $this->installConfig('node'); + + $this->createContentType(['type' => 'alpha']); + $this->createContentType(['type' => 'bravo']); + + EntityTestBundle::create(['id' => 'alpha'])->save(); + EntityTestBundle::create(['id' => 'bravo'])->save(); $this->entity = ContentLanguageSettings::create([ - 'target_entity_type_id' => 'user', - 'target_bundle' => 'user', + 'target_entity_type_id' => 'node', + 'target_bundle' => 'alpha', ]); $this->entity->save(); } + /** + * Tests that the target bundle of the language content settings is checked. + */ + public function testTargetBundleMustExist(): void { + $this->entity->set('target_bundle', 'superhero'); + $this->assertValidationErrors([ + '' => "The 'target_bundle' property cannot be changed.", + 'target_bundle' => "The 'superhero' bundle does not exist on the 'node' entity type.", + ]); + } + + /** + * {@inheritdoc} + */ + public function testImmutableProperties(array $valid_values = []): 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 f03d601e9c4..a09145a94d4 100644 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php @@ -3,8 +3,11 @@ namespace Drupal\Tests\layout_builder\Kernel; use Drupal\Core\Entity\Entity\EntityViewMode; +use Drupal\Core\Entity\EntityDisplayRepositoryInterface; +use Drupal\entity_test\Entity\EntityTestBundle; use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; /** * Tests validation of Layout Builder's entity_view_display entities. @@ -14,10 +17,19 @@ use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; */ class LayoutBuilderEntityViewDisplayValidationTest extends ConfigEntityValidationTestBase { + use ContentTypeCreationTrait; + /** * {@inheritdoc} */ - protected static $modules = ['layout_builder', 'user']; + protected static $modules = [ + 'entity_test', + 'field', + 'layout_builder', + 'node', + 'text', + 'user', + ]; /** * {@inheritdoc} @@ -25,18 +37,22 @@ class LayoutBuilderEntityViewDisplayValidationTest extends ConfigEntityValidatio protected function setUp(): void { parent::setUp(); + $this->installConfig('node'); + $this->createContentType(['type' => 'one']); + $this->createContentType(['type' => 'two']); + + EntityTestBundle::create(['id' => 'one'])->save(); + EntityTestBundle::create(['id' => 'two'])->save(); + EntityViewMode::create([ - 'id' => 'user.layout', + 'id' => 'node.layout', 'label' => 'Layout', - 'targetEntityType' => 'user', + 'targetEntityType' => 'node', ])->save(); - $this->entity = LayoutBuilderEntityViewDisplay::create([ - 'mode' => 'layout', - 'label' => 'Layout', - 'targetEntityType' => 'user', - 'bundle' => 'user', - ]); + $this->entity = $this->container->get(EntityDisplayRepositoryInterface::class) + ->getViewDisplay('node', 'one', 'layout'); + $this->assertInstanceOf(LayoutBuilderEntityViewDisplay::class, $this->entity); $this->entity->save(); } @@ -49,4 +65,14 @@ class LayoutBuilderEntityViewDisplayValidationTest extends ConfigEntityValidatio $this->markTestSkipped(); } + /** + * {@inheritdoc} + */ + public function testImmutableProperties(array $valid_values = []): void { + parent::testImmutableProperties([ + 'targetEntityType' => 'entity_test_with_bundle', + 'bundle' => 'two', + ]); + } + } diff --git a/core/modules/media_library/tests/src/Functional/MediaLibraryDisplayModeTest.php b/core/modules/media_library/tests/src/Functional/MediaLibraryDisplayModeTest.php index 91fe2428d2e..8c1d0249609 100644 --- a/core/modules/media_library/tests/src/Functional/MediaLibraryDisplayModeTest.php +++ b/core/modules/media_library/tests/src/Functional/MediaLibraryDisplayModeTest.php @@ -83,6 +83,9 @@ class MediaLibraryDisplayModeTest extends BrowserTestBase { // Display modes are created on install. $this->container->get('module_installer')->install(['media_library']); + // The container was rebuilt during module installation, so ensure we have + // an up-to-date reference to it. + $this->container = $this->kernel->getContainer(); // For a non-image media type without a mapped name field, the media_library // form mode should only contain the name field. @@ -183,6 +186,10 @@ class MediaLibraryDisplayModeTest extends BrowserTestBase { $this->assertFormDisplay($type_id, FALSE, FALSE); $this->assertViewDisplay($type_id, 'medium'); + // Now that all our media types have been created, ensure the bundle info + // cache is up-to-date. + $this->container->get('entity_type.bundle.info')->clearCachedBundles(); + // Delete a form and view display. EntityFormDisplay::load('media.type_one.media_library')->delete(); EntityViewDisplay::load('media.type_one.media_library')->delete(); diff --git a/core/modules/media_library/tests/src/Kernel/MediaLibraryAccessTest.php b/core/modules/media_library/tests/src/Kernel/MediaLibraryAccessTest.php index f2b92ea9645..207b1f32417 100644 --- a/core/modules/media_library/tests/src/Kernel/MediaLibraryAccessTest.php +++ b/core/modules/media_library/tests/src/Kernel/MediaLibraryAccessTest.php @@ -4,8 +4,8 @@ namespace Drupal\Tests\media_library\Kernel; use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResultReasonInterface; -use Drupal\entity_test\Entity\EntityTest; use Drupal\entity_test\Entity\EntityTestBundle; +use Drupal\entity_test\Entity\EntityTestWithBundle; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\KernelTests\KernelTestBase; @@ -50,7 +50,7 @@ class MediaLibraryAccessTest extends KernelTestBase { $this->installEntitySchema('user'); $this->installEntitySchema('file'); $this->installSchema('file', 'file_usage'); - $this->installEntitySchema('entity_test'); + $this->installEntitySchema('entity_test_with_bundle'); $this->installEntitySchema('filter_format'); $this->installEntitySchema('media'); $this->installConfig([ @@ -67,7 +67,7 @@ class MediaLibraryAccessTest extends KernelTestBase { $field_storage = FieldStorageConfig::create([ 'type' => 'entity_reference', 'field_name' => 'field_test_media', - 'entity_type' => 'entity_test', + 'entity_type' => 'entity_test_with_bundle', 'settings' => [ 'target_type' => 'media', ], @@ -92,7 +92,7 @@ class MediaLibraryAccessTest extends KernelTestBase { // Create a media library state to test access. $state = MediaLibraryState::create('media_library.opener.field_widget', ['file', 'image'], 'file', 2, [ - 'entity_type_id' => 'entity_test', + 'entity_type_id' => 'entity_test_with_bundle', 'bundle' => 'test', 'field_name' => 'field_test_media', ]); @@ -187,7 +187,7 @@ class MediaLibraryAccessTest extends KernelTestBase { /** @var \Drupal\media_library\MediaLibraryUiBuilder $ui_builder */ $ui_builder = $this->container->get('media_library.ui_builder'); - $forbidden_entity = EntityTest::create([ + $forbidden_entity = EntityTestWithBundle::create([ 'type' => 'test', // This label will automatically cause an access denial. // @see \Drupal\entity_test\EntityTestAccessControlHandler::checkAccess() @@ -206,7 +206,7 @@ class MediaLibraryAccessTest extends KernelTestBase { $access_result = $ui_builder->checkAccess($this->createUser(), $state); $this->assertAccess($access_result, FALSE, NULL, [], ['url.query_args']); - $neutral_entity = EntityTest::create([ + $neutral_entity = EntityTestWithBundle::create([ 'type' => 'test', // This label will result in neutral access. // @see \Drupal\entity_test\EntityTestAccessControlHandler::checkAccess() @@ -262,7 +262,7 @@ class MediaLibraryAccessTest extends KernelTestBase { public function testFieldWidgetEntityFieldAccess(string $field_type) { $field_storage = FieldStorageConfig::create([ 'type' => $field_type, - 'entity_type' => 'entity_test', + 'entity_type' => 'entity_test_with_bundle', // The media_library_test module will deny access to this field. // @see media_library_test_entity_field_access() 'field_name' => 'field_media_no_access', @@ -286,7 +286,7 @@ class MediaLibraryAccessTest extends KernelTestBase { // Test that access is denied even without an entity to work with. $state = MediaLibraryState::create('media_library.opener.field_widget', ['file', 'image'], 'file', 2, [ - 'entity_type_id' => 'entity_test', + 'entity_type_id' => 'entity_test_with_bundle', 'bundle' => 'test', 'field_name' => $field_storage->getName(), ]); @@ -294,7 +294,7 @@ class MediaLibraryAccessTest extends KernelTestBase { $this->assertAccess($access_result, FALSE, 'Field access denied by test module', [], ['url.query_args', 'user.permissions']); // Assert that field access is also checked with a real entity. - $entity = EntityTest::create([ + $entity = EntityTestWithBundle::create([ 'type' => 'test', 'name' => $this->randomString(), ]); @@ -323,7 +323,7 @@ class MediaLibraryAccessTest extends KernelTestBase { // Create a media library state to test access. $state = MediaLibraryState::create('media_library.opener.field_widget', ['file', 'image'], 'file', 2, [ - 'entity_type_id' => 'entity_test', + 'entity_type_id' => 'entity_test_with_bundle', 'bundle' => 'test', 'field_name' => 'field_test_media', ]); diff --git a/core/modules/system/tests/src/Functional/Entity/EntityAddUITest.php b/core/modules/system/tests/src/Functional/Entity/EntityAddUITest.php index 3e867098145..44a8bf2bc6f 100644 --- a/core/modules/system/tests/src/Functional/Entity/EntityAddUITest.php +++ b/core/modules/system/tests/src/Functional/Entity/EntityAddUITest.php @@ -141,7 +141,13 @@ class EntityAddUITest extends BrowserTestBase { $this->drupalLogin($admin_user); entity_test_create_bundle('test', 'Test label', 'entity_test_mul'); - // Delete the default bundle, so that we can rely on our own. + // Delete the default bundle, so that we can rely on our own. The form + // display has to be deleted first to prevent schema errors when fields + // attached to the deleted bundle are themselves deleted, which triggers + // an update of the form display. + $this->container->get('entity_display.repository') + ->getFormDisplay('entity_test_mul', 'entity_test_mul') + ->delete(); entity_test_delete_bundle('entity_test_mul', 'entity_test_mul'); // One bundle exists, confirm redirection to the add-form. diff --git a/core/modules/taxonomy/tests/src/Kernel/TermEntityReferenceTest.php b/core/modules/taxonomy/tests/src/Kernel/TermEntityReferenceTest.php index 9be55c85d55..a6ec897c6fa 100644 --- a/core/modules/taxonomy/tests/src/Kernel/TermEntityReferenceTest.php +++ b/core/modules/taxonomy/tests/src/Kernel/TermEntityReferenceTest.php @@ -82,6 +82,8 @@ class TermEntityReferenceTest extends KernelTestBase { 'cardinality' => 1, ]); $field_storage->save(); + + entity_test_create_bundle('test_bundle'); $field = FieldConfig::create([ 'field_storage' => $field_storage, 'entity_type' => 'entity_test', diff --git a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php index 71caf675a25..bcc2a303964 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php @@ -3,7 +3,9 @@ namespace Drupal\KernelTests\Core\Entity; use Drupal\Core\Field\Entity\BaseFieldOverride; +use Drupal\entity_test\Entity\EntityTestBundle; use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; /** * Tests validation of base_field_override entities. @@ -13,10 +15,12 @@ use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase; */ class BaseFieldOverrideValidationTest extends ConfigEntityValidationTestBase { + use ContentTypeCreationTrait; + /** * {@inheritdoc} */ - protected static $modules = ['user']; + protected static $modules = ['entity_test', 'field', 'node', 'text', 'user']; /** * {@inheritdoc} @@ -24,13 +28,31 @@ class BaseFieldOverrideValidationTest extends ConfigEntityValidationTestBase { protected function setUp(): void { parent::setUp(); - $fields = $this->container->get('entity_field.manager') - ->getBaseFieldDefinitions('user'); + $this->installConfig('node'); + $this->createContentType(['type' => 'one']); + $this->createContentType(['type' => 'another']); - $this->entity = BaseFieldOverride::createFromBaseFieldDefinition(reset($fields), 'user'); + EntityTestBundle::create(['id' => 'one'])->save(); + EntityTestBundle::create(['id' => 'another'])->save(); + + $fields = $this->container->get('entity_field.manager') + ->getBaseFieldDefinitions('node'); + + $this->entity = BaseFieldOverride::createFromBaseFieldDefinition(reset($fields), 'one'); $this->entity->save(); } + /** + * Tests that the target bundle of the field is checked. + */ + public function testTargetBundleMustExist(): void { + $this->entity->set('bundle', 'nope'); + $this->assertValidationErrors([ + '' => "The 'bundle' property cannot be changed.", + 'bundle' => "The 'nope' bundle does not exist on the 'node' entity type.", + ]); + } + /** * {@inheritdoc} */ @@ -40,7 +62,10 @@ class BaseFieldOverrideValidationTest extends ConfigEntityValidationTestBase { // settings from the *old* field_type won't match the config schema for the // settings of the *new* field_type. $this->entity->set('settings', []); - parent::testImmutableProperties($valid_values); + parent::testImmutableProperties([ + 'entity_type' => 'entity_test_with_bundle', + 'bundle' => 'another', + ]); } } diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleExistsConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleExistsConstraintValidatorTest.php new file mode 100644 index 00000000000..2c2b2192cc4 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleExistsConstraintValidatorTest.php @@ -0,0 +1,145 @@ + 'foo', + 'label' => 'Test', + ])->save(); + } + + /** + * Tests that the constraint validator will only work with strings. + */ + public function testValueMustBeAString(): void { + $definition = DataDefinition::create('any') + ->addConstraint('EntityBundleExists', 'entity_test_with_bundle'); + + $this->expectException(UnexpectedTypeException::class); + $this->expectExceptionMessage('Expected argument of type "string", "int" given'); + $this->container->get('typed_data_manager') + ->create($definition, 39) + ->validate(); + } + + /** + * Tests validating a bundle of a known (static) entity type ID. + */ + public function testEntityTypeIdIsStatic(): void { + $definition = DataDefinition::create('string') + ->addConstraint('EntityBundleExists', 'entity_test_with_bundle'); + + $violations = $this->container->get('typed_data_manager') + ->create($definition, 'bar') + ->validate(); + $this->assertCount(1, $violations); + $this->assertSame("The 'bar' bundle does not exist on the 'entity_test_with_bundle' entity type.", (string) $violations->get(0)->getMessage()); + $this->assertSame('', $violations->get(0)->getPropertyPath()); + } + + /** + * Tests getting the entity type ID from the parent property path. + * + * @param string $constraint_value + * The entity type ID to supply to the validation constraint. Must be a + * dynamic token starting with %. + * @param string $resolved_entity_type_id + * The actual entity type ID which should be checked for the existence of + * a bundle. + * + * @testWith ["%parent.entity_type_id", "entity_test_with_bundle"] + * ["%paren.entity_type_id", "%paren.entity_type_id"] + */ + public function testEntityTypeIdFromParent(string $constraint_value, string $resolved_entity_type_id): void { + /** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager */ + $typed_data_manager = $this->container->get('typed_data_manager'); + + $this->assertStringStartsWith('%', $constraint_value); + $value_definition = DataDefinition::create('string') + ->addConstraint('EntityBundleExists', $constraint_value); + + $parent_definition = MapDataDefinition::create() + ->setPropertyDefinition('entity_type_id', DataDefinition::create('string')) + ->setPropertyDefinition('bundle', $value_definition); + + $violations = $typed_data_manager->create($parent_definition, [ + 'entity_type_id' => 'entity_test_with_bundle', + 'bundle' => 'bar', + ])->validate(); + $this->assertCount(1, $violations); + $this->assertSame("The 'bar' bundle does not exist on the '$resolved_entity_type_id' entity type.", (string) $violations->get(0)->getMessage()); + $this->assertSame('bundle', $violations->get(0)->getPropertyPath()); + } + + /** + * Tests getting the entity type ID from a deeply nested property path. + */ + public function testEntityTypeIdFromMultipleParents(): void { + $tree_definition = MapDataDefinition::create() + ->setPropertyDefinition('info', MapDataDefinition::create() + ->setPropertyDefinition('entity_type_id', DataDefinition::create('string')) + ) + ->setPropertyDefinition('info2', MapDataDefinition::create() + ->setPropertyDefinition('bundle', DataDefinition::create('string') + ->addConstraint('EntityBundleExists', '%parent.%parent.info.entity_type_id') + ) + ); + + $violations = $this->container->get('typed_data_manager') + ->create($tree_definition, [ + 'info' => [ + 'entity_type_id' => 'entity_test_with_bundle', + ], + 'info2' => [ + 'bundle' => 'bar', + ], + ]) + ->validate(); + $this->assertCount(1, $violations); + $this->assertSame("The 'bar' bundle does not exist on the 'entity_test_with_bundle' entity type.", (string) $violations->get(0)->getMessage()); + $this->assertSame('info2.bundle', $violations->get(0)->getPropertyPath()); + } + + /** + * Tests when the constraint's entityTypeId value is not valid. + */ + public function testInvalidEntityTypeId(): void { + $entity_type_id = $this->randomMachineName(); + $definition = DataDefinition::create('string') + ->addConstraint('EntityBundleExists', $entity_type_id); + + $violations = $this->container->get('typed_data_manager') + ->create($definition, 'bar') + ->validate(); + $this->assertCount(1, $violations); + $this->assertSame("The 'bar' bundle does not exist on the '$entity_type_id' entity type.", (string) $violations->get(0)->getMessage()); + $this->assertSame('', $violations->get(0)->getPropertyPath()); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php index 2b2eecdb2dc..dca5203e382 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php @@ -3,9 +3,13 @@ namespace Drupal\KernelTests\Core\Entity; use Drupal\Core\Entity\Display\EntityFormDisplayInterface; -use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\Core\Entity\Entity\EntityFormMode; +use Drupal\Core\Entity\EntityDisplayRepositoryInterface; +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; /** * Tests validation of entity_form_display entities. @@ -13,25 +17,41 @@ use Drupal\field\Entity\FieldStorageConfig; * @group Entity * @group Validation */ -class EntityFormDisplayValidationTest extends EntityFormModeValidationTest { +class EntityFormDisplayValidationTest extends ConfigEntityValidationTestBase { + + use ContentTypeCreationTrait; /** * {@inheritdoc} */ protected bool $hasLabel = FALSE; + /** + * {@inheritdoc} + */ + protected static $modules = ['entity_test', 'field', 'node', 'text', 'user']; + /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); - $this->entity = EntityFormDisplay::create([ - 'targetEntityType' => 'user', - 'bundle' => 'user', - // The mode was created by the parent class. - 'mode' => 'test', - ]); + $this->installConfig('node'); + $this->createContentType(['type' => 'one']); + $this->createContentType(['type' => 'two']); + + EntityTestBundle::create(['id' => 'one'])->save(); + EntityTestBundle::create(['id' => 'two'])->save(); + + EntityFormMode::create([ + 'id' => 'node.test', + 'label' => 'Test', + 'targetEntityType' => 'node', + ])->save(); + + $this->entity = $this->container->get(EntityDisplayRepositoryInterface::class) + ->getFormDisplay('node', 'one', 'test'); $this->entity->save(); } @@ -40,7 +60,6 @@ class EntityFormDisplayValidationTest extends EntityFormModeValidationTest { */ public function testMultilineTextFieldWidgetPlaceholder(): void { // First, create a field for which widget settings exist. - $this->enableModules(['field', 'text']); $text_field_storage_config = FieldStorageConfig::create([ 'type' => 'text_with_summary', 'field_name' => 'novel', @@ -76,4 +95,25 @@ class EntityFormDisplayValidationTest extends EntityFormModeValidationTest { $this->assertValidationErrors([]); } + /** + * Tests that the target bundle of the entity form display is checked. + */ + public function testTargetBundleMustExist(): void { + $this->entity->set('bundle', 'superhero'); + $this->assertValidationErrors([ + '' => "The 'bundle' property cannot be changed.", + 'bundle' => "The 'superhero' bundle does not exist on the 'node' entity type.", + ]); + } + + /** + * {@inheritdoc} + */ + public function testImmutableProperties(array $valid_values = []): void { + parent::testImmutableProperties([ + 'targetEntityType' => 'entity_test_with_bundle', + 'bundle' => 'two', + ]); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php index f941802c143..425d1050dc4 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php @@ -96,7 +96,7 @@ class EntityQueryTest extends EntityKernelTestBase { do { $bundle = $this->randomMachineName(); } while ($bundles && strtolower($bundles[0]) >= strtolower($bundle)); - entity_test_create_bundle($bundle); + entity_test_create_bundle($bundle, entity_type: $field_storage->getTargetEntityTypeId()); foreach ($field_storages as $field_storage) { FieldConfig::create([ 'field_storage' => $field_storage, @@ -562,6 +562,7 @@ class EntityQueryTest extends EntityKernelTestBase { ]); $field_storage->save(); $bundle = $this->randomMachineName(); + entity_test_create_bundle($bundle); FieldConfig::create([ 'field_storage' => $field_storage, 'bundle' => $bundle, @@ -813,6 +814,7 @@ class EntityQueryTest extends EntityKernelTestBase { */ public function testCaseSensitivity() { $bundle = $this->randomMachineName(); + entity_test_create_bundle($bundle, entity_type: 'entity_test_mulrev'); $field_storage = FieldStorageConfig::create([ 'field_name' => 'field_ci', diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php index 8b93ce26bab..8596a96365f 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php @@ -2,7 +2,11 @@ namespace Drupal\KernelTests\Core\Entity; -use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\Core\Entity\Entity\EntityViewMode; +use Drupal\Core\Entity\EntityDisplayRepositoryInterface; +use Drupal\entity_test\Entity\EntityTestBundle; +use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; /** * Tests validation of entity_view_display entities. @@ -10,26 +14,63 @@ use Drupal\Core\Entity\Entity\EntityViewDisplay; * @group Entity * @group Validation */ -class EntityViewDisplayValidationTest extends EntityViewModeValidationTest { +class EntityViewDisplayValidationTest extends ConfigEntityValidationTestBase { + + use ContentTypeCreationTrait; /** * {@inheritdoc} */ protected bool $hasLabel = FALSE; + /** + * {@inheritdoc} + */ + protected static $modules = ['entity_test', 'field', 'node', 'text', 'user']; + /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); - $this->entity = EntityViewDisplay::create([ - 'targetEntityType' => 'user', - 'bundle' => 'user', - // The mode was created by the parent class. - 'mode' => 'test', - ]); + $this->installConfig('node'); + $this->createContentType(['type' => 'one']); + $this->createContentType(['type' => 'two']); + + EntityTestBundle::create(['id' => 'one'])->save(); + EntityTestBundle::create(['id' => 'two'])->save(); + + EntityViewMode::create([ + 'id' => 'node.test', + 'label' => 'Test', + 'targetEntityType' => 'node', + ])->save(); + + $this->entity = $this->container->get(EntityDisplayRepositoryInterface::class) + ->getViewDisplay('node', 'one', 'test'); $this->entity->save(); } + /** + * Tests that the target bundle of the entity view display is checked. + */ + public function testTargetBundleMustExist(): void { + $this->entity->set('bundle', 'superhero'); + $this->assertValidationErrors([ + '' => "The 'bundle' property cannot be changed.", + 'bundle' => "The 'superhero' bundle does not exist on the 'node' entity type.", + ]); + } + + /** + * {@inheritdoc} + */ + public function testImmutableProperties(array $valid_values = []): void { + parent::testImmutableProperties([ + 'targetEntityType' => 'entity_test_with_bundle', + 'bundle' => 'two', + ]); + } + }