ConfigManager.php

Same filename and directory in other branches
  1. 9 core/lib/Drupal/Core/Config/ConfigManager.php
  2. 8.9.x core/lib/Drupal/Core/Config/ConfigManager.php
  3. 10 core/lib/Drupal/Core/Config/ConfigManager.php

Namespace

Drupal\Core\Config

File

core/lib/Drupal/Core/Config/ConfigManager.php

View source
<?php

namespace Drupal\Core\Config;

use Drupal\Component\Diff\Diff;
use Drupal\Core\Config\Entity\ConfigDependencyManager;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ExtensionPathResolver;
use Drupal\Core\Serialization\Yaml;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
 * The ConfigManager provides helper functions for the configuration system.
 */
class ConfigManager implements ConfigManagerInterface {
    use StringTranslationTrait;
    use StorageCopyTrait;
    
    /**
     * The entity type manager.
     *
     * @var \Drupal\Core\Entity\EntityTypeManagerInterface
     */
    protected $entityTypeManager;
    
    /**
     * The entity repository.
     *
     * @var \Drupal\Core\Entity\EntityRepositoryInterface
     */
    protected $entityRepository;
    
    /**
     * The configuration factory.
     *
     * @var \Drupal\Core\Config\ConfigFactoryInterface
     */
    protected $configFactory;
    
    /**
     * The typed config manager.
     *
     * @var \Drupal\Core\Config\TypedConfigManagerInterface
     */
    protected $typedConfigManager;
    
    /**
     * The active configuration storage.
     *
     * @var \Drupal\Core\Config\StorageInterface
     */
    protected $activeStorage;
    
    /**
     * The event dispatcher.
     *
     * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
     */
    protected $eventDispatcher;
    
    /**
     * The configuration collection info.
     *
     * @var \Drupal\Core\Config\ConfigCollectionInfo
     */
    protected $configCollectionInfo;
    
    /**
     * The configuration storages keyed by collection name.
     *
     * @var \Drupal\Core\Config\StorageInterface[]
     */
    protected $storages;
    
    /**
     * The extension path resolver.
     *
     * @var \Drupal\Core\Extension\ExtensionPathResolver
     */
    protected $extensionPathResolver;
    
    /**
     * Creates ConfigManager objects.
     *
     * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
     *   The entity type manager.
     * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
     *   The configuration factory.
     * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager
     *   The typed config manager.
     * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
     *   The string translation service.
     * @param \Drupal\Core\Config\StorageInterface $active_storage
     *   The active configuration storage.
     * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
     *   The event dispatcher.
     * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
     *   The entity repository.
     * @param \Drupal\Core\Extension\ExtensionPathResolver $extension_path_resolver
     *   The extension path resolver.
     */
    public function __construct(EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typed_config_manager, TranslationInterface $string_translation, StorageInterface $active_storage, EventDispatcherInterface $event_dispatcher, EntityRepositoryInterface $entity_repository, ExtensionPathResolver $extension_path_resolver) {
        $this->entityTypeManager = $entity_type_manager;
        $this->configFactory = $config_factory;
        $this->typedConfigManager = $typed_config_manager;
        $this->stringTranslation = $string_translation;
        $this->activeStorage = $active_storage;
        $this->eventDispatcher = $event_dispatcher;
        $this->entityRepository = $entity_repository;
        $this->extensionPathResolver = $extension_path_resolver;
    }
    
    /**
     * {@inheritdoc}
     */
    public function getEntityTypeIdByName($name) {
        foreach ($this->entityTypeManager
            ->getDefinitions() as $entity_type_id => $entity_type) {
            if ($entity_type instanceof ConfigEntityTypeInterface && ($config_prefix = $entity_type->getConfigPrefix()) && str_starts_with($name, $config_prefix . '.')) {
                return $entity_type_id;
            }
        }
        return NULL;
    }
    
    /**
     * {@inheritdoc}
     */
    public function loadConfigEntityByName($name) {
        $entity_type_id = $this->getEntityTypeIdByName($name);
        if ($entity_type_id) {
            $entity_type = $this->entityTypeManager
                ->getDefinition($entity_type_id);
            $id = substr($name, strlen($entity_type->getConfigPrefix()) + 1);
            return $this->entityTypeManager
                ->getStorage($entity_type_id)
                ->load($id);
        }
        return NULL;
    }
    
    /**
     * {@inheritdoc}
     */
    public function getEntityTypeManager() {
        return $this->entityTypeManager;
    }
    
    /**
     * {@inheritdoc}
     */
    public function getConfigFactory() {
        return $this->configFactory;
    }
    
