3 namespace Drupal\Core\EventSubscriber;
5 use Drupal\Core\Config\Config;
6 use Drupal\Core\Config\ConfigImporter;
7 use Drupal\Core\Config\ConfigImporterEvent;
8 use Drupal\Core\Config\ConfigImportValidateEventSubscriberBase;
9 use Drupal\Core\Config\ConfigNameException;
10 use Drupal\Core\Extension\ThemeHandlerInterface;
13 * Config import subscriber for config import events.
15 class ConfigImportSubscriber extends ConfigImportValidateEventSubscriberBase {
20 * @var \Drupal\Core\Extension\Extension[]
27 * @var \Drupal\Core\Extension\Extension[]
29 protected $moduleData;
34 * @var \Drupal\Core\Extension\ThemeHandlerInterface
36 protected $themeHandler;
39 * Constructs the ConfigImportSubscriber.
41 * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
44 public function __construct(ThemeHandlerInterface $theme_handler) {
45 $this->themeHandler = $theme_handler;
49 * Validates the configuration to be imported.
51 * @param \Drupal\Core\Config\ConfigImporterEvent $event
52 * The Event to process.
54 * @throws \Drupal\Core\Config\ConfigNameException
56 public function onConfigImporterValidate(ConfigImporterEvent $event) {
57 foreach (['delete', 'create', 'update'] as $op) {
58 foreach ($event->getConfigImporter()->getUnprocessedConfiguration($op) as $name) {
60 Config::validateName($name);
62 catch (ConfigNameException $e) {
63 $message = $this->t('The config name @config_name is invalid.', ['@config_name' => $name]);
64 $event->getConfigImporter()->logError($message);
68 $config_importer = $event->getConfigImporter();
69 if ($config_importer->getStorageComparer()->getSourceStorage()->exists('core.extension')) {
70 $this->validateModules($config_importer);
71 $this->validateThemes($config_importer);
72 $this->validateDependencies($config_importer);
75 $config_importer->logError($this->t('The core.extension configuration does not exist.'));
80 * Validates module installations and uninstallations.
82 * @param \Drupal\Core\Config\ConfigImporter $config_importer
83 * The configuration importer.
85 protected function validateModules(ConfigImporter $config_importer) {
86 $core_extension = $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension');
88 // Get the install profile from the site's configuration.
89 $current_core_extension = $config_importer->getStorageComparer()->getTargetStorage()->read('core.extension');
90 $install_profile = isset($current_core_extension['profile']) ? $current_core_extension['profile'] : NULL;
92 // Ensure the profile is not changing.
93 if ($install_profile !== $core_extension['profile']) {
94 if (drupal_installation_attempted()) {
95 $config_importer->logError($this->t('The selected installation profile %install_profile does not match the profile stored in configuration %config_profile.', [
96 '%install_profile' => $install_profile,
97 '%config_profile' => $core_extension['profile'],
99 // If this error has occurred the other checks are irrelevant.
103 $config_importer->logError($this->t('Cannot change the install profile from %profile to %new_profile once Drupal is installed.', [
104 '%profile' => $install_profile,
105 '%new_profile' => $core_extension['profile'],
110 // Get a list of modules with dependency weights as values.
111 $module_data = $this->getModuleData();
112 $nonexistent_modules = array_keys(array_diff_key($core_extension['module'], $module_data));
113 foreach ($nonexistent_modules as $module) {
114 $config_importer->logError($this->t('Unable to install the %module module since it does not exist.', ['%module' => $module]));
117 // Ensure that all modules being installed have their dependencies met.
118 $installs = $config_importer->getExtensionChangelist('module', 'install');
119 foreach ($installs as $module) {
120 $missing_dependencies = [];
121 foreach (array_keys($module_data[$module]->requires) as $required_module) {
122 if (!isset($core_extension['module'][$required_module])) {
123 $missing_dependencies[] = $module_data[$required_module]->info['name'];
126 if (!empty($missing_dependencies)) {
127 $module_name = $module_data[$module]->info['name'];
128 $message = $this->formatPlural(count($missing_dependencies),
129 'Unable to install the %module module since it requires the %required_module module.',
130 'Unable to install the %module module since it requires the %required_module modules.',
131 ['%module' => $module_name, '%required_module' => implode(', ', $missing_dependencies)]
133 $config_importer->logError($message);
137 // Ensure that all modules being uninstalled are not required by modules
138 // that will be installed after the import.
139 $uninstalls = $config_importer->getExtensionChangelist('module', 'uninstall');
140 foreach ($uninstalls as $module) {
141 foreach (array_keys($module_data[$module]->required_by) as $dependent_module) {
142 if ($module_data[$dependent_module]->status && !in_array($dependent_module, $uninstalls, TRUE) && $dependent_module !== $install_profile) {
143 $module_name = $module_data[$module]->info['name'];
144 $dependent_module_name = $module_data[$dependent_module]->info['name'];
145 $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]));
150 // Ensure that the install profile is not being uninstalled.
151 if (in_array($install_profile, $uninstalls, TRUE)) {
152 $profile_name = $module_data[$install_profile]->info['name'];
153 $config_importer->logError($this->t('Unable to uninstall the %profile profile since it is the install profile.', ['%profile' => $profile_name]));
158 * Validates theme installations and uninstallations.
160 * @param \Drupal\Core\Config\ConfigImporter $config_importer
161 * The configuration importer.
163 protected function validateThemes(ConfigImporter $config_importer) {
164 $core_extension = $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension');
165 // Get all themes including those that are not installed.
166 $theme_data = $this->getThemeData();
167 $installs = $config_importer->getExtensionChangelist('theme', 'install');
168 foreach ($installs as $key => $theme) {
169 if (!isset($theme_data[$theme])) {
170 $config_importer->logError($this->t('Unable to install the %theme theme since it does not exist.', ['%theme' => $theme]));
171 // Remove non-existing installs from the list so we can validate theme
172 // dependencies later.
173 unset($installs[$key]);
177 // Ensure that all themes being installed have their dependencies met.
178 foreach ($installs as $theme) {
179 foreach (array_keys($theme_data[$theme]->requires) as $required_theme) {
180 if (!isset($core_extension['theme'][$required_theme])) {
181 $theme_name = $theme_data[$theme]->info['name'];
182 $required_theme_name = $theme_data[$required_theme]->info['name'];
183 $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]));
188 // Ensure that all themes being uninstalled are not required by themes that
189 // will be installed after the import.
190 $uninstalls = $config_importer->getExtensionChangelist('theme', 'uninstall');
191 foreach ($uninstalls as $theme) {
192 foreach (array_keys($theme_data[$theme]->required_by) as $dependent_theme) {
193 if ($theme_data[$dependent_theme]->status && !in_array($dependent_theme, $uninstalls, TRUE)) {
194 $theme_name = $theme_data[$theme]->info['name'];
195 $dependent_theme_name = $theme_data[$dependent_theme]->info['name'];
196 $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]));
203 * Validates configuration being imported does not have unmet dependencies.
205 * @param \Drupal\Core\Config\ConfigImporter $config_importer
206 * The configuration importer.
208 protected function validateDependencies(ConfigImporter $config_importer) {
209 $core_extension = $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension');
210 $existing_dependencies = [
211 'config' => $config_importer->getStorageComparer()->getSourceStorage()->listAll(),
212 'module' => array_keys($core_extension['module']),
213 'theme' => array_keys($core_extension['theme']),
216 $theme_data = $this->getThemeData();
217 $module_data = $this->getModuleData();
219 // Validate the dependencies of all the configuration. We have to validate
220 // the entire tree because existing configuration might depend on
221 // configuration that is being deleted.
222 foreach ($config_importer->getStorageComparer()->getSourceStorage()->listAll() as $name) {
223 // Ensure that the config owner is installed. This checks all
224 // configuration including configuration entities.
225 list($owner,) = explode('.', $name, 2);
226 if ($owner !== 'core') {
228 if (!isset($core_extension['module'][$owner]) && isset($module_data[$owner])) {
229 $message = $this->t('Configuration %name depends on the %owner module that will not be installed after import.', [
231 '%owner' => $module_data[$owner]->info['name'],
234 elseif (!isset($core_extension['theme'][$owner]) && isset($theme_data[$owner])) {
235 $message = $this->t('Configuration %name depends on the %owner theme that will not be installed after import.', [
237 '%owner' => $theme_data[$owner]->info['name'],
240 elseif (!isset($core_extension['module'][$owner]) && !isset($core_extension['theme'][$owner])) {
241 $message = $this->t('Configuration %name depends on the %owner extension that will not be installed after import.', [
248 $config_importer->logError($message);
253 $data = $config_importer->getStorageComparer()->getSourceStorage()->read($name);
254 // Configuration entities have dependencies on modules, themes, and other
255 // configuration entities that we can validate. Their content dependencies
256 // are not validated since we assume that they are soft dependencies.
257 // Configuration entities can be identified by having 'dependencies' and
259 if (isset($data['dependencies']) && isset($data['uuid'])) {
260 $dependencies_to_check = array_intersect_key($data['dependencies'], array_flip(['module', 'theme', 'config']));
261 foreach ($dependencies_to_check as $type => $dependencies) {
262 $diffs = array_diff($dependencies, $existing_dependencies[$type]);
263 if (!empty($diffs)) {
267 $message = $this->formatPlural(
269 'Configuration %name depends on the %module module that will not be installed after import.',
270 'Configuration %name depends on modules (%module) that will not be installed after import.',
271 ['%name' => $name, '%module' => implode(', ', $this->getNames($diffs, $module_data))]
275 $message = $this->formatPlural(
277 'Configuration %name depends on the %theme theme that will not be installed after import.',
278 'Configuration %name depends on themes (%theme) that will not be installed after import.',
279 ['%name' => $name, '%theme' => implode(', ', $this->getNames($diffs, $theme_data))]
283 $message = $this->formatPlural(
285 'Configuration %name depends on the %config configuration that will not exist after import.',
286 'Configuration %name depends on configuration (%config) that will not exist after import.',
287 ['%name' => $name, '%config' => implode(', ', $diffs)]
293 $config_importer->logError($message);
304 * @return \Drupal\Core\Extension\Extension[]
306 protected function getThemeData() {
307 if (!isset($this->themeData)) {
308 $this->themeData = $this->themeHandler->rebuildThemeData();
310 return $this->themeData;
316 * @return \Drupal\Core\Extension\Extension[]
318 protected function getModuleData() {
319 if (!isset($this->moduleData)) {
320 $this->moduleData = system_rebuild_module_data();
322 return $this->moduleData;
326 * Gets human readable extension names.
328 * @param array $names
329 * A list of extension machine names.
330 * @param \Drupal\Core\Extension\Extension[] $extension_data
334 * A list of human-readable extension names, or machine names if
335 * human-readable names are not available.
337 protected function getNames(array $names, array $extension_data) {
338 return array_map(function ($name) use ($extension_data) {
339 if (isset($extension_data[$name])) {
340 $name = $extension_data[$name]->info['name'];