ConfigImportSubscriber.php

Same filename in this branch
  1. 11.x core/modules/content_moderation/src/EventSubscriber/ConfigImportSubscriber.php
Same filename and directory in other branches
  1. 9 core/modules/content_moderation/src/EventSubscriber/ConfigImportSubscriber.php
  2. 9 core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
  3. 8.9.x core/modules/content_moderation/src/EventSubscriber/ConfigImportSubscriber.php
  4. 8.9.x core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
  5. 10 core/modules/content_moderation/src/EventSubscriber/ConfigImportSubscriber.php
  6. 10 core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php

Namespace

Drupal\Core\EventSubscriber

File

core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php

View source
<?php

namespace Drupal\Core\EventSubscriber;

use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\ConfigImporterEvent;
use Drupal\Core\Config\ConfigImportValidateEventSubscriberBase;
use Drupal\Core\Config\ConfigNameException;
use Drupal\Core\Extension\ConfigImportModuleUninstallValidatorInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ThemeExtensionList;
use Drupal\Core\Installer\InstallerKernel;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;

/**
 * Config import subscriber for config import events.
 */
class ConfigImportSubscriber extends ConfigImportValidateEventSubscriberBase {
    
    /**
     * Theme data.
     *
     * @var \Drupal\Core\Extension\Extension[]
     */
    protected $themeData;
    
    /**
     * Module extension list.
     *
     * @var \Drupal\Core\Extension\ModuleExtensionList
     */
    protected $moduleExtensionList;
    
    /**
     * The theme extension list.
     *
     * @var \Drupal\Core\Extension\ThemeExtensionList
     */
    protected ThemeExtensionList $themeList;
    
    /**
     * Constructs the ConfigImportSubscriber.
     *
     * @param \Drupal\Core\Extension\ThemeExtensionList $theme_extension_list
     *   The theme extension list.
     * @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
     *   The module extension list.
     * @param \Traversable $uninstallValidators
     *   The uninstall validator services.
     */
    public function __construct(ThemeExtensionList $theme_extension_list, ModuleExtensionList $extension_list_module, \Traversable $uninstallValidators) {
        $this->themeList = $theme_extension_list;
        $this->moduleExtensionList = $extension_list_module;
    }
    
    /**
     * Validates the configuration to be imported.
     *
     * @param \Drupal\Core\Config\ConfigImporterEvent $event
     *   The Event to process.
     *
     * @throws \Drupal\Core\Config\ConfigNameException
     */
    public function onConfigImporterValidate(ConfigImporterEvent $event) {
        foreach ([
            'delete',
            'create',
            'update',
        ] as $op) {
            foreach ($event->getConfigImporter()
                ->getUnprocessedConfiguration($op) as $name) {
                try {
                    Config::validateName($name);
                } catch (ConfigNameException $e) {
                    $message = $this->t('The config name @config_name is invalid.', [
                        '@config_name' => $name,
                    ]);
                    $event->getConfigImporter()
                        ->logError($message);
                }
            }
        }
        $config_importer = $event->getConfigImporter();
        if ($config_importer->getStorageComparer()
            ->getSourceStorage()
            ->exists('core.extension')) {
            $this->validateModules($config_importer);
            $this->validateThemes($config_importer);
            $this->validateDependencies($config_importer);
        }
        else {
            $config_importer->logError($this->t('The core.extension configuration does not exist.'));
        }
    }
    
