diff --git a/core/lib/Drupal/Core/Recipe/RecipeRunner.php b/core/lib/Drupal/Core/Recipe/RecipeRunner.php index 4310a742ab04c9a03e5b3e430675f2479d848e29..9c00b0e363c02cec5790901892cf1386e1b8efd3 100644 --- a/core/lib/Drupal/Core/Recipe/RecipeRunner.php +++ b/core/lib/Drupal/Core/Recipe/RecipeRunner.php @@ -4,6 +4,8 @@ namespace Drupal\Core\Recipe; +use Drupal\Core\Config\Action\ConfigActionException; +use Drupal\Core\Config\ConfigManagerInterface; use Drupal\Core\Config\FileStorage; use Drupal\Core\Config\InstallStorage; use Drupal\Core\Config\StorageInterface; @@ -93,11 +95,14 @@ protected static function processInstall(InstallConfigurator $install, StorageIn * The recipe being applied. */ protected static function processConfiguration(Recipe $recipe): void { + /** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */ + $config_manager = \Drupal::service(ConfigManagerInterface::class); + $config_installer = new RecipeConfigInstaller( \Drupal::service('config.factory'), \Drupal::service('config.storage'), \Drupal::service('config.typed'), - \Drupal::service('config.manager'), + $config_manager, \Drupal::service('event_dispatcher'), NULL, \Drupal::service('extension.path.resolver')); @@ -118,6 +123,13 @@ protected static function processConfiguration(Recipe $recipe): void { /** @var \Drupal\Core\Config\Action\ConfigActionManager $config_action_manager */ $config_action_manager = \Drupal::service('plugin.manager.config_action'); foreach ($config->config['actions'] as $config_name => $actions) { + // If this config name contains an input value, it must begin with the + // config prefix of a known entity type. + if (str_contains($config_name, '${') && empty($config_manager->getEntityTypeIdByName($config_name))) { + throw new ConfigActionException("The entity type for the config name '$config_name' could not be identified."); + } + $config_name = str_replace($keys, $replace, $config_name); + foreach ($actions as $action_id => $data) { $config_action_manager->applyAction($action_id, $config_name, static::replaceInputValues($data, $replace)); } diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php index cc60172a9605ff5279abc7c29d34c5650416b961..cd3f14644e9ea7a1bfa7de1dcc767c3ac8483b9f 100644 --- a/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php +++ b/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php @@ -6,6 +6,7 @@ use Drupal\Component\Uuid\UuidInterface; use Drupal\contact\Entity\ContactForm; +use Drupal\Core\Config\Action\ConfigActionException; use Drupal\Core\Recipe\ConsoleInputCollector; use Drupal\Core\Recipe\InputCollectorInterface; use Drupal\Core\Recipe\Recipe; @@ -14,6 +15,7 @@ use Drupal\Core\TypedData\TypedDataInterface; use Drupal\FunctionalTests\Core\Recipe\RecipeTestTrait; use Drupal\KernelTests\KernelTestBase; +use Drupal\node\Entity\NodeType; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Style\StyleInterface; use Symfony\Component\Validator\Exception\ValidationFailedException; @@ -229,4 +231,45 @@ public function testLiterals(): void { $this->assertSame('int is 1234, bool is and float is 3.141', $config->get('slogan')); } + /** + * Tests using input values in entity IDs for config actions. + */ + public function testInputInConfigEntityIds(): void { + $this->assertFalse(\Drupal::moduleHandler()->moduleExists('node')); + + $collector = new class () implements InputCollectorInterface { + + /** + * {@inheritdoc} + */ + public function collectValue(string $name, DataDefinitionInterface $definition, mixed $default_value): mixed { + return $default_value; + } + + }; + $recipe = Recipe::createFromDirectory('core/tests/fixtures/recipes/input_test'); + $recipe->input->collectAll($collector); + RecipeRunner::processRecipe($recipe); + $this->assertInstanceOf(NodeType::class, NodeType::load('test')); + + // Using an input placeholder in a non-identifying part of the config entity + // ID should cause an exception. + $recipe = $this->createRecipe([ + 'name' => 'Invalid use of an input in config entity ID', + 'config' => [ + 'actions' => [ + 'node.${anything}.test' => [ + 'createIfNotExists' => [ + 'id' => 'test', + ], + ], + ], + ], + ]); + $recipe->input->collectAll($collector); + $this->expectException(ConfigActionException::class); + $this->expectExceptionMessage("The entity type for the config name 'node.\${anything}.test' could not be identified."); + RecipeRunner::processRecipe($recipe); + } + } diff --git a/core/tests/fixtures/recipes/input_test/recipe.yml b/core/tests/fixtures/recipes/input_test/recipe.yml index 279bc7fee4522e2d5114de55bd4c65e68258f090..7e8964429a72b4f589ae93830ff8459b4a12e675 100644 --- a/core/tests/fixtures/recipes/input_test/recipe.yml +++ b/core/tests/fixtures/recipes/input_test/recipe.yml @@ -3,6 +3,8 @@ recipes: # Depend on another recipe in order to prove that we can collect input # from recipes that depend on other recipes. - no_extensions +install: + - node input: owner: data_type: string @@ -27,8 +29,17 @@ input: default: source: value value: false + node_type: + data_type: string + description: 'The ID of a node type to create' + default: + source: value + value: 'test' config: actions: + node.type.${node_type}: + createIfNotExists: + name: Test Content Type system.site: simpleConfigUpdate: name: "${owner}'s Turf"