3 namespace Drupal\Core\Config;
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Core\Config\Entity\ConfigDependencyManager;
7 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
9 class ConfigInstaller implements ConfigInstallerInterface {
12 * The configuration factory.
14 * @var \Drupal\Core\Config\ConfigFactoryInterface
16 protected $configFactory;
19 * The active configuration storages, keyed by collection.
21 * @var \Drupal\Core\Config\StorageInterface[]
23 protected $activeStorages;
26 * The typed configuration manager.
28 * @var \Drupal\Core\Config\TypedConfigManagerInterface
30 protected $typedConfig;
33 * The configuration manager.
35 * @var \Drupal\Core\Config\ConfigManagerInterface
37 protected $configManager;
40 * The event dispatcher.
42 * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
44 protected $eventDispatcher;
47 * The configuration storage that provides the default configuration.
49 * @var \Drupal\Core\Config\StorageInterface
51 protected $sourceStorage;
54 * Is configuration being created as part of a configuration sync.
58 protected $isSyncing = FALSE;
61 * The name of the currently active installation profile.
65 protected $installProfile;
68 * Constructs the configuration installer.
70 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
71 * The configuration factory.
72 * @param \Drupal\Core\Config\StorageInterface $active_storage
73 * The active configuration storage.
74 * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
75 * The typed configuration manager.
76 * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
77 * The configuration manager.
78 * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
79 * The event dispatcher.
80 * @param string $install_profile
81 * The name of the currently active installation profile.
83 public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher, $install_profile) {
84 $this->configFactory = $config_factory;
85 $this->activeStorages[$active_storage->getCollectionName()] = $active_storage;
86 $this->typedConfig = $typed_config;
87 $this->configManager = $config_manager;
88 $this->eventDispatcher = $event_dispatcher;
89 $this->installProfile = $install_profile;
95 public function installDefaultConfig($type, $name) {
96 $extension_path = $this->drupalGetPath($type, $name);
97 // Refresh the schema cache if the extension provides configuration schema
99 if (is_dir($extension_path . '/' . InstallStorage::CONFIG_SCHEMA_DIRECTORY) || $type == 'theme') {
100 $this->typedConfig->clearCachedDefinitions();
103 $default_install_path = $this->getDefaultConfigDirectory($type, $name);
104 if (is_dir($default_install_path)) {
105 if (!$this->isSyncing()) {
106 $storage = new FileStorage($default_install_path, StorageInterface::DEFAULT_COLLECTION);
110 // The configuration importer sets the source storage on the config
111 // installer. The configuration importer handles all of the
112 // configuration entity imports. We only need to ensure that simple
113 // configuration is created when the extension is installed.
114 $storage = $this->getSourceStorage();
115 $prefix = $name . '.';
118 // Gets profile storages to search for overrides if necessary.
119 $profile_storages = $this->getProfileStorages($name);
121 // Gather information about all the supported collections.
122 $collection_info = $this->configManager->getConfigCollectionInfo();
123 foreach ($collection_info->getCollectionNames() as $collection) {
124 $config_to_create = $this->getConfigToCreate($storage, $collection, $prefix, $profile_storages);
125 // If we're installing a profile ensure configuration that is overriding
127 if ($name == $this->drupalGetProfile()) {
128 $existing_configuration = $this->getActiveStorages($collection)->listAll();
129 $config_to_create = array_diff_key($config_to_create, array_flip($existing_configuration));
131 if (!empty($config_to_create)) {
132 $this->createConfiguration($collection, $config_to_create);
137 // During a drupal installation optional configuration is installed at the
138 // end of the installation process.
139 // @see install_install_profile()
140 if (!$this->isSyncing() && !$this->drupalInstallationAttempted()) {
141 $optional_install_path = $extension_path . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
142 if (is_dir($optional_install_path)) {
143 // Install any optional config the module provides.
144 $storage = new FileStorage($optional_install_path, StorageInterface::DEFAULT_COLLECTION);
145 $this->installOptionalConfig($storage, '');
147 // Install any optional configuration entities whose dependencies can now
148 // be met. This searches all the installed modules config/optional
150 $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, FALSE, $this->installProfile);
151 $this->installOptionalConfig($storage, [$type => $name]);
154 // Reset all the static caches and list caches.
155 $this->configFactory->reset();
161 public function installOptionalConfig(StorageInterface $storage = NULL, $dependency = []) {
162 $profile = $this->drupalGetProfile();
163 $enabled_extensions = $this->getEnabledExtensions();
164 $existing_config = $this->getActiveStorages()->listAll();
166 // Create the storages to read configuration from.
168 // Search the install profile's optional configuration too.
169 $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE, $this->installProfile);
170 // The extension install storage ensures that overrides are used.
171 $profile_storage = NULL;
173 elseif (!empty($profile)) {
174 // Creates a profile storage to search for overrides.
175 $profile_install_path = $this->drupalGetPath('module', $profile) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
176 $profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION);
179 // Profile has not been set yet. For example during the first steps of the
180 // installer or during unit tests.
181 $profile_storage = NULL;
184 // Build the list of possible configuration to create.
185 $list = $storage->listAll();
186 if ($profile_storage && !empty($dependency)) {
187 // Only add the optional profile configuration into the list if we are
188 // have a dependency to check. This ensures that optional profile
189 // configuration is not unexpectedly re-created after being deleted.
190 $list = array_unique(array_merge($list, $profile_storage->listAll()));
193 // Filter the list of configuration to only include configuration that
194 // should be created.
195 $list = array_filter($list, function ($config_name) use ($existing_config) {
196 // Only list configuration that:
197 // - does not already exist
198 // - is a configuration entity (this also excludes config that has an
199 // implicit dependency on modules that are not yet installed)
200 return !in_array($config_name, $existing_config) && $this->configManager->getEntityTypeIdByName($config_name);
203 $all_config = array_merge($existing_config, $list);
204 $all_config = array_combine($all_config, $all_config);
205 $config_to_create = $storage->readMultiple($list);
206 // Check to see if the corresponding override storage has any overrides or
207 // new configuration that can be installed.
208 if ($profile_storage) {
209 $config_to_create = $profile_storage->readMultiple($list) + $config_to_create;
211 // Sort $config_to_create in the order of the least dependent first.
212 $dependency_manager = new ConfigDependencyManager();
213 $dependency_manager->setData($config_to_create);
214 $config_to_create = array_merge(array_flip($dependency_manager->sortAll()), $config_to_create);
215 if (!empty($dependency)) {
216 // In order to work out dependencies we need the full config graph.
217 $dependency_manager->setData($this->getActiveStorages()->readMultiple($existing_config) + $config_to_create);
218 $dependencies = $dependency_manager->getDependentEntities(key($dependency), reset($dependency));
221 foreach ($config_to_create as $config_name => $data) {
222 // Remove configuration where its dependencies cannot be met.
223 $remove = !$this->validateDependencies($config_name, $data, $enabled_extensions, $all_config);
224 // Remove configuration that is not dependent on $dependency, if it is
226 if (!$remove && !empty($dependency)) {
227 $remove = !isset($dependencies[$config_name]);
231 // Remove from the list of configuration to create.
232 unset($config_to_create[$config_name]);
233 // Remove from the list of all configuration. This ensures that any
234 // configuration that depends on this configuration is also removed.
235 unset($all_config[$config_name]);
239 // Create the optional configuration if there is any left after filtering.
240 if (!empty($config_to_create)) {
241 $this->createConfiguration(StorageInterface::DEFAULT_COLLECTION, $config_to_create, TRUE);
246 * Gets configuration data from the provided storage to create.
248 * @param StorageInterface $storage
249 * The configuration storage to read configuration from.
250 * @param string $collection
251 * The configuration collection to use.
252 * @param string $prefix
253 * (optional) Limit to configuration starting with the provided string.
254 * @param \Drupal\Core\Config\StorageInterface[] $profile_storages
255 * An array of storage interfaces containing profile configuration to check
259 * An array of configuration data read from the source storage keyed by the
260 * configuration object name.
262 protected function getConfigToCreate(StorageInterface $storage, $collection, $prefix = '', array $profile_storages = []) {
263 if ($storage->getCollectionName() != $collection) {
264 $storage = $storage->createCollection($collection);
266 $data = $storage->readMultiple($storage->listAll($prefix));
268 // Check to see if the corresponding override storage has any overrides.
269 foreach ($profile_storages as $profile_storage) {
270 if ($profile_storage->getCollectionName() != $collection) {
271 $profile_storage = $profile_storage->createCollection($collection);
273 $data = $profile_storage->readMultiple(array_keys($data)) + $data;
279 * Creates configuration in a collection based on the provided list.
281 * @param string $collection
282 * The configuration collection.
283 * @param array $config_to_create
284 * An array of configuration data to create, keyed by name.
286 protected function createConfiguration($collection, array $config_to_create) {
287 // Order the configuration to install in the order of dependencies.
288 if ($collection == StorageInterface::DEFAULT_COLLECTION) {
289 $dependency_manager = new ConfigDependencyManager();
290 $config_names = $dependency_manager
291 ->setData($config_to_create)
295 $config_names = array_keys($config_to_create);
298 foreach ($config_names as $name) {
299 // Allow config factory overriders to use a custom configuration object if
300 // they are responsible for the collection.
301 $overrider = $this->configManager->getConfigCollectionInfo()->getOverrideService($collection);
303 $new_config = $overrider->createConfigObject($name, $collection);
306 $new_config = new Config($name, $this->getActiveStorages($collection), $this->eventDispatcher, $this->typedConfig);
308 if ($config_to_create[$name] !== FALSE) {
309 $new_config->setData($config_to_create[$name]);
310 // Add a hash to configuration created through the installer so it is
311 // possible to know if the configuration was created by installing an
312 // extension and to track which version of the default config was used.
313 if (!$this->isSyncing() && $collection == StorageInterface::DEFAULT_COLLECTION) {
314 $new_config->set('_core.default_config_hash', Crypt::hashBase64(serialize($config_to_create[$name])));
317 if ($collection == StorageInterface::DEFAULT_COLLECTION && $entity_type = $this->configManager->getEntityTypeIdByName($name)) {
318 // If we are syncing do not create configuration entities. Pluggable
319 // configuration entities can have dependencies on modules that are
320 // not yet enabled. This approach means that any code that expects
321 // default configuration entities to exist will be unstable after the
322 // module has been enabled and before the config entity has been
324 if ($this->isSyncing()) {
327 /** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage */
328 $entity_storage = $this->configManager
330 ->getStorage($entity_type);
332 $id = $entity_storage->getIDFromConfigName($name, $entity_storage->getEntityType()->getConfigPrefix());
333 // It is possible that secondary writes can occur during configuration
334 // creation. Updates of such configuration are allowed.
335 if ($this->getActiveStorages($collection)->exists($name)) {
336 $entity = $entity_storage->load($id);
337 $entity = $entity_storage->updateFromStorageRecord($entity, $new_config->get());
340 $entity = $entity_storage->createFromStorageRecord($new_config->get());
342 if ($entity->isInstallable()) {
343 $entity->trustData()->save();
344 if ($id !== $entity->id()) {
345 trigger_error(sprintf('The configuration name "%s" does not match the ID "%s"', $name, $entity->id()), E_USER_WARNING);
350 $new_config->save(TRUE);
358 public function installCollectionDefaultConfig($collection) {
359 $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection, $this->drupalInstallationAttempted(), $this->installProfile);
360 // Only install configuration for enabled extensions.
361 $enabled_extensions = $this->getEnabledExtensions();
362 $config_to_install = array_filter($storage->listAll(), function ($config_name) use ($enabled_extensions) {
363 $provider = mb_substr($config_name, 0, strpos($config_name, '.'));
364 return in_array($provider, $enabled_extensions);
366 if (!empty($config_to_install)) {
367 $this->createConfiguration($collection, $storage->readMultiple($config_to_install));
368 // Reset all the static caches and list caches.
369 $this->configFactory->reset();
376 public function setSourceStorage(StorageInterface $storage) {
377 $this->sourceStorage = $storage;
382 * Gets the configuration storage that provides the default configuration.
384 * @return \Drupal\Core\Config\StorageInterface|null
385 * The configuration storage that provides the default configuration.
386 * Returns null if the source storage has not been set.
388 public function getSourceStorage() {
389 return $this->sourceStorage;
393 * Gets the configuration storage that provides the active configuration.
395 * @param string $collection
396 * (optional) The configuration collection. Defaults to the default
399 * @return \Drupal\Core\Config\StorageInterface
400 * The configuration storage that provides the default configuration.
402 protected function getActiveStorages($collection = StorageInterface::DEFAULT_COLLECTION) {
403 if (!isset($this->activeStorages[$collection])) {
404 $this->activeStorages[$collection] = reset($this->activeStorages)->createCollection($collection);
406 return $this->activeStorages[$collection];
412 public function setSyncing($status) {
414 $this->sourceStorage = NULL;
416 $this->isSyncing = $status;
423 public function isSyncing() {
424 return $this->isSyncing;
428 * Finds pre-existing configuration objects for the provided extension.
430 * Extensions can not be installed if configuration objects exist in the
431 * active storage with the same names. This can happen in a number of ways,
433 * - if a user has created configuration with the same name as that provided
435 * - if the extension provides default configuration that does not depend on
436 * it and the extension has been uninstalled and is about to the
440 * Array of configuration object names that already exist keyed by
443 protected function findPreExistingConfiguration(StorageInterface $storage) {
444 $existing_configuration = [];
445 // Gather information about all the supported collections.
446 $collection_info = $this->configManager->getConfigCollectionInfo();
448 foreach ($collection_info->getCollectionNames() as $collection) {
449 $config_to_create = array_keys($this->getConfigToCreate($storage, $collection));
450 $active_storage = $this->getActiveStorages($collection);
451 foreach ($config_to_create as $config_name) {
452 if ($active_storage->exists($config_name)) {
453 $existing_configuration[$collection][] = $config_name;
457 return $existing_configuration;
463 public function checkConfigurationToInstall($type, $name) {
464 if ($this->isSyncing()) {
465 // Configuration is assumed to already be checked by the config importer
466 // validation events.
469 $config_install_path = $this->getDefaultConfigDirectory($type, $name);
470 if (!is_dir($config_install_path)) {
474 $storage = new FileStorage($config_install_path, StorageInterface::DEFAULT_COLLECTION);
476 $enabled_extensions = $this->getEnabledExtensions();
477 // Add the extension that will be enabled to the list of enabled extensions.
478 $enabled_extensions[] = $name;
479 // Gets profile storages to search for overrides if necessary.
480 $profile_storages = $this->getProfileStorages($name);
482 // Check the dependencies of configuration provided by the module.
483 list($invalid_default_config, $missing_dependencies) = $this->findDefaultConfigWithUnmetDependencies($storage, $enabled_extensions, $profile_storages);
484 if (!empty($invalid_default_config)) {
485 throw UnmetDependenciesException::create($name, array_unique($missing_dependencies, SORT_REGULAR));
488 // Install profiles can not have config clashes. Configuration that
489 // has the same name as a module's configuration will be used instead.
490 if ($name != $this->drupalGetProfile()) {
491 // Throw an exception if the module being installed contains configuration
492 // that already exists. Additionally, can not continue installing more
493 // modules because those may depend on the current module being installed.
494 $existing_configuration = $this->findPreExistingConfiguration($storage);
495 if (!empty($existing_configuration)) {
496 throw PreExistingConfigException::create($name, $existing_configuration);
502 * Finds default configuration with unmet dependencies.
504 * @param \Drupal\Core\Config\StorageInterface $storage
505 * The storage containing the default configuration.
506 * @param array $enabled_extensions
507 * A list of all the currently enabled modules and themes.
508 * @param \Drupal\Core\Config\StorageInterface[] $profile_storages
509 * An array of storage interfaces containing profile configuration to check
513 * An array containing:
514 * - A list of configuration that has unmet dependencies.
515 * - An array that will be filled with the missing dependency names, keyed
516 * by the dependents' names.
518 protected function findDefaultConfigWithUnmetDependencies(StorageInterface $storage, array $enabled_extensions, array $profile_storages = []) {
519 $missing_dependencies = [];
520 $config_to_create = $this->getConfigToCreate($storage, StorageInterface::DEFAULT_COLLECTION, '', $profile_storages);
521 $all_config = array_merge($this->configFactory->listAll(), array_keys($config_to_create));
522 foreach ($config_to_create as $config_name => $config) {
523 if ($missing = $this->getMissingDependencies($config_name, $config, $enabled_extensions, $all_config)) {
524 $missing_dependencies[$config_name] = $missing;
528 array_intersect_key($config_to_create, $missing_dependencies),
529 $missing_dependencies,
534 * Validates an array of config data that contains dependency information.
536 * @param string $config_name
537 * The name of the configuration object that is being validated.
539 * Configuration data.
540 * @param array $enabled_extensions
541 * A list of all the currently enabled modules and themes.
542 * @param array $all_config
543 * A list of all the active configuration names.
546 * TRUE if all dependencies are present, FALSE otherwise.
548 protected function validateDependencies($config_name, array $data, array $enabled_extensions, array $all_config) {
549 if (!isset($data['dependencies'])) {
550 // Simple config or a config entity without dependencies.
551 list($provider) = explode('.', $config_name, 2);
552 return in_array($provider, $enabled_extensions, TRUE);
555 $missing = $this->getMissingDependencies($config_name, $data, $enabled_extensions, $all_config);
556 return empty($missing);
560 * Returns an array of missing dependencies for a config object.
562 * @param string $config_name
563 * The name of the configuration object that is being validated.
565 * Configuration data.
566 * @param array $enabled_extensions
567 * A list of all the currently enabled modules and themes.
568 * @param array $all_config
569 * A list of all the active configuration names.
572 * A list of missing config dependencies.
574 protected function getMissingDependencies($config_name, array $data, array $enabled_extensions, array $all_config) {
576 if (isset($data['dependencies'])) {
577 list($provider) = explode('.', $config_name, 2);
578 $all_dependencies = $data['dependencies'];
580 // Ensure enforced dependencies are included.
581 if (isset($all_dependencies['enforced'])) {
582 $all_dependencies = array_merge($all_dependencies, $data['dependencies']['enforced']);
583 unset($all_dependencies['enforced']);
585 // Ensure the configuration entity type provider is in the list of
587 if (!isset($all_dependencies['module']) || !in_array($provider, $all_dependencies['module'])) {
588 $all_dependencies['module'][] = $provider;
591 foreach ($all_dependencies as $type => $dependencies) {
596 $list_to_check = $enabled_extensions;
599 $list_to_check = $all_config;
602 if (!empty($list_to_check)) {
603 $missing = array_merge($missing, array_diff($dependencies, $list_to_check));
612 * Gets the list of enabled extensions including both modules and themes.
615 * A list of enabled extensions which includes both modules and themes.
617 protected function getEnabledExtensions() {
618 // Read enabled extensions directly from configuration to avoid circular
619 // dependencies on ModuleHandler and ThemeHandler.
620 $extension_config = $this->configFactory->get('core.extension');
621 $enabled_extensions = (array) $extension_config->get('module');
622 $enabled_extensions += (array) $extension_config->get('theme');
623 // Core can provide configuration.
624 $enabled_extensions['core'] = 'core';
625 return array_keys($enabled_extensions);
629 * Gets the profile storage to use to check for profile overrides.
631 * The install profile can override module configuration during a module
632 * install. Both the install and optional directories are checked for matching
633 * configuration. This allows profiles to override default configuration for
634 * modules they do not depend on.
636 * @param string $installing_name
637 * (optional) The name of the extension currently being installed.
639 * @return \Drupal\Core\Config\StorageInterface[]|null
640 * Storages to access configuration from the installation profile. If we're
641 * installing the profile itself, then it will return an empty array as the
642 * profile storage should not be used.
644 protected function getProfileStorages($installing_name = '') {
645 $profile = $this->drupalGetProfile();
646 $profile_storages = [];
647 if ($profile && $profile != $installing_name) {
648 $profile_path = $this->drupalGetPath('module', $profile);
649 foreach ([InstallStorage::CONFIG_INSTALL_DIRECTORY, InstallStorage::CONFIG_OPTIONAL_DIRECTORY] as $directory) {
650 if (is_dir($profile_path . '/' . $directory)) {
651 $profile_storages[] = new FileStorage($profile_path . '/' . $directory, StorageInterface::DEFAULT_COLLECTION);
655 return $profile_storages;
659 * Gets an extension's default configuration directory.
661 * @param string $type
662 * Type of extension to install.
663 * @param string $name
664 * Name of extension to install.
667 * The extension's default configuration directory.
669 protected function getDefaultConfigDirectory($type, $name) {
670 return $this->drupalGetPath($type, $name) . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY;
674 * Wrapper for drupal_get_path().
677 * The type of the item; one of 'core', 'profile', 'module', 'theme', or
680 * The name of the item for which the path is requested. Ignored for
684 * The path to the requested item or an empty string if the item is not
687 protected function drupalGetPath($type, $name) {
688 return drupal_get_path($type, $name);
692 * Gets the install profile from settings.
694 * @return string|null
695 * The name of the installation profile or NULL if no installation profile
696 * is currently active. This is the case for example during the first steps
697 * of the installer or during unit tests.
699 protected function drupalGetProfile() {
700 return $this->installProfile;
704 * Wrapper for drupal_installation_attempted().
707 * TRUE if a Drupal installation is currently being attempted.
709 protected function drupalInstallationAttempted() {
710 return drupal_installation_attempted();