Issue #2090115 by alexpott, xjm: Don't install a module when its default configuration has unmet dependencies

8.0.x
catch 2015-03-22 18:53:45 +00:00
parent d6ea0767a3
commit 40cc6312d9
85 changed files with 1034 additions and 285 deletions

View File

@ -741,6 +741,8 @@ function install_tasks($install_state) {
),
'install_profile_themes' => array(
),
'install_install_profile' => array(
),
'install_import_translations' => array(
'display_name' => t('Set up translations'),
'display' => $needs_translations,
@ -1022,10 +1024,6 @@ function install_base_system(&$install_state) {
// State can be set to the database now that system.module is installed.
$modules = $install_state['profile_info']['dependencies'];
// The installation profile is also a module, which needs to be installed
// after all the dependencies have been installed.
$modules[] = drupal_get_profile();
\Drupal::state()->set('install_profile_modules', array_diff($modules, array('system')));
$install_state['base_system_verified'] = TRUE;
}
@ -1571,10 +1569,7 @@ function install_profile_modules(&$install_state) {
// the modules.
$required = array();
$non_required = array();
// Although the profile module is marked as required, it needs to go after
// every dependency, including non-required ones. So clear its required
// flag for now to allow it to install late.
$files[$install_state['parameters']['profile']]->info['required'] = FALSE;
// Add modules that other modules depend on.
foreach ($modules as $module) {
if ($files[$module]->requires) {
@ -1633,6 +1628,24 @@ function install_profile_themes(&$install_state) {
}
}
/**
* Installs the install profile.
*
* @param $install_state
* An array of information about the current installation state.
*/
function install_install_profile(&$install_state) {
\Drupal::service('module_installer')->install(array(drupal_get_profile()), FALSE);
// Install all available optional config. During installation the module order
// is determined by dependencies. If there are no dependencies between modules
// then the order in which they are installed is dependent on random factors
// like PHP version. Optional configuration therefore might or might not be
// created depending on this order. Ensuring that we have installed all of the
// optional configuration whose dependencies can be met at this point removes
// any disparities that this creates.
\Drupal::service('config.installer')->installOptionalConfig();
}
/**
* Prepares the system for import and downloads additional translations.
*

View File

@ -786,8 +786,7 @@ class ConfigImporter {
$this->setProcessedExtension($type, $op, $name);
\Drupal::service('config.installer')
->setSyncing(FALSE)
->resetSourceStorage();
->setSyncing(FALSE);
}
/**

View File

@ -9,6 +9,7 @@ namespace Drupal\Core\Config;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\Entity\ConfigDependencyManager;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Site\Settings;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@ -89,36 +90,66 @@ class ConfigInstaller implements ConfigInstallerInterface {
* {@inheritdoc}
*/
public function installDefaultConfig($type, $name) {
$extension_path = drupal_get_path($type, $name);
$extension_path = $this->drupalGetPath($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();
}
// Gather information about all the supported collections.
$collection_info = $this->configManager->getConfigCollectionInfo();
$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 . '.';
}
// Read enabled extensions directly from configuration to avoid circular
// dependencies with ModuleHandler and ThemeHandler.
$extension_config = $this->configFactory->get('core.extension');
$modules = (array) $extension_config->get('module');
// Unless we are installing the profile, remove it from the list.
if ($install_profile = Settings::get('install_profile')) {
if ($name !== $install_profile) {
unset($modules[$install_profile]);
// Gets a profile storage to search for overrides if necessary.
$profile_storage = $this->getProfileStorage($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_storage);
// If we're installing a profile ensure configuration that is overriding
// is excluded.
if ($name == $this->drupalGetProfile()) {
$existing_configuration = $this->getActiveStorages($collection)->listAll();
$config_to_create = array_diff_key($config_to_create, array_flip($existing_configuration));
}
if (!empty($config_to_create)) {
$this->createConfiguration($collection, $config_to_create);
}
}
}
$enabled_extensions = array_keys($modules);
$enabled_extensions += array_keys((array) $extension_config->get('theme'));
// Core can provide configuration.
$enabled_extensions[] = 'core';
foreach ($collection_info->getCollectionNames(TRUE) as $collection) {
$config_to_install = $this->listDefaultConfigToInstall($type, $name, $collection, $enabled_extensions);
if (!empty($config_to_install)) {
$this->createConfiguration($collection, $config_to_install);
// During a drupal installation optional configuration is installed at the
// end of the installation process.
// @see install_install_profile()
if (!$this->isSyncing() && !$this->drupalInstallationAttempted()) {
$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 type this extension
// provides. This searches all the installed modules config/optional
// directories.
$provides_config_entity_type = array_reduce($this->configManager->getEntityManager()->getDefinitions(), function ($return, EntityTypeInterface $entity_type) use ($name) {
return $return ?: $entity_type->getProvider() && $entity_type->getConfigPrefix();
}, FALSE);
if ($provides_config_entity_type) {
$storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, FALSE);
$this->installOptionalConfig($storage, $name . '.');
}
}
@ -127,49 +158,76 @@ class ConfigInstaller implements ConfigInstallerInterface {
}
/**
* Lists default configuration for an extension that is available to install.
*
* This looks in the extension's config/install directory and all of the
* currently enabled extensions config/install directories for configuration
* that begins with the extension's name.
*
* @param string $type
* The extension type; e.g., 'module' or 'theme'.
* @param string $name
* The name of the module or theme to install default configuration for.
* @param string $collection
* The configuration collection to install.
* @param array $enabled_extensions
* A list of all the currently enabled modules and themes.
*
* @return array
* The list of configuration objects to create.
* {@inheritdoc}
*/
protected function listDefaultConfigToInstall($type, $name, $collection, array $enabled_extensions) {
// Get all default configuration owned by this extension.
$source_storage = $this->getSourceStorage($collection);
$config_to_install = $source_storage->listAll($name . '.');
// If not installing the core base system default configuration, work out if
// this extension provides default configuration for any other enabled
// extensions.
$extension_path = drupal_get_path($type, $name);
if ($type !== 'core' && is_dir($extension_path . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY)) {
$default_storage = new FileStorage($extension_path . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection);
$extension_provided_config = array_filter($default_storage->listAll(), function ($config_name) use ($config_to_install, $enabled_extensions) {
// Ensure that we have not already discovered the config to install.
if (in_array($config_name, $config_to_install)) {
return FALSE;
}
// Ensure the configuration is provided by an enabled module.
$provider = Unicode::substr($config_name, 0, strpos($config_name, '.'));
return in_array($provider, $enabled_extensions);
});
$config_to_install = array_merge($config_to_install, $extension_provided_config);
public function installOptionalConfig(StorageInterface $storage = NULL, $prefix = '') {
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);
// The extension install storage ensures that overrides are used.
$profile_storage = NULL;
}
else {
// Creates a profile storage to search for overrides.
$profile_install_path = $this->drupalGetPath('module', $this->drupalGetProfile()) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
$profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION);
}
return $config_to_install;
$collection_info = $this->configManager->getConfigCollectionInfo();
$enabled_extensions = $this->getEnabledExtensions();
foreach ($collection_info->getCollectionNames() as $collection) {
if (!$this->configManager->supportsConfigurationEntities($collection)) {
continue;
}
$existing_config = $this->getActiveStorages($collection)->listAll($prefix);
$config_to_create = $this->getConfigToCreate($storage, $collection, $prefix, $profile_storage);
$all_config = array_merge($existing_config, array_keys($config_to_create));
foreach ($config_to_create as $config_name => $data) {
// Exclude configuration that:
// - already exists
// - is a not configuration entity
// - or its dependencies cannot be met.
if (in_array($config_name, $existing_config) ||
!$this->configManager->getEntityTypeIdByName($config_name) ||
!$this->validateDependencies($config_name, $data, $enabled_extensions, $all_config)) {
unset($config_to_create[$config_name]);
}
}
if (!empty($config_to_create)) {
$this->createConfiguration($collection, $config_to_create, TRUE);
}
}
}
/**
* 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.
*
* @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 = '', StorageInterface $profile_storage = NULL) {
if ($storage->getCollectionName() != $collection) {
$storage = $storage->createCollection($collection);
}
$data = $storage->readMultiple($storage->listAll($prefix));
// Check to see if the corresponding override storage has any overrides.
if ($profile_storage) {
if ($profile_storage->getCollectionName() != $collection) {
$profile_storage = $profile_storage->createCollection($collection);
}
$data = $profile_storage->readMultiple(array_keys($data)) + $data;
}
return $data;
}
/**
@ -177,24 +235,23 @@ class ConfigInstaller implements ConfigInstallerInterface {
*
* @param string $collection
* The configuration collection.
* @param array $config_to_install
* A list of configuration object names to create.
* @param array $config_to_create
* An array of configuration data to create, keyed by name.
*/
protected function createConfiguration($collection, array $config_to_install) {
protected function createConfiguration($collection, array $config_to_create) {
// Order the configuration to install in the order of dependencies.
$data = $this->getSourceStorage($collection)->readMultiple($config_to_install);
$config_entity_support = $this->configManager->supportsConfigurationEntities($collection);
if ($config_entity_support) {
$dependency_manager = new ConfigDependencyManager();
$config_to_install = $dependency_manager
->setData($data)
$config_names = $dependency_manager
->setData($config_to_create)
->sortAll();
}
else {
$config_names = array_keys($config_to_create);
}
// Remove configuration that already exists in the active storage.
$config_to_install = array_diff($config_to_install, $this->getActiveStorages($collection)->listAll());
foreach ($config_to_install as $name) {
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);
@ -204,18 +261,17 @@ class ConfigInstaller implements ConfigInstallerInterface {
else {
$new_config = new Config($name, $this->getActiveStorages($collection), $this->eventDispatcher, $this->typedConfig);
}
if ($data[$name] !== FALSE) {
$new_config->setData($data[$name]);
if ($config_to_create[$name] !== FALSE) {
$new_config->setData($config_to_create[$name]);
}
if ($config_entity_support && $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) {
if ($this->isSyncing()) {
continue;
}
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage */
@ -246,16 +302,15 @@ class ConfigInstaller implements ConfigInstallerInterface {
* {@inheritdoc}
*/
public function installCollectionDefaultConfig($collection) {
$config_to_install = $this->getSourceStorage($collection)->listAll();
$extension_config = $this->configFactory->get('core.extension');
$enabled_extensions = array_keys((array) $extension_config->get('module'));
$enabled_extensions += array_keys((array) $extension_config->get('theme'));
$config_to_install = array_filter($config_to_install, function ($config_name) use ($enabled_extensions) {
$storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection, $this->drupalInstallationAttempted());
// Only install configuration for enabled extensions.
$enabled_extensions = $this->getEnabledExtensions();
$config_to_install = array_filter($storage->listAll(), function ($config_name) use ($enabled_extensions) {
$provider = Unicode::substr($config_name, 0, strpos($config_name, '.'));
return in_array($provider, $enabled_extensions);
});
if (!empty($config_to_install)) {
$this->createConfiguration($collection, $config_to_install);
$this->createConfiguration($collection, $storage->readMultiple($config_to_install));
// Reset all the static caches and list caches.
$this->configFactory->reset();
}
@ -269,34 +324,14 @@ class ConfigInstaller implements ConfigInstallerInterface {
return $this;
}
/**
* {@inheritdoc}
*/
public function resetSourceStorage() {
$this->sourceStorage = null;
return $this;
}
/**
* Gets the configuration storage that provides the default configuration.
*
* @param string $collection
* (optional) The configuration collection. Defaults to the default
* collection.
*
* @return \Drupal\Core\Config\StorageInterface
* @return \Drupal\Core\Config\StorageInterface|null
* The configuration storage that provides the default configuration.
* Returns null if the source storage has not been set.
*/
public function getSourceStorage($collection = StorageInterface::DEFAULT_COLLECTION) {
if (!isset($this->sourceStorage)) {
// Default to using the ExtensionInstallStorage which searches extension's
// config directories for default configuration. Only include the profile
// configuration during Drupal installation.
$this->sourceStorage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection, drupal_installation_attempted());
}
if ($this->sourceStorage->getCollectionName() != $collection) {
$this->sourceStorage = $this->sourceStorage->createCollection($collection);
}
public function getSourceStorage() {
return $this->sourceStorage;
}
@ -321,6 +356,9 @@ class ConfigInstaller implements ConfigInstallerInterface {
* {@inheritdoc}
*/
public function setSyncing($status) {
if (!$status) {
$this->sourceStorage = NULL;
}
$this->isSyncing = $status;
return $this;
}
@ -333,24 +371,30 @@ class ConfigInstaller implements ConfigInstallerInterface {
}
/**
* {@inheritdoc}
* 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.
*/
public function findPreExistingConfiguration($type, $name) {
protected function findPreExistingConfiguration(StorageInterface $storage) {
$existing_configuration = array();
// Gather information about all the supported collections.
$collection_info = $this->configManager->getConfigCollectionInfo();
// Read enabled extensions directly from configuration to avoid circular
// dependencies on ModuleHandler and ThemeHandler.
$extension_config = $this->configFactory->get('core.extension');
$enabled_extensions = array_keys((array) $extension_config->get('module'));
$enabled_extensions += array_keys((array) $extension_config->get('theme'));
// Add the extension that will be enabled to the list of enabled extensions.
$enabled_extensions[] = $name;
foreach ($collection_info->getCollectionNames(TRUE) as $collection) {
$config_to_install = $this->listDefaultConfigToInstall($type, $name, $collection, $enabled_extensions);
foreach ($collection_info->getCollectionNames() as $collection) {
$config_to_create = array_keys($this->getConfigToCreate($storage, $collection));
$active_storage = $this->getActiveStorages($collection);
foreach ($config_to_install as $config_name) {
foreach ($config_to_create as $config_name) {
if ($active_storage->exists($config_name)) {
$existing_configuration[$collection][] = $config_name;
}
@ -358,4 +402,225 @@ class ConfigInstaller implements ConfigInstallerInterface {
}
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 a profile storage to search for overrides if necessary.
$profile_storage = $this->getProfileStorage($name);
// Check the dependencies of configuration provided by the module.
$invalid_default_config = $this->findDefaultConfigWithUnmetDependencies($storage, $enabled_extensions, $profile_storage);
if (!empty($invalid_default_config)) {
throw UnmetDependenciesException::create($name, $invalid_default_config);
}
// 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 array $enabled_extensions
* A list of all the currently enabled modules and themes.
*
* @return array
* List of configuration that has unmet dependencies
*/
protected function findDefaultConfigWithUnmetDependencies(StorageInterface $storage, array $enabled_extensions, StorageInterface $profile_storage = NULL) {
$config_to_create = $this->getConfigToCreate($storage, StorageInterface::DEFAULT_COLLECTION, '', $profile_storage);
$all_config = array_merge($this->configFactory->listAll(), array_keys($config_to_create));
return array_filter(array_keys($config_to_create), function($config_name) use ($enabled_extensions, $all_config, $config_to_create) {
return !$this->validateDependencies($config_name, $config_to_create[$config_name], $enabled_extensions, $all_config);
});
}
/**
* 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 the dependencies are met, FALSE if not.
*/
protected function validateDependencies($config_name, array $data, array $enabled_extensions, array $all_config) {
// All the migrate tests will fail if we check since they install the
// migrate_drupal module but only set up the dependencies for the single
// migration they are testing.
if (strpos($config_name, 'migrate.migration.') === 0) {
return TRUE;
}
if (isset($data['dependencies'])) {
$all_dependencies = $data['dependencies'];
// Ensure enforced dependencies are included.
if (isset($all_dependencies['enforced'])) {
$all_dependencies = array_merge($all_dependencies, $data['dependencies']['enforced']);
unset($all_dependencies['enforced']);
}
// Ensure the configuration entity type provider is in the list of
// dependencies.
list($provider) = explode('.', $config_name, 2);
if (!isset($all_dependencies['module'])) {
$all_dependencies['module'][] = $provider;
}
elseif (!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_diff($dependencies, $list_to_check);
if (!empty($missing)) {
return FALSE;
}
}
}
}
return TRUE;
}
/**
* 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.
*
* @param string $installing_name
* (optional) The name of the extension currently being installed.
*
* @return \Drupal\Core\Config\StorageInterface|null
* A storage to access configuration from the installation profile. If a
* Drupal installation is not in progress or we're installing the profile
* itself, then it will return NULL as the profile storage should not be
* used.
*/
protected function getProfileStorage($installing_name = '') {
$profile = $this->drupalGetProfile();
if ($this->drupalInstallationAttempted() && $profile != $installing_name) {
// Profiles should not contain optional configuration so always use the
// install directory.
$profile_install_path = $this->getDefaultConfigDirectory('module', $profile);
$profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION);
}
else {
$profile_storage = NULL;
}
return $profile_storage;
}
/**
* 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->drupalGetPath($type, $name) . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY;
}
/**
* Wrapper for drupal_get_path().
*
* @param $type
* The type of the item; one of 'core', 'profile', 'module', 'theme', or
* 'theme_engine'.
* @param $name
* The name of the item for which the path is requested. Ignored for
* $type 'core'.
*
* @return string
* The path to the requested item or an empty string if the item is not
* found.
*/
protected function drupalGetPath($type, $name) {
return drupal_get_path($type, $name);
}
/**
* Gets the install profile from settings.
*
* @return string|null $profile
* 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() {
// Settings is safe to use because settings.php is written before any module
// is installed.
return Settings::get('install_profile');
}
/**
* Wrapper for drupal_installation_attempted().
*
* @return bool
* TRUE if a Drupal installation is currently being attempted.
*/
protected function drupalInstallationAttempted() {
return drupal_installation_attempted();
}
}

View File

@ -37,6 +37,24 @@ interface ConfigInstallerInterface {
*/
public function installDefaultConfig($type, $name);
/**
* Installs optional configuration.
*
* Optional configuration is only installed if:
* - the configuration does not exist already.
* - it's a configuration entity.
* - its dependencies can be met.
*
* @param \Drupal\Core\Config\StorageInterface
* (optional) The configuration storage to search for optional
* configuration. If not provided, all enabled extension's optional
* configuration directories will be searched.
* @param string $prefix
* (optional) If set, limits the installed configuration to only
* configuration beginning with the provided value.
*/
public function installOptionalConfig(StorageInterface $storage = NULL, $prefix = '');
/**
* Installs all default configuration in the specified collection.
*
@ -59,13 +77,6 @@ interface ConfigInstallerInterface {
*/
public function setSourceStorage(StorageInterface $storage);
/**
* Resets the configuration storage that provides the default configuration.
*
* @return $this
*/
public function resetSourceStorage();
/**
* Sets the status of the isSyncing flag.
*
@ -85,25 +96,16 @@ interface ConfigInstallerInterface {
public function 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.
* Checks the configuration that will be installed for an extension.
*
* @param string $type
* Type of extension to install.
* @param string $name
* Name of extension to install.
*
* @return array
* Array of configuration objects that already exist keyed by collection.
* @throws \Drupal\Core\Config\UnmetDependenciesException
* @throws \Drupal\Core\Config\PreExistingConfigException
*/
public function findPreExistingConfiguration($type, $name);
public function checkConfigurationToInstall($type, $name);
}

View File

@ -45,7 +45,7 @@ class ExtensionInstallStorage extends InstallStorage {
* default collection.
* @param bool $include_profile
* (optional) Whether to include the install profile in extensions to
* search.
* search and to get overrides from.
*/
public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, $include_profile = TRUE) {
$this->configStorage = $config_storage;
@ -82,27 +82,24 @@ class ExtensionInstallStorage extends InstallStorage {
$this->folders = array();
$this->folders += $this->getComponentNames('core', array('core'));
$install_profile = Settings::get('install_profile');
$extensions = $this->configStorage->read('core.extension');
if (!empty($extensions['module'])) {
$modules = $extensions['module'];
if (!$this->includeProfile) {
if ($install_profile = Settings::get('install_profile')) {
unset($modules[$install_profile]);
}
}
// Remove the install profile as this is handled later.
unset($modules[$install_profile]);
$this->folders += $this->getComponentNames('module', array_keys($modules));
}
if (!empty($extensions['theme'])) {
$this->folders += $this->getComponentNames('theme', array_keys($extensions['theme']));
}
// The install profile can override module default configuration. We do
// this by replacing the config file path from the module/theme with the
// install profile version if there are any duplicates.
$profile_folders = $this->getComponentNames('profile', array(drupal_get_profile()));
$folders_to_replace = array_intersect_key($profile_folders, $this->folders);
if (!empty($folders_to_replace)) {
$this->folders = array_merge($this->folders, $folders_to_replace);
if ($this->includeProfile) {
// The install profile can override module default configuration. We do
// this by replacing the config file path from the module/theme with the
// install profile version if there are any duplicates.
$profile_folders = $this->getComponentNames('profile', array(drupal_get_profile()));
$this->folders = $profile_folders + $this->folders;
}
}
return $this->folders;

View File

@ -27,6 +27,11 @@ class InstallStorage extends FileStorage {
*/
const CONFIG_INSTALL_DIRECTORY = 'config/install';
/**
* Extension sub-directory containing optional configuration for installation.
*/
const CONFIG_OPTIONAL_DIRECTORY = 'config/optional';
/**
* Extension sub-directory containing configuration schema.
*/

View File

@ -0,0 +1,95 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\UnmetDependenciesException.
*/
namespace Drupal\Core\Config;
use Drupal\Component\Utility\String;
use Drupal\Core\StringTranslation\TranslationInterface;
/**
* An exception thrown if configuration has unmet dependencies.
*/
class UnmetDependenciesException extends ConfigException {
/**
* A list of configuration objects that have unmet dependencies.
*
* @var array
*/
protected $configObjects = [];
/**
* The name of the extension that is being installed.
*
* @var string
*/
protected $extension;
/**
* Gets the list of configuration objects that have unmet dependencies.
*
* @return array
* A list of configuration objects that have unmet dependencies.
*/
public function getConfigObjects() {
return $this->configObjects;
}
/**
* Gets the name of the extension that is being installed.
*
* @return string
* The name of the extension that is being installed.
*/
public function getExtension() {
return $this->extension;
}
/**
* Gets a translated message from the exception.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*
* @return string
*/
public function getTranslatedMessage(TranslationInterface $string_translation, $extension) {
return $string_translation->formatPlural(
count($this->getConfigObjects()),
'Unable to install @extension, %config_names has unmet dependencies.',
'Unable to install @extension, %config_names have unmet dependencies.',
[
'%config_names' => implode(', ', $this->getConfigObjects()),
'@extension' => $extension,
]
);
}
/**
* Creates an exception for an extension and a list of configuration objects.
*
* @param $extension
* The name of the extension that is being installed.
* @param array $config_objects
* A list of configuration object names that have unmet dependencies
*
* @return \Drupal\Core\Config\PreExistingConfigException
*/
public static function create($extension, array $config_objects) {
$message = String::format('Configuration objects (@config_names) provided by @extension have unmet dependencies',
array(
'@config_names' => implode(', ', $config_objects),
'@extension' => $extension
)
);
$e = new static($message);
$e->configObjects = $config_objects;
$e->extension = $extension;
return $e;
}
}

View File

@ -151,17 +151,9 @@ class ModuleInstaller implements ModuleInstallerInterface {
)));
}
// Install profiles can not have config clashes. Configuration that
// has the same name as a module's configuration will be used instead.
if ($module != drupal_get_profile()) {
// Validate default configuration of this module. Bail if unable to
// install. Should not continue installing more modules because those
// may depend on this one.
$existing_configuration = $config_installer->findPreExistingConfiguration('module', $module);
if (!empty($existing_configuration)) {
throw PreExistingConfigException::create($module, $existing_configuration);
}
}
// Check the validity of the default configuration. This will throw
// exceptions if the configuration is not valid.
$config_installer->checkConfigurationToInstall('module', $module);
$extension_config
->set("module.$module", 0)
@ -249,12 +241,6 @@ class ModuleInstaller implements ModuleInstallerInterface {
->setSyncing(TRUE)
->setSourceStorage($source_storage);
}
else {
// If we're not in a config synchronization reset the source storage
// so that the extension install storage will pick up the new
// configuration.
$config_installer->resetSourceStorage();
}
\Drupal::service('config.installer')->installDefaultConfig('module', $module);
// If the module has no current updates, but has some that were

View File

@ -258,10 +258,7 @@ class ThemeHandler implements ThemeHandlerInterface {
// Validate default configuration of the theme. If there is existing
// configuration then stop installing.
$existing_configuration = $this->configInstaller->findPreExistingConfiguration('theme', $key);
if (!empty($existing_configuration)) {
throw PreExistingConfigException::create($key, $existing_configuration);
}
$this->configInstaller->checkConfigurationToInstall('theme', $key);
// The value is not used; the weight is ignored for themes currently.
$extension_config
@ -288,16 +285,6 @@ class ThemeHandler implements ThemeHandlerInterface {
// Only install default configuration if this theme has not been installed
// already.
if (!isset($installed_themes[$key])) {
// The default config installation storage only knows about the
// currently installed list of themes, so it has to be reset in order to
// pick up the default config of the newly installed theme. However, do
// not reset the source storage when synchronizing configuration, since
// that would needlessly trigger a reload of the whole configuration to
// be imported.
if (!$this->configInstaller->isSyncing()) {
$this->configInstaller->resetSourceStorage();
}
// Install default configuration of the theme.
$this->configInstaller->installDefaultConfig('theme', $key);
}

View File

@ -149,6 +149,7 @@ class BlockStorageUnitTest extends KernelTestBase {
* Tests the installation of default blocks.
*/
public function testDefaultBlocks() {
\Drupal::service('theme_handler')->install(['classy']);
$entities = $this->controller->loadMultiple();
$this->assertTrue(empty($entities), 'There are no blocks initially.');

View File

@ -4,3 +4,5 @@ description: 'Provides test blocks.'
package: Testing
version: VERSION
core: 8.x
dependencies:
- block

View File

@ -19,7 +19,7 @@ class BlockContentPageViewTest extends BlockContentTestBase {
*
* @var array
*/
public static $modules = array('block', 'block_content', 'block_content_test');
public static $modules = array('block_content_test');
/**
* Checks block edit and fallback functionality.

View File

@ -4,3 +4,5 @@ description: "Support module for custom block related testing."
package: Testing
version: VERSION
core: 8.x
dependencies:
- block_content

View File

@ -4,7 +4,7 @@ dependencies:
module:
- block_content
theme:
- stark
- classy
id: foobargorilla
theme: classy
region: content

View File

@ -5,4 +5,8 @@ cache: true
targetEntityType: node
dependencies:
module:
- book
- node
enforced:
module:
- book

View File

@ -42,7 +42,7 @@ class CommentDefaultFormatterCacheTagsTest extends EntityUnitTestBase {
// Install tables and config needed to render comments.
$this->installSchema('comment', array('comment_entity_statistics'));
$this->installConfig(array('system', 'filter'));
$this->installConfig(array('system', 'filter', 'comment'));
// Comment rendering generates links, so build the router.
$this->installSchema('system', array('router'));

View File

@ -79,7 +79,7 @@ class CommentFieldAccessTest extends EntityUnitTestBase {
*/
protected function setUp() {
parent::setUp();
$this->installConfig(array('user'));
$this->installConfig(array('user', 'comment'));
$this->installSchema('comment', array('comment_entity_statistics'));
}

View File

@ -25,6 +25,16 @@ class ConfigEntityListTest extends WebTestBase {
*/
public static $modules = array('config_test');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Delete the override config_test entity since it is not required by this
// test.
\Drupal::entityManager()->getStorage('config_test')->load('override')->delete();
}
/**
* Tests entity list builder methods.
*/

View File

@ -38,7 +38,7 @@ class ConfigImportRecreateTest extends KernelTestBase {
parent::setUp();
$this->installEntitySchema('node');
$this->installConfig(array('field'));
$this->installConfig(array('field', 'node'));
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
@ -94,8 +94,8 @@ class ConfigImportRecreateTest extends KernelTestBase {
// will be recreated.
$creates = $this->configImporter->getUnprocessedConfiguration('create');
$deletes = $this->configImporter->getUnprocessedConfiguration('delete');
$this->assertEqual(4, count($creates), 'There are 4 configuration items to create.');
$this->assertEqual(4, count($deletes), 'There are 4 configuration items to delete.');
$this->assertEqual(5, count($creates), 'There are 5 configuration items to create.');
$this->assertEqual(5, count($deletes), 'There are 5 configuration items to delete.');
$this->assertEqual(0, count($this->configImporter->getUnprocessedConfiguration('update')), 'There are no configuration items to update.');
$this->assertIdentical($creates, array_reverse($deletes), 'Deletes and creates contain the same configuration names in opposite orders due to dependencies.');

View File

@ -32,7 +32,7 @@ class ConfigImporterTest extends KernelTestBase {
*
* @var array
*/
public static $modules = array('config_test', 'system', 'config_import_test', 'config_test_language');
public static $modules = array('config_test', 'system', 'config_import_test');
protected function setUp() {
parent::setUp();
@ -541,24 +541,4 @@ class ConfigImporterTest extends KernelTestBase {
$this->assertTrue($this->container->get('config.storage')->exists($config_name));
}
/**
* Tests imported configuration entities with and without language information.
*/
function testLanguage() {
// Test imported configuration with implicit language code.
$data = $this->container->get('config.storage.installer')->read('config_test.dynamic.dotted.english');
$this->assertTrue(!isset($data['langcode']));
$this->assertEqual(
$this->config('config_test.dynamic.dotted.english')->get('langcode'),
'en'
);
// Test imported configuration with explicit language code.
$data = $this->container->get('config.storage.installer')->read('config_test.dynamic.dotted.french');
$this->assertEqual($data['langcode'], 'fr');
$this->assertEqual(
$this->config('config_test.dynamic.dotted.french')->get('langcode'),
'fr'
);
}
}

View File

@ -0,0 +1,120 @@
<?php
/**
* @file
* Contains \Drupal\config\Tests\ConfigInstallProfileOverrideTest.
*/
namespace Drupal\config\Tests;
use Drupal\Core\Config\InstallStorage;
use Drupal\simpletest\WebTestBase;
use Drupal\Core\Config\FileStorage;
use Drupal\system\Entity\Action;
use Drupal\tour\Entity\Tour;
/**
* Tests installation and removal of configuration objects in install, disable
* and uninstall functionality.
*
* @group config
*/
class ConfigInstallProfileOverrideTest extends WebTestBase {
/**
* The profile to install as a basis for testing.
*
* @var string
*/
protected $profile = 'testing_config_overrides';
/**
* Tests install profile config changes.
*/
function testInstallProfileConfigOverwrite() {
$config_name = 'system.cron';
// The expected configuration from the system module.
$expected_original_data = array(
'threshold' => array(
'autorun' => 0,
'requirements_warning' => 172800,
'requirements_error' => 1209600,
),
);
// The expected active configuration altered by the install profile.
$expected_profile_data = array(
'threshold' => array(
'autorun' => 0,
'requirements_warning' => 259200,
'requirements_error' => 1209600,
),
);
// Verify that the original data matches. We have to read the module config
// file directly, because the install profile default system.cron.yml
// configuration file was used to create the active configuration.
$config_dir = drupal_get_path('module', 'system') . '/'. InstallStorage::CONFIG_INSTALL_DIRECTORY;
$this->assertTrue(is_dir($config_dir));
$source_storage = new FileStorage($config_dir);
$data = $source_storage->read($config_name);
$this->assertIdentical($data, $expected_original_data);
// Verify that active configuration matches the expected data, which was
// created from the testing install profile's system.cron.yml file.
$config = $this->config($config_name);
$this->assertIdentical($config->get(), $expected_profile_data);
// Ensure that the configuration entity has the expected dependencies and
// overrides.
$action = Action::load('user_block_user_action');
$this->assertEqual($action->label(), 'Overridden block the selected user(s)');
$action = Action::load('user_cancel_user_action');
$this->assertEqual($action->label(), 'Cancel the selected user account(s)', 'Default configuration that is not overridden is not affected.');
// Ensure that optional configuration can be overridden.
$tour = Tour::load('language');
$this->assertEqual(count($tour->getTips()), 1, 'Optional configuration can be overridden. The language tour only has one tip');
$tour = Tour::load('language-add');
$this->assertEqual(count($tour->getTips()), 3, 'Optional configuration that is not overridden is not affected.');
// Ensure that optional configuration from a profile is created if
// dependencies are met.
$this->assertEqual(Tour::load('testing_config_overrides')->label(), 'Config override test');
// Ensure that optional configuration from a profile is not created if
// dependencies are not met. Cannot use the entity system since the entity
// type does not exist.
$optional_dir = drupal_get_path('module', 'testing_config_overrides') . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
$optional_storage = new FileStorage($optional_dir);
foreach (['config_test.dynamic.dotted.default', 'config_test.dynamic.override','config_test.dynamic.override_unmet'] as $id) {
$this->assertTrue(\Drupal::config($id)->isNew(), "The config_test entity $id contained in the profile's optional directory does not exist.");
// Make that we don't get false positives from the assertion above.
$this->assertTrue($optional_storage->exists($id), "The config_test entity $id does exist in the profile's optional directory.");
}
// Install the config_test module and ensure that the override from the
// install profile is not used. Optional configuration can not override
// configuration in a modules config/install directory.
$this->container->get('module_installer')->install(['config_test']);
$this->rebuildContainer();
$config_test_storage = \Drupal::entityManager()->getStorage('config_test');
$this->assertEqual($config_test_storage->load('dotted.default')->label(), 'Default', 'The config_test entity is not overridden by the profile optional configuration.');
// Test that override of optional configuration does work.
$this->assertEqual($config_test_storage->load('override')->label(), 'Override', 'The optional config_test entity is overridden by the profile optional configuration.');
// Test that override of optional configuration which introduces an unmet
// dependency does not get created.
$this->assertNull($config_test_storage->load('override_unmet'), 'The optional config_test entity with unmet dependencies is not created.');
$this->container->get('module_installer')->install(['dblog']);
$this->rebuildContainer();
// Just installing db_log does not create the optional configuration.
$this->assertNull($config_test_storage->load('override_unmet'), 'The optional config_test entity with unmet dependencies is not created.');
// Install all available optional configuration.
$this->container->get('config.installer')->installOptionalConfig();
$this->assertEqual($config_test_storage->load('override_unmet')->label(), 'Override', 'The optional config_test entity is overridden by the profile optional configuration.');
}
}

View File

@ -0,0 +1,100 @@
<?php
/**
* @file
* Contains \Drupal\config\Tests\ConfigInstallProfileUnmetDependenciesTest.
*/
namespace Drupal\config\Tests;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Config\InstallStorage;
use Drupal\simpletest\InstallerTestBase;
/**
* Tests install profile config overrides can not add unmet dependencies.
*
* @group Config
*/
class ConfigInstallProfileUnmetDependenciesTest extends InstallerTestBase {
/**
* The installation profile to install.
*
* @var string
*/
protected $profile = 'testing_config_overrides';
/**
* Set to TRUE if the expected exception is thrown.
*
* @var bool
*/
protected $expectedException = FALSE;
protected function setUp() {
// Copy the testing_config_overrides install profile so we can change the
// configuration to include a dependency that can not be met. File API
// functions are not available yet.
$dest = $this->siteDirectory . '/profiles/testing_config_overrides';
mkdir($dest, 0777, TRUE);
$source = DRUPAL_ROOT . '/core/profiles/testing_config_overrides';
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $item) {
if ($item->isDir()) {
mkdir($dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName());
}
else {
copy($item, $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName());
}
}
// Add a dependency that can not be met because User is installed before
// Action.
$config_file = $dest . DIRECTORY_SEPARATOR . InstallStorage::CONFIG_INSTALL_DIRECTORY . DIRECTORY_SEPARATOR . 'system.action.user_block_user_action.yml';
$action = Yaml::decode(file_get_contents($config_file));
$action['dependencies']['module'][] = 'action';
file_put_contents($config_file, Yaml::encode($action));
parent::setUp();
}
/**
* {@inheritdoc}
*
* Override the error method so we can test for the expected exception.
*/
protected function error($message = '', $group = 'Other', array $caller = NULL) {
if ($group == 'User notice') {
// Since 'User notice' is set by trigger_error() which is used for debug
// set the message to a status of 'debug'.
return $this->assert('debug', $message, 'Debug', $caller);
}
if ($group == 'Drupal\Core\Config\UnmetDependenciesException') {
$this->expectedException = TRUE;
return FALSE;
}
return $this->assert('exception', $message, $group, $caller);
}
/**
* {@inheritdoc}
*/
protected function setUpSite() {
// This step is not reached due to the exception.
}
/**
* Confirms that the installation succeeded.
*/
public function testInstalled() {
if ($this->expectedException) {
$this->pass('Expected Drupal\Core\Config\UnmetDependenciesException exception thrown');
}
else {
$this->fail('Expected Drupal\Core\Config\UnmetDependenciesException exception thrown');
}
}
}

View File

@ -9,6 +9,7 @@ namespace Drupal\config\Tests;
use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\UnmetDependenciesException;
use Drupal\simpletest\KernelTestBase;
/**
@ -19,11 +20,17 @@ use Drupal\simpletest\KernelTestBase;
*/
class ConfigInstallTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['system'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', ['router']);
// Ensure the global variable being asserted by this test does not exist;
// a previous test executed in this request/process might have set it.
@ -48,11 +55,7 @@ class ConfigInstallTest extends KernelTestBase {
$this->assertFalse(\Drupal::service('config.typed')->hasConfigSchema('config_schema_test.schema_in_install'), 'Configuration schema for config_schema_test.schema_in_install does not exist.');
// Install the test module.
$this->enableModules(array('config_test', 'config_schema_test'));
$this->installConfig(array('config_test', 'config_schema_test'));
// After module installation the new schema should exist.
$this->assertTrue(\Drupal::service('config.typed')->hasConfigSchema('config_schema_test.schema_in_install'), 'Configuration schema for config_schema_test.schema_in_install exists.');
$this->installModules(array('config_test'));
// Verify that default module config exists.
\Drupal::configFactory()->reset($default_config);
@ -71,6 +74,13 @@ class ConfigInstallTest extends KernelTestBase {
$this->assertFalse(isset($GLOBALS['hook_config_test']['predelete']));
$this->assertFalse(isset($GLOBALS['hook_config_test']['delete']));
// Install the schema test module.
$this->enableModules(array('config_schema_test'));
$this->installConfig(array('config_schema_test'));
// After module installation the new schema should exist.
$this->assertTrue(\Drupal::service('config.typed')->hasConfigSchema('config_schema_test.schema_in_install'), 'Configuration schema for config_schema_test.schema_in_install exists.');
// Ensure that data type casting is applied during config installation.
$config = $this->config('config_schema_test.schema_in_install');
$this->assertIdentical($config->get('integer'), 1);
@ -170,8 +180,7 @@ class ConfigInstallTest extends KernelTestBase {
);
\Drupal::state()->set('config_collection_install_test.collection_names', $collections);
// Install the test module.
$this->enableModules(array('config_test', 'config_collection_install_test'));
$this->installConfig(array('config_test'));
$this->installModules(array('config_test', 'config_collection_install_test'));
/** @var \Drupal\Core\Config\StorageInterface $active_storage */
$active_storage = \Drupal::service('config.storage');
$this->assertEqual($collections, $active_storage->getAllCollectionNames());
@ -189,4 +198,56 @@ class ConfigInstallTest extends KernelTestBase {
$this->assertIdentical(array('label' => 'entity'), $data);
}
/**
* Tests the configuration with unmet dependencies is not installed.
*/
public function testDependencyChecking() {
$this->installModules(['config_test']);
try {
$this->installModules(['config_install_dependency_test']);
$this->fail('Expected UnmetDependenciesException not thrown.');
}
catch (UnmetDependenciesException $e) {
$this->assertEqual($e->getExtension(), 'config_install_dependency_test');
$this->assertEqual($e->getConfigObjects(), ['config_test.dynamic.other_module_test_with_dependency']);
$this->assertEqual($e->getMessage(), 'Configuration objects (config_test.dynamic.other_module_test_with_dependency) provided by config_install_dependency_test have unmet dependencies');
}
$this->installModules(['config_other_module_config_test']);
$this->installModules(['config_install_dependency_test']);
$this->assertTrue(entity_load('config_test', 'other_module_test_with_dependency'), 'The config_test.dynamic.other_module_test_with_dependency configuration has been created during install.');
}
/**
* Tests imported configuration entities with and without language information.
*/
function testLanguage() {
$this->installModules(['config_test_language']);
// Test imported configuration with implicit language code.
$data = $this->container->get('config.storage.installer')->read('config_test.dynamic.dotted.english');
$this->assertTrue(!isset($data['langcode']));
$this->assertEqual(
$this->config('config_test.dynamic.dotted.english')->get('langcode'),
'en'
);
// Test imported configuration with explicit language code.
$data = $this->container->get('config.storage.installer')->read('config_test.dynamic.dotted.french');
$this->assertEqual($data['langcode'], 'fr');
$this->assertEqual(
$this->config('config_test.dynamic.dotted.french')->get('langcode'),
'fr'
);
}
/**
* Installs a module.
*
* @param array $modules
* The module names.
*/
protected function installModules(array $modules) {
$this->container->get('module_installer')->install($modules);
$this->container = \Drupal::getContainer();
}
}

View File

@ -7,12 +7,10 @@
namespace Drupal\config\Tests;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\Config\StorageInterface;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\simpletest\WebTestBase;
use Drupal\Core\Config\FileStorage;
/**
* Tests installation and removal of configuration objects in install, disable
@ -119,43 +117,6 @@ class ConfigInstallWebTest extends WebTestBase {
$this->assertIdentical($config_entity->get('label'), 'Default integration config label');
}
/**
* Tests install profile config changes.
*/
function testInstallProfileConfigOverwrite() {
$config_name = 'system.cron';
// The expected configuration from the system module.
$expected_original_data = array(
'threshold' => array(
'autorun' => 0,
'requirements_warning' => 172800,
'requirements_error' => 1209600,
),
);
// The expected active configuration altered by the install profile.
$expected_profile_data = array(
'threshold' => array(
'autorun' => 0,
'requirements_warning' => 259200,
'requirements_error' => 1209600,
),
);
// Verify that the original data matches. We have to read the module config
// file directly, because the install profile default system.cron.yml
// configuration file was used to create the active configuration.
$config_dir = drupal_get_path('module', 'system') . '/'. InstallStorage::CONFIG_INSTALL_DIRECTORY;
$this->assertTrue(is_dir($config_dir));
$source_storage = new FileStorage($config_dir);
$data = $source_storage->read($config_name);
$this->assertIdentical($data, $expected_original_data);
// Verify that active configuration matches the expected data, which was
// created from the testing install profile's system.cron.yml file.
$config = $this->config($config_name);
$this->assertIdentical($config->get(), $expected_profile_data);
}
/**
* Tests pre-existing configuration detection.
*/
@ -214,4 +175,21 @@ class ConfigInstallWebTest extends WebTestBase {
$this->assertEqual($e->getMessage(), 'Configuration objects (config_test.dynamic.dotted.default, language/fr/config_test.dynamic.dotted.default) provided by config_clash_test_theme already exist in active configuration');
}
}
/**
* Tests unmet dependencies detection.
*/
public function testUnmetDependenciesInstall() {
$this->drupalLogin($this->adminUser);
// We need to install separately since config_install_dependency_test does
// not depend on config_test and order is important.
$this->drupalPostForm('admin/modules', array('modules[Testing][config_test][enable]' => TRUE), t('Save configuration'));
$this->drupalPostForm('admin/modules', array('modules[Testing][config_install_dependency_test][enable]' => TRUE), t('Save configuration'));
$this->assertRaw('Unable to install Config install dependency test, <em class="placeholder">config_test.dynamic.other_module_test_with_dependency</em> has unmet dependencies.');
$this->drupalPostForm('admin/modules', array('modules[Testing][config_other_module_config_test][enable]' => TRUE), t('Save configuration'));
$this->drupalPostForm('admin/modules', array('modules[Testing][config_install_dependency_test][enable]' => TRUE), t('Save configuration'));
$this->rebuildContainer();
$this->assertTrue(entity_load('config_test', 'other_module_test_with_dependency'), 'The config_test.dynamic.other_module_test_with_dependency configuration has been created during install.');
}
}

View File

@ -7,8 +7,6 @@
namespace Drupal\config\Tests;
use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\Config\StorageInterface;
use Drupal\simpletest\WebTestBase;
/**
@ -59,18 +57,10 @@ class ConfigOtherModuleTest extends WebTestBase {
// Default configuration provided by config_test should still exist.
$this->assertTrue(entity_load('config_test', 'dotted.default', TRUE), 'The configuration is not deleted.');
// Re-enable module to test that pre-existing default configuration throws
// an error.
$msg = "The expected PreExistingConfigException is thrown by reinstalling config_other_module_config_test.";
try {
$this->installModule('config_other_module_config_test');
$this->fail($msg);
}
catch (PreExistingConfigException $e) {
$this->pass($msg);
$this->assertEqual($e->getExtension(), 'config_other_module_config_test');
$this->assertEqual($e->getConfigObjects(), [StorageInterface::DEFAULT_COLLECTION => ['config_test.dynamic.other_module_test']]);
}
// Re-enable module to test that pre-existing optional configuration does
// not throw an error.
$this->installModule('config_other_module_config_test');
$this->assertTrue(\Drupal::moduleHandler()->moduleExists('config_other_module_config_test'), 'The config_other_module_config_test module is installed.');
}
/**

View File

@ -0,0 +1,11 @@
id: other_module_test_with_dependency
label: 'Other module test with dependency'
weight: 0
style: ''
status: true
langcode: en
protected_property: Default
dependencies:
enforced:
module:
- config_other_module_config_test

View File

@ -0,0 +1,5 @@
name: 'Config install dependency test'
type: module
package: Testing
version: VERSION
core: 8.x

View File

@ -0,0 +1,6 @@
# See \Drupal\config\Tests\ConfigInstallProfileOverrideTest
id: override
label: Default
weight: 0
protected_property: Default
status: 1

View File

@ -0,0 +1,10 @@
# See \Drupal\config\Tests\ConfigInstallProfileOverrideTest
# This configuration entity has dependencies which would be met.
id: override_unmet
label: Default
weight: 0
protected_property: Default
status: 1
dependencies:
module:
- tour

View File

@ -3,3 +3,5 @@ type: module
package: Testing
version: VERSION
core: 8.x
dependencies:
- config_test

View File

@ -32,7 +32,7 @@ class MessageEntityTest extends EntityUnitTestBase {
protected function setUp() {
parent::setUp();
$this->installConfig(array('contact'));
$this->installConfig(array('contact', 'contact_test'));
}
/**

View File

@ -28,6 +28,7 @@ class EditorFileUsageTest extends EntityUnitTestBase {
$this->installEntitySchema('file');
$this->installSchema('node', array('node_access'));
$this->installSchema('file', array('file_usage'));
$this->installConfig(['node']);
// Add text formats.
$filtered_html_format = entity_create('filter_format', array(

View File

@ -6,3 +6,6 @@ package: Testing
version: VERSION
dependencies:
- entity_reference
- node
- user
- views

View File

@ -31,6 +31,7 @@ class FieldImportChangeTest extends FieldUnitTestBase {
* Tests importing an updated field.
*/
function testImportChange() {
$this->installConfig(['field_test_config']);
$field_storage_id = 'field_test_import';
$field_id = "entity_test.entity_test.$field_storage_id";
$field_config_name = "field.field.$field_id";

View File

@ -33,6 +33,7 @@ class FieldImportDeleteTest extends FieldUnitTestBase {
* Tests deleting field storages and fields as part of config import.
*/
public function testImportDelete() {
$this->installConfig(['field_test_config']);
// At this point there are 5 field configuration objects in the active
// storage.
// - field.storage.entity_test.field_test_import

View File

@ -28,7 +28,7 @@ class EntityDisplayTest extends KernelTestBase {
parent::setUp();
$this->installSchema('system', array('router'));
$this->installEntitySchema('node');
$this->installConfig(array('field'));
$this->installConfig(array('field', 'node'));
}
/**

View File

@ -29,7 +29,7 @@ class FilterAPITest extends EntityUnitTestBase {
parent::setUp();
$this->installSchema('system', array('router'));
$this->installConfig(array('system', 'filter'));
$this->installConfig(array('system', 'filter', 'filter_test'));
}
/**

View File

@ -4,3 +4,5 @@ description: 'Tests filter hooks and functions.'
package: Testing
version: VERSION
core: 8.x
dependencies:
- filter

View File

@ -35,6 +35,7 @@ class EntityTest extends NormalizerTestBase {
$this->installSchema('system', array('sequences'));
$this->installSchema('comment', array('comment_entity_statistics'));
$this->installEntitySchema('taxonomy_term');
$this->installConfig(['node', 'comment']);
}
/**

View File

@ -427,3 +427,5 @@ display:
id: block_1
display_title: Block
position: 1
display_options:
display_extenders: { }

View File

@ -37,7 +37,7 @@ class NodeBodyFieldStorageTest extends KernelTestBase {
$this->installSchema('user', 'users_data');
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->installConfig(array('field'));
$this->installConfig(array('field', 'node'));
}
/**

View File

@ -30,7 +30,7 @@ class NodeTokenReplaceTest extends TokenReplaceUnitTestBase {
*/
protected function setUp() {
parent::setUp();
$this->installConfig(array('filter'));
$this->installConfig(array('filter', 'node'));
$node_type = entity_create('node_type', array('type' => 'article', 'name' => 'Article'));
$node_type->save();

View File

@ -211,6 +211,8 @@ EOS;
* Tests expected behavior of installConfig().
*/
function testInstallConfig() {
// The user module has configuration that depends on system.
$this->enableModules(array('system'));
$module = 'user';
// Verify that default config can only be installed for enabled modules.

View File

@ -9,6 +9,7 @@ namespace Drupal\system\Controller;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\Config\UnmetDependenciesException;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Routing\RouteBuilderInterface;
@ -144,6 +145,9 @@ class ThemeController extends ControllerBase {
'error'
);
}
catch (UnmetDependenciesException $e) {
drupal_set_message($e->getTranslatedMessage($this->getStringTranslation(), $theme), 'error');
}
return $this->redirect('system.themes_page');
}

View File

@ -8,6 +8,7 @@
namespace Drupal\system\Form;
use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\Config\UnmetDependenciesException;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\Form\ConfirmFormBase;
@ -176,6 +177,13 @@ class ModulesListConfirmForm extends ConfirmFormBase {
);
return;
}
catch (UnmetDependenciesException $e) {
drupal_set_message(
$e->getTranslatedMessage($this->getStringTranslation(), $this->modules['install'][$e->getExtension()]),
'error'
);
return;
}
}
// Gets module list after install process, flushes caches and displays a

View File

@ -10,6 +10,7 @@ namespace Drupal\system\Form;
use Drupal\Component\Utility\String;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\Config\UnmetDependenciesException;
use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Entity\EntityManagerInterface;
@ -536,6 +537,13 @@ class ModulesListForm extends FormBase {
);
return;
}
catch (UnmetDependenciesException $e) {
drupal_set_message(
$e->getTranslatedMessage($this->getStringTranslation(), $modules['install'][$e->getExtension()]),
'error'
);
return;
}
}
// Gets module list after install process, flushes caches and displays a

View File

@ -52,6 +52,7 @@ class EntityCrudHookTest extends EntityUnitTestBase {
$this->installSchema('file', array('file_usage'));
$this->installSchema('node', array('node_access'));
$this->installSchema('comment', array('comment_entity_statistics'));
$this->installConfig(['node', 'comment']);
}
/**

View File

@ -31,6 +31,15 @@ class InstallerExistingSettingsTest extends InstallerTestBase {
'required' => TRUE,
);
// Actually the install profile should be skipped to because it is written
// to settings.php.
// @todo https://www.drupal.org/node/2451369 Fix install_profile so that it
// is written to an existing settings.php if possible or if set used.
$this->settings['settings']['install_profile'] = (object) array(
'value' => 'testing',
'required' => TRUE,
);
// Pre-configure database credentials.
$connection_info = Database::getConnectionInfo();
unset($connection_info['default']['pdo']);

View File

@ -7,3 +7,7 @@ status: true
langcode: en
locked: false
pattern: 'U'
dependencies:
enforced:
theme:
- test_basetheme

View File

@ -6,3 +6,5 @@ version: VERSION
core: 8.x
required: true
configure: user.admin_index
dependencies:
- system

View File

@ -142,8 +142,8 @@ class ModuleTest extends ViewUnitTestBase {
* Tests the load wrapper/helper functions.
*/
public function testLoadFunctions() {
$this->enableModules(['text', 'node']);
$this->installConfig(['node']);
$this->enableModules(array('field', 'text', 'node'));
$this->installConfig(array('node'));
$storage = $this->container->get('entity.manager')->getStorage('view');
// Test views_view_is_enabled/disabled.

View File

@ -84,7 +84,7 @@ class ViewExecutableTest extends ViewUnitTestBase {
$this->installEntitySchema('node');
$this->installEntitySchema('comment');
$this->installSchema('comment', array('comment_entity_statistics'));
$this->installConfig(array('field'));
$this->installConfig(array('system', 'field', 'node', 'comment'));
entity_create('node_type', array(
'type' => 'page',

View File

@ -7,3 +7,5 @@ dependencies:
- node
- block
- dblog
themes:
- stark

View File

@ -0,0 +1,9 @@
id: user_block_user_action
label: 'Overridden block the selected user(s)'
status: true
langcode: en
type: user
plugin: user_block_user_action
dependencies:
module:
- user

View File

@ -0,0 +1,13 @@
id: language
module: language
label: Language
langcode: en
routes:
- route_name: entity.configurable_language.collection
tips:
language-overview:
id: language-overview
plugin: text
label: Languages
body: '<p>The "Languages" page allows you to add, edit, delete, and reorder languages for the site.</p>'
weight: 1

View File

@ -0,0 +1,6 @@
id: dotted.default
label: 'Default install profile override'
weight: 0
protected_property: Default
# Intentionally commented out to verify default status behavior.
# status: 1

View File

@ -0,0 +1,8 @@
id: override
label: Override
weight: 0
protected_property: Default
status: 1
dependencies:
module:
- tour

View File

@ -0,0 +1,8 @@
id: override_unmet
label: Override
weight: 0
protected_property: Default
status: 1
dependencies:
module:
- dblog

View File

@ -0,0 +1,13 @@
id: testing_config_overrides
module: testing_config_overrides
label: Config override test
langcode: en
routes:
- route_name: entity.configurable_language.collection
tips:
language-overview:
id: language-overview
plugin: text
label: Languages
body: '<p>The "Languages" page allows you to add, edit, delete, and reorder languages for the site.</p>'
weight: 1

View File

@ -0,0 +1,10 @@
name: Testing config overrides
type: profile
description: 'Minimal profile for running tests with config overrides in a profile.'
version: VERSION
core: 8.x
hidden: true
dependencies:
- action
- language
- tour