Commit de724027 authored by catch's avatar catch

Issue #2788777 by alexpott, bircher, jribeiro, Eli-T, mpotter, douggreen, GoZ,...

Issue #2788777 by alexpott, bircher, jribeiro, Eli-T, mpotter, douggreen, GoZ, DamienMcKenna, Dane Powell, jibran, szeidler, Alumei, andypost, dawehner, johndevman: Allow a site-specific profile to be installed from existing config
parent 34637de7
......@@ -6,6 +6,11 @@
*/
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\ConfigImporterException;
use Drupal\Core\Config\Importer\ConfigImporterBatch;
use Drupal\Core\Config\StorageComparer;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseExceptionWrapper;
......@@ -198,6 +203,10 @@ function install_state_defaults() {
// The last task that was completed during the previous installation
// request.
'completed_task' => NULL,
// Partial configuration cached during an installation from existing config.
'config' => NULL,
// The path to the configuration to install when installing from config.
'config_install_path' => NULL,
// TRUE when there are valid config directories.
'config_verified' => FALSE,
// TRUE when there is a valid database connection.
......@@ -473,9 +482,13 @@ function install_begin_request($class_loader, &$install_state) {
// @todo Remove as part of https://www.drupal.org/node/2186491
drupal_get_filename('module', 'system', 'core/modules/system/system.info.yml');
// Use the language from the profile configuration, if available, to override
// the language previously set in the parameters.
if (isset($install_state['profile_info']['distribution']['langcode'])) {
// Use the language from profile configuration if available.
if (!empty($install_state['config_install_path']) && $install_state['config']['system.site']) {
$install_state['parameters']['langcode'] = $install_state['config']['system.site']['default_langcode'];
}
elseif (isset($install_state['profile_info']['distribution']['langcode'])) {
// Otherwise, Use the language from the profile configuration, if available,
// to override the language previously set in the parameters.
$install_state['parameters']['langcode'] = $install_state['profile_info']['distribution']['langcode'];
}
......@@ -818,6 +831,30 @@ function install_tasks($install_state) {
],
];
if (!empty($install_state['config_install_path'])) {
// The chosen profile indicates that rather than installing a new site, an
// instance of the same site should be installed from the given
// configuration.
// That means we need to remove the steps installing the extensions and
// replace them with a configuration synchronization step.
unset($tasks['install_download_translation']);
$key = array_search('install_profile_modules', array_keys($tasks), TRUE);
unset($tasks['install_profile_modules']);
unset($tasks['install_profile_themes']);
unset($tasks['install_install_profile']);
$config_tasks = [
'install_config_import_batch' => [
'display_name' => t('Install configuration'),
'type' => 'batch',
],
'install_config_download_translations' => [],
'install_config_revert_install_changes' => [],
];
$tasks = array_slice($tasks, 0, $key, TRUE) +
$config_tasks +
array_slice($tasks, $key, NULL, TRUE);
}
// Now add any tasks defined by the installation profile.
if (!empty($install_state['parameters']['profile'])) {
// Load the profile install file, because it is not always loaded when
......@@ -1494,6 +1531,14 @@ function install_load_profile(&$install_state) {
$profile = $install_state['parameters']['profile'];
$install_state['profiles'][$profile]->load();
$install_state['profile_info'] = install_profile_info($profile, isset($install_state['parameters']['langcode']) ? $install_state['parameters']['langcode'] : 'en');
// If the profile has a config/sync directory copy the information to the
// install_state global.
if (!empty($install_state['profile_info']['config_install_path'])) {
$install_state['config_install_path'] = $install_state['profile_info']['config_install_path'];
if (!empty($install_state['profile_info']['config'])) {
$install_state['config'] = $install_state['profile_info']['config'];
}
}
}
/**
......@@ -2260,3 +2305,134 @@ function install_write_profile($install_state) {
throw new InstallProfileMismatchException($install_state['parameters']['profile'], $settings_profile, $settings_path, \Drupal::translation());
}
}
/**
* Creates a batch for the config importer to process.
*
* @see install_tasks()
*/
function install_config_import_batch() {
// We need to manually trigger the installation of core-provided entity types,
// as those will not be handled by the module installer.
// @see install_profile_modules()
install_core_entity_type_definitions();
// Get the sync storage.
$sync = \Drupal::service('config.storage.sync');
// Match up the site UUIDs, the install_base_system install task will have
// installed the system module and created a new UUID.
$system_site = $sync->read('system.site');
\Drupal::configFactory()->getEditable('system.site')->set('uuid', $system_site['uuid'])->save();
// Create the storage comparer and the config importer.
$config_manager = \Drupal::service('config.manager');
$storage_comparer = new StorageComparer($sync, \Drupal::service('config.storage'), $config_manager);
$storage_comparer->createChangelist();
$config_importer = new ConfigImporter(
$storage_comparer,
\Drupal::service('event_dispatcher'),
$config_manager,
\Drupal::service('lock.persistent'),
\Drupal::service('config.typed'),
\Drupal::service('module_handler'),
\Drupal::service('module_installer'),
\Drupal::service('theme_handler'),
\Drupal::service('string_translation')
);
try {
$sync_steps = $config_importer->initialize();
$batch_builder = new BatchBuilder();
$batch_builder
->setFinishCallback([ConfigImporterBatch::class, 'finish'])
->setTitle(t('Importing configuration'))
->setInitMessage(t('Starting configuration import.'))
->setErrorMessage(t('Configuration import has encountered an error.'));
foreach ($sync_steps as $sync_step) {
$batch_builder->addOperation([ConfigImporterBatch::class, 'process'], [$config_importer, $sync_step]);
}
return $batch_builder->toArray();
}
catch (ConfigImporterException $e) {
global $install_state;
// There are validation errors.
$messenger = \Drupal::messenger();
$messenger->addError(t('The configuration synchronization failed validation.'));
foreach ($config_importer->getErrors() as $message) {
$messenger->addError($message);
}
install_display_output(['#title' => t('Configuration validation')], $install_state);
}
}
/**
* Replaces install_download_translation() during configuration installs.
*
* @param array $install_state
* An array of information about the current installation state.
*
* @return string
* A themed status report, or an exception if there are requirement errors.
* Upon successful download the page is reloaded and no output is returned.
*
* @see install_download_translation()
*/
function install_config_download_translations(&$install_state) {
$needs_download = isset($install_state['parameters']['langcode']) && !isset($install_state['translations'][$install_state['parameters']['langcode']]) && $install_state['parameters']['langcode'] !== 'en';
if ($needs_download) {
return install_download_translation($install_state);
}
}
/**
* Reverts configuration if hook_install() implementations have made changes.
*
* This step ensures that the final configuration matches the configuration
* provided to the installer.
*/
function install_config_revert_install_changes() {
global $install_state;
$config_manager = \Drupal::service('config.manager');
$storage_comparer = new StorageComparer(\Drupal::service('config.storage.sync'), \Drupal::service('config.storage'), $config_manager);
$storage_comparer->createChangelist();
if ($storage_comparer->hasChanges()) {
$config_importer = new ConfigImporter(
$storage_comparer,
\Drupal::service('event_dispatcher'),
$config_manager,
\Drupal::service('lock.persistent'),
\Drupal::service('config.typed'),
\Drupal::service('module_handler'),
\Drupal::service('module_installer'),
\Drupal::service('theme_handler'),
\Drupal::service('string_translation')
);
try {
$config_importer->import();
}
catch (ConfigImporterException $e) {
global $install_state;
$messenger = \Drupal::messenger();
// There are validation errors.
$messenger->addError(t('The configuration synchronization failed validation.'));
foreach ($config_importer->getErrors() as $message) {
$messenger->addError($message);
}
install_display_output(['#title' => t('Configuration validation')], $install_state);
}
// At this point the configuration should match completely.
if (\Drupal::moduleHandler()->moduleExists('language')) {
// If the English language exists at this point we need to ensure
// install_download_additional_translations_operations() does not delete
// it.
if (ConfigurableLanguage::load('en')) {
$install_state['profile_info']['keep_english'] = TRUE;
}
}
}
}
......@@ -10,6 +10,7 @@
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\OpCodeCache;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\Site\Settings;
......@@ -481,12 +482,20 @@ function _drupal_rewrite_settings_dump_one(\stdClass $variable, $prefix = '', $s
* @see update_prepare_d8_bootstrap()
*/
function drupal_install_config_directories() {
global $config_directories;
global $config_directories, $install_state;
// Add a randomized config directory name to settings.php, unless it was
// manually defined in the existing already.
// If settings.php does not contain a config sync directory name we need to
// configure one.
if (empty($config_directories[CONFIG_SYNC_DIRECTORY])) {
$config_directories[CONFIG_SYNC_DIRECTORY] = \Drupal::service('site.path') . '/files/config_' . Crypt::randomBytesBase64(55) . '/sync';
if (empty($install_state['config_install_path'])) {
// Add a randomized config directory name to settings.php
$config_directories[CONFIG_SYNC_DIRECTORY] = \Drupal::service('site.path') . '/files/config_' . Crypt::randomBytesBase64(55) . '/sync';
}
else {
// Install profiles can contain a config sync directory. If they do,
// 'config_install_path' is a path to the directory.
$config_directories[CONFIG_SYNC_DIRECTORY] = $install_state['config_install_path'];
}
$settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) [
'value' => $config_directories[CONFIG_SYNC_DIRECTORY],
'required' => TRUE,
......@@ -1099,9 +1108,10 @@ function install_profile_info($profile, $langcode = 'en') {
'version' => NULL,
'hidden' => FALSE,
'php' => DRUPAL_MINIMUM_PHP,
'config_install_path' => NULL,
];
$profile_file = drupal_get_path('profile', $profile) . "/$profile.info.yml";
$info = \Drupal::service('info_parser')->parse($profile_file);
$profile_path = drupal_get_path('profile', $profile);
$info = \Drupal::service('info_parser')->parse("$profile_path/$profile.info.yml");
$info += $defaults;
// drupal_required_modules() includes the current profile as a dependency.
......@@ -1114,6 +1124,12 @@ function install_profile_info($profile, $langcode = 'en') {
// remove any duplicates.
$info['install'] = array_unique(array_merge($info['install'], $required, $info['dependencies'], $locale));
// If the profile has a config/sync directory use that to install drupal.
if (is_dir($profile_path . '/config/sync')) {
$info['config_install_path'] = $profile_path . '/config/sync';
$sync = new FileStorage($profile_path . '/config/sync');
$info['config']['system.site'] = $sync->read('system.site');
}
$cache[$profile][$langcode] = $info;
}
return $cache[$profile][$langcode];
......
......@@ -405,6 +405,14 @@ protected function createExtensionChangelist() {
$module_list = array_reverse($module_list);
$this->extensionChangelist['module']['install'] = array_intersect(array_keys($module_list), $install);
// If we're installing the install profile ensure it comes last. This will
// occur when installing a site from configuration.
$install_profile_key = array_search($new_extensions['profile'], $this->extensionChangelist['module']['install'], TRUE);
if ($install_profile_key !== FALSE) {
unset($this->extensionChangelist['module']['install'][$install_profile_key]);
$this->extensionChangelist['module']['install'][] = $new_extensions['profile'];
}
// Work out what themes to install and to uninstall.
$this->extensionChangelist['theme']['install'] = array_keys(array_diff_key($new_extensions['theme'], $current_extensions['theme']));
$this->extensionChangelist['theme']['uninstall'] = array_keys(array_diff_key($current_extensions['theme'], $new_extensions['theme']));
......
<?php
namespace Drupal\Core\Config\Importer;
use Drupal\Core\Config\ConfigImporter;
/**
* Methods for running the ConfigImporter in a batch.
*
* @see \Drupal\Core\Config\ConfigImporter
*/
class ConfigImporterBatch {
/**
* Processes the config import batch and persists the importer.
*
* @param \Drupal\Core\Config\ConfigImporter $config_importer
* The batch config importer object to persist.
* @param string $sync_step
* The synchronization step to do.
* @param array $context
* The batch context.
*/
public static function process(ConfigImporter $config_importer, $sync_step, &$context) {
if (!isset($context['sandbox']['config_importer'])) {
$context['sandbox']['config_importer'] = $config_importer;
}
$config_importer = $context['sandbox']['config_importer'];
$config_importer->doSyncStep($sync_step, $context);
if ($errors = $config_importer->getErrors()) {
if (!isset($context['results']['errors'])) {
$context['results']['errors'] = [];
}
$context['results']['errors'] = array_merge($errors, $context['results']['errors']);
}
}
/**
* Finish batch.
*
* This function is a static function to avoid serializing the ConfigSync
* object unnecessarily.
*
* @param bool $success
* Indicate that the batch API tasks were all completed successfully.
* @param array $results
* An array of all the results that were updated in update_do_one().
* @param array $operations
* A list of the operations that had not been completed by the batch API.
*/
public static function finish($success, $results, $operations) {
$messenger = \Drupal::messenger();
if ($success) {
if (!empty($results['errors'])) {
$logger = \Drupal::logger('config_sync');
foreach ($results['errors'] as $error) {
$messenger->addError($error);
$logger->error($error);
}
$messenger->addWarning(t('The configuration was imported with errors.'));
}
elseif (!drupal_installation_attempted()) {
// Display a success message when not installing Drupal.
$messenger->addStatus(t('The configuration was imported successfully.'));
}
}
else {
// An error occurred.
// $operations contains the operations that remained unprocessed.
$error_operation = reset($operations);
$message = t('An error occurred while processing %error_operation with arguments: @arguments', ['%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE)]);
$messenger->addError($message);
}
}
}
......@@ -121,6 +121,7 @@ protected function getEditableConfigNames() {
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
global $install_state;
$form['#title'] = $this->t('Configure site');
// Warn about settings.php permissions risk
......@@ -148,12 +149,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
$form['site_information'] = [
'#type' => 'fieldgroup',
'#title' => $this->t('Site information'),
'#access' => empty($install_state['config_install_path']),
];
$form['site_information']['site_name'] = [
'#type' => 'textfield',
'#title' => $this->t('Site name'),
'#required' => TRUE,
'#weight' => -20,
'#access' => empty($install_state['config_install_path']),
];
$form['site_information']['site_mail'] = [
'#type' => 'email',
......@@ -162,6 +165,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#description' => $this->t("Automated emails, such as registration information, will be sent from this address. Use an address ending in your site's domain to help prevent these emails from being flagged as spam."),
'#required' => TRUE,
'#weight' => -15,
'#access' => empty($install_state['config_install_path']),
];
$form['admin_account'] = [
......@@ -191,6 +195,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
$form['regional_settings'] = [
'#type' => 'fieldgroup',
'#title' => $this->t('Regional settings'),
'#access' => empty($install_state['config_install_path']),
];
$countries = $this->countryManager->getList();
$form['regional_settings']['site_default_country'] = [
......@@ -201,6 +206,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#options' => $countries,
'#description' => $this->t('Select the default country for the site.'),
'#weight' => 0,
'#access' => empty($install_state['config_install_path']),
];
$form['regional_settings']['date_default_timezone'] = [
'#type' => 'select',
......@@ -211,17 +217,20 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#description' => $this->t('By default, dates in this site will be displayed in the chosen time zone.'),
'#weight' => 5,
'#attributes' => ['class' => ['timezone-detect']],
'#access' => empty($install_state['config_install_path']),
];
$form['update_notifications'] = [
'#type' => 'fieldgroup',
'#title' => $this->t('Update notifications'),
'#description' => $this->t('The system will notify you when updates and important security releases are available for installed components. Anonymous information about your site is sent to <a href=":drupal">Drupal.org</a>.', [':drupal' => 'https://www.drupal.org']),
'#access' => empty($install_state['config_install_path']),
];
$form['update_notifications']['enable_update_status_module'] = [
'#type' => 'checkbox',
'#title' => $this->t('Check for updates automatically'),
'#default_value' => 1,
'#access' => empty($install_state['config_install_path']),
];
$form['update_notifications']['enable_update_status_emails'] = [
'#type' => 'checkbox',
......@@ -232,6 +241,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'input[name="enable_update_status_module"]' => ['checked' => TRUE],
],
],
'#access' => empty($install_state['config_install_path']),
];
$form['actions'] = ['#type' => 'actions'];
......@@ -258,21 +268,25 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('system.site')
->set('name', (string) $form_state->getValue('site_name'))
->set('mail', (string) $form_state->getValue('site_mail'))
->save(TRUE);
global $install_state;
$this->config('system.date')
->set('timezone.default', (string) $form_state->getValue('date_default_timezone'))
->set('country.default', (string) $form_state->getValue('site_default_country'))
->save(TRUE);
if (empty($install_state['config_install_path'])) {
$this->config('system.site')
->set('name', (string) $form_state->getValue('site_name'))
->set('mail', (string) $form_state->getValue('site_mail'))
->save(TRUE);
$this->config('system.date')
->set('timezone.default', (string) $form_state->getValue('date_default_timezone'))
->set('country.default', (string) $form_state->getValue('site_default_country'))
->save(TRUE);
}
$account_values = $form_state->getValue('account');
// Enable update.module if this option was selected.
$update_status_module = $form_state->getValue('enable_update_status_module');
if ($update_status_module) {
if (empty($install_state['config_install_path']) && $update_status_module) {
$this->moduleInstaller->install(['file', 'update'], FALSE);
// Add the site maintenance account's email address to the list of
......
......@@ -4,6 +4,7 @@
use Drupal\Core\Config\ConfigImporterException;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\Importer\ConfigImporterBatch;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleInstallerInterface;
......@@ -337,14 +338,14 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
$sync_steps = $config_importer->initialize();
$batch = [
'operations' => [],
'finished' => [get_class($this), 'finishBatch'],
'finished' => [ConfigImporterBatch::class, 'finish'],
'title' => t('Synchronizing configuration'),
'init_message' => t('Starting configuration synchronization.'),
'progress_message' => t('Completed step @current of @total.'),
'error_message' => t('Configuration synchronization has encountered an error.'),
];
foreach ($sync_steps as $sync_step) {
$batch['operations'][] = [[get_class($this), 'processBatch'], [$config_importer, $sync_step]];
$batch['operations'][] = [[ConfigImporterBatch::class, 'process'], [$config_importer, $sync_step]];
}
batch_set($batch);
......@@ -368,20 +369,15 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
* The synchronization step to do.
* @param array $context
* The batch context.
*
* @deprecated in Drupal 8.6.0 and will be removed before 9.0.0. Use
* \Drupal\Core\Config\Importer\ConfigImporterBatch::process() instead.
*
* @see https://www.drupal.org/node/2897299
*/
public static function processBatch(ConfigImporter $config_importer, $sync_step, &$context) {
if (!isset($context['sandbox']['config_importer'])) {
$context['sandbox']['config_importer'] = $config_importer;
}
$config_importer = $context['sandbox']['config_importer'];
$config_importer->doSyncStep($sync_step, $context);
if ($errors = $config_importer->getErrors()) {
if (!isset($context['results']['errors'])) {
$context['results']['errors'] = [];
}
$context['results']['errors'] = array_merge($context['results']['errors'], $errors);
}
@trigger_error('\Drupal\config\Form\ConfigSync::processBatch() deprecated in Drupal 8.6.0 and will be removed before 9.0.0. Use \Drupal\Core\Config\Importer\ConfigImporterBatch::process() instead. See https://www.drupal.org/node/2897299');
ConfigImporterBatch::process($config_importer, $sync_step, $context);
}
/**
......@@ -389,27 +385,15 @@ public static function processBatch(ConfigImporter $config_importer, $sync_step,
*
* This function is a static function to avoid serializing the ConfigSync
* object unnecessarily.
*
* @deprecated in Drupal 8.6.0 and will be removed before 9.0.0. Use
* \Drupal\Core\Config\Importer\ConfigImporterBatch::finish() instead.
*
* @see https://www.drupal.org/node/2897299
*/
public static function finishBatch($success, $results, $operations) {
if ($success) {
if (!empty($results['errors'])) {
foreach ($results['errors'] as $error) {
\Drupal::messenger()->addError($error);
\Drupal::logger('config_sync')->error($error);
}
\Drupal::messenger()->addWarning(\Drupal::translation()->translate('The configuration was imported with errors.'));
}
else {
\Drupal::messenger()->addStatus(\Drupal::translation()->translate('The configuration was imported successfully.'));
}
}
else {
// An error occurred.
// $operations contains the operations that remained unprocessed.
$error_operation = reset($operations);
$message = \Drupal::translation()->translate('An error occurred while processing %error_operation with arguments: @arguments', ['%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE)]);
\Drupal::messenger()->addError($message);
}
@trigger_error('\Drupal\config\Form\ConfigSync::finishBatch() deprecated in Drupal 8.6.0 and will be removed before 9.0.0. Use \Drupal\Core\Config\Importer\ConfigImporterBatch::finish() instead. See https://www.drupal.org/node/2897299');
ConfigImporterBatch::finish($success, $results, $operations);
}
}
......@@ -32,6 +32,7 @@ protected function setUp() {
$this->markTestSkipped('This test has to be run from the CLI');
}
$this->installConfig(['system']);
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
// Set up the ConfigImporter object for testing.
......
......@@ -59,7 +59,7 @@ protected function setUp() {
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installEntitySchema('content_moderation_state');
$this->installConfig('content_moderation');
$this->installConfig(['system', 'content_moderation']);
NodeType::create([
'type' => 'example',
......
......@@ -33,6 +33,7 @@ class ContentTranslationConfigImportTest extends KernelTestBase {
protected function setUp() {
parent::setUp();
$this->installConfig(['system']);
$this->installEntitySchema('entity_test_mul');
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
......
......@@ -26,7 +26,7 @@ protected function setUp() {
parent::setUp();
// Set default storage backend.
$this->installConfig(['field', 'node_test_config']);
$this->installConfig(['system', 'field', 'node_test_config']);
}
/**
......
......@@ -28,7 +28,7 @@ protected function setUp() {
$this->installEntitySchema('user');
// Set default storage backend.
$this->installConfig(['field']);
$this->installConfig(['system', 'field']);
}
/**
......
......@@ -72,6 +72,9 @@ public function onConfigImporterValidateNotEmpty(ConfigImporterEvent $event) {
* The config import event.
*/
public function onConfigImporterValidateSiteUUID(ConfigImporterEvent $event) {
if (!$event->getConfigImporter()->getStorageComparer()->getSourceStorage()->exists('system.site')) {
$event->getConfigImporter()->logError($this->t('This import does not contain system.site configuration, so has been rejected.'));
}
if (!$event->getConfigImporter()->getStorageComparer()->validateSiteUuid()) {
$event->getConfigImporter()->logError($this->t('Site UUID in source storage does not match the target storage.'));
}
......
......@@ -1002,6 +1002,19 @@ function system_requirements($phase) {
];
}
// During installs from configuration don't support install profiles that
// implement hook_install.
if ($phase == 'install' && !empty($install_state['config_install_path'])) {
$install_hook = $install_state['parameters']['profile'] . '_install';
if (function_exists($install_hook)) {
$requirements['config_install'] = [
'title' => t('Configuration install'),
'value' => $install_state['parameters']['profile'],
'description' => t('The selected profile has a hook_install() implementation and therefore can not be installed from configuration.'),
'severity' => REQUIREMENT_ERROR,
];
}
}
return $requirements;
}
......
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that installing from existing configuration works.
*
* @group Installer
*/
class InstallerExistingConfigMultilingualTest extends InstallerExistingConfigTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_config_install_multilingual';
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
return __DIR__ . '/../../../fixtures/config_install/multilingual.tar.gz';
}
}
<?php
namespace Drupal\FunctionalTests\Installer;
/**
* Verifies that profiles invalid config can not be installed.
*
* @group Installer
*/
class InstallerExistingConfigNoConfigTest extends InstallerExistingConfigTestBase {
protected $profile = 'no_config_profile';
/**
* Final installer step: Configure site.
*/
protected function setUpSite() {
// There are errors therefore there is nothing to do here.
return;
}
/**
* {@inheritdoc}
*/
protected function getConfigTarball() {
return __DIR__ . '/../../../fixtures/config_install/testing_config_install_no_config.