    /**
     * Validates module installations and uninstallations.
     *
     * @param \Drupal\Core\Config\ConfigImporter $config_importer
     *   The configuration importer.
     */
    protected function validateModules(ConfigImporter $config_importer) {
        $core_extension = $config_importer->getStorageComparer()
            ->getSourceStorage()
            ->read('core.extension');
        // Get the install profile from the site's configuration.
        $current_core_extension = $config_importer->getStorageComparer()
            ->getTargetStorage()
            ->read('core.extension');
        $install_profile = $current_core_extension['profile'] ?? NULL;
        $new_install_profile = $core_extension['profile'] ?? NULL;
        // Ensure the profile is not changing.
        if ($install_profile !== $new_install_profile) {
            if (InstallerKernel::installationAttempted()) {
                $config_importer->logError($this->t('The selected installation profile %install_profile does not match the profile stored in configuration %config_profile.', [
                    '%install_profile' => $install_profile,
                    '%config_profile' => $new_install_profile,
                ]));
                // If this error has occurred the other checks are irrelevant.
                return;
            }
            elseif ($new_install_profile) {
                $config_importer->logError($this->t('Cannot change the install profile from %profile to %new_profile once Drupal is installed.', [
                    '%profile' => $install_profile,
                    '%new_profile' => $new_install_profile,
                ]));
            }
        }
        elseif ($new_install_profile && !isset($core_extension['module'][$new_install_profile])) {
            $config_importer->logError($this->t('The install profile %profile is not in the list of installed modules.', [
                '%profile' => $new_install_profile,
            ]));
        }
        // Get a list of modules with dependency weights as values.
        $module_data = $this->moduleExtensionList
            ->getList();
        $nonexistent_modules = array_keys(array_diff_key($core_extension['module'], $module_data));
        foreach ($nonexistent_modules as $module) {
            $config_importer->logError($this->t('Unable to install the %module module since it does not exist.', [
                '%module' => $module,
            ]));
        }
        // Ensure that all modules being installed have their dependencies met.
        $installs = $config_importer->getExtensionChangelist('module', 'install');
        foreach ($installs as $module) {
            $missing_dependencies = [];
            foreach (array_keys($module_data[$module]->requires) as $required_module) {
                if (!isset($core_extension['module'][$required_module])) {
                    $missing_dependencies[] = $module_data[$required_module]->info['name'];
                }
            }
            if (!empty($missing_dependencies)) {
                $module_name = $module_data[$module]->info['name'];
                $message = $this->formatPlural(count($missing_dependencies), 'Unable to install the %module module since it requires the %required_module module.', 'Unable to install the %module module since it requires the %required_module modules.', [
                    '%module' => $module_name,
                    '%required_module' => implode(', ', $missing_dependencies),
                ]);
                $config_importer->logError($message);
            }
        }
        // Ensure that all modules being uninstalled are not required by modules
        // that will be installed after the import.
        $uninstalls = $config_importer->getExtensionChangelist('module', 'uninstall');
        foreach ($uninstalls as $module) {
            foreach (array_keys($module_data[$module]->required_by) as $dependent_module) {
                if ($module_data[$dependent_module]->status && !in_array($dependent_module, $uninstalls, TRUE) && $dependent_module !== $install_profile) {
                    $module_name = $module_data[$module]->info['name'];
                    $dependent_module_name = $module_data[$dependent_module]->info['name'];
                    $config_importer->logError($this->t('Unable to uninstall the %module module since the %dependent_module module is installed.', [
                        '%module' => $module_name,
                        '%dependent_module' => $dependent_module_name,
                    ]));
                }
            }
            // Ensure that modules can be uninstalled.
            foreach ($this->uninstallValidators as $validator) {
                $reasons = $validator instanceof ConfigImportModuleUninstallValidatorInterface ? $validator->validateConfigImport($module, $config_importer->getStorageComparer()
                    ->getSourceStorage()) : $validator->validate($module);
                foreach ($reasons as $reason) {
                    $config_importer->logError($this->t('Unable to uninstall the %module module because: @reason.', [
                        '%module' => $module_data[$module]->info['name'],
                        '@reason' => $reason,
                    ]));
                }
            }
        }
    }
    
