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

Issue #3439923 by alexpott, longwave, thejimbirch, Wim Leers, phenaproxima,...

Issue #3439923 by alexpott, longwave, thejimbirch, Wim Leers, phenaproxima, immaculatexavier, nedjo, bircher, deviantintegral, franz, narendraR, omkar.podey, srishtiiee, Rajab Natshah, millnut, mondrake, amateescu, larowlan, sonfd, tasc, vasike: Add recipes api as experimental API to core

(cherry picked from commit 54be1ad9)
parent 285eb15b
No related branches found
No related tags found
18 merge requests!12212Issue #3445525 by alexpott, japerry, catch, mglaman, longwave: Add BC layer...,!10602Issue #3438769 by vinmayiswamy, antonnavi, michelle, amateescu: Sub workspace does not clear,!10301Issue #3469309 by mstrelan, smustgrave, moshe weitzman: Use one-time login...,!10187Issue #3487488 by dakwamine: ExtensionMimeTypeGuesser::guessMimeType must support file names with "0" (zero) like foo.0.zip,!9929Issue #3445469 by pooja_sharma, smustgrave: Add additional test coverage for...,!9787Resolve issue 3479427 - bootstrap barrio issue under Windows,!9742Issue #3463908 by catch, quietone: Split OptionsFieldUiTest into two,!9526Issue #3458177 by mondrake, catch, quietone, godotislate, longwave, larowlan,...,!8949Backport .gitlabci.yml changes.,!8738Issue #3424162 by camilledavis, dineshkumarbollu, smustgrave: Claro...,!8704Make greek characters available in ckeditor5,!8597Draft: Issue #3442259 by catch, quietone, dww: Reduce time of Migrate Upgrade tests...,!8533Issue #3446962 by kim.pepper: Remove incorrectly added...,!8517Issue #3443748 by NexusNovaz, smustgrave: Testcase creates false positive,!7445Issue #3440169: When using drupalGet(), provide an associative array for $headers,!6502Draft: Resolve #2938524 "Plach testing issue",!38582585169-10.1.x,!3226Issue #2987537: Custom menu link entity type should not declare "bundle" entity key
Pipeline #166227 passed
Pipeline: drupal

