ConfigActionManager.php

Same filename in other branches
  1. 10 core/lib/Drupal/Core/Config/Action/ConfigActionManager.php

Namespace

Drupal\Core\Config\Action

File

core/lib/Drupal/Core/Config/Action/ConfigActionManager.php

View source
<?php

declare (strict_types=1);
namespace Drupal\Core\Config\Action;

use Drupal\Component\Plugin\Exception\PluginNotFoundException;
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 {
    
    /**
     * Information about all deprecated plugin IDs.
     *
     * @var string[]
     */
    private static array $deprecatedPluginIds = [
        'entity_create:ensure_exists' => [
            'replacement' => 'entity_create:createIfNotExists',
            'message' => 'The plugin ID "entity_create:ensure_exists" is deprecated in drupal:10.3.1 and will be removed in drupal:12.0.0. Use "entity_create:createIfNotExists" instead. See https://www.drupal.org/node/3458273.',
        ],
        'simple_config_update' => [
            'replacement' => 'simpleConfigUpdate',
            'message' => 'The plugin ID "simple_config_update" is deprecated in drupal:10.3.1 and will be removed in drupal:12.0.0. Use "simpleConfigUpdate" instead. See https://www.drupal.org/node/3458273.',
        ],
    ];
    
    /**
     * 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, ConfigManagerInterface $configManager, StorageInterface $configStorage, TypedConfigManagerInterface $typedConfig, 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;
            }
        }
        try {
            
            /** @var \Drupal\Core\Config\Action\ConfigActionPluginInterface $action */
            $action = $this->createInstance($action_id);
        } catch (PluginNotFoundException $e) {
            $entity_type = $this->configManager
                ->getEntityTypeIdByName($configName);
            if ($entity_type) {
                $action_ids = $this->getShorthandActionIdsForEntityType($entity_type);
                $valid_ids = implode(', ', array_keys($action_ids));
                throw new PluginNotFoundException($action_id, sprintf('The "%s" entity does not support the "%s" config action. Valid config actions for %s are: %s', $entity_type, $action_id, $entity_type, $valid_ids));
            }
            throw $e;
        }
        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;
    }
    
    /**
     * {@inheritdoc}
     */
    public function alterDefinitions(&$definitions) : void {
        // Adds backwards compatibility for plugins that have been renamed.
        foreach (self::$deprecatedPluginIds as $legacy => $new_plugin_id) {
            $definitions[$legacy] = $definitions[$new_plugin_id['replacement']];
        }
        parent::alterDefinitions($definitions);
    }
    
    /**
     * {@inheritdoc}
     */
    public function createInstance($plugin_id, array $configuration = []) {
        $instance = parent::createInstance($plugin_id, $configuration);
        // Trigger deprecation notices for renamed plugins.
        if (array_key_exists($plugin_id, self::$deprecatedPluginIds)) {
            // phpcs:ignore Drupal.Semantics.FunctionTriggerError
            @trigger_error(self::$deprecatedPluginIds[$plugin_id]['message'], E_USER_DEPRECATED);
        }
        return $instance;
    }

}

Classes

Title Deprecated Summary
ConfigActionManager

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.