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

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

parent a885cc86
No related branches found
No related tags found
28 merge requests!11131[10.4.x-only-DO-NOT-MERGE]: Issue ##2842525 Ajax attached to Views exposed filter form does not trigger callbacks,!9470[10.3.x-only-DO-NOT-MERGE]: #3331771 Fix file_get_contents(): Passing null to parameter,!8736Update the Documention As per the Function uses.,!8540Issue #3457061: Bootstrap Modal dialog Not closing after 10.3.0 Update,!8528Issue #3456871 by Tim Bozeman: Support NULL services,!8513Issue #3453786: DefaultSelection should document why values for target_bundles NULL and [] behave as they do,!8373Issue #3427374 by danflanagan8, Vighneshh: taxonomy_tid ViewsArgumentDefault...,!8256Issue #3445896 by mstrelan, mondrake: PHPUnit\Runner\ErrorHandler::__construct...,!8126Added escape fucntionality on admintoolbar close icon,!7927Issue #3445425: \Drupal\Core\Template\Attribute should implement Countable interface,!3878Removed unused condition head title for views,!3818Issue #2140179: $entity->original gets stale between updates,!3742Issue #3328429: Create item list field formatter for displaying ordered and unordered lists,!3731Claro: role=button on status report items,!3651Issue #3347736: Create new SDC component for Olivero (header-search),!3531Issue #3336994: StringFormatter always displays links to entity even if the user in context does not have access,!3355Issue #3209129: Scrolling problems when adding a block via layout builder,!3154Fixes #2987987 - CSRF token validation broken on routes with optional parameters.,!3133core/modules/system/css/components/hidden.module.css,!2812Issue #3312049: [Followup] Fix Drupal.Commenting.FunctionComment.MissingReturnType returns for NULL,!2378Issue #2875033: Optimize joins and table selection in SQL entity query implementation,!2062Issue #3246454: Add weekly granularity to views date sort,!1105Issue #3025039: New non translatable field on translatable content throws error,!877Issue #2708101: Default value for link text is not saved,!617Issue #3043725: Provide a Entity Handler for user cancelation,!579Issue #2230909: Simple decimals fail to pass validation,!560Move callback classRemove outside of the loop,!555Issue #3202493
Pipeline #174086 canceled
......@@ -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.
Finish editing this message first!
Please register or to comment