    /**
     * Validates theme installations and uninstallations.
     *
     * @param \Drupal\Core\Config\ConfigImporter $config_importer
     *   The configuration importer.
     */
    protected function validateThemes(ConfigImporter $config_importer) {
        $core_extension = $config_importer->getStorageComparer()
            ->getSourceStorage()
            ->read('core.extension');
        // Get all themes including those that are not installed.
        $theme_data = $this->getThemeData();
        $module_data = $this->moduleExtensionList
            ->getList();
        $nonexistent_themes = array_keys(array_diff_key($core_extension['theme'], $theme_data));
        foreach ($nonexistent_themes as $theme) {
            $config_importer->logError($this->t('Unable to install the %theme theme since it does not exist.', [
                '%theme' => $theme,
            ]));
        }
        // Ensure that all themes being installed have their dependencies met.
        $installs = $config_importer->getExtensionChangelist('theme', 'install');
        foreach ($installs as $theme) {
            $module_dependencies = $theme_data[$theme]->module_dependencies;
            // $theme_data[$theme]->requires contains both theme and module
            // dependencies keyed by the extension machine names.
            // $theme_data[$theme]->module_dependencies contains only the module
            // dependencies keyed by the module extension machine name. Therefore, we
            // can find the theme dependencies by finding array keys for 'requires'
            // that are not in $module_dependencies.
            $theme_dependencies = array_diff_key($theme_data[$theme]->requires, $module_dependencies);
            foreach (array_keys($theme_dependencies) as $required_theme) {
                if (!isset($core_extension['theme'][$required_theme])) {
                    $theme_name = $theme_data[$theme]->info['name'];
                    $required_theme_name = $theme_data[$required_theme]->info['name'];
                    $config_importer->logError($this->t('Unable to install the %theme theme since it requires the %required_theme theme.', [
                        '%theme' => $theme_name,
                        '%required_theme' => $required_theme_name,
                    ]));
                }
            }
            foreach (array_keys($module_dependencies) as $required_module) {
                if (!isset($core_extension['module'][$required_module])) {
                    $theme_name = $theme_data[$theme]->info['name'];
                    $required_module_name = $module_data[$required_module]->info['name'];
                    $config_importer->logError($this->t('Unable to install the %theme theme since it requires the %required_module module.', [
                        '%theme' => $theme_name,
                        '%required_module' => $required_module_name,
                    ]));
                }
            }
        }
        // Ensure that all themes being uninstalled are not required by themes that
        // will be installed after the import.
        $uninstalls = $config_importer->getExtensionChangelist('theme', 'uninstall');
        foreach ($uninstalls as $theme) {
            foreach (array_keys($theme_data[$theme]->required_by) as $dependent_theme) {
                if ($theme_data[$dependent_theme]->status && !in_array($dependent_theme, $uninstalls, TRUE)) {
                    $theme_name = $theme_data[$theme]->info['name'];
                    $dependent_theme_name = $theme_data[$dependent_theme]->info['name'];
                    $config_importer->logError($this->t('Unable to uninstall the %theme theme since the %dependent_theme theme is installed.', [
                        '%theme' => $theme_name,
                        '%dependent_theme' => $dependent_theme_name,
                    ]));
                }
            }
        }
    }
    
