class RecipeRunner
Same name and namespace in other branches
- 11.x core/lib/Drupal/Core/Recipe/RecipeRunner.php \Drupal\Core\Recipe\RecipeRunner
- 10 core/lib/Drupal/Core/Recipe/RecipeRunner.php \Drupal\Core\Recipe\RecipeRunner
Applies a recipe.
This class is currently static and use \Drupal::service() in order to put off having to solve issues caused by container rebuilds during module install and configuration import.
@internal This API is experimental.
@todo https://www.drupal.org/i/3439717 Determine if there is a better to inject and re-inject services.
Hierarchy
- class \Drupal\Core\Recipe\RecipeRunner
Expanded class hierarchy of RecipeRunner
14 files declare their use of RecipeRunner
- AddItemToToolbarConfigActionTest.php in core/
modules/ ckeditor5/ tests/ src/ Kernel/ ConfigAction/ AddItemToToolbarConfigActionTest.php - AddModerationConfigActionTest.php in core/
modules/ content_moderation/ tests/ src/ Kernel/ ConfigAction/ AddModerationConfigActionTest.php - AddToAllBundlesConfigActionTest.php in core/
modules/ field/ tests/ src/ Kernel/ AddToAllBundlesConfigActionTest.php - ConfigActionValidationTest.php in core/
tests/ Drupal/ KernelTests/ Core/ Recipe/ ConfigActionValidationTest.php - ConfigConfiguratorTest.php in core/
tests/ Drupal/ KernelTests/ Core/ Recipe/ ConfigConfiguratorTest.php
File
-
core/
lib/ Drupal/ Core/ Recipe/ RecipeRunner.php, line 32
Namespace
Drupal\Core\RecipeView source
final class RecipeRunner {
/**
* @param \Drupal\Core\Recipe\Recipe $recipe
* The recipe to apply.
*/
public static function processRecipe(Recipe $recipe) : void {
static::processRecipes($recipe->recipes);
static::processInstall($recipe->install, $recipe->config
->getConfigStorage());
static::processConfiguration($recipe);
static::processContent($recipe->content);
static::triggerEvent($recipe);
}
/**
* Triggers the RecipeAppliedEvent.
*
* @param \Drupal\Core\Recipe\Recipe $recipe
* The recipe to apply.
* @param array<mixed>|null $context
* The batch context if called by a batch.
*/
public static function triggerEvent(Recipe $recipe, ?array &$context = NULL) : void {
$event = new RecipeAppliedEvent($recipe);
\Drupal::service(EventDispatcherInterface::class)->dispatch($event);
$context['message'] = t('Applied %recipe recipe.', [
'%recipe' => $recipe->name,
]);
$context['results']['recipe'][] = $recipe->name;
}
/**
* Applies any recipes listed by the recipe.
*
* @param \Drupal\Core\Recipe\RecipeConfigurator $recipes
* The list of recipes to apply.
*/
protected static function processRecipes(RecipeConfigurator $recipes) : void {
foreach ($recipes->recipes as $recipe) {
static::processRecipe($recipe);
}
}
/**
* Installs the extensions.
*
* @param \Drupal\Core\Recipe\InstallConfigurator $install
* The list of extensions to install.
* @param \Drupal\Core\Config\StorageInterface $recipeConfigStorage
* The recipe's configuration storage. Used to override extension provided
* configuration.
*/
protected static function processInstall(InstallConfigurator $install, StorageInterface $recipeConfigStorage) : void {
if (!empty($install->modules)) {
foreach (array_chunk($install->modules, Settings::get('core.multi_module_install_batch_size', 20)) as $modules_chunk) {
static::installModules($modules_chunk, $recipeConfigStorage);
}
}
// Themes can depend on modules so have to be installed after modules.
foreach ($install->themes as $name) {
static::installTheme($name, $recipeConfigStorage);
}
}
/**
* Creates configuration and applies configuration actions.
*
* @param \Drupal\Core\Recipe\Recipe $recipe
* 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'), $config_manager, \Drupal::service('event_dispatcher'), NULL, \Drupal::service('extension.path.resolver'));
$config = $recipe->config;
// Create configuration that is either supplied by the recipe or listed in
// the config.import section that does not exist.
$config_installer->installRecipeConfig($config);
if (!empty($config->config['actions'])) {
$values = $recipe->input
->getValues();
// Wrap the replacement strings with `${` and `}`, which is a fairly
// common style of placeholder.
$keys = array_map(fn($k) => sprintf('${%s}', $k), array_keys($values));
$replace = array_combine($keys, $values);
// Process the actions.
/** @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));
}
}
}
}
/**
* Creates content contained in a recipe.
*
* @param \Drupal\Core\DefaultContent\Finder $content
* The content finder object for the recipe.
*/
protected static function processContent(Finder $content) : void {
/** @var \Drupal\Core\DefaultContent\Importer $importer */
$importer = \Drupal::service(Importer::class);
$importer->setLogger(\Drupal::logger('recipe'));
$importer->importContent($content, Existing::Skip);
}
/**
* Converts a recipe into a series of batch operations.
*
* @param \Drupal\Core\Recipe\Recipe $recipe
* The recipe to convert to batch operations.
*
* @return array<int, array{0: callable, 1: array{mixed}}>
* The array of batch operations. Each value is an array with two values.
* The first value is a callable and the second value are the arguments to
* pass to the callable.
*
* @see \Drupal\Core\Batch\BatchBuilder::addOperation()
*/
public static function toBatchOperations(Recipe $recipe) : array {
$modules = [];
$themes = [];
$recipes = [];
return static::toBatchOperationsRecipe($recipe, $recipes, $modules, $themes);
}
/**
* Helper method to convert a recipe to batch operations.
*
* @param \Drupal\Core\Recipe\Recipe $recipe
* The recipe to convert to batch operations.
* @param string[] $recipes
* The paths of the recipes that have already been converted to batch
* operations.
* @param string[] $modules
* The modules that will already be installed due to previous recipes in the
* batch.
* @param string[] $themes
* The themes that will already be installed due to previous recipes in the
* batch.
*
* @return array<int, array{0: callable, 1: array{mixed}}>
* The array of batch operations. Each value is an array with two values.
* The first value is a callable and the second value are the arguments to
* pass to the callable.
*/
protected static function toBatchOperationsRecipe(Recipe $recipe, array $recipes, array &$modules, array &$themes) : array {
if (in_array($recipe->path, $recipes, TRUE)) {
return [];
}
$steps = [];
$recipes[] = $recipe->path;
foreach ($recipe->recipes->recipes as $sub_recipe) {
$steps = array_merge($steps, static::toBatchOperationsRecipe($sub_recipe, $recipes, $modules, $themes));
}
$steps = array_merge($steps, static::toBatchOperationsInstall($recipe, $modules, $themes));
if ($recipe->config
->hasTasks()) {
$steps[] = [
[
RecipeRunner::class,
'installConfig',
],
[
$recipe,
],
];
}
if (!empty($recipe->content->data)) {
$steps[] = [
[
RecipeRunner::class,
'installContent',
],
[
$recipe,
],
];
}
$steps[] = [
[
RecipeRunner::class,
'triggerEvent',
],
[
$recipe,
],
];
return $steps;
}
/**
* Converts a recipe's install tasks to batch operations.
*
* @param \Drupal\Core\Recipe\Recipe $recipe
* The recipe to convert install tasks to batch operations.
* @param string[] $modules
* The modules that will already be installed due to previous recipes in the
* batch.
* @param string[] $themes
* The themes that will already be installed due to previous recipes in the
* batch.
*
* @return array<int, array{0: callable, 1: array{mixed}}>
* The array of batch operations. Each value is an array with two values.
* The first value is a callable and the second value are the arguments to
* pass to the callable.
*/
protected static function toBatchOperationsInstall(Recipe $recipe, array &$modules, array &$themes) : array {
$new_modules = [];
foreach ($recipe->install->modules as $name) {
if (in_array($name, $modules, TRUE)) {
continue;
}
$new_modules[] = $name;
$modules[] = $name;
}
$steps = [];
if (!empty($new_modules)) {
foreach (array_chunk($new_modules, Settings::get('core.multi_module_install_batch_size', 20)) as $modules_chunk) {
$steps[] = [
[
RecipeRunner::class,
'installModules',
],
[
$modules_chunk,
$recipe,
],
];
}
}
foreach ($recipe->install->themes as $name) {
if (in_array($name, $themes, TRUE)) {
continue;
}
$themes[] = $name;
$steps[] = [
[
RecipeRunner::class,
'installTheme',
],
[
$name,
$recipe,
],
];
}
return $steps;
}
/**
* Installs a module for a recipe.
*
* @param string $module
* The name of the module to install.
* @param \Drupal\Core\Config\StorageInterface|\Drupal\Core\Recipe\Recipe $recipeConfigStorage
* The recipe or recipe's config storage.
* @param array<mixed>|null $context
* The batch context if called by a batch.
*
* @deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Use
* \Drupal\Core\Recipe\RecipeRunner::installModules() instead.
*
* @see https://www.drupal.org/node/3579527
*/
public static function installModule(string $module, StorageInterface|Recipe $recipeConfigStorage, ?array &$context = NULL) : void {
@trigger_error(__METHOD__ . ' is deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Use \\Drupal\\Core\\Recipe\\RecipeRunner::installModules() instead. See https://www.drupal.org/node/3579527', E_USER_DEPRECATED);
static::installModules([
$module,
], $recipeConfigStorage, $context);
}
/**
* Installs modules for a recipe.
*
* @param string[] $modules
* The names of the modules to install. It is up to the caller to ensure
* that the number of modules to install conforms to the
* 'core.multi_module_install_batch_size' setting.
* @param \Drupal\Core\Config\StorageInterface|\Drupal\Core\Recipe\Recipe $recipeConfigStorage
* The recipe or recipe's config storage.
* @param array<mixed>|null $context
* The batch context if called by a batch.
*/
public static function installModules(array $modules, StorageInterface|Recipe $recipeConfigStorage, ?array &$context = NULL) : void {
if (empty($modules)) {
throw new \InvalidArgumentException('No modules provided.');
}
if ($recipeConfigStorage instanceof Recipe) {
$recipeConfigStorage = $recipeConfigStorage->config
->getConfigStorage();
}
// Disable configuration entity install but use the config directories from
// the modules.
\Drupal::service('config.installer')->setSyncing(TRUE);
// Allow the recipe to override simple configuration from the modules.
$storage = new RecipeOverrideConfigStorage($recipeConfigStorage, RecipeMultipleModulesConfigStorage::createFromModuleList($modules, \Drupal::service('extension.list.module')));
\Drupal::service('config.installer')->setSourceStorage($storage);
\Drupal::service('module_installer')->install($modules);
\Drupal::service('config.installer')->setSyncing(FALSE);
$module_list = \Drupal::service('extension.list.module');
$module_names = array_map($module_list->getName(...), $modules);
$context['message'] = new PluralTranslatableMarkup(count($modules), 'Installed %modules module.', 'Installed @count modules: %modules.', [
'%modules' => implode(', ', $module_names),
]);
if (isset($context['results']['module'])) {
$context['results']['module'] = array_merge($context['results']['module'], $modules);
}
else {
$context['results']['module'] = $modules;
}
}
/**
* Installs a theme for a recipe.
*
* @param string $theme
* The name of the theme to install.
* @param \Drupal\Core\Config\StorageInterface|\Drupal\Core\Recipe\Recipe $recipeConfigStorage
* The recipe or recipe's config storage.
* @param array<mixed>|null $context
* The batch context if called by a batch.
*/
public static function installTheme(string $theme, StorageInterface|Recipe $recipeConfigStorage, ?array &$context = NULL) : void {
if ($recipeConfigStorage instanceof Recipe) {
$recipeConfigStorage = $recipeConfigStorage->config
->getConfigStorage();
}
// Disable configuration entity install.
\Drupal::service('config.installer')->setSyncing(TRUE);
$default_install_path = \Drupal::service('extension.list.theme')->get($theme)
->getPath() . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY;
// Allow the recipe to override simple configuration from the theme.
$storage = new RecipeOverrideConfigStorage($recipeConfigStorage, new FileStorage($default_install_path, StorageInterface::DEFAULT_COLLECTION));
\Drupal::service('config.installer')->setSourceStorage($storage);
\Drupal::service('theme_installer')->install([
$theme,
]);
\Drupal::service('config.installer')->setSyncing(FALSE);
$context['message'] = t('Installed %theme theme.', [
'%theme' => \Drupal::service('extension.list.theme')->getName($theme),
]);
$context['results']['theme'][] = $theme;
}
/**
* Installs a config for a recipe.
*
* @param \Drupal\Core\Recipe\Recipe $recipe
* The recipe to install config for.
* @param array<mixed>|null $context
* The batch context if called by a batch.
*/
public static function installConfig(Recipe $recipe, ?array &$context = NULL) : void {
static::processConfiguration($recipe);
$context['message'] = t('Installed configuration for %recipe recipe.', [
'%recipe' => $recipe->name,
]);
$context['results']['config'][] = $recipe->name;
}
/**
* Installs a content for a recipe.
*
* @param \Drupal\Core\Recipe\Recipe $recipe
* The recipe to install content for.
* @param array<mixed>|null $context
* The batch context if called by a batch.
*/
public static function installContent(Recipe $recipe, ?array &$context = NULL) : void {
static::processContent($recipe->content);
$context['message'] = t('Created content for %recipe recipe.', [
'%recipe' => $recipe->name,
]);
$context['results']['content'][] = $recipe->name;
}
/**
* @param mixed $data
* The data that will have placeholders replaced.
* @param array<string, mixed> $replace
* An array whose keys are the placeholders to be replaced, and whose values
* are the replacements.
*
* @return mixed
* The passed data, with placeholders replaced.
*/
private static function replaceInputValues(mixed $data, array $replace) : mixed {
if (is_string($data)) {
$data = str_replace(array_keys($replace), $replace, $data);
}
elseif (is_array($data)) {
foreach ($data as $key => $value) {
$data[$key] = static::replaceInputValues($value, $replace);
}
}
return $data;
}
}
Members
| Title Sort descending | Deprecated | Modifiers | Object type | Summary |
|---|---|---|---|---|
| RecipeRunner::installConfig | public static | function | Installs a config for a recipe. | |
| RecipeRunner::installContent | public static | function | Installs a content for a recipe. | |
| RecipeRunner::installModule | Deprecated | public static | function | Installs a module for a recipe. |
| RecipeRunner::installModules | public static | function | Installs modules for a recipe. | |
| RecipeRunner::installTheme | public static | function | Installs a theme for a recipe. | |
| RecipeRunner::processConfiguration | protected static | function | Creates configuration and applies configuration actions. | |
| RecipeRunner::processContent | protected static | function | Creates content contained in a recipe. | |
| RecipeRunner::processInstall | protected static | function | Installs the extensions. | |
| RecipeRunner::processRecipe | public static | function | ||
| RecipeRunner::processRecipes | protected static | function | Applies any recipes listed by the recipe. | |
| RecipeRunner::replaceInputValues | private static | function | ||
| RecipeRunner::toBatchOperations | public static | function | Converts a recipe into a series of batch operations. | |
| RecipeRunner::toBatchOperationsInstall | protected static | function | Converts a recipe's install tasks to batch operations. | |
| RecipeRunner::toBatchOperationsRecipe | protected static | function | Helper method to convert a recipe to batch operations. | |
| RecipeRunner::triggerEvent | public static | function | Triggers the RecipeAppliedEvent. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.