configFactory = $config_factory; $this->activeStorages[$active_storage->getCollectionName()] = $active_storage; $this->typedConfig = $typed_config; $this->configManager = $config_manager; $this->eventDispatcher = $event_dispatcher; $this->installProfile = $install_profile; $this->extensionPathResolver = $extension_path_resolver; } /** * {@inheritdoc} */ public function installDefaultConfig($type, $name) { $extension_path = $this->extensionPathResolver->getPath($type, $name); // Refresh the schema cache if the extension provides configuration schema // or is a theme. if (is_dir($extension_path . '/' . InstallStorage::CONFIG_SCHEMA_DIRECTORY) || $type == 'theme') { $this->typedConfig->clearCachedDefinitions(); } $default_install_path = $this->getDefaultConfigDirectory($type, $name); if (is_dir($default_install_path)) { if (!$this->isSyncing()) { $storage = new FileStorage($default_install_path, StorageInterface::DEFAULT_COLLECTION); $prefix = ''; } else { // The configuration importer sets the source storage on the config // installer. The configuration importer handles all of the // configuration entity imports. We only need to ensure that simple // configuration is created when the extension is installed. $storage = $this->getSourceStorage(); $prefix = $name . '.'; } // Gets profile storages to search for overrides if necessary. $profile_storages = $this->getProfileStorages($name); // Gather information about all the supported collections. $collection_info = $this->configManager->getConfigCollectionInfo(); foreach ($collection_info->getCollectionNames() as $collection) { $config_to_create = $this->getConfigToCreate($storage, $collection, $prefix, $profile_storages); if ($name == $this->drupalGetProfile()) { // If we're installing a profile ensure simple configuration that // already exists is excluded as it will have already been written. // This means that if the configuration is changed by something else // during the install it will not be overwritten again. $existing_configuration = array_filter($this->getActiveStorages($collection)->listAll(), function ($config_name) { return !$this->configManager->getEntityTypeIdByName($config_name); }); $config_to_create = array_diff_key($config_to_create, array_flip($existing_configuration)); } if (!empty($config_to_create)) { $this->createConfiguration($collection, $config_to_create); } } } // During a drupal installation optional configuration is installed at the // end of the installation process. Once the install profile is installed // optional configuration should be installed as usual. // @see install_install_profile() $profile_installed = in_array($this->drupalGetProfile(), $this->getEnabledExtensions(), TRUE); if (!$this->isSyncing() && (!InstallerKernel::installationAttempted() || $profile_installed)) { $optional_install_path = $extension_path . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY; if (is_dir($optional_install_path)) { // Install any optional config the module provides. $storage = new FileStorage($optional_install_path, StorageInterface::DEFAULT_COLLECTION); $this->installOptionalConfig($storage, ''); } // Install any optional configuration entities whose dependencies can now // be met. This searches all the installed modules config/optional // directories. $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, FALSE, $this->installProfile); $this->installOptionalConfig($storage, [$type => $name]); } // Reset all the static caches and list caches. $this->configFactory->reset(); } /** * {@inheritdoc} */ public function installOptionalConfig(StorageInterface $storage = NULL, $dependency = []) { $profile = $this->drupalGetProfile(); $enabled_extensions = $this->getEnabledExtensions(); $existing_config = $this->getActiveStorages()->listAll(); // Create the storages to read configuration from. if (!$storage) { // Search the install profile's optional configuration too. $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE, $this->installProfile); // The extension install storage ensures that overrides are used. $profile_storage = NULL; } elseif (!empty($profile)) { // Creates a profile storage to search for overrides. $profile_install_path = $this->extensionPathResolver->getPath('module', $profile) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY; $profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION); } else { // Profile has not been set yet. For example during the first steps of the // installer or during unit tests. $profile_storage = NULL; } // Build the list of possible configuration to create. $list = $storage->listAll(); if ($profile_storage && !empty($dependency)) { // Only add the optional profile configuration into the list if we are // have a dependency to check. This ensures that optional profile // configuration is not unexpectedly re-created after being deleted. $list = array_unique(array_merge($list, $profile_storage->listAll())); } // Filter the list of configuration to only include configuration that // should be created. $list = array_filter($list, function ($config_name) use ($existing_config) { // Only list configuration that: // - does not already exist // - is a configuration entity (this also excludes config that has an // implicit dependency on modules that are not yet installed) return !in_array($config_name, $existing_config) && $this->configManager->getEntityTypeIdByName($config_name); }); $all_config = array_merge($existing_config, $list); $all_config = array_combine($all_config, $all_config); $config_to_create = $storage->readMultiple($list); // Check to see if the corresponding override storage has any overrides or // new configuration that can be installed. if ($profile_storage) { $config_to_create = $profile_storage->readMultiple($list) + $config_to_create; } // Sort $config_to_create in the order of the least dependent first. $dependency_manager = new ConfigDependencyManager(); $dependency_manager->setData($config_to_create); $config_to_create = array_merge(array_flip($dependency_manager->sortAll()), $config_to_create); if (!empty($dependency)) { // In order to work out dependencies we need the full config graph. $dependency_manager->setData($this->getActiveStorages()->readMultiple($existing_config) + $config_to_create); $dependencies = $dependency_manager->getDependentEntities(key($dependency), reset($dependency)); } foreach ($config_to_create as $config_name => $data) { // Remove configuration where its dependencies cannot be met. $remove = !$this->validateDependencies($config_name, $data, $enabled_extensions, $all_config); // Remove configuration that is not dependent on $dependency, if it is // defined. if (!$remove && !empty($dependency)) { $remove = !isset($dependencies[$config_name]); } if ($remove) { // Remove from the list of configuration to create. unset($config_to_create[$config_name]); // Remove from the list of all configuration. This ensures that any // configuration that depends on this configuration is also removed. unset($all_config[$config_name]); } } // Create the optional configuration if there is any left after filtering. if (!empty($config_to_create)) { $this->createConfiguration(StorageInterface::DEFAULT_COLLECTION, $config_to_create); } } /** * Gets configuration data from the provided storage to create. * * @param StorageInterface $storage * The configuration storage to read configuration from. * @param string $collection * The configuration collection to use. * @param string $prefix * (optional) Limit to configuration starting with the provided string. * @param \Drupal\Core\Config\StorageInterface[] $profile_storages * An array of storage interfaces containing profile configuration to check * for overrides. * * @return array * An array of configuration data read from the source storage keyed by the * configuration object name. */ protected function getConfigToCreate(StorageInterface $storage, $collection, $prefix = '', array $profile_storages = []) { if ($storage->getCollectionName() != $collection) { $storage = $storage->createCollection($collection); } $data = $storage->readMultiple($storage->listAll($prefix)); // Check to see if configuration provided by the install profile has any // overrides. foreach ($profile_storages as $profile_storage) { if ($profile_storage->getCollectionName() != $collection) { $profile_storage = $profile_storage->createCollection($collection); } $profile_overrides = $profile_storage->readMultiple(array_keys($data)); if (InstallerKernel::installationAttempted()) { // During installation overrides of simple configuration are applied // immediately. Configuration entities that are overridden will be // updated when the profile is installed. This allows install profiles // to provide configuration entity overrides that have dependencies that // cannot be met when the module provided configuration entity is // created. foreach ($profile_overrides as $name => $override_data) { // The only way to determine if they are configuration entities is the // presence of a dependencies key. if (!isset($override_data['dependencies'])) { $data[$name] = $override_data; } } } else { // Allow install profiles to provide overridden configuration for new // extensions that are being enabled after Drupal has already been // installed. This allows profiles to ship new extensions in version // updates without requiring additional code to apply the overrides. $data = $profile_overrides + $data; } } return $data; } /** * Creates configuration in a collection based on the provided list. * * @param string $collection * The configuration collection. * @param array $config_to_create * An array of configuration data to create, keyed by name. */ protected function createConfiguration($collection, array $config_to_create) { // Order the configuration to install in the order of dependencies. if ($collection == StorageInterface::DEFAULT_COLLECTION) { $dependency_manager = new ConfigDependencyManager(); $config_names = $dependency_manager ->setData($config_to_create) ->sortAll(); } else { $config_names = array_keys($config_to_create); } foreach ($config_names as $name) { // Allow config factory overriders to use a custom configuration object if // they are responsible for the collection. $overrider = $this->configManager->getConfigCollectionInfo()->getOverrideService($collection); if ($overrider) { $new_config = $overrider->createConfigObject($name, $collection); } else { $new_config = new Config($name, $this->getActiveStorages($collection), $this->eventDispatcher, $this->typedConfig); } if ($config_to_create[$name] !== FALSE) { // Add a hash to configuration created through the installer so it is // possible to know if the configuration was created by installing an // extension and to track which version of the default config was used. if (!$this->isSyncing() && $collection == StorageInterface::DEFAULT_COLLECTION) { $config_to_create[$name] = [ '_core' => [ 'default_config_hash' => Crypt::hashBase64(serialize($config_to_create[$name])), ], ] + $config_to_create[$name]; } $new_config->setData($config_to_create[$name]); } if ($collection == StorageInterface::DEFAULT_COLLECTION && $entity_type = $this->configManager->getEntityTypeIdByName($name)) { // If we are syncing do not create configuration entities. Pluggable // configuration entities can have dependencies on modules that are // not yet enabled. This approach means that any code that expects // default configuration entities to exist will be unstable after the // module has been enabled and before the config entity has been // imported. if ($this->isSyncing()) { continue; } /** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage */ $entity_storage = $this->configManager ->getEntityTypeManager() ->getStorage($entity_type); $id = $entity_storage->getIDFromConfigName($name, $entity_storage->getEntityType()->getConfigPrefix()); // It is possible that secondary writes can occur during configuration // creation. Updates of such configuration are allowed. if ($this->getActiveStorages($collection)->exists($name)) { $entity = $entity_storage->load($id); $entity = $entity_storage->updateFromStorageRecord($entity, $new_config->get()); } else { $entity = $entity_storage->createFromStorageRecord($new_config->get()); } if ($entity->isInstallable()) { $entity->trustData()->save(); if ($id !== $entity->id()) { throw new \LogicException(sprintf('The configuration name "%s" does not match the ID "%s"', $name, $entity->id())); } } } else { $new_config->save(TRUE); } } } /** * {@inheritdoc} */ public function installCollectionDefaultConfig($collection) { $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection, InstallerKernel::installationAttempted(), $this->installProfile); // Only install configuration for enabled extensions. $enabled_extensions = $this->getEnabledExtensions(); $config_to_install = array_filter($storage->listAll(), function ($config_name) use ($enabled_extensions) { $provider = mb_substr($config_name, 0, strpos($config_name, '.')); return in_array($provider, $enabled_extensions); }); if (!empty($config_to_install)) { $this->createConfiguration($collection, $storage->readMultiple($config_to_install)); // Reset all the static caches and list caches. $this->configFactory->reset(); } } /** * {@inheritdoc} */ public function setSourceStorage(StorageInterface $storage) { $this->sourceStorage = $storage; return $this; } /** * {@inheritdoc} */ public function getSourceStorage() { return $this->sourceStorage; } /** * Gets the configuration storage that provides the active configuration. * * @param string $collection * (optional) The configuration collection. Defaults to the default * collection. * * @return \Drupal\Core\Config\StorageInterface * The configuration storage that provides the default configuration. */ protected function getActiveStorages($collection = StorageInterface::DEFAULT_COLLECTION) { if (!isset($this->activeStorages[$collection])) { $this->activeStorages[$collection] = reset($this->activeStorages)->createCollection($collection); } return $this->activeStorages[$collection]; } /** * {@inheritdoc} */ public function setSyncing($status) { if (!$status) { $this->sourceStorage = NULL; } $this->isSyncing = $status; return $this; } /** * {@inheritdoc} */ public function isSyncing() { return $this->isSyncing; } /** * Finds pre-existing configuration objects for the provided extension. * * Extensions can not be installed if configuration objects exist in the * active storage with the same names. This can happen in a number of ways, * commonly: * - if a user has created configuration with the same name as that provided * by the extension. * - if the extension provides default configuration that does not depend on * it and the extension has been uninstalled and is about to the * reinstalled. * * @return array * Array of configuration object names that already exist keyed by * collection. */ protected function findPreExistingConfiguration(StorageInterface $storage) { $existing_configuration = []; // Gather information about all the supported collections. $collection_info = $this->configManager->getConfigCollectionInfo(); foreach ($collection_info->getCollectionNames() as $collection) { $config_to_create = array_keys($this->getConfigToCreate($storage, $collection)); $active_storage = $this->getActiveStorages($collection); foreach ($config_to_create as $config_name) { if ($active_storage->exists($config_name)) { $existing_configuration[$collection][] = $config_name; } } } return $existing_configuration; } /** * {@inheritdoc} */ public function checkConfigurationToInstall($type, $name) { if ($this->isSyncing()) { // Configuration is assumed to already be checked by the config importer // validation events. return; } $config_install_path = $this->getDefaultConfigDirectory($type, $name); if (!is_dir($config_install_path)) { return; } $storage = new FileStorage($config_install_path, StorageInterface::DEFAULT_COLLECTION); $enabled_extensions = $this->getEnabledExtensions(); // Add the extension that will be enabled to the list of enabled extensions. $enabled_extensions[] = $name; // Gets profile storages to search for overrides if necessary. $profile_storages = $this->getProfileStorages($name); // Check the dependencies of configuration provided by the module. [$invalid_default_config, $missing_dependencies] = $this->findDefaultConfigWithUnmetDependencies($storage, $enabled_extensions, $profile_storages); if (!empty($invalid_default_config)) { throw UnmetDependenciesException::create($name, array_unique($missing_dependencies, SORT_REGULAR)); } // Install profiles can not have config clashes. Configuration that // has the same name as a module's configuration will be used instead. if ($name != $this->drupalGetProfile()) { // Throw an exception if the module being installed contains configuration // that already exists. Additionally, can not continue installing more // modules because those may depend on the current module being installed. $existing_configuration = $this->findPreExistingConfiguration($storage); if (!empty($existing_configuration)) { throw PreExistingConfigException::create($name, $existing_configuration); } } } /** * Finds default configuration with unmet dependencies. * * @param \Drupal\Core\Config\StorageInterface $storage * The storage containing the default configuration. * @param array $enabled_extensions * A list of all the currently enabled modules and themes. * @param \Drupal\Core\Config\StorageInterface[] $profile_storages * An array of storage interfaces containing profile configuration to check * for overrides. * * @return array * An array containing: * - A list of configuration that has unmet dependencies. * - An array that will be filled with the missing dependency names, keyed * by the dependents' names. */ protected function findDefaultConfigWithUnmetDependencies(StorageInterface $storage, array $enabled_extensions, array $profile_storages = []) { $missing_dependencies = []; $config_to_create = $this->getConfigToCreate($storage, StorageInterface::DEFAULT_COLLECTION, '', $profile_storages); $all_config = array_merge($this->configFactory->listAll(), array_keys($config_to_create)); foreach ($config_to_create as $config_name => $config) { if ($missing = $this->getMissingDependencies($config_name, $config, $enabled_extensions, $all_config)) { $missing_dependencies[$config_name] = $missing; } } return [ array_intersect_key($config_to_create, $missing_dependencies), $missing_dependencies, ]; } /** * Validates an array of config data that contains dependency information. * * @param string $config_name * The name of the configuration object that is being validated. * @param array $data * Configuration data. * @param array $enabled_extensions * A list of all the currently enabled modules and themes. * @param array $all_config * A list of all the active configuration names. * * @return bool * TRUE if all dependencies are present, FALSE otherwise. */ protected function validateDependencies($config_name, array $data, array $enabled_extensions, array $all_config) { if (!isset($data['dependencies'])) { // Simple config or a config entity without dependencies. [$provider] = explode('.', $config_name, 2); return in_array($provider, $enabled_extensions, TRUE); } $missing = $this->getMissingDependencies($config_name, $data, $enabled_extensions, $all_config); return empty($missing); } /** * Returns an array of missing dependencies for a config object. * * @param string $config_name * The name of the configuration object that is being validated. * @param array $data * Configuration data. * @param array $enabled_extensions * A list of all the currently enabled modules and themes. * @param array $all_config * A list of all the active configuration names. * * @return array * A list of missing config dependencies. */ protected function getMissingDependencies($config_name, array $data, array $enabled_extensions, array $all_config) { $missing = []; if (isset($data['dependencies'])) { [$provider] = explode('.', $config_name, 2); $all_dependencies = $data['dependencies']; // Ensure enforced dependencies are included. if (isset($all_dependencies['enforced'])) { $all_dependencies = NestedArray::mergeDeep($all_dependencies, $data['dependencies']['enforced']); unset($all_dependencies['enforced']); } // Ensure the configuration entity type provider is in the list of // dependencies. if (!isset($all_dependencies['module']) || !in_array($provider, $all_dependencies['module'])) { $all_dependencies['module'][] = $provider; } foreach ($all_dependencies as $type => $dependencies) { $list_to_check = []; switch ($type) { case 'module': case 'theme': $list_to_check = $enabled_extensions; break; case 'config': $list_to_check = $all_config; break; } if (!empty($list_to_check)) { $missing = array_merge($missing, array_diff($dependencies, $list_to_check)); } } } return $missing; } /** * Gets the list of enabled extensions including both modules and themes. * * @return array * A list of enabled extensions which includes both modules and themes. */ protected function getEnabledExtensions() { // Read enabled extensions directly from configuration to avoid circular // dependencies on ModuleHandler and ThemeHandler. $extension_config = $this->configFactory->get('core.extension'); $enabled_extensions = (array) $extension_config->get('module'); $enabled_extensions += (array) $extension_config->get('theme'); // Core can provide configuration. $enabled_extensions['core'] = 'core'; return array_keys($enabled_extensions); } /** * Gets the profile storage to use to check for profile overrides. * * The install profile can override module configuration during a module * install. Both the install and optional directories are checked for matching * configuration. This allows profiles to override default configuration for * modules they do not depend on. * * @param string $installing_name * (optional) The name of the extension currently being installed. * * @return \Drupal\Core\Config\StorageInterface[]|null * Storages to access configuration from the installation profile. If we're * installing the profile itself, then it will return an empty array as the * profile storage should not be used. */ protected function getProfileStorages($installing_name = '') { $profile = $this->drupalGetProfile(); $profile_storages = []; if ($profile && $profile != $installing_name) { $profile_path = $this->extensionPathResolver->getPath('module', $profile); foreach ([InstallStorage::CONFIG_INSTALL_DIRECTORY, InstallStorage::CONFIG_OPTIONAL_DIRECTORY] as $directory) { if (is_dir($profile_path . '/' . $directory)) { $profile_storages[] = new FileStorage($profile_path . '/' . $directory, StorageInterface::DEFAULT_COLLECTION); } } } return $profile_storages; } /** * Gets an extension's default configuration directory. * * @param string $type * Type of extension to install. * @param string $name * Name of extension to install. * * @return string * The extension's default configuration directory. */ protected function getDefaultConfigDirectory($type, $name) { return $this->extensionPathResolver->getPath($type, $name) . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY; } /** * Gets the install profile from settings. * * @return string|null * The name of the installation profile or NULL if no installation profile * is currently active. This is the case for example during the first steps * of the installer or during unit tests. */ protected function drupalGetProfile() { return $this->installProfile; } }