    /**
     * {@inheritdoc}
     */
    public function diff(StorageInterface $source_storage, StorageInterface $target_storage, $source_name, $target_name = NULL, $collection = StorageInterface::DEFAULT_COLLECTION) {
        if ($collection != StorageInterface::DEFAULT_COLLECTION) {
            $source_storage = $source_storage->createCollection($collection);
            $target_storage = $target_storage->createCollection($collection);
        }
        if (!isset($target_name)) {
            $target_name = $source_name;
        }
        // The output should show configuration object differences formatted as YAML.
        // But the configuration is not necessarily stored in files. Therefore, they
        // need to be read and parsed, and lastly, dumped into YAML strings.
        $source_data = explode("\n", Yaml::encode($source_storage->read($source_name)));
        $target_data = explode("\n", Yaml::encode($target_storage->read($target_name)));
        // Check for new or removed files.
        if ($source_data === [
            'false',
        ]) {
            // Added file.
            // Cast the result of t() to a string, as the diff engine doesn't know
            // about objects.
            $source_data = [
                (string) $this->t('File added'),
            ];
        }
        if ($target_data === [
            'false',
        ]) {
            // Deleted file.
            // Cast the result of t() to a string, as the diff engine doesn't know
            // about objects.
            $target_data = [
                (string) $this->t('File removed'),
            ];
        }
        return new Diff($source_data, $target_data);
    }
    
    /**
     * {@inheritdoc}
     */
    public function createSnapshot(StorageInterface $source_storage, StorageInterface $snapshot_storage) {
        self::replaceStorageContents($source_storage, $snapshot_storage);
    }
    
    /**
     * {@inheritdoc}
     */
    public function uninstall($type, $name) {
        $entities = $this->getConfigEntitiesToChangeOnDependencyRemoval($type, [
            $name,
        ], FALSE);
        // Fix all dependent configuration entities.
        
        /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
        foreach ($entities['update'] as $entity) {
            $entity->save();
        }
        // Remove all dependent configuration entities.
        foreach ($entities['delete'] as $entity) {
            $entity->setUninstalling(TRUE);
            $entity->delete();
        }
        $config_names = $this->configFactory
            ->listAll($name . '.');
        foreach ($config_names as $config_name) {
            $this->configFactory
                ->getEditable($config_name)
                ->delete();
        }
        // Remove any matching configuration from collections.
        foreach ($this->activeStorage
            ->getAllCollectionNames() as $collection) {
            $collection_storage = $this->activeStorage
                ->createCollection($collection);
            $overrider = $this->getConfigCollectionInfo()
                ->getOverrideService($collection);
            foreach ($collection_storage->listAll($name . '.') as $config_name) {
                if ($overrider) {
                    $config = $overrider->createConfigObject($config_name, $collection);
                }
                else {
                    $config = new Config($config_name, $collection_storage, $this->eventDispatcher, $this->typedConfigManager);
                }
                $config->initWithData($collection_storage->read($config_name));
                $config->delete();
            }
        }
        $schema_dir = $this->extensionPathResolver
            ->getPath($type, $name) . '/' . InstallStorage::CONFIG_SCHEMA_DIRECTORY;
        if (is_dir($schema_dir)) {
            // Refresh the schema cache if uninstalling an extension that provides
            // configuration schema.
            $this->typedConfigManager
                ->clearCachedDefinitions();
        }
    }
    
    /**
     * {@inheritdoc}
     */
    public function getConfigDependencyManager() {
        $dependency_manager = new ConfigDependencyManager();
        // Read all configuration using the factory. This ensures that multiple
        // deletes during the same request benefit from the static cache. Using the
        // factory also ensures configuration entity dependency discovery has no
        // dependencies on the config entity classes. Assume data with UUID is a
        // config entity. Only configuration entities can be depended on so we can
        // ignore everything else.
        $data = array_map(function ($config) {
            $data = $config->get();
            if (isset($data['uuid'])) {
                return $data;
            }
            return FALSE;
        }, $this->configFactory
            ->loadMultiple($this->activeStorage
            ->listAll()));
        $dependency_manager->setData(array_filter($data));
        return $dependency_manager;
    }
    
    /**
     * {@inheritdoc}
     */
    public function findConfigEntityDependencies($type, array $names, ?ConfigDependencyManager $dependency_manager = NULL) {
        if (!$dependency_manager) {
            $dependency_manager = $this->getConfigDependencyManager();
        }
        $dependencies = [];
        foreach ($names as $name) {
            $dependencies[] = $dependency_manager->getDependentEntities($type, $name);
        }
        return array_merge(...$dependencies);
    }
    
