diff --git a/core/lib/Drupal/Core/Recipe/Recipe.php b/core/lib/Drupal/Core/Recipe/Recipe.php index 54307bc98971b2b0391e47c2bf0fb1f37248c32c..888f54e4f42cfdea0afd0142c1c011dfa824efa3 100644 --- a/core/lib/Drupal/Core/Recipe/Recipe.php +++ b/core/lib/Drupal/Core/Recipe/Recipe.php @@ -6,6 +6,7 @@ use Drupal\Core\DefaultContent\Finder; use Drupal\Core\Extension\Dependency; +use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Extension\ModuleExtensionList; use Drupal\Core\Extension\ThemeExtensionList; use Drupal\Component\Serialization\Yaml; @@ -60,6 +61,8 @@ final class Recipe { * The default content finder. * @param string $path * The recipe's path. + * @param array $extra + * Any extra information to expose to specific modules. */ public function __construct( public readonly string $name, @@ -71,6 +74,7 @@ public function __construct( public readonly InputConfigurator $input, public readonly Finder $content, public readonly string $path, + private readonly array $extra, ) {} /** @@ -90,7 +94,7 @@ public static function createFromDirectory(string $path): static { $config = new ConfigConfigurator($recipe_data['config'], $path, \Drupal::service('config.storage')); $input = new InputConfigurator($recipe_data['input'] ?? [], $recipes, basename($path), \Drupal::typedDataManager()); $content = new Finder($path . '/content'); - return new static($recipe_data['name'], $recipe_data['description'], $recipe_data['type'], $recipes, $install, $config, $input, $content, $path); + return new static($recipe_data['name'], $recipe_data['description'], $recipe_data['type'], $recipes, $install, $config, $input, $content, $path, $recipe_data['extra'] ?? []); } /** @@ -296,6 +300,12 @@ private static function parse(string $file): array { 'content' => new Optional([ new Type('array'), ]), + 'extra' => new Optional([ + new Sequentially([ + new Type('associative_array'), + new Callback(self::validateKeysAreValidExtensionNames(...)), + ]), + ]), ]); $recipe_data = Yaml::decode($recipe_contents); @@ -423,4 +433,40 @@ private static function validateConfigActions(mixed $value, ExecutionContextInte } } + /** + * Validates that the keys of an array are valid extension names. + * + * Note that the keys do not have to be the names of extensions that are + * installed, or even extensions that exist. They just have to follow the + * form of a valid extension name. + * + * @param array $value + * The array being validated. + * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context + * The validator execution context. + */ + private static function validateKeysAreValidExtensionNames(array $value, ExecutionContextInterface $context): void { + $keys = array_keys($value); + foreach ($keys as $key) { + if (!preg_match(ExtensionDiscovery::PHP_FUNCTION_PATTERN, $key)) { + $context->addViolation('%name is not a valid extension name.', [ + '%name' => $key, + ]); + } + } + } + + /** + * Returns extra information to expose to a particular extension. + * + * @param string $extension_name + * The name of a Drupal extension. + * + * @return mixed + * The extra data exposed to the given extension, or NULL if there is none. + */ + public function getExtra(string $extension_name): mixed { + return $this->extra[$extension_name] ?? NULL; + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeTest.php index b63861276ff34092cfb8ddc522fa3a2a03d99144..e9f5d3e0bf018238a72f80588a5247e23e663977 100644 --- a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeTest.php +++ b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeTest.php @@ -98,4 +98,19 @@ public function testImplicitlyRequiredModule(): void { $this->assertIsObject($recipe); } + /** + * Tests getting extra extension-specific info from a recipe. + * + * @covers ::getExtra + */ + public function testExtra(): void { + $recipe = $this->createRecipe([ + 'name' => 'Getting extra info', + 'extra' => [ + 'special_sauce' => 'Wasabi', + ], + ]); + $this->assertSame('Wasabi', $recipe->getExtra('special_sauce')); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php index 8e4f621b2ad3a8821657b3333754c559df360c0a..62f14df4d2020757c73f5ccc9faa943032266766 100644 --- a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php @@ -719,6 +719,45 @@ public static function providerRecipeValidation(): iterable { default: source: config config: ['system.site', 'mail'] +YAML, + NULL, + ]; + yield 'extra is present and not an array' => [ + <<<YAML +name: Bad extra +extra: 'yes!' +YAML, + [ + '[extra]' => ['This value should be of type associative_array.'], + ], + ]; + yield 'extra is an indexed array' => [ + <<<YAML +name: Bad extra +extra: + - one + - two +YAML, + [ + '[extra]' => ['This value should be of type associative_array.'], + ], + ]; + yield 'invalid key in extra' => [ + <<<YAML +name: Bad extra +extra: + 'not a valid extension name': true +YAML, + [ + '[extra]' => ['not a valid extension name is not a valid extension name.'], + ], + ]; + yield 'valid extra' => [ + <<<YAML +name: Bad extra +extra: + project_browser: + yes: sir YAML, NULL, ];