Skip to content
Snippets Groups Projects
Verified Commit 43ce451d authored by Dave Long's avatar Dave Long
Browse files

Issue #3447210 by alexpott, jnicola: Move RecipeDiscovery into RecipeConfigurator

(cherry picked from commit 2e3e8746)
parent 8a26f338
Branches
No related tags found
6 merge requests!11958Issue #3490507 by alexpott, smustgrave: Fix bogus mocking in...,!11769Issue #3517987: Add option to contextual filters to encode slashes in query parameter.,!11185Issue #3477324 by andypost, alexpott: Fix usage of str_getcsv() and fgetcsv() for PHP 8.4,!9944Issue #3483353: Consider making the createCopy config action optionally fail...,!8325Update file Sort.php,!8095Expose document root on install
Pipeline #173926 passed
Pipeline: drupal

#173928

    ......@@ -57,8 +57,7 @@ public function __construct(
    public static function createFromDirectory(string $path): static {
    $recipe_data = self::parse($path . '/recipe.yml');
    $recipe_discovery = static::getRecipeDiscovery(dirname($path));
    $recipes = new RecipeConfigurator(is_array($recipe_data['recipes']) ? $recipe_data['recipes'] : [], $recipe_discovery);
    $recipes = new RecipeConfigurator(is_array($recipe_data['recipes']) ? $recipe_data['recipes'] : [], dirname($path));
    $install = new InstallConfigurator($recipe_data['install'], \Drupal::service('extension.list.module'), \Drupal::service('extension.list.theme'));
    $config = new ConfigConfigurator($recipe_data['config'], $path, \Drupal::service('config.storage'));
    $content = new Finder($path . '/content');
    ......@@ -90,7 +89,7 @@ private static function parse(string $file): array {
    // recipes.
    // @see ::validateRecipeExists()
    // @see ::validateConfigActions()
    $discovery = self::getRecipeDiscovery(dirname($file, 2));
    $include_path = dirname($file, 2);
    $constraints = new Collection([
    'name' => new Required([
    ......@@ -136,7 +135,7 @@ private static function parse(string $file): array {
    ),
    new Callback(
    callback: self::validateRecipeExists(...),
    payload: $discovery,
    payload: $include_path,
    ),
    ]),
    ]),
    ......@@ -177,7 +176,7 @@ private static function parse(string $file): array {
    new NotBlank(),
    new Callback(
    callback: self::validateConfigActions(...),
    payload: $discovery,
    payload: $include_path,
    ),
    ]),
    ]),
    ......@@ -232,33 +231,21 @@ private static function validateExtensionIsAvailable(string $value, ExecutionCon
    * The machine name of the recipe to look for.
    * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
    * The validator execution context.
    * @param \Drupal\Core\Recipe\RecipeDiscovery $discovery
    * A discovery object to find other recipes.
    * @param string $include_path
    * The recipe's include path.
    */
    private static function validateRecipeExists(string $name, ExecutionContextInterface $context, RecipeDiscovery $discovery): void {
    private static function validateRecipeExists(string $name, ExecutionContextInterface $context, string $include_path): void {
    if (empty($name)) {
    return;
    }
    try {
    $discovery->getRecipe($name);
    RecipeConfigurator::getIncludedRecipe($include_path, $name);
    }
    catch (UnknownRecipeException) {
    $context->addViolation('The %name recipe does not exist.', ['%name' => $name]);
    }
    }
    /**
    * Gets the recipe discovery object for a recipe.
    *
    * @param string $recipeDirectory
    * The directory the contains the recipe.
    *
    * @return \Drupal\Core\Recipe\RecipeDiscovery
    */
    private static function getRecipeDiscovery(string $recipeDirectory): RecipeDiscovery {
    return new RecipeDiscovery($recipeDirectory);
    }
    /**
    * Validates that the corresponding extension is enabled for a config action.
    *
    ......@@ -266,10 +253,10 @@ private static function getRecipeDiscovery(string $recipeDirectory): RecipeDisco
    * The config action; not used.
    * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
    * The validator execution context.
    * @param \Drupal\Core\Recipe\RecipeDiscovery $discovery
    * A discovery object to find other recipes.
    * @param string $include_path
    * The recipe's include path.
    */
    private static function validateConfigActions(mixed $value, ExecutionContextInterface $context, RecipeDiscovery $discovery): void {
    private static function validateConfigActions(mixed $value, ExecutionContextInterface $context, string $include_path): void {
    $config_name = str_replace(['[config][actions]', '[', ']'], '', $context->getPropertyPath());
    [$config_provider] = explode('.', $config_name);
    if ($config_provider === 'core') {
    ......@@ -279,7 +266,7 @@ private static function validateConfigActions(mixed $value, ExecutionContextInte
    $recipe_being_validated = $context->getRoot();
    assert(is_array($recipe_being_validated));
    $configurator = new RecipeConfigurator($recipe_being_validated['recipes'] ?? [], $discovery);
    $configurator = new RecipeConfigurator($recipe_being_validated['recipes'] ?? [], $include_path);
    // The config provider must either be an already-installed module or theme,
    // or an extension being installed by this recipe or a recipe it depends on.
    ......
    ......@@ -10,17 +10,57 @@
    */
    final class RecipeConfigurator {
    /**
    * @var \Drupal\Core\Recipe\Recipe[]
    */
    public readonly array $recipes;
    /**
    * @param string[] $recipes
    * A list of recipes for a recipe to apply. The recipes will be applied in
    * the order listed.
    * @param \Drupal\Core\Recipe\RecipeDiscovery $recipeDiscovery
    * Recipe discovery.
    * @param string $include_path
    * The recipe's include path.
    */
    public function __construct(array $recipes, RecipeDiscovery $recipeDiscovery) {
    $this->recipes = array_map([$recipeDiscovery, 'getRecipe'], $recipes);
    public function __construct(array $recipes, string $include_path) {
    $this->recipes = array_map(fn(string $name) => static::getIncludedRecipe($include_path, $name), $recipes);
    }
    /**
    * Gets an included recipe object.
    *
    * @param string $include_path
    * The recipe's include path.
    * @param string $name
    * The machine name of the recipe to get.
    *
    * @return \Drupal\Core\Recipe\Recipe
    * The recipe object.
    *
    * @throws \Drupal\Core\Recipe\UnknownRecipeException
    * Thrown when the recipe cannot be found.
    */
    public static function getIncludedRecipe(string $include_path, string $name): Recipe {
    // In order to allow recipes to include core provided recipes, $name can be
    // a Drupal root relative path to a recipe folder. For example, a recipe can
    // include the core provided 'article_tags' recipe by listing the recipe as
    // 'core/recipes/article_tags'. It is strongly recommended not to rely on
    // relative paths for including recipes. Required recipes should be put in
    // the same parent directory as the recipe being applied. Note, only linux
    // style directory separators are supported. PHP on Windows can resolve the
    // mix of directory separators.
    if (str_contains($name, '/')) {
    $path = \Drupal::root() . "/$name/recipe.yml";
    }
    else {
    $path = $include_path . "/$name/recipe.yml";
    }
    if (file_exists($path)) {
    return Recipe::createFromDirectory(dirname($path));
    }
    $search_path = dirname($path, 2);
    throw new UnknownRecipeException($name, $search_path, sprintf("Can not find the %s recipe, search path: %s", $name, $search_path));
    }
    /**
    ......
    <?php
    declare(strict_types=1);
    namespace Drupal\Core\Recipe;
    /**
    * @internal
    * This API is experimental.
    */
    final class RecipeDiscovery {
    /**
    * Constructs a recipe discovery object.
    *
    * @param string $path
    * The path will be searched folders containing a recipe.yml. There will be
    * no traversal further into the directory structure.
    */
    public function __construct(protected string $path) {
    }
    /**
    * Gets a recipe object.
    *
    * @param string $name
    * The machine name of the recipe to find.
    *
    * @return \Drupal\Core\Recipe\Recipe
    * The recipe object.
    *
    * @throws \Drupal\Core\Recipe\UnknownRecipeException
    * Thrown when the recipe cannot be found.
    */
    public function getRecipe(string $name): Recipe {
    // In order to allow recipes to include core provided recipes, $name can be
    // a Drupal root relative path to a recipe folder. For example, a recipe can
    // include the core provided 'article_tags' recipe by listing the recipe as
    // 'core/recipes/article_tags'. It is strongly recommended not to rely on
    // relative paths for including recipes. Required recipes should be put in
    // the same parent directory as the recipe being applied. Note, only linux
    // style directory separators are supported. PHP on Windows can resolve the
    // mix of directory separators.
    if (str_contains($name, '/')) {
    $path = \Drupal::root() . "/$name/recipe.yml";
    }
    else {
    $path = $this->path . "/$name/recipe.yml";
    }
    if (file_exists($path)) {
    return Recipe::createFromDirectory(dirname($path));
    }
    $search_path = dirname($path, 2);
    throw new UnknownRecipeException($name, $search_path, sprintf("Can not find the %s recipe, search path: %s", $name, $search_path));
    }
    }
    ......@@ -6,20 +6,19 @@
    use Drupal\Core\Recipe\Recipe;
    use Drupal\Core\Recipe\RecipeConfigurator;
    use Drupal\Core\Recipe\RecipeDiscovery;
    use Drupal\Core\Recipe\UnknownRecipeException;
    use Drupal\KernelTests\KernelTestBase;
    /**
    * @covers \Drupal\Core\Recipe\RecipeConfigurator
    * @coversDefaultClass \Drupal\Core\Recipe\RecipeConfigurator
    * @group Recipe
    */
    class RecipeConfiguratorTest extends KernelTestBase {
    public function testRecipeConfigurator(): void {
    $discovery = new RecipeDiscovery('core/tests/fixtures/recipes');
    $recipe_configurator = new RecipeConfigurator(
    ['install_two_modules', 'install_node_with_config', 'recipe_include'],
    $discovery
    'core/tests/fixtures/recipes'
    );
    // Private method "listAllRecipes".
    $reflection = new \ReflectionMethod('\Drupal\Core\Recipe\RecipeConfigurator', 'listAllRecipes');
    ......@@ -50,4 +49,37 @@ public function testRecipeConfigurator(): void {
    $this->assertEquals(1, array_count_values($recipe_extensions)['field']);
    }
    /**
    * Tests that RecipeConfigurator can load recipes.
    *
    * @testWith ["install_two_modules", "Install two modules"]
    * ["recipe_include", "Recipe include"]
    *
    * @covers ::getIncludedRecipe
    */
    public function testIncludedRecipeLoader(string $recipe, string $name): void {
    $recipe = RecipeConfigurator::getIncludedRecipe('core/tests/fixtures/recipes', $recipe);
    $this->assertSame($name, $recipe->name);
    }
    /**
    * Tests exception thrown when RecipeConfigurator cannot find a recipe.
    *
    * @testWith ["no_recipe"]
    * ["does_not_exist"]
    *
    * @covers ::getIncludedRecipe
    */
    public function testIncludedRecipeLoaderException(string $recipe): void {
    try {
    RecipeConfigurator::getIncludedRecipe('core/tests/fixtures/recipes', $recipe);
    $this->fail('Expected exception not thrown');
    }
    catch (UnknownRecipeException $e) {
    $this->assertSame($recipe, $e->recipe);
    $this->assertSame('core/tests/fixtures/recipes', $e->searchPath);
    $this->assertSame('Can not find the ' . $recipe . ' recipe, search path: ' . $e->searchPath, $e->getMessage());
    }
    }
    }
    <?php
    declare(strict_types=1);
    namespace Drupal\KernelTests\Core\Recipe;
    use Drupal\Core\Recipe\RecipeDiscovery;
    use Drupal\Core\Recipe\UnknownRecipeException;
    use Drupal\KernelTests\KernelTestBase;
    /**
    * @coversDefaultClass \Drupal\Core\Recipe\RecipeDiscovery
    * @group Recipe
    */
    class RecipeDiscoveryTest extends KernelTestBase {
    /**
    * Tests that recipe discovery can find recipes.
    *
    * @testWith ["install_two_modules", "Install two modules"]
    * ["recipe_include", "Recipe include"]
    */
    public function testRecipeDiscovery(string $recipe, string $name): void {
    $discovery = new RecipeDiscovery('core/tests/fixtures/recipes');
    $recipe = $discovery->getRecipe($recipe);
    $this->assertSame($name, $recipe->name);
    }
    /**
    * Tests the exception thrown when recipe discovery cannot find a recipe.
    *
    * @testWith ["no_recipe"]
    * ["does_not_exist"]
    */
    public function testRecipeDiscoveryException(string $recipe): void {
    $discovery = new RecipeDiscovery('core/tests/fixtures/recipes');
    try {
    $discovery->getRecipe($recipe);
    $this->fail('Expected exception not thrown');
    }
    catch (UnknownRecipeException $e) {
    $this->assertSame($recipe, $e->recipe);
    $this->assertSame('core/tests/fixtures/recipes', $e->searchPath);
    $this->assertSame('Can not find the ' . $recipe . ' recipe, search path: ' . $e->searchPath, $e->getMessage());
    }
    }
    }
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Please register or to comment