    /**
     * {@inheritdoc}
     */
    public function findConfigEntityDependenciesAsEntities($type, array $names, ?ConfigDependencyManager $dependency_manager = NULL) {
        $dependencies = $this->findConfigEntityDependencies($type, $names, $dependency_manager);
        $entities = [];
        $definitions = $this->entityTypeManager
            ->getDefinitions();
        foreach ($dependencies as $config_name => $dependency) {
            // Group by entity type to efficient load entities using
            // \Drupal\Core\Entity\EntityStorageInterface::loadMultiple().
            $entity_type_id = $this->getEntityTypeIdByName($config_name);
            // It is possible that a non-configuration entity will be returned if a
            // simple configuration object has a UUID key. This would occur if the
            // dependents of the system module are calculated since system.site has
            // a UUID key.
            if ($entity_type_id) {
                $id = substr($config_name, strlen($definitions[$entity_type_id]->getConfigPrefix()) + 1);
                $entities[$entity_type_id][] = $id;
            }
        }
        $entities_to_return = [];
        foreach ($entities as $entity_type_id => $entities_to_load) {
            $storage = $this->entityTypeManager
                ->getStorage($entity_type_id);
            // Remove the keys since there are potential ID clashes from different
            // configuration entity types.
            $entities_to_return[] = array_values($storage->loadMultiple($entities_to_load));
        }
        return array_merge(...$entities_to_return);
    }
    
    /**
     * {@inheritdoc}
     */
    public function getConfigEntitiesToChangeOnDependencyRemoval($type, array $names, $dry_run = TRUE) {
        $dependency_manager = $this->getConfigDependencyManager();
        // Store the list of dependents in three separate variables. This allows us
        // to determine how the dependency graph changes as entities are fixed by
        // calling the onDependencyRemoval() method.
        // The list of original dependents on $names. This list never changes.
        $original_dependents = $this->findConfigEntityDependenciesAsEntities($type, $names, $dependency_manager);
        // The current list of dependents on $names. This list is recalculated when
        // calling an entity's onDependencyRemoval() method results in the entity
        // changing. This list is passed to each entity's onDependencyRemoval()
        // method as the list of affected entities.
        $current_dependents = $original_dependents;
        // The list of dependents to process. This list changes as entities are
        // processed and are either fixed or deleted.
        $dependents_to_process = $original_dependents;
        // Initialize other variables.
        $affected_uuids = [];
        $return = [
            'update' => [],
            'delete' => [],
            'unchanged' => [],
        ];
        // Try to fix the dependents and find out what will happen to the dependency
        // graph. Entities are processed in the order of most dependent first. For
        // example, this ensures that Menu UI third party dependencies on node types
        // are fixed before processing the node type's other dependents.
        while ($dependent = array_pop($dependents_to_process)) {
            
            /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $dependent */
            if ($dry_run) {
                // Clone the entity so any changes do not change any static caches.
                $dependent = clone $dependent;
            }
            $fixed = FALSE;
            if ($this->callOnDependencyRemoval($dependent, $current_dependents, $type, $names)) {
                // Recalculate dependencies and update the dependency graph data.
                $dependent->calculateDependencies();
                $dependency_manager->updateData($dependent->getConfigDependencyName(), $dependent->getDependencies());
                // Based on the updated data rebuild the list of current dependents.
                // This will remove entities that are no longer dependent after the
                // recalculation.
                $current_dependents = $this->findConfigEntityDependenciesAsEntities($type, $names, $dependency_manager);
                // Rebuild the list of entities that we need to process using the new
                // list of current dependents and removing any entities that we've
                // already processed.
                $dependents_to_process = array_filter($current_dependents, function ($current_dependent) use ($affected_uuids) {
                    return !in_array($current_dependent->uuid(), $affected_uuids);
                });
                // Ensure that the dependent has actually been fixed. It is possible
                // that other dependencies cause it to still be in the list.
                $fixed = TRUE;
                foreach ($dependents_to_process as $key => $entity) {
                    if ($entity->uuid() == $dependent->uuid()) {
                        $fixed = FALSE;
                        unset($dependents_to_process[$key]);
                        break;
                    }
                }
                if ($fixed) {
                    $affected_uuids[] = $dependent->uuid();
                    $return['update'][] = $dependent;
                }
            }
            // If the entity cannot be fixed then it has to be deleted.
            if (!$fixed) {
                $affected_uuids[] = $dependent->uuid();
                // Deletes should occur in the order of the least dependent first. For
                // example, this ensures that fields are removed before field storages.
                array_unshift($return['delete'], $dependent);
            }
        }
        // Use the list of affected UUIDs to filter the original list to work out
        // which configuration entities are unchanged.
        $return['unchanged'] = array_filter($original_dependents, function ($dependent) use ($affected_uuids) {
            return !in_array($dependent->uuid(), $affected_uuids);
        });
        return $return;
    }
    