    /**
     * Validates configuration being imported does not have unmet dependencies.
     *
     * @param \Drupal\Core\Config\ConfigImporter $config_importer
     *   The configuration importer.
     */
    protected function validateDependencies(ConfigImporter $config_importer) {
        $core_extension = $config_importer->getStorageComparer()
            ->getSourceStorage()
            ->read('core.extension');
        $existing_dependencies = [
            'config' => $config_importer->getStorageComparer()
                ->getSourceStorage()
                ->listAll(),
            'module' => array_keys($core_extension['module']),
            'theme' => array_keys($core_extension['theme']),
        ];
        $theme_data = $this->getThemeData();
        $module_data = $this->moduleExtensionList
            ->getList();
        // Validate the dependencies of all the configuration. We have to validate
        // the entire tree because existing configuration might depend on
        // configuration that is being deleted.
        foreach ($config_importer->getStorageComparer()
            ->getSourceStorage()
            ->listAll() as $name) {
            // Ensure that the config owner is installed. This checks all
            // configuration including configuration entities.
            [
                $owner,
            ] = explode('.', $name, 2);
            if ($owner !== 'core') {
                $message = FALSE;
                if (!isset($core_extension['module'][$owner]) && isset($module_data[$owner])) {
                    $message = $this->t('Configuration %name depends on the %owner module that will not be installed after import.', [
                        '%name' => $name,
                        '%owner' => $module_data[$owner]->info['name'],
                    ]);
                }
                elseif (!isset($core_extension['theme'][$owner]) && isset($theme_data[$owner])) {
                    $message = $this->t('Configuration %name depends on the %owner theme that will not be installed after import.', [
                        '%name' => $name,
                        '%owner' => $theme_data[$owner]->info['name'],
                    ]);
                }
                elseif (!isset($core_extension['module'][$owner]) && !isset($core_extension['theme'][$owner])) {
                    $message = $this->t('Configuration %name depends on the %owner extension that will not be installed after import.', [
                        '%name' => $name,
                        '%owner' => $owner,
                    ]);
                }
                if ($message) {
                    $config_importer->logError($message);
                    continue;
                }
            }
            $data = $config_importer->getStorageComparer()
                ->getSourceStorage()
                ->read($name);
            // Configuration entities have dependencies on modules, themes, and other
            // configuration entities that we can validate. Their content dependencies
            // are not validated since we assume that they are soft dependencies.
            // Configuration entities can be identified by having 'dependencies' and
            // 'uuid' keys.
            if (isset($data['dependencies']) && isset($data['uuid'])) {
                $dependencies_to_check = array_intersect_key($data['dependencies'], array_flip([
                    'module',
                    'theme',
                    'config',
                ]));
                foreach ($dependencies_to_check as $type => $dependencies) {
                    $diffs = array_diff($dependencies, $existing_dependencies[$type]);
                    if (!empty($diffs)) {
                        $message = FALSE;
                        switch ($type) {
                            case 'module':
                                $message = $this->formatPlural(count($diffs), 'Configuration %name depends on the %module module that will not be installed after import.', 'Configuration %name depends on modules (%module) that will not be installed after import.', [
                                    '%name' => $name,
                                    '%module' => implode(', ', $this->getNames($diffs, $module_data)),
                                ]);
                                break;
                            case 'theme':
                                $message = $this->formatPlural(count($diffs), 'Configuration %name depends on the %theme theme that will not be installed after import.', 'Configuration %name depends on themes (%theme) that will not be installed after import.', [
                                    '%name' => $name,
                                    '%theme' => implode(', ', $this->getNames($diffs, $theme_data)),
                                ]);
                                break;
                            case 'config':
                                $message = $this->formatPlural(count($diffs), 'Configuration %name depends on the %config configuration that will not exist after import.', 'Configuration %name depends on configuration (%config) that will not exist after import.', [
                                    '%name' => $name,
                                    '%config' => implode(', ', $diffs),
                                ]);
                                break;
                        }
                        if ($message) {
                            $config_importer->logError($message);
                        }
                    }
                }
            }
        }
    }
    
    /**
     * Gets theme data.
     *
     * @return \Drupal\Core\Extension\Extension[]
     */
    protected function getThemeData() {
        if (!isset($this->themeData)) {
            $this->themeData = $this->themeList
                ->reset()
                ->getList();
        }
        return $this->themeData;
    }
    
    /**
     * Gets human readable extension names.
     *
     * @param array $names
     *   A list of extension machine names.
     * @param \Drupal\Core\Extension\Extension[] $extension_data
     *   Extension data.
     *
     * @return array
     *   A list of human-readable extension names, or machine names if
     *   human-readable names are not available.
     */
    protected function getNames(array $names, array $extension_data) {
        return array_map(function ($name) use ($extension_data) {
            if (isset($extension_data[$name])) {
                $name = $extension_data[$name]->info['name'];
            }
            return $name;
        }, $names);
    }

}

Classes

Title Deprecated Summary
ConfigImportSubscriber Config import subscriber for config import events.

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