#166259

    Pipeline: drupal

    #166252

      Pipeline: drupal

      #166242

        +1
        Showing
        with 1259 additions and 29 deletions
        ......@@ -67,6 +67,17 @@ parameters:
        services:
        _defaults:
        autoconfigure: true
        plugin.manager.config_action:
        class: Drupal\Core\Config\Action\ConfigActionManager
        parent: default_plugin_manager
        arguments: ['@config.manager', '@config.storage', '@config.typed', '@config.factory']
        Drupal\Core\DefaultContent\Importer:
        autowire: true
        Drupal\Core\DefaultContent\AdminAccountSwitcher:
        arguments:
        $isSuperUserAccessEnabled: '%security.enable_super_user%'
        autowire: true
        public: false
        # Simple cache contexts, directly derived from the request context.
        cache_context.ip:
        class: Drupal\Core\Cache\Context\IpCacheContext
        ......@@ -385,6 +396,14 @@ services:
        public: false
        tags:
        - { name: backend_overridable }
        config.storage.checkpoint:
        class: Drupal\Core\Config\Checkpoint\CheckpointStorage
        arguments: [ '@config.storage', '@config.checkpoints', '@keyvalue' ]
        Drupal\Core\Config\Checkpoint\CheckpointStorageInterface: '@config.storage.checkpoint'
        config.checkpoints:
        class: Drupal\Core\Config\Checkpoint\LinearHistory
        arguments: [ '@state', '@datetime.time' ]
        Drupal\Core\Config\Checkpoint\CheckpointListInterface: '@config.checkpoints'
        config.import_transformer:
        class: Drupal\Core\Config\ImportStorageTransformer
        arguments: ['@event_dispatcher', '@database', '@lock', '@lock.persistent']
        ......
        ......@@ -26,6 +26,8 @@
        use Drupal\Core\Installer\InstallerKernel;
        use Drupal\Core\Language\Language;
        use Drupal\Core\Language\LanguageManager;
        use Drupal\Core\Recipe\Recipe;
        use Drupal\Core\Recipe\RecipeRunner;
        use Drupal\Core\Site\Settings;
        use Drupal\Core\StringTranslation\Translator\FileTranslation;
        use Drupal\Core\StackMiddleware\ReverseProxyMiddleware;
        ......@@ -839,6 +841,27 @@ function install_tasks($install_state) {
        array_slice($tasks, $key, NULL, TRUE);
        }
        if (!empty($install_state['parameters']['recipe'])) {
        // The install state indicates that we are installing from a recipe.
        $key = array_search('install_profile_modules', array_keys($tasks), TRUE);
        unset($tasks['install_profile_modules']);
        unset($tasks['install_profile_themes']);
        unset($tasks['install_install_profile']);
        $recipe_tasks = [
        'install_recipe_required_modules' => [
        'display_name' => t('Install required modules'),
        'type' => 'batch',
        ],
        'install_recipe_batch' => [
        'display_name' => t('Install recipe'),
        'type' => 'batch',
        ],
        ];
        $tasks = array_slice($tasks, 0, $key, TRUE) +
        $recipe_tasks +
        array_slice($tasks, $key, NULL, TRUE);
        }
        // Now add any tasks defined by the installation profile.
        if (!empty($install_state['parameters']['profile'])) {
        // Load the profile install file, because it is not always loaded when
        ......@@ -2548,3 +2571,71 @@ function _install_config_locale_overrides_process_batch(array $names, array $lan
        }
        $context['finished'] = 1;
        }
        /**
        * Installs required modules prior to applying a recipe via the installer.
        *
        * @see install_tasks()
        *
        * @internal
        * All installer code is internal.
        */
        function install_recipe_required_modules() {
        // We need to manually trigger the installation of core-provided entity types,
        // as those will not be handled by the module installer.
        // @see install_profile_modules()
        install_core_entity_type_definitions();
        $batch_builder = new BatchBuilder();
        $batch_builder
        ->setFinishCallback([ConfigImporterBatch::class, 'finish'])
        ->setTitle(t('Installing required modules'))
        ->setInitMessage(t('Starting required module installation.'))
        ->setErrorMessage(t('Required module installation has encountered an error.'));
        $files = \Drupal::service('extension.list.module')->getList();
        // Always install required modules first.
        $required = [];
        foreach ($files as $module => $extension) {
        if (!empty($extension->info['required'])) {
        $required[$module] = $extension->sort;
        }
        }
        arsort($required);
        // The system module is already installed. See install_base_system().
        unset($required['system']);
        foreach ($required as $module => $weight) {
        $batch_builder->addOperation(
        '_install_module_batch',
        [$module, $files[$module]->info['name']],
        );
        }
        return $batch_builder->toArray();
        }
        /**
        * Creates a batch for the recipe system to process.
        *
        * @see install_tasks()
        *
        * @internal
        * This API is experimental.
        */
        function install_recipe_batch(&$install_state) {
        $batch_builder = new BatchBuilder();
        $batch_builder
        ->setTitle(t('Installing recipe'))
        ->setInitMessage(t('Starting recipe installation.'))
        ->setErrorMessage(t('Recipe installation has encountered an error.'));
        $recipe = Recipe::createFromDirectory($install_state['parameters']['recipe']);
        foreach (RecipeRunner::toBatchOperations($recipe) as $step) {
        $batch_builder->addOperation(...$step);
        }
        return $batch_builder->toArray();
        }
        ......@@ -48,11 +48,12 @@ public function __construct($class_loader) {
        protected function configure() {
        $this->setName('install')
        ->setDescription('Installs a Drupal demo site. This is not meant for production and might be too simple for custom development. It is a quick and easy way to get Drupal running.')
        ->addArgument('install-profile', InputArgument::OPTIONAL, 'Install profile to install the site in.')
        ->addArgument('install-profile-or-recipe', InputArgument::OPTIONAL, 'Install profile or recipe directory from which to install the site.')
        ->addOption('langcode', NULL, InputOption::VALUE_OPTIONAL, 'The language to install the site in.', 'en')
        ->addOption('site-name', NULL, InputOption::VALUE_OPTIONAL, 'Set the site name.', 'Drupal')
        ->addUsage('demo_umami --langcode fr')
        ->addUsage('standard --site-name QuickInstall');
        ->addUsage('standard --site-name QuickInstall')
        ->addUsage('core/recipes/standard --site-name RecipeBuiltSite');
        parent::configure();
        }
        ......@@ -78,15 +79,43 @@ protected function execute(InputInterface $input, OutputInterface $output): int
        return 0;
        }
        $install_profile = $input->getArgument('install-profile');
        if ($install_profile && !$this->validateProfile($install_profile, $io)) {
        return 1;
        }
        if (!$install_profile) {
        $install_profile_or_recipe = $input->getArgument('install-profile-or-recipe');
        if (!$install_profile_or_recipe) {
        // User did not provide a recipe or install profile.
        $install_profile = $this->selectProfile($io);
        }
        // Determine if an install profile or a recipe has been provided.
        elseif ($this->validateProfile($install_profile_or_recipe)) {
        // User provided an install profile.
        $install_profile = $install_profile_or_recipe;
        }
        elseif ($this->validateRecipe($install_profile_or_recipe)) {
        // User provided a recipe.
        $recipe = $install_profile_or_recipe;
        }
        else {
        $error_msg = sprintf("'%s' is not a valid install profile or recipe.", $install_profile_or_recipe);
        return $this->install($this->classLoader, $io, $install_profile, $input->getOption('langcode'), $this->getSitePath(), $input->getOption('site-name'));
        // If it does not look like a path make suggestions based upon available
        // profiles.
        if (!str_contains('/', $install_profile_or_recipe)) {
        $alternatives = [];
        foreach (array_keys($this->getProfiles(TRUE, FALSE)) as $profile_name) {
        $lev = levenshtein($install_profile_or_recipe, $profile_name);
        if ($lev <= strlen($profile_name) / 4 || str_contains($profile_name, $install_profile_or_recipe)) {
        $alternatives[] = $profile_name;
        }
        }
        if (!empty($alternatives)) {
        $error_msg .= sprintf(" Did you mean '%s'?", implode("' or '", $alternatives));
        }
        }
        $io->getErrorStyle()->error($error_msg);
        return 1;
        }
        return $this->install($this->classLoader, $io, $install_profile ?? '', $input->getOption('langcode'), $this->getSitePath(), $input->getOption('site-name'), $recipe ?? '');
        }
        /**
        ......@@ -123,6 +152,8 @@ protected function isDrupalInstalled() {
        * The path to install the site to, like 'sites/default'.
        * @param string $site_name
        * The site name.
        * @param string $recipe
        * The recipe to use for installing.
        *
        * @throws \Exception
        * Thrown when failing to create the $site_path directory or settings.php.
        ......@@ -130,7 +161,7 @@ protected function isDrupalInstalled() {
        * @return int
        * The command exit status.
        */
        protected function install($class_loader, SymfonyStyle $io, $profile, $langcode, $site_path, $site_name) {
        protected function install($class_loader, SymfonyStyle $io, $profile, $langcode, $site_path, $site_name, string $recipe) {
        $password = Crypt::randomBytesBase64(12);
        $parameters = [
        'interactive' => FALSE,
        ......@@ -165,6 +196,9 @@ protected function install($class_loader, SymfonyStyle $io, $profile, $langcode,
        ],
        ],
        ];
        if ($recipe) {
        $parameters['parameters']['recipe'] = $recipe;
        }
        // Create the directory and settings.php if not there so that the installer
        // works.
        ......@@ -276,29 +310,29 @@ protected function selectProfile(SymfonyStyle $io) {
        *
        * @param string $install_profile
        * Install profile to validate.
        * @param \Symfony\Component\Console\Style\SymfonyStyle $io
        * Symfony style output decorator.
        *
        * @return bool
        * TRUE if the profile is valid, FALSE if not.
        */
        protected function validateProfile($install_profile, SymfonyStyle $io) {
        protected function validateProfile($install_profile): bool {
        // Allow people to install hidden and non-distribution profiles if they
        // supply the argument.
        $profiles = $this->getProfiles(TRUE, FALSE);
        if (!isset($profiles[$install_profile])) {
        $error_msg = sprintf("'%s' is not a valid install profile.", $install_profile);
        $alternatives = [];
        foreach (array_keys($profiles) as $profile_name) {
        $lev = levenshtein($install_profile, $profile_name);
        if ($lev <= strlen($profile_name) / 4 || str_contains($profile_name, $install_profile)) {
        $alternatives[] = $profile_name;
        }
        }
        if (!empty($alternatives)) {
        $error_msg .= sprintf(" Did you mean '%s'?", implode("' or '", $alternatives));
        }
        $io->getErrorStyle()->error($error_msg);
        return array_key_exists($install_profile, $this->getProfiles(TRUE, FALSE));
        }
        /**
        * Validates a user provided recipe.
        *
        * @param string $recipe
        * The path to the recipe to validate.
        *
        * @return bool
        * TRUE if the recipe exists, FALSE if not.
        */
        protected function validateRecipe(string $recipe): bool {
        // It is impossible to validate a recipe fully at this point because that
        // requires a container.
        if (!is_dir($recipe) || !is_file($recipe . '/recipe.yml')) {
        return FALSE;
        }
        return TRUE;
        ......
        ......@@ -28,7 +28,7 @@ class QuickStartCommand extends Command {
        protected function configure() {
        $this->setName('quick-start')
        ->setDescription('Installs a Drupal site and runs a web server. This is not meant for production and might be too simple for custom development. It is a quick and easy way to get Drupal running.')
        ->addArgument('install-profile', InputArgument::OPTIONAL, 'Install profile to install the site in.')
        ->addArgument('install-profile-or-recipe', InputArgument::OPTIONAL, 'Install profile or recipe directory from which to install the site.')
        ->addOption('langcode', NULL, InputOption::VALUE_OPTIONAL, 'The language to install the site in. Defaults to en.', 'en')
        ->addOption('site-name', NULL, InputOption::VALUE_OPTIONAL, 'Set the site name. Defaults to Drupal.', 'Drupal')
        ->addOption('host', NULL, InputOption::VALUE_OPTIONAL, 'Provide a host for the server to run on. Defaults to 127.0.0.1.', '127.0.0.1')
        ......@@ -36,7 +36,8 @@ protected function configure() {
        ->addOption('suppress-login', 's', InputOption::VALUE_NONE, 'Disable opening a login URL in a browser.')
        ->addUsage('demo_umami --langcode fr')
        ->addUsage('standard --site-name QuickInstall --host localhost --port 8080')
        ->addUsage('minimal --host my-site.com --port 80');
        ->addUsage('minimal --host my-site.com --port 80')
        ->addUsage('core/recipes/standard --site-name MyDrupalRecipe');
        parent::configure();
        }
        ......@@ -49,7 +50,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
        $arguments = [
        'command' => 'install',
        'install-profile' => $input->getArgument('install-profile'),
        'install-profile-or-recipe' => $input->getArgument('install-profile-or-recipe'),
        '--langcode' => $input->getOption('langcode'),
        '--site-name' => $input->getOption('site-name'),
        ];
        ......
        <?php
        declare(strict_types=1);
        namespace Drupal\Core\Config\Action\Attribute;
        // cspell:ignore inflector
        use Drupal\Core\Config\Action\Exists;
        use Drupal\Core\StringTranslation\TranslatableMarkup;
        /**
        * @internal
        * This API is experimental.
        */
        #[\Attribute(\Attribute::TARGET_METHOD)]
        final class ActionMethod {
        /**
        * @param \Drupal\Core\Config\Action\Exists $exists
        * Determines behavior of action depending on entity existence.
        * @param \Drupal\Core\StringTranslation\TranslatableMarkup|string $adminLabel
        * The admin label for the user interface.
        * @param bool|string $pluralize
        * Determines whether to create a pluralized version of the method to enable
        * the action to be called multiple times before saving the entity. The
        * default behavior is to create an action with a plural form as determined
        * by \Symfony\Component\String\Inflector\EnglishInflector::pluralize().
        * For example, 'grantPermission' has a pluralized version of
        * 'grantPermissions'. If a string is provided this will be the full action
        * ID. For example, if the method is called 'addArray' this can be set to
        * 'addMultipleArrays'. Set to FALSE if a pluralized version does not make
        * logical sense.
        */
        public function __construct(
        public readonly Exists $exists = Exists::ErrorIfNotExists,
        public readonly TranslatableMarkup|string $adminLabel = '',
        public readonly bool|string $pluralize = TRUE
        ) {
        }
        }
        <?php
        declare(strict_types=1);
        namespace Drupal\Core\Config\Action\Attribute;
        use Drupal\Component\Plugin\Attribute\Plugin;
        use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
        use Drupal\Core\StringTranslation\TranslatableMarkup;
        /**
        * Defines a ConfigAction attribute object.
        *
        * Plugin Namespace: Plugin\ConfigAction
        *
        * @ingroup config_action_api
        *
        * @internal
        * This API is experimental.
        *
        * @see \Drupal\Core\Config\Action\ConfigActionPluginInterface
        * @see \Drupal\Core\Config\Action\ConfigActionManager
        * @see plugin_api
        */
        #[\Attribute(\Attribute::TARGET_CLASS)]
        final class ConfigAction extends Plugin {
        /**
        * Constructs a ConfigAction attribute.
        *
        * @param string $id
        * The plugin ID.
        * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $admin_label
        * The administrative label of the config action. This is optional when
        * using a deriver, but in that case the deriver should add an admin label.
        * @param string[] $entity_types
        * (optional) Allows action shorthand IDs for the listed config entity
        * types. If '*' is present in the array then it can apply to all entity
        * types. An empty array means that shorthand action IDs are not available
        * for this plugin. See ConfigActionManager::convertActionToPluginId().
        * @param class-string|null $deriver
        * (optional) The deriver class.
        *
        * @see \Drupal\Core\Config\Action\ConfigActionManager::convertActionToPluginId()
        */
        public function __construct(
        public readonly string $id,
        public readonly ?TranslatableMarkup $admin_label = NULL,
        public readonly array $entity_types = [],
        public readonly ?string $deriver = NULL,
        ) {
        if ($this->admin_label === NULL && $this->deriver === NULL) {
        throw new InvalidPluginDefinitionException($id, sprintf("The '%s' config action plugin must have either an admin label or a deriver", $id));
        }
        }
        }
        <?php
        declare(strict_types=1);
        namespace Drupal\Core\Config\Action;
        /**
        * @internal
        * This API is experimental.
        */
        final class ConfigActionException extends \RuntimeException {
        }
        <?php
        declare(strict_types=1);
        namespace Drupal\Core\Config\Action;
        use Drupal\Component\Plugin\PluginBase;
        use Drupal\Core\Cache\CacheBackendInterface;
        use Drupal\Core\Config\Action\Attribute\ConfigAction;
        use Drupal\Core\Config\ConfigFactoryInterface;
        use Drupal\Core\Config\ConfigManagerInterface;
        use Drupal\Core\Config\Schema\Mapping;
        use Drupal\Core\Config\StorageInterface;
        use Drupal\Core\Config\TypedConfigManagerInterface;
        use Drupal\Core\Extension\ModuleHandlerInterface;
        use Drupal\Core\Plugin\DefaultPluginManager;
        use Drupal\Core\Recipe\InvalidConfigException;
        use Drupal\Core\Validation\Plugin\Validation\Constraint\FullyValidatableConstraint;
        /**
        * @defgroup config_action_api Config Action API
        * @{
        * Information about the classes and interfaces that make up the Config Action
        * API.
        *
        * Configuration actions are plugins that manipulate simple configuration or
        * configuration entities. The configuration action plugin manager can apply
        * configuration actions. For example, the API is leveraged by recipes to create
        * roles if they do not exist already and grant permissions to those roles.
        *
        * To define a configuration action in a module you need to:
        * - Define a Config Action plugin by creating a new class that implements the
        * \Drupal\Core\Config\Action\ConfigActionPluginInterface, in namespace
        * Plugin\ConfigAction under your module namespace. For more information about
        * creating plugins, see the @link plugin_api Plugin API topic. @endlink
        * - Config action plugins use the attributes defined by
        * \Drupal\Core\Config\Action\Attribute\ConfigAction. See the
        * @link attribute Attributes topic @endlink for more information about
        * attributes.
        *
        * Further information and examples:
        * - \Drupal\Core\Config\Action\Plugin\ConfigAction\EntityMethod derives
        * configuration actions from config entity methods which have the
        * \Drupal\Core\Config\Action\Attribute\ActionMethod attribute.
        * - \Drupal\Core\Config\Action\Plugin\ConfigAction\EntityCreate allows you to
        * create configuration entities if they do not exist.
        * - \Drupal\Core\Config\Action\Plugin\ConfigAction\SimpleConfigUpdate allows
        * you to update simple configuration using a config action.
        * @}
        *
        * @internal
        * This API is experimental.
        */
        class ConfigActionManager extends DefaultPluginManager {
        /**
        * Constructs a new \Drupal\Core\Config\Action\ConfigActionManager object.
        *
        * @param \Traversable $namespaces
        * An object that implements \Traversable which contains the root paths
        * keyed by the corresponding namespace to look for plugin implementations.
        * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
        * Cache backend instance to use.
        * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
        * The module handler to invoke the alter hook with.
        * @param \Drupal\Core\Config\ConfigManagerInterface $configManager
        * The config manager.
        * @param \Drupal\Core\Config\StorageInterface $configStorage
        * The active config storage.
        * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfig
        * The typed configuration manager service.
        * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
        * The config factory service.
        */
        public function __construct(
        \Traversable $namespaces,
        CacheBackendInterface $cache_backend,
        ModuleHandlerInterface $module_handler,
        protected readonly ConfigManagerInterface $configManager,
        protected readonly StorageInterface $configStorage,
        protected readonly TypedConfigManagerInterface $typedConfig,
        protected readonly ConfigFactoryInterface $configFactory,
        ) {
        assert($namespaces instanceof \ArrayAccess, '$namespaces can be accessed like an array');
        // Enable this namespace to be searched for plugins.
        $namespaces[__NAMESPACE__] = 'core/lib/Drupal/Core/Config/Action';
        parent::__construct('Plugin/ConfigAction', $namespaces, $module_handler, ConfigActionPluginInterface::class, ConfigAction::class);
        $this->alterInfo('config_action');
        $this->setCacheBackend($cache_backend, 'config_action');
        }
        /**
        * Applies a config action.
        *
        * @param string $action_id
        * The ID of the action to apply. This can be a complete configuration
        * action plugin ID or a shorthand action ID that is available for the
        * entity type of the provided configuration name.
        * @param string $configName
        * The configuration name. This may be the full name of a config object, or
        * it may contain wildcards (to target all config entities of a specific
        * type, or a subset thereof). See
        * ConfigActionManager::getConfigNamesMatchingExpression() for more detail.
        * @param mixed $data
        * The data for the action.
        *
        * @throws \Drupal\Component\Plugin\Exception\PluginException
        * Thrown when the config action cannot be found.
        * @throws \Drupal\Core\Config\Action\ConfigActionException
        * Thrown when the config action fails to apply.
        *
        * @see \Drupal\Core\Config\Action\ConfigActionManager::getConfigNamesMatchingExpression()
        */
        public function applyAction(string $action_id, string $configName, mixed $data): void {
        if (!$this->hasDefinition($action_id)) {
        // Get the full plugin ID from the shorthand map, if it is available.
        $entity_type = $this->configManager->getEntityTypeIdByName($configName);
        if ($entity_type) {
        $action_id = $this->getShorthandActionIdsForEntityType($entity_type)[$action_id] ?? $action_id;
        }
        }
        /** @var \Drupal\Core\Config\Action\ConfigActionPluginInterface $action */
        $action = $this->createInstance($action_id);
        foreach ($this->getConfigNamesMatchingExpression($configName) as $name) {
        $action->apply($name, $data);
        $typed_config = $this->typedConfig->createFromNameAndData($name, $this->configFactory->get($name)->getRawData());
        // All config objects are mappings.
        assert($typed_config instanceof Mapping);
        foreach ($typed_config->getConstraints() as $constraint) {
        // Only validate the config if it has explicitly been marked as being
        // validatable.
        if ($constraint instanceof FullyValidatableConstraint) {
        /** @var \Symfony\Component\Validator\ConstraintViolationList $violations */
        $violations = $typed_config->validate();
        if (count($violations) > 0) {
        throw new InvalidConfigException($violations, $typed_config);
        }
        break;
        }
        }
        }
        }
        /**
        * Gets the names of all active config objects that match an expression.
        *
        * @param string $expression
        * The expression to match. This may be the full name of a config object,
        * or it may contain wildcards (to target all config entities of a specific
        * type, or a subset thereof). For example:
        * - `user.role.*` would target all user roles.
        * - `user.role.anonymous` would target only the anonymous user role.
        * - `core.entity_view_display.node.*.default` would target the default
        * view display of every content type.
        * - `core.entity_form_display.*.*.default` would target the default form
        * display of every bundle of every entity type.
        * The expression MUST begin with the prefix of a config entity type --
        * for example, `field.field.` in the case of fields, or `user.role.` for
        * user roles. The prefix cannot contain wildcards.
        *
        * @return string[]
        * The names of all active config objects that match the expression.
        *
        * @throws \Drupal\Core\Config\Action\ConfigActionException
        * Thrown if the expression does not match any known config entity type's
        * prefix, or if the expression cannot be parsed.
        */
        private function getConfigNamesMatchingExpression(string $expression): array {
        // If there are no wildcards, we can return the config name as-is.
        if (!str_contains($expression, '.*')) {
        return [$expression];
        }
        $entity_type = $this->configManager->getEntityTypeIdByName($expression);
        if (empty($entity_type)) {
        throw new ConfigActionException("No installed config entity type uses the prefix in the expression '$expression'. Either there is a typo in the expression or this recipe should install an additional module or depend on another recipe.");
        }
        /** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
        $entity_type = $this->configManager->getEntityTypeManager()
        ->getDefinition($entity_type);
        $prefix = $entity_type->getConfigPrefix();
        // Convert the expression to a regular expression. We assume that * should
        // match the characters allowed by
        // \Drupal\Core\Config\ConfigBase::validateName(), which is permissive.
        $expression = str_replace('\\*', '[^.:?*<>"\'\/\\\\]+', preg_quote($expression));
        $matches = @preg_grep("/^$expression$/", $this->configStorage->listAll("$prefix."));
        if ($matches === FALSE) {
        throw new ConfigActionException("The expression '$expression' could not be parsed.");
        }
        return $matches;
        }
        /**
        * Gets a map of shorthand action IDs to plugin IDs for an entity type.
        *
        * @param string $entityType
        * The entity type ID to get the map for.
        *
        * @return string[]
        * An array of plugin IDs keyed by shorthand action ID for the provided
        * entity type.
        */
        protected function getShorthandActionIdsForEntityType(string $entityType): array {
        $map = [];
        foreach ($this->getDefinitions() as $plugin_id => $definition) {
        if (in_array($entityType, $definition['entity_types'], TRUE) || in_array('*', $definition['entity_types'], TRUE)) {
        $regex = '/' . PluginBase::DERIVATIVE_SEPARATOR . '([^' . PluginBase::DERIVATIVE_SEPARATOR . ']*)$/';
        $action_id = preg_match($regex, $plugin_id, $matches) ? $matches[1] : $plugin_id;
        if (isset($map[$action_id])) {
        throw new DuplicateConfigActionIdException(sprintf('The plugins \'%s\' and \'%s\' both resolve to the same shorthand action ID for the \'%s\' entity type', $plugin_id, $map[$action_id], $entityType));
        }
        $map[$action_id] = $plugin_id;
        }
        }
        return $map;
        }
        }
        <?php
        declare(strict_types=1);
        namespace Drupal\Core\Config\Action;
        /**
        * @internal
        * This API is experimental.
        */
        interface ConfigActionPluginInterface {
        /**
        * Applies the config action.
        *
        * @param string $configName
        * The name of the config to apply the action to.
        * @param mixed $value
        * The value for the action to use.
        *
        * @throws ConfigActionException
        */
        public function apply(string $configName, mixed $value): void;
        }
        <?php
        declare(strict_types=1);
        namespace Drupal\Core\Config\Action;
        /**
        * Exception thrown if there are conflicting shorthand action IDs.
        *
        * @internal
        * This API is experimental.
        */
        class DuplicateConfigActionIdException extends \RuntimeException {
        }
        <?php
        declare(strict_types=1);
        namespace Drupal\Core\Config\Action;
        /**
        * @internal
        * This API is experimental.
        */
        final class EntityMethodException extends \RuntimeException {
        }
        <?php
        // phpcs:ignoreFile
        declare(strict_types=1);
        namespace Drupal\Core\Config\Action;
        use Drupal\Core\Config\Entity\ConfigEntityInterface;
        /**
        * @internal
        * This API is experimental.
        */
        enum Exists {
        case ErrorIfExists;
        case ErrorIfNotExists;
        case ReturnEarlyIfExists;
        case ReturnEarlyIfNotExists;
        /**
        * Determines if an action should return early depending on $entity.
        *
        * @param string $configName
        * The config name supplied to the action.
        * @param \Drupal\Core\Config\Entity\ConfigEntityInterface|null $entity
        * The entity, if it exists.
        *
        * @return bool
        * TRUE if the action should return early, FALSE if not.
        *
        * @throws \Drupal\Core\Config\Action\ConfigActionException
        * Thrown depending on $entity and the value of $this.
        */
        public function returnEarly(string $configName, ?ConfigEntityInterface $entity): bool {
        return match (TRUE) {
        $this === self::ReturnEarlyIfExists && $entity !== NULL,
        $this === self::ReturnEarlyIfNotExists && $entity === NULL => TRUE,
        $this === self::ErrorIfExists && $entity !== NULL => throw new ConfigActionException(sprintf('Entity %s exists', $configName)),
        $this === self::ErrorIfNotExists && $entity === NULL => throw new ConfigActionException(sprintf('Entity %s does not exist', $configName)),
        default => FALSE
        };
        }
        }
        <?php
        declare(strict_types=1);
        namespace Drupal\Core\Config\Action\Plugin\ConfigAction\Deriver;
        use Drupal\Component\Plugin\Derivative\DeriverBase;
        use Drupal\Core\Config\Action\Exists;
        use Drupal\Core\StringTranslation\StringTranslationTrait;
        /**
        * @internal
        * This API is experimental.
        */
        final class EntityCreateDeriver extends DeriverBase {
        use StringTranslationTrait;
        /**
        * {@inheritdoc}
        */
        public function getDerivativeDefinitions($base_plugin_definition) {
        // These derivatives apply to all entity types.
        $base_plugin_definition['entity_types'] = ['*'];
        $this->derivatives['ensure_exists'] = $base_plugin_definition + ['constructor_args' => ['exists' => Exists::ReturnEarlyIfExists]];
        $this->derivatives['ensure_exists']['admin_label'] = $this->t('Ensure entity exists');
        $this->derivatives['create'] = $base_plugin_definition + ['constructor_args' => ['exists' => Exists::ErrorIfExists]];
        $this->derivatives['create']['admin_label'] = $this->t('Entity create');
        return $this->derivatives;
        }
        }
        <?php
        declare(strict_types=1);
        namespace Drupal\Core\Config\Action\Plugin\ConfigAction\Deriver;
        // cspell:ignore inflector
        use Drupal\Component\Plugin\Derivative\DeriverBase;
        use Drupal\Component\Plugin\PluginBase;
        use Drupal\Core\Config\Action\Attribute\ActionMethod;
        use Drupal\Core\Config\Action\EntityMethodException;
        use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
        use Drupal\Core\Entity\EntityTypeManagerInterface;
        use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
        use Drupal\Core\StringTranslation\StringTranslationTrait;
        use Symfony\Component\DependencyInjection\ContainerInterface;
        use Symfony\Component\String\Inflector\EnglishInflector;
        use Symfony\Component\String\Inflector\InflectorInterface;
        /**
        * Derives config action methods from attributed config entity methods.
        *
        * @internal
        * This API is experimental.
        */
        final class EntityMethodDeriver extends DeriverBase implements ContainerDeriverInterface {
        use StringTranslationTrait;
        /**
        * Inflector to pluralize words.
        */
        protected readonly InflectorInterface $inflector;
        /**
        * Constructs new EntityMethodDeriver.
        *
        * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
        * The entity type manager.
        */
        public function __construct(protected readonly EntityTypeManagerInterface $entityTypeManager) {
        $this->inflector = new EnglishInflector();
        }
        /**
        * {@inheritdoc}
        */
        public static function create(ContainerInterface $container, $base_plugin_id) {
        return new static(
        $container->get('entity_type.manager')
        );
        }
        /**
        * {@inheritdoc}
        */
        public function getDerivativeDefinitions($base_plugin_definition) {
        // Scan all the config entity classes for attributes.
        foreach ($this->entityTypeManager->getDefinitions() as $entity_type) {
        if ($entity_type instanceof ConfigEntityTypeInterface) {
        $reflectionClass = new \ReflectionClass($entity_type->getClass());
        while ($reflectionClass) {
        foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
        // Only process a method if it is declared on the current class.
        // Methods on the parent class will be processed later. This allows
        // for a parent to have an attribute and an overriding class does
        // not need one. For example,
        // \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::setComponent()
        // and \Drupal\Core\Entity\EntityDisplayBase::setComponent().
        if ($method->getDeclaringClass()->getName() === $reflectionClass->getName()) {
        foreach ($method->getAttributes(ActionMethod::class) as $attribute) {
        $this->processMethod($method, $attribute->newInstance(), $entity_type, $base_plugin_definition);
        }
        }
        }
        $reflectionClass = $reflectionClass->getParentClass();
        }
        }
        }
        return $this->derivatives;
        }
        /**
        * Processes a method to create derivatives.
        *
        * @param \ReflectionMethod $method
        * The entity method.
        * @param \Drupal\Core\Config\Action\Attribute\ActionMethod $action_attribute
        * The entity method attribute.
        * @param \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type
        * The entity type.
        * @param array $derivative
        * The base plugin definition that will used to create the derivative.
        */
        private function processMethod(\ReflectionMethod $method, ActionMethod $action_attribute, ConfigEntityTypeInterface $entity_type, array $derivative): void {
        $derivative['admin_label'] = $action_attribute->adminLabel ?: $this->t('@entity_type @method', ['@entity_type' => $entity_type->getLabel(), '@method' => $method->name]);
        $derivative['constructor_args'] = [
        'method' => $method->name,
        'exists' => $action_attribute->exists,
        'numberOfParams' => $method->getNumberOfParameters(),
        'numberOfRequiredParams' => $method->getNumberOfRequiredParameters(),
        'pluralized' => FALSE,
        ];
        $derivative['entity_types'] = [$entity_type->id()];
        // Build a config action identifier from the entity type's config
        // prefix and the method name. For example, the Role entity adds a
        // 'user.role:grantPermission' action.
        $this->addDerivative($method->name, $entity_type, $derivative, $method->name);
        $pluralized_name = match(TRUE) {
        is_string($action_attribute->pluralize) => $action_attribute->pluralize,
        $action_attribute->pluralize === FALSE => '',
        default => $this->inflector->pluralize($method->name)[0]
        };
        // Add a pluralized version of the plugin.
        if (strlen($pluralized_name) > 0) {
        $derivative['constructor_args']['pluralized'] = TRUE;
        $derivative['admin_label'] = $this->t('@admin_label (multiple calls)', ['@admin_label' => $derivative['admin_label']]);
        $this->addDerivative($pluralized_name, $entity_type, $derivative, $method->name);
        }
        }
        /**
        * Adds a derivative.
        *
        * @param string $action_id
        * The action ID.
        * @param \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type
        * The entity type.
        * @param array $derivative
        * The derivative definition.
        * @param string $methodName
        * The method name.
        */
        private function addDerivative(string $action_id, ConfigEntityTypeInterface $entity_type, array $derivative, string $methodName): void {
        $id = $entity_type->getConfigPrefix() . PluginBase::DERIVATIVE_SEPARATOR . $action_id;
        if (isset($this->derivatives[$id])) {
        throw new EntityMethodException(sprintf('Duplicate action can not be created for ID \'%s\' for %s::%s(). The existing action is for the ::%s() method', $id, $entity_type->getClass(), $methodName, $this->derivatives[$id]['constructor_args']['method']));
        }
        $this->derivatives[$id] = $derivative;
        }
        }
        <?php
        declare(strict_types=1);
        namespace Drupal\Core\Config\Action\Plugin\ConfigAction\Deriver;
        use Drupal\Component\Plugin\Derivative\DeriverBase;
        use Drupal\Core\Entity\EntityTypeManagerInterface;
        use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
        use Symfony\Component\DependencyInjection\Container;
        use Symfony\Component\DependencyInjection\ContainerInterface;
        /**
        * @internal
        * This API is experimental.
        */
        final class PermissionsPerBundleDeriver extends DeriverBase implements ContainerDeriverInterface {
        public function __construct(
        private readonly EntityTypeManagerInterface $entityTypeManager,
        ) {}
        /**
        * {@inheritdoc}
        */
        public static function create(ContainerInterface $container, $base_plugin_id) {
        return new static(
        $container->get(EntityTypeManagerInterface::class),
        );
        }
        /**
        * {@inheritdoc}
        */
        public function getDerivativeDefinitions($base_plugin_definition) {
        foreach ($this->entityTypeManager->getDefinitions() as $id => $entity_type) {
        if ($entity_type->getPermissionGranularity() === 'bundle' && ($bundle_entity_type = $entity_type->getBundleEntityType()) !== NULL) {
        // Convert unique plugin IDs, like `taxonomy_vocabulary`, into strings
        // like `TaxonomyVocabulary`.
        $suffix = Container::camelize($bundle_entity_type);
        $this->derivatives["grantPermissionsForEach{$suffix}"] = [
        'target_entity_type' => $id,
        ] + $base_plugin_definition;
        }
        }
        return parent::getDerivativeDefinitions($base_plugin_definition);
        }
        }
        <?php
        declare(strict_types=1);
        namespace Drupal\Core\Config\Action\Plugin\ConfigAction;
        use Drupal\Core\Config\Action\Attribute\ConfigAction;
        use Drupal\Core\Config\Action\ConfigActionException;
        use Drupal\Core\Config\Action\ConfigActionPluginInterface;
        use Drupal\Core\Config\Action\Exists;
        use Drupal\Core\Config\Action\Plugin\ConfigAction\Deriver\EntityCreateDeriver;
        use Drupal\Core\Config\ConfigManagerInterface;
        use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
        use Symfony\Component\DependencyInjection\ContainerInterface;
        /**
        * @internal
        * This API is experimental.
        */
        #[ConfigAction(
        id: 'entity_create',
        deriver: EntityCreateDeriver::class,
        )]
        final class EntityCreate implements ConfigActionPluginInterface, ContainerFactoryPluginInterface {
        /**
        * Constructs a EntityCreate object.
        *
        * @param \Drupal\Core\Config\ConfigManagerInterface $configManager
        * The config manager.
        * @param \Drupal\Core\Config\Action\Exists $exists
        * Determines behavior of action depending on entity existence.
        */
        public function __construct(
        protected readonly ConfigManagerInterface $configManager,
        protected readonly Exists $exists
        ) {
        }
        /**
        * {@inheritdoc}
        */
        public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
        assert(is_array($plugin_definition) && is_array($plugin_definition['constructor_args']), '$plugin_definition contains the expected settings');
        return new static($container->get('config.manager'), ...$plugin_definition['constructor_args']);
        }
        /**
        * {@inheritdoc}
        */
        public function apply(string $configName, mixed $value): void {
        if (!is_array($value)) {
        throw new ConfigActionException(sprintf("The value provided to create %s must be an array", $configName));
        }
        /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface|null $entity */
        $entity = $this->configManager->loadConfigEntityByName($configName);
        if ($this->exists->returnEarly($configName, $entity)) {
        return;
        }
        $entity_type_manager = $this->configManager->getEntityTypeManager();
        $entity_type_id = $this->configManager->getEntityTypeIdByName($configName);
        if ($entity_type_id === NULL) {
        throw new ConfigActionException(sprintf("Cannot determine a config entity type from %s", $configName));
        }
        /** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
        $entity_type = $entity_type_manager->getDefinition($entity_type_id);
        $id = substr($configName, strlen($entity_type->getConfigPrefix()) + 1);
        $entity_type_manager
        ->getStorage($entity_type->id())
        ->create($value + ['id' => $id])
        ->save();
        }
        }
        <?php
        declare(strict_types=1);
        namespace Drupal\Core\Config\Action\Plugin\ConfigAction;
        use Drupal\Core\Config\Action\Attribute\ConfigAction;
        use Drupal\Core\Config\Action\ConfigActionPluginInterface;
        use Drupal\Core\Config\Action\EntityMethodException;
        use Drupal\Core\Config\Action\Exists;
        use Drupal\Core\Config\Action\Plugin\ConfigAction\Deriver\EntityMethodDeriver;
        use Drupal\Core\Config\ConfigManagerInterface;
        use Drupal\Core\Config\Entity\ConfigEntityInterface;
        use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
        use Symfony\Component\DependencyInjection\ContainerInterface;
        /**
        * Makes config entity methods with the ActionMethod attribute into actions.
        *
        * For example, adding the ActionMethod attribute to
        * \Drupal\user\Entity\Role::grantPermission() allows permissions to be added to
        * roles via config actions.
        *
        * When calling \Drupal\Core\Config\Action\ConfigActionManager::applyAction()
        * the $data parameter is mapped to the method's arguments using the following
        * rules:
        * - If $data is not an array, the method must only have one argument or one
        * required argument.
        * - If $data is an array and the method only accepts a single argument, the
        * array will be passed to the first argument.
        * - If $data is an array and the method accepts more than one argument, $data
        * will be unpacked into the method arguments.
        *
        * @internal
        * This API is experimental.
        *
        * @see \Drupal\Core\Config\Action\Attribute\ActionMethod
        */
        #[ConfigAction(
        id: 'entity_method',
        deriver: EntityMethodDeriver::class,
        )]
        final class EntityMethod implements ConfigActionPluginInterface, ContainerFactoryPluginInterface {
        /**
        * Constructs a EntityMethod object.
        *
        * @param string $pluginId
        * The config action plugin ID.
        * @param \Drupal\Core\Config\ConfigManagerInterface $configManager
        * The config manager.
        * @param string $method
        * The method to call on the config entity.
        * @param \Drupal\Core\Config\Action\Exists $exists
        * Determines behavior of action depending on entity existence.
        * @param int $numberOfParams
        * The number of parameters the method has.
        * @param int $numberOfRequiredParams
        * The number of required parameters the method has.
        * @param bool $pluralized
        * Determines whether an array maps to multiple calls.
        */
        public function __construct(
        protected readonly string $pluginId,
        protected readonly ConfigManagerInterface $configManager,
        protected readonly string $method,
        protected readonly Exists $exists,
        protected readonly int $numberOfParams,
        protected readonly int $numberOfRequiredParams,
        protected readonly bool $pluralized
        ) {
        }
        /**
        * {@inheritdoc}
        */
        public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
        assert(is_array($plugin_definition) && is_array($plugin_definition['constructor_args']), '$plugin_definition contains the expected settings');
        return new static(
        $plugin_id,
        $container->get('config.manager'),
        ...$plugin_definition['constructor_args']
        );
        }
        /**
        * {@inheritdoc}
        */
        public function apply(string $configName, mixed $value): void {
        /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface|null $entity */
        $entity = $this->configManager->loadConfigEntityByName($configName);
        if ($this->exists->returnEarly($configName, $entity)) {
        return;
        }
        $entity = $this->pluralized ? $this->applyPluralized($entity, $value) : $this->applySingle($entity, $value);
        $entity->save();
        }
        /**
        * Applies the action to entity treating the $values array as multiple calls.
        *
        * @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity
        * The entity to apply the action to.
        * @param mixed $values
        * The values for the action to use.
        *
        * @return \Drupal\Core\Config\Entity\ConfigEntityInterface
        * The unsaved entity with the action applied.
        */
        private function applyPluralized(ConfigEntityInterface $entity, mixed $values): ConfigEntityInterface {
        if (!is_array($values)) {
        throw new EntityMethodException(sprintf('The pluralized entity method config action \'%s\' requires an array value in order to call %s::%s() multiple times', $this->pluginId, $entity->getEntityType()->getClass(), $this->method));
        }
        foreach ($values as $value) {
        $entity = $this->applySingle($entity, $value);
        }
        return $entity;
        }
        /**
        * Applies the action to entity treating the $values array a single call.
        *
        * @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity
        * The entity to apply the action to.
        * @param mixed $value
        * The value for the action to use.
        *
        * @return \Drupal\Core\Config\Entity\ConfigEntityInterface
        * The unsaved entity with the action applied.
        */
        private function applySingle(ConfigEntityInterface $entity, mixed $value): ConfigEntityInterface {
        // If $value is not an array then we only support calling the method if the
        // number of parameters or required parameters is 1. If there is only 1
        // parameter and $value is an array then assume that the parameter expects
        // an array.
        if (!is_array($value) || $this->numberOfParams === 1) {
        if ($this->numberOfRequiredParams !== 1 && $this->numberOfParams !== 1) {
        throw new EntityMethodException(sprintf('Entity method config action \'%s\' requires an array value. The number of parameters or required parameters for %s::%s() is not 1', $this->pluginId, $entity->getEntityType()->getClass(), $this->method));
        }
        $entity->{$this->method}($value);
        }
        else {
        $entity->{$this->method}(...$value);
        }
        return $entity;
        }
        }
        <?php
        declare(strict_types=1);
        namespace Drupal\Core\Config\Action\Plugin\ConfigAction;
        use Drupal\Component\Assertion\Inspector;
        use Drupal\Core\Config\Action\Attribute\ConfigAction;
        use Drupal\Core\Config\Action\ConfigActionException;
        use Drupal\Core\Config\Action\ConfigActionPluginInterface;
        use Drupal\Core\Config\Action\Plugin\ConfigAction\Deriver\PermissionsPerBundleDeriver;
        use Drupal\Core\Config\ConfigManagerInterface;
        use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
        use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
        use Drupal\user\RoleInterface;
        use Symfony\Component\DependencyInjection\ContainerInterface;
        /**
        * @internal
        * This API is experimental.
        */
        #[ConfigAction(
        id: 'permissions_per_bundle',
        entity_types: ['user_role'],
        deriver: PermissionsPerBundleDeriver::class,
        )]
        final class PermissionsPerBundle implements ConfigActionPluginInterface, ContainerFactoryPluginInterface {
        public function __construct(
        private readonly ConfigManagerInterface $configManager,
        private readonly EntityTypeBundleInfoInterface $entityTypeBundleInfo,
        private readonly string $targetEntityType,
        ) {}
        /**
        * {@inheritdoc}
        */
        public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
        assert(is_array($plugin_definition));
        $target_entity_type = $plugin_definition['target_entity_type'];
        return new static(
        $container->get(ConfigManagerInterface::class),
        $container->get(EntityTypeBundleInfoInterface::class),
        $target_entity_type,
        );
        }
        /**
        * {@inheritdoc}
        */
        public function apply(string $configName, mixed $value): void {
        $role = $this->configManager->loadConfigEntityByName($configName);
        if (!($role instanceof RoleInterface)) {
        throw new ConfigActionException(sprintf("Cannot determine role from %s", $configName));
        }
        assert(is_string($value) || is_array($value));
        [$permissions, $except_bundles] = self::parseValue($value);
        if (empty($permissions) || !Inspector::assertAllMatch('%bundle', $permissions, TRUE)) {
        throw new ConfigActionException(sprintf("The permissions provided %s must be an array of strings that contain '%%bundle'.", var_export($value, TRUE)));
        }
        $bundles = $this->entityTypeBundleInfo->getBundleInfo($this->targetEntityType);
        foreach (array_keys($bundles) as $bundle_id) {
        if (in_array($bundle_id, $except_bundles, TRUE)) {
        continue;
        }
        /** @var string[] $actual_permissions */
        $actual_permissions = str_replace('%bundle', $bundle_id, $permissions);
        array_walk($actual_permissions, $role->grantPermission(...));
        }
        $role->save();
        }
        /**
        * Parses the value supplied to ::apply().
        *
        * @param string|array<string|string[]> $value
        * One of:
        * - A single string (a permission template).
        * - An array of strings (several permission templates).
        * - An array with a `permissions` element, and an optional `except`
        * element, either of which can be an array or a string. `except` accepts
        * a single bundle, or a list of bundles, to exclude from the permissions
        * being granted.
        *
        * @return array<int, array<int<0, max>, array<string>|string>>
        * An indexed array with two elements: the array of permissions to grant,
        * and the list of bundles to ignore.
        */
        private static function parseValue(string|array $value): array {
        if (is_string($value)) {
        return [[$value], []];
        }
        if (array_is_list($value)) {
        return [$value, []];
        }
        $permissions = $value['permissions'] ?? [];
        $except_bundles = $value['except'] ?? [];
        return [(array) $permissions, (array) $except_bundles];
        }
        }
        <?php
        declare(strict_types=1);
        namespace Drupal\Core\Config\Action\Plugin\ConfigAction;
        use Drupal\Core\Config\Action\Attribute\ConfigAction;
        use Drupal\Core\Config\Action\ConfigActionException;
        use Drupal\Core\Config\Action\ConfigActionPluginInterface;
        use Drupal\Core\Config\ConfigFactoryInterface;
        use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
        use Drupal\Core\StringTranslation\TranslatableMarkup;
        use Symfony\Component\DependencyInjection\ContainerInterface;
        /**
        * @internal
        * This API is experimental.
        */
        #[ConfigAction(
        id: 'simple_config_update',
        admin_label: new TranslatableMarkup('Simple configuration update'),
        )]
        final class SimpleConfigUpdate implements ConfigActionPluginInterface, ContainerFactoryPluginInterface {
        /**
        * Constructs a SimpleConfigUpdate object.
        *
        * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
        * The config factory.
        */
        public function __construct(
        protected readonly ConfigFactoryInterface $configFactory,
        ) {
        }
        /**
        * {@inheritdoc}
        */
        public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
        return new static($container->get('config.factory'));
        }
        /**
        * {@inheritdoc}
        */
        public function apply(string $configName, mixed $value): void {
        $config = $this->configFactory->getEditable($configName);
        // @todo https://www.drupal.org/i/3439713 Should we error if this is a
        // config entity?
        if ($config->isNew()) {
        throw new ConfigActionException(sprintf('Config %s does not exist so can not be updated', $configName));
        }
        // Expect $value to be an array whose keys are the config keys to update.
        if (!is_array($value)) {
        throw new ConfigActionException(sprintf('Config %s can not be updated because $value is not an array', $configName));
        }
        foreach ($value as $key => $value) {
        $config->set($key, $value);
        }
        $config->save();
        }
        }
        <?php
        declare(strict_types=1);
        namespace Drupal\Core\Config\Checkpoint;
        /**
        * A value object to store information about a checkpoint.
        *
        * @internal
        * This API is experimental.
        */
        final class Checkpoint {
        /**
        * Constructs a checkpoint object.
        *
        * @param string $id
        * The checkpoint's ID.
        * @param \Stringable|string $label
        * The human-readable label.
        * @param int $timestamp
        * The timestamp when the checkpoint was created.
        * @param string|null $parent
        * The ID of the checkpoint's parent.
        */
        public function __construct(
        public readonly string $id,
        public readonly \Stringable|string $label,
        public readonly int $timestamp,
        public readonly ?string $parent,
        ) {
        }
        }
        0% Loading or .
        You are about to add 0 people to the discussion. Proceed with caution.
        Please register or to comment