    /**
     * {@inheritdoc}
     */
    public function getConfigCollectionInfo() {
        if (!isset($this->configCollectionInfo)) {
            $this->configCollectionInfo = new ConfigCollectionInfo();
            $this->eventDispatcher
                ->dispatch($this->configCollectionInfo, ConfigCollectionEvents::COLLECTION_INFO);
        }
        return $this->configCollectionInfo;
    }
    
    /**
     * Calls an entity's onDependencyRemoval() method.
     *
     * A helper method to call onDependencyRemoval() with the correct list of
     * affected entities. This list should only contain dependencies on the
     * entity. Configuration and content entity dependencies will be converted
     * into entity objects.
     *
     * @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity
     *   The entity to call onDependencyRemoval() on.
     * @param \Drupal\Core\Config\Entity\ConfigEntityInterface[] $dependent_entities
     *   The list of dependent configuration entities.
     * @param string $type
     *   The type of dependency being checked. Either 'module', 'theme', 'config'
     *   or 'content'.
     * @param array $names
     *   The specific names to check. If $type equals 'module' or 'theme' then it
     *   should be a list of module names or theme names. In the case of 'config'
     *   or 'content' it should be a list of configuration dependency names.
     *
     * @return bool
     *   TRUE if the entity has changed as a result of calling the
     *   onDependencyRemoval() method, FALSE if not.
     */
    protected function callOnDependencyRemoval(ConfigEntityInterface $entity, array $dependent_entities, $type, array $names) {
        $entity_dependencies = $entity->getDependencies();
        if (empty($entity_dependencies)) {
            // No dependent entities nothing to do.
            return FALSE;
        }
        $affected_dependencies = [
            'config' => [],
            'content' => [],
            'module' => [],
            'theme' => [],
        ];
        // Work out if any of the entity's dependencies are going to be affected.
        if (isset($entity_dependencies[$type])) {
            // Work out which dependencies the entity has in common with the provided
            // $type and $names.
            $affected_dependencies[$type] = array_intersect($entity_dependencies[$type], $names);
            // If the dependencies are entities we need to convert them into objects.
            if ($type == 'config' || $type == 'content') {
                $affected_dependencies[$type] = array_map(function ($name) use ($type) {
                    if ($type == 'config') {
                        return $this->loadConfigEntityByName($name);
                    }
                    else {
                        // Ignore the bundle.
                        [
                            $entity_type_id,
                            ,
                            $uuid,
                        ] = explode(':', $name);
                        return $this->entityRepository
                            ->loadEntityByConfigTarget($entity_type_id, $uuid);
                    }
                }, $affected_dependencies[$type]);
            }
        }
        // Merge any other configuration entities into the list of affected
        // dependencies if necessary.
        if (isset($entity_dependencies['config'])) {
            foreach ($dependent_entities as $dependent_entity) {
                if (in_array($dependent_entity->getConfigDependencyName(), $entity_dependencies['config'])) {
                    $affected_dependencies['config'][] = $dependent_entity;
                }
            }
        }
        // Key the entity arrays by config dependency name to make searching easy.
        foreach ([
            'config',
            'content',
        ] as $dependency_type) {
            $affected_dependencies[$dependency_type] = array_combine(array_map(function ($entity) {
                return $entity->getConfigDependencyName();
            }, $affected_dependencies[$dependency_type]), $affected_dependencies[$dependency_type]);
        }
        // Inform the entity.
        return $entity->onDependencyRemoval($affected_dependencies);
    }
    
    /**
     * {@inheritdoc}
     */
    public function findMissingContentDependencies() {
        $content_dependencies = [];
        $missing_dependencies = [];
        foreach ($this->activeStorage
            ->readMultiple($this->activeStorage
            ->listAll()) as $config_data) {
            if (isset($config_data['dependencies']['content'])) {
                $content_dependencies[] = $config_data['dependencies']['content'];
            }
            if (isset($config_data['dependencies']['enforced']['content'])) {
                $content_dependencies[] = $config_data['dependencies']['enforced']['content'];
            }
        }
        $unique_content_dependencies = array_unique(array_merge(...$content_dependencies));
        foreach ($unique_content_dependencies as $content_dependency) {
            // Format of the dependency is entity_type:bundle:uuid.
            [
                $entity_type,
                $bundle,
                $uuid,
            ] = explode(':', $content_dependency, 3);
            if (!$this->entityRepository
                ->loadEntityByUuid($entity_type, $uuid)) {
                $missing_dependencies[$uuid] = [
                    'entity_type' => $entity_type,
                    'bundle' => $bundle,
                    'uuid' => $uuid,
                ];
            }
        }
        return $missing_dependencies;
    }

}

Classes

Title Deprecated Summary
ConfigManager The ConfigManager provides helper functions for the configuration system.

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