class ConfigImporter

Same name in other branches
  1. 8.9.x core/lib/Drupal/Core/Config/ConfigImporter.php \Drupal\Core\Config\ConfigImporter
  2. 10 core/lib/Drupal/Core/Config/ConfigImporter.php \Drupal\Core\Config\ConfigImporter
  3. 11.x core/lib/Drupal/Core/Config/ConfigImporter.php \Drupal\Core\Config\ConfigImporter

Defines a configuration importer.

A config importer imports the changes into the configuration system. To determine which changes to import a StorageComparer in used.

Hierarchy

Expanded class hierarchy of ConfigImporter

See also

\Drupal\Core\Config\StorageComparerInterface

The ConfigImporter has an identifier which is used to construct event names. The Events fired during an import are:

\Drupal\Core\Config\ConfigImporterEvent

18 files declare their use of ConfigImporter
ConfigImporterBatch.php in core/lib/Drupal/Core/Config/Importer/ConfigImporterBatch.php
ConfigImporterFieldPurger.php in core/modules/field/src/ConfigImporterFieldPurger.php
ConfigImporterMissingContentTest.php in core/tests/Drupal/KernelTests/Core/Config/ConfigImporterMissingContentTest.php
ConfigImporterTest.php in core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php
ConfigImportRecreateTest.php in core/tests/Drupal/KernelTests/Core/Config/ConfigImportRecreateTest.php

... See full list

File

core/lib/Drupal/Core/Config/ConfigImporter.php, line 38

Namespace

Drupal\Core\Config
View source
class ConfigImporter {
    use StringTranslationTrait;
    use DependencySerializationTrait;
    
    /**
     * The name used to identify the lock.
     */
    const LOCK_NAME = 'config_importer';
    
    /**
     * The storage comparer used to discover configuration changes.
     *
     * @var \Drupal\Core\Config\StorageComparerInterface
     */
    protected $storageComparer;
    
    /**
     * The event dispatcher used to notify subscribers.
     *
     * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
     */
    protected $eventDispatcher;
    
    /**
     * The configuration manager.
     *
     * @var \Drupal\Core\Config\ConfigManagerInterface
     */
    protected $configManager;
    
    /**
     * The used lock backend instance.
     *
     * @var \Drupal\Core\Lock\LockBackendInterface
     */
    protected $lock;
    
    /**
     * The typed config manager.
     *
     * @var \Drupal\Core\Config\TypedConfigManagerInterface
     */
    protected $typedConfigManager;
    
    /**
     * List of configuration file changes processed by the import().
     *
     * @var array
     */
    protected $processedConfiguration;
    
    /**
     * List of extension changes processed by the import().
     *
     * @var array
     */
    protected $processedExtensions;
    
    /**
     * List of extension changes to be processed by the import().
     *
     * @var array
     */
    protected $extensionChangelist;
    
    /**
     * Indicates changes to import have been validated.
     *
     * @var bool
     */
    protected $validated;
    
    /**
     * The module handler.
     *
     * @var \Drupal\Core\Extension\ModuleHandlerInterface
     */
    protected $moduleHandler;
    
    /**
     * The theme handler.
     *
     * @var \Drupal\Core\Extension\ThemeHandlerInterface
     */
    protected $themeHandler;
    
    /**
     * Flag set to import system.theme during processing theme install and uninstalls.
     *
     * @var bool
     */
    protected $processedSystemTheme = FALSE;
    
    /**
     * A log of any errors encountered.
     *
     * If errors are logged during the validation event the configuration
     * synchronization will not occur. If errors occur during an import then best
     * efforts are made to complete the synchronization.
     *
     * @var array
     */
    protected $errors = [];
    
    /**
     * The total number of extensions to process.
     *
     * @var int
     */
    protected $totalExtensionsToProcess = 0;
    
    /**
     * The total number of configuration objects to process.
     *
     * @var int
     */
    protected $totalConfigurationToProcess = 0;
    
    /**
     * The module installer.
     *
     * @var \Drupal\Core\Extension\ModuleInstallerInterface
     */
    protected $moduleInstaller;
    
    /**
     * The module extension list.
     *
     * @var \Drupal\Core\Extension\ModuleExtensionList
     */
    protected $moduleExtensionList;
    
    /**
     * The theme extension list.
     *
     * @var \Drupal\Core\Extension\ThemeExtensionList
     */
    protected $themeExtensionList;
    
    /**
     * Constructs a configuration import object.
     *
     * @param \Drupal\Core\Config\StorageComparerInterface $storage_comparer
     *   A storage comparer object used to determine configuration changes and
     *   access the source and target storage objects.
     * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
     *   The event dispatcher used to notify subscribers of config import events.
     * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
     *   The configuration manager.
     * @param \Drupal\Core\Lock\LockBackendInterface $lock
     *   The lock backend to ensure multiple imports do not occur at the same time.
     * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
     *   The typed configuration manager.
     * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
     *   The module handler
     * @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer
     *   The module installer.
     * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
     *   The theme handler
     * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
     *   The string translation service.
     * @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
     *   The module extension list.
     * @param \Drupal\Core\Extension\ThemeExtensionList $extension_list_theme
     *   The theme extension list.
     */
    public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, LockBackendInterface $lock, TypedConfigManagerInterface $typed_config, ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, ThemeHandlerInterface $theme_handler, TranslationInterface $string_translation, ModuleExtensionList $extension_list_module, ThemeExtensionList $extension_list_theme = NULL) {
        $this->moduleExtensionList = $extension_list_module;
        $this->storageComparer = $storage_comparer;
        $this->eventDispatcher = $event_dispatcher;
        $this->configManager = $config_manager;
        $this->lock = $lock;
        $this->typedConfigManager = $typed_config;
        $this->moduleHandler = $module_handler;
        $this->moduleInstaller = $module_installer;
        $this->themeHandler = $theme_handler;
        $this->stringTranslation = $string_translation;
        if ($extension_list_theme === NULL) {
            $extension_list_theme = \Drupal::service('extension.list.theme');
        }
        $this->themeExtensionList = $extension_list_theme;
        foreach ($this->storageComparer
            ->getAllCollectionNames() as $collection) {
            $this->processedConfiguration[$collection] = $this->storageComparer
                ->getEmptyChangelist();
        }
        $this->processedExtensions = $this->getEmptyExtensionsProcessedList();
    }
    
    /**
     * Logs an error message.
     *
     * @param string $message
     *   The message to log.
     */
    public function logError($message) {
        $this->errors[] = $message;
    }
    
    /**
     * Returns error messages created while running the import.
     *
     * @return array
     *   List of messages.
     */
    public function getErrors() {
        return $this->errors;
    }
    
    /**
     * Gets the configuration storage comparer.
     *
     * @return \Drupal\Core\Config\StorageComparerInterface
     *   Storage comparer object used to calculate configuration changes.
     */
    public function getStorageComparer() {
        return $this->storageComparer;
    }
    
    /**
     * Resets the storage comparer and processed list.
     *
     * @return $this
     *   The ConfigImporter instance.
     */
    public function reset() {
        $this->storageComparer
            ->reset();
        // Empty all the lists.
        foreach ($this->storageComparer
            ->getAllCollectionNames() as $collection) {
            $this->processedConfiguration[$collection] = $this->storageComparer
                ->getEmptyChangelist();
        }
        $this->extensionChangelist = $this->processedExtensions = $this->getEmptyExtensionsProcessedList();
        $this->validated = FALSE;
        $this->processedSystemTheme = FALSE;
        return $this;
    }
    
    /**
     * Gets an empty list of extensions to process.
     *
     * @return array
     *   An empty list of extensions to process.
     */
    protected function getEmptyExtensionsProcessedList() {
        return [
            'module' => [
                'install' => [],
                'uninstall' => [],
            ],
            'theme' => [
                'install' => [],
                'uninstall' => [],
            ],
        ];
    }
    
    /**
     * Checks if there are any unprocessed configuration changes.
     *
     * @return bool
     *   TRUE if there are changes to process and FALSE if not.
     */
    public function hasUnprocessedConfigurationChanges() {
        foreach ($this->storageComparer
            ->getAllCollectionNames() as $collection) {
            foreach ([
                'delete',
                'create',
                'rename',
                'update',
            ] as $op) {
                if (count($this->getUnprocessedConfiguration($op, $collection))) {
                    return TRUE;
                }
            }
        }
        return FALSE;
    }
    
    /**
     * Gets list of processed changes.
     *
     * @param string $collection
     *   (optional) The configuration collection to get processed changes for.
     *   Defaults to the default collection.
     *
     * @return array
     *   An array containing a list of processed changes.
     */
    public function getProcessedConfiguration($collection = StorageInterface::DEFAULT_COLLECTION) {
        return $this->processedConfiguration[$collection];
    }
    
    /**
     * Sets a change as processed.
     *
     * @param string $collection
     *   The configuration collection to set a change as processed for.
     * @param string $op
     *   The change operation performed, either delete, create, rename, or update.
     * @param string $name
     *   The name of the configuration processed.
     */
    protected function setProcessedConfiguration($collection, $op, $name) {
        $this->processedConfiguration[$collection][$op][] = $name;
    }
    
    /**
     * Gets a list of unprocessed changes for a given operation.
     *
     * @param string $op
     *   The change operation to get the unprocessed list for, either delete,
     *   create, rename, or update.
     * @param string $collection
     *   (optional) The configuration collection to get unprocessed changes for.
     *   Defaults to the default collection.
     *
     * @return array
     *   An array of configuration names.
     */
    public function getUnprocessedConfiguration($op, $collection = StorageInterface::DEFAULT_COLLECTION) {
        return array_diff($this->storageComparer
            ->getChangelist($op, $collection), $this->processedConfiguration[$collection][$op]);
    }
    
    /**
     * Gets list of processed extension changes.
     *
     * @return array
     *   An array containing a list of processed extension changes.
     */
    public function getProcessedExtensions() {
        return $this->processedExtensions;
    }
    
    /**
     * Sets an extension change as processed.
     *
     * @param string $type
     *   The type of extension, either 'theme' or 'module'.
     * @param string $op
     *   The change operation performed, either install or uninstall.
     * @param string $name
     *   The name of the extension processed.
     */
    protected function setProcessedExtension($type, $op, $name) {
        $this->processedExtensions[$type][$op][] = $name;
    }
    
    /**
     * Populates the extension change list.
     */
    protected function createExtensionChangelist() {
        // Create an empty changelist.
        $this->extensionChangelist = $this->getEmptyExtensionsProcessedList();
        // Read the extensions information to determine changes.
        $current_extensions = $this->storageComparer
            ->getTargetStorage()
            ->read('core.extension');
        $new_extensions = $this->storageComparer
            ->getSourceStorage()
            ->read('core.extension');
        // If there is no extension information in sync then exit. This is probably
        // due to an empty sync directory.
        if (!$new_extensions) {
            return;
        }
        // Reset the module list in case a stale cache item has been set by another
        // process during deployment.
        $this->moduleExtensionList
            ->reset();
        // Get a list of modules with dependency weights as values.
        $module_data = $this->moduleExtensionList
            ->getList();
        // Use the actual module weights.
        $module_list = array_combine(array_keys($module_data), array_keys($module_data));
        $module_list = array_map(function ($module) use ($module_data) {
            return $module_data[$module]->sort;
        }, $module_list);
        // Determine which modules to uninstall.
        $uninstall = array_keys(array_diff_key($current_extensions['module'], $new_extensions['module']));
        // Sort the list of newly uninstalled extensions by their weights, so that
        // dependencies are uninstalled last. Extensions of the same weight are
        // sorted in reverse alphabetical order, to ensure the order is exactly
        // opposite from installation. For example, this module list:
        // @code
        // array(
        //   'actions' => 0,
        //   'ban' => 0,
        //   'options' => -2,
        //   'text' => -1,
        // );
        // @endcode
        // will result in the following sort order:
        // 1. -2   options
        // 2. -1   text
        // 3.  0 0 ban
        // 4.  0 1 actions
        // @todo Move this sorting functionality to the extension system.
        array_multisort(array_values($module_list), SORT_ASC, array_keys($module_list), SORT_DESC, $module_list);
        $this->extensionChangelist['module']['uninstall'] = array_intersect(array_keys($module_list), $uninstall);
        // Determine which modules to install.
        $install = array_keys(array_diff_key($new_extensions['module'], $current_extensions['module']));
        // Ensure that installed modules are sorted in exactly the reverse order
        // (with dependencies installed first, and modules of the same weight sorted
        // in alphabetical order).
        $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'];
        }
        // Get a list of themes with dependency weights as values.
        $theme_data = $this->themeExtensionList
            ->getList();
        // Use the actual theme weights.
        $theme_list = array_combine(array_keys($theme_data), array_keys($theme_data));
        $theme_list = array_map(function ($theme) use ($theme_data) {
            return $theme_data[$theme]->sort;
        }, $theme_list);
        array_multisort(array_values($theme_list), SORT_ASC, array_keys($theme_list), SORT_DESC, $theme_list);
        // Work out what themes to install and to uninstall.
        $uninstall = array_keys(array_diff_key($current_extensions['theme'], $new_extensions['theme']));
        $this->extensionChangelist['theme']['uninstall'] = array_intersect(array_keys($theme_list), $uninstall);
        // Ensure that installed themes are sorted in exactly the reverse order
        // (with dependencies installed first, and themes of the same weight sorted
        // in alphabetical order).
        $install = array_keys(array_diff_key($new_extensions['theme'], $current_extensions['theme']));
        $theme_list = array_reverse($theme_list);
        $this->extensionChangelist['theme']['install'] = array_intersect(array_keys($theme_list), $install);
    }
    
    /**
     * Gets a list changes for extensions.
     *
     * @param string $type
     *   The type of extension, either 'theme' or 'module'.
     * @param string $op
     *   The change operation to get the unprocessed list for, either install
     *   or uninstall.
     *
     * @return array
     *   An array of extension names.
     */
    public function getExtensionChangelist($type, $op = NULL) {
        if ($op) {
            return $this->extensionChangelist[$type][$op];
        }
        return $this->extensionChangelist[$type];
    }
    
    /**
     * Gets a list of unprocessed changes for extensions.
     *
     * @param string $type
     *   The type of extension, either 'theme' or 'module'.
     *
     * @return array
     *   An array of extension names.
     */
    protected function getUnprocessedExtensions($type) {
        $changelist = $this->getExtensionChangelist($type);
        return [
            'install' => array_diff($changelist['install'], $this->processedExtensions[$type]['install']),
            'uninstall' => array_diff($changelist['uninstall'], $this->processedExtensions[$type]['uninstall']),
        ];
    }
    
    /**
     * Imports the changelist to the target storage.
     *
     * @return $this
     *   The ConfigImporter instance.
     *
     * @throws \Drupal\Core\Config\ConfigException
     */
    public function import() {
        if ($this->hasUnprocessedConfigurationChanges()) {
            $sync_steps = $this->initialize();
            foreach ($sync_steps as $step) {
                $context = [];
                do {
                    $this->doSyncStep($step, $context);
                } while ($context['finished'] < 1);
            }
        }
        return $this;
    }
    
    /**
     * Calls a config import step.
     *
     * @param string|callable $sync_step
     *   The step to do. Either a method on the ConfigImporter class or a
     *   callable.
     * @param array $context
     *   A batch context array. If the config importer is not running in a batch
     *   the only array key that is used is $context['finished']. A process needs
     *   to set $context['finished'] = 1 when it is done.
     *
     * @throws \InvalidArgumentException
     *   Exception thrown if the $sync_step can not be called.
     */
    public function doSyncStep($sync_step, &$context) {
        if (is_string($sync_step) && method_exists($this, $sync_step)) {
            \Drupal::service('config.installer')->setSyncing(TRUE);
            $this->{$sync_step}($context);
        }
        elseif (is_callable($sync_step)) {
            \Drupal::service('config.installer')->setSyncing(TRUE);
            call_user_func_array($sync_step, [
                &$context,
                $this,
            ]);
        }
        else {
            throw new \InvalidArgumentException('Invalid configuration synchronization step');
        }
        \Drupal::service('config.installer')->setSyncing(FALSE);
    }
    
    /**
     * Initializes the config importer in preparation for processing a batch.
     *
     * @return array
     *   An array of \Drupal\Core\Config\ConfigImporter method names and callables
     *   that are invoked to complete the import. If there are modules or themes
     *   to process then an extra step is added.
     *
     * @throws \Drupal\Core\Config\ConfigImporterException
     *   If the configuration is already importing.
     */
    public function initialize() {
        // Ensure that the changes have been validated.
        $this->validate();
        if (!$this->lock
            ->acquire(static::LOCK_NAME)) {
            // Another process is synchronizing configuration.
            throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_NAME));
        }
        $sync_steps = [];
        $modules = $this->getUnprocessedExtensions('module');
        foreach ([
            'install',
            'uninstall',
        ] as $op) {
            $this->totalExtensionsToProcess += count($modules[$op]);
        }
        $themes = $this->getUnprocessedExtensions('theme');
        foreach ([
            'install',
            'uninstall',
        ] as $op) {
            $this->totalExtensionsToProcess += count($themes[$op]);
        }
        // We have extensions to process.
        if ($this->totalExtensionsToProcess > 0) {
            $sync_steps[] = 'processExtensions';
        }
        $sync_steps[] = 'processConfigurations';
        $sync_steps[] = 'processMissingContent';
        // Allow modules to add new steps to configuration synchronization.
        $this->moduleHandler
            ->alter('config_import_steps', $sync_steps, $this);
        $sync_steps[] = 'finish';
        return $sync_steps;
    }
    
    /**
     * Processes extensions as a batch operation.
     *
     * @param array|\ArrayAccess $context
     *   The batch context.
     */
    protected function processExtensions(&$context) {
        $operation = $this->getNextExtensionOperation();
        if (!empty($operation)) {
            $this->processExtension($operation['type'], $operation['op'], $operation['name']);
            $context['message'] = t('Synchronizing extensions: @op @name.', [
                '@op' => $operation['op'],
                '@name' => $operation['name'],
            ]);
            $processed_count = count($this->processedExtensions['module']['install']) + count($this->processedExtensions['module']['uninstall']);
            $processed_count += count($this->processedExtensions['theme']['uninstall']) + count($this->processedExtensions['theme']['install']);
            $context['finished'] = $processed_count / $this->totalExtensionsToProcess;
        }
        else {
            $context['finished'] = 1;
        }
    }
    
    /**
     * Processes configuration as a batch operation.
     *
     * @param array|\ArrayAccess $context
     *   The batch context.
     */
    protected function processConfigurations(&$context) {
        // The first time this is called we need to calculate the total to process.
        // This involves recalculating the changelist which will ensure that if
        // extensions have been processed any configuration affected will be taken
        // into account.
        if ($this->totalConfigurationToProcess == 0) {
            $this->storageComparer
                ->reset();
            foreach ($this->storageComparer
                ->getAllCollectionNames() as $collection) {
                foreach ([
                    'delete',
                    'create',
                    'rename',
                    'update',
                ] as $op) {
                    $this->totalConfigurationToProcess += count($this->getUnprocessedConfiguration($op, $collection));
                }
            }
            // Adjust the totals for system.theme.
            // @see \Drupal\Core\Config\ConfigImporter::processExtension
            if ($this->processedSystemTheme) {
                $this->totalConfigurationToProcess++;
            }
        }
        $operation = $this->getNextConfigurationOperation();
        if (!empty($operation)) {
            if ($this->checkOp($operation['collection'], $operation['op'], $operation['name'])) {
                $this->processConfiguration($operation['collection'], $operation['op'], $operation['name']);
            }
            if ($operation['collection'] == StorageInterface::DEFAULT_COLLECTION) {
                $context['message'] = $this->t('Synchronizing configuration: @op @name.', [
                    '@op' => $operation['op'],
                    '@name' => $operation['name'],
                ]);
            }
            else {
                $context['message'] = $this->t('Synchronizing configuration: @op @name in @collection.', [
                    '@op' => $operation['op'],
                    '@name' => $operation['name'],
                    '@collection' => $operation['collection'],
                ]);
            }
            $processed_count = 0;
            foreach ($this->storageComparer
                ->getAllCollectionNames() as $collection) {
                foreach ([
                    'delete',
                    'create',
                    'rename',
                    'update',
                ] as $op) {
                    $processed_count += count($this->processedConfiguration[$collection][$op]);
                }
            }
            $context['finished'] = $processed_count / $this->totalConfigurationToProcess;
        }
        else {
            $context['finished'] = 1;
        }
    }
    
    /**
     * Handles processing of missing content.
     *
     * @param array|\ArrayAccess $context
     *   Standard batch context.
     */
    protected function processMissingContent(&$context) {
        $sandbox =& $context['sandbox']['config'];
        if (!isset($sandbox['missing_content'])) {
            $missing_content = $this->configManager
                ->findMissingContentDependencies();
            $sandbox['missing_content']['data'] = $missing_content;
            $sandbox['missing_content']['total'] = count($missing_content);
        }
        else {
            $missing_content = $sandbox['missing_content']['data'];
        }
        if (!empty($missing_content)) {
            $event = new MissingContentEvent($missing_content);
            // Fire an event to allow listeners to create the missing content.
            $this->eventDispatcher
                ->dispatch($event, ConfigEvents::IMPORT_MISSING_CONTENT);
            $sandbox['missing_content']['data'] = $event->getMissingContent();
        }
        $current_count = count($sandbox['missing_content']['data']);
        if ($current_count) {
            $context['message'] = $this->t('Resolving missing content');
            $context['finished'] = ($sandbox['missing_content']['total'] - $current_count) / $sandbox['missing_content']['total'];
        }
        else {
            $context['finished'] = 1;
        }
    }
    
    /**
     * Finishes the batch.
     *
     * @param array|\ArrayAccess $context
     *   The batch context.
     */
    protected function finish(&$context) {
        $this->eventDispatcher
            ->dispatch(new ConfigImporterEvent($this), ConfigEvents::IMPORT);
        // The import is now complete.
        $this->lock
            ->release(static::LOCK_NAME);
        $this->reset();
        $context['message'] = t('Finalizing configuration synchronization.');
        $context['finished'] = 1;
    }
    
    /**
     * Gets the next extension operation to perform.
     *
     * Uninstalls are processed first with themes coming before modules. Then
     * installs are processed with modules coming before themes. This order is
     * necessary because themes can depend on modules.
     *
     * @return array|bool
     *   An array containing the next operation and extension name to perform it
     *   on. If there is nothing left to do returns FALSE;
     */
    protected function getNextExtensionOperation() {
        foreach ([
            'uninstall',
            'install',
        ] as $op) {
            $types = $op === 'uninstall' ? [
                'theme',
                'module',
            ] : [
                'module',
                'theme',
            ];
            foreach ($types as $type) {
                $unprocessed = $this->getUnprocessedExtensions($type);
                if (!empty($unprocessed[$op])) {
                    return [
                        'op' => $op,
                        'type' => $type,
                        'name' => array_shift($unprocessed[$op]),
                    ];
                }
            }
        }
        return FALSE;
    }
    
    /**
     * Gets the next configuration operation to perform.
     *
     * @return array|bool
     *   An array containing the next operation and configuration name to perform
     *   it on. If there is nothing left to do returns FALSE;
     */
    protected function getNextConfigurationOperation() {
        // The order configuration operations is processed is important. Deletes
        // have to come first so that recreates can work.
        foreach ($this->storageComparer
            ->getAllCollectionNames() as $collection) {
            foreach ([
                'delete',
                'create',
                'rename',
                'update',
            ] as $op) {
                $config_names = $this->getUnprocessedConfiguration($op, $collection);
                if (!empty($config_names)) {
                    return [
                        'op' => $op,
                        'name' => array_shift($config_names),
                        'collection' => $collection,
                    ];
                }
            }
        }
        return FALSE;
    }
    
    /**
     * Dispatches validate event for a ConfigImporter object.
     *
     * Events should throw a \Drupal\Core\Config\ConfigImporterException to
     * prevent an import from occurring.
     *
     * @throws \Drupal\Core\Config\ConfigImporterException
     *   Exception thrown if the validate event logged any errors.
     */
    public function validate() {
        if (!$this->validated) {
            $this->errors = [];
            // Create the list of installs and uninstalls.
            $this->createExtensionChangelist();
            // Validate renames.
            foreach ($this->getUnprocessedConfiguration('rename') as $name) {
                $names = $this->storageComparer
                    ->extractRenameNames($name);
                $old_entity_type_id = $this->configManager
                    ->getEntityTypeIdByName($names['old_name']);
                $new_entity_type_id = $this->configManager
                    ->getEntityTypeIdByName($names['new_name']);
                if ($old_entity_type_id != $new_entity_type_id) {
                    $this->logError($this->t('Entity type mismatch on rename. @old_type not equal to @new_type for existing configuration @old_name and staged configuration @new_name.', [
                        '@old_type' => $old_entity_type_id,
                        '@new_type' => $new_entity_type_id,
                        '@old_name' => $names['old_name'],
                        '@new_name' => $names['new_name'],
                    ]));
                }
                // Has to be a configuration entity.
                if (!$old_entity_type_id) {
                    $this->logError($this->t('Rename operation for simple configuration. Existing configuration @old_name and staged configuration @new_name.', [
                        '@old_name' => $names['old_name'],
                        '@new_name' => $names['new_name'],
                    ]));
                }
            }
            $this->eventDispatcher
                ->dispatch(new ConfigImporterEvent($this), ConfigEvents::IMPORT_VALIDATE);
            if (count($this->getErrors())) {
                $errors = array_merge([
                    'There were errors validating the config synchronization.',
                ], $this->getErrors());
                throw new ConfigImporterException(implode(PHP_EOL, $errors));
            }
            else {
                $this->validated = TRUE;
            }
        }
        return $this;
    }
    
    /**
     * Processes a configuration change.
     *
     * @param string $collection
     *   The configuration collection to process changes for.
     * @param string $op
     *   The change operation.
     * @param string $name
     *   The name of the configuration to process.
     *
     * @throws \Exception
     *   Thrown when the import process fails, only thrown when no importer log is
     *   set, otherwise the exception message is logged and the configuration
     *   is skipped.
     */
    protected function processConfiguration($collection, $op, $name) {
        try {
            $processed = FALSE;
            if ($collection == StorageInterface::DEFAULT_COLLECTION) {
                $processed = $this->importInvokeOwner($collection, $op, $name);
            }
            if (!$processed) {
                $this->importConfig($collection, $op, $name);
            }
        } catch (\Exception $e) {
            $this->logError($this->t('Unexpected error during import with operation @op for @name: @message', [
                '@op' => $op,
                '@name' => $name,
                '@message' => $e->getMessage(),
            ]));
            // Error for that operation was logged, mark it as processed so that
            // the import can continue.
            $this->setProcessedConfiguration($collection, $op, $name);
        }
    }
    
    /**
     * Processes an extension change.
     *
     * @param string $type
     *   The type of extension, either 'module' or 'theme'.
     * @param string $op
     *   The change operation.
     * @param string $name
     *   The name of the extension to process.
     */
    protected function processExtension($type, $op, $name) {
        // Set the config installer to use the sync directory instead of the
        // extensions own default config directories.
        \Drupal::service('config.installer')->setSourceStorage($this->storageComparer
            ->getSourceStorage());
        if ($type == 'module') {
            $this->moduleInstaller
                ->{$op}([
                $name,
            ], FALSE);
            // Installing a module can cause a kernel boot therefore reinject all the
            // services.
            $this->reInjectMe();
            // During a module install or uninstall the container is rebuilt and the
            // module handler is called. This causes the container's instance of the
            // module handler not to have loaded all the enabled modules.
            $this->moduleHandler
                ->loadAll();
        }
        if ($type == 'theme') {
            // Theme uninstalls possible remove default or admin themes therefore we
            // need to import this before doing any. If there are no uninstalls and
            // the default or admin theme is changing this will be picked up whilst
            // processing configuration.
            if ($op == 'uninstall' && $this->processedSystemTheme === FALSE) {
                $this->importConfig(StorageInterface::DEFAULT_COLLECTION, 'update', 'system.theme');
                $this->configManager
                    ->getConfigFactory()
                    ->reset('system.theme');
                $this->processedSystemTheme = TRUE;
            }
            \Drupal::service('theme_installer')->{$op}([
                $name,
            ]);
        }
        $this->setProcessedExtension($type, $op, $name);
    }
    
    /**
     * Checks that the operation is still valid.
     *
     * During a configuration import secondary writes and deletes are possible.
     * This method checks that the operation is still valid before processing a
     * configuration change.
     *
     * @param string $collection
     *   The configuration collection.
     * @param string $op
     *   The change operation.
     * @param string $name
     *   The name of the configuration to process.
     *
     * @return bool
     *   TRUE is to continue processing, FALSE otherwise.
     *
     * @throws \Drupal\Core\Config\ConfigImporterException
     */
    protected function checkOp($collection, $op, $name) {
        if ($op == 'rename') {
            $names = $this->storageComparer
                ->extractRenameNames($name);
            $target_exists = $this->storageComparer
                ->getTargetStorage($collection)
                ->exists($names['new_name']);
            if ($target_exists) {
                // If the target exists, the rename has already occurred as the
                // result of a secondary configuration write. Change the operation
                // into an update. This is the desired behavior since renames often
                // have to occur together. For example, renaming a node type must
                // also result in renaming its fields and entity displays.
                $this->storageComparer
                    ->moveRenameToUpdate($name);
                return FALSE;
            }
            return TRUE;
        }
        $target_exists = $this->storageComparer
            ->getTargetStorage($collection)
            ->exists($name);
        switch ($op) {
            case 'delete':
                if (!$target_exists) {
                    // The configuration has already been deleted. For example, a field
                    // is automatically deleted if all the instances are.
                    $this->setProcessedConfiguration($collection, $op, $name);
                    return FALSE;
                }
                break;
            case 'create':
                if ($target_exists) {
                    // If the target already exists, use the entity storage to delete it
                    // again, if is a simple config, delete it directly.
                    if ($entity_type_id = $this->configManager
                        ->getEntityTypeIdByName($name)) {
                        $entity_storage = $this->configManager
                            ->getEntityTypeManager()
                            ->getStorage($entity_type_id);
                        $entity_type = $this->configManager
                            ->getEntityTypeManager()
                            ->getDefinition($entity_type_id);
                        $entity = $entity_storage->load($entity_storage->getIDFromConfigName($name, $entity_type->getConfigPrefix()));
                        $entity->delete();
                        $this->logError($this->t('Deleted and replaced configuration entity "@name"', [
                            '@name' => $name,
                        ]));
                    }
                    else {
                        $this->storageComparer
                            ->getTargetStorage($collection)
                            ->delete($name);
                        $this->logError($this->t('Deleted and replaced configuration "@name"', [
                            '@name' => $name,
                        ]));
                    }
                    return TRUE;
                }
                break;
            case 'update':
                if (!$target_exists) {
                    $this->logError($this->t('Update target "@name" is missing.', [
                        '@name' => $name,
                    ]));
                    // Mark as processed so that the synchronization continues. Once the
                    // the current synchronization is complete it will show up as a
                    // create.
                    $this->setProcessedConfiguration($collection, $op, $name);
                    return FALSE;
                }
                break;
        }
        return TRUE;
    }
    
    /**
     * Writes a configuration change from the source to the target storage.
     *
     * @param string $collection
     *   The configuration collection.
     * @param string $op
     *   The change operation.
     * @param string $name
     *   The name of the configuration to process.
     */
    protected function importConfig($collection, $op, $name) {
        // Allow config factory overriders to use a custom configuration object if
        // they are responsible for the collection.
        $overrider = $this->configManager
            ->getConfigCollectionInfo()
            ->getOverrideService($collection);
        if ($overrider) {
            $config = $overrider->createConfigObject($name, $collection);
        }
        else {
            $config = new Config($name, $this->storageComparer
                ->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
        }
        if ($op == 'delete') {
            $config->delete();
        }
        else {
            $data = $this->storageComparer
                ->getSourceStorage($collection)
                ->read($name);
            $config->setData($data ? $data : []);
            $config->save();
        }
        $this->setProcessedConfiguration($collection, $op, $name);
    }
    
    /**
     * Invokes import* methods on configuration entity storage.
     *
     * Allow modules to take over configuration change operations for higher-level
     * configuration data.
     *
     * @todo Add support for other extension types; e.g., themes etc.
     *
     * @param string $collection
     *   The configuration collection.
     * @param string $op
     *   The change operation to get the unprocessed list for, either delete,
     *   create, rename, or update.
     * @param string $name
     *   The name of the configuration to process.
     *
     * @return bool
     *   TRUE if the configuration was imported as a configuration entity. FALSE
     *   otherwise.
     *
     * @throws \Drupal\Core\Entity\EntityStorageException
     *   Thrown if the data is owned by an entity type, but the entity storage
     *   does not support imports.
     */
    protected function importInvokeOwner($collection, $op, $name) {
        // Renames are handled separately.
        if ($op == 'rename') {
            return $this->importInvokeRename($collection, $name);
        }
        // Validate the configuration object name before importing it.
        // Config::validateName($name);
        if ($entity_type = $this->configManager
            ->getEntityTypeIdByName($name)) {
            $old_config = new Config($name, $this->storageComparer
                ->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
            if ($old_data = $this->storageComparer
                ->getTargetStorage($collection)
                ->read($name)) {
                $old_config->initWithData($old_data);
            }
            $data = $this->storageComparer
                ->getSourceStorage($collection)
                ->read($name);
            $new_config = new Config($name, $this->storageComparer
                ->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
            if ($data !== FALSE) {
                $new_config->setData($data);
            }
            $method = 'import' . ucfirst($op);
            $entity_storage = $this->configManager
                ->getEntityTypeManager()
                ->getStorage($entity_type);
            // Call to the configuration entity's storage to handle the configuration
            // change.
            if (!$entity_storage instanceof ImportableEntityStorageInterface) {
                throw new EntityStorageException(sprintf('The entity storage "%s" for the "%s" entity type does not support imports', get_class($entity_storage), $entity_type));
            }
            $entity_storage->{$method}($name, $new_config, $old_config);
            $this->setProcessedConfiguration($collection, $op, $name);
            return TRUE;
        }
        return FALSE;
    }
    
    /**
     * Imports a configuration entity rename.
     *
     * @param string $collection
     *   The configuration collection.
     * @param string $rename_name
     *   The rename configuration name, as provided by
     *   \Drupal\Core\Config\StorageComparer::createRenameName().
     *
     * @return bool
     *   TRUE if the configuration was imported as a configuration entity. FALSE
     *   otherwise.
     *
     * @throws \Drupal\Core\Entity\EntityStorageException
     *   Thrown if the data is owned by an entity type, but the entity storage
     *   does not support imports.
     *
     * @see \Drupal\Core\Config\ConfigImporter::createRenameName()
     */
    protected function importInvokeRename($collection, $rename_name) {
        $names = $this->storageComparer
            ->extractRenameNames($rename_name);
        $entity_type_id = $this->configManager
            ->getEntityTypeIdByName($names['old_name']);
        $old_config = new Config($names['old_name'], $this->storageComparer
            ->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
        if ($old_data = $this->storageComparer
            ->getTargetStorage($collection)
            ->read($names['old_name'])) {
            $old_config->initWithData($old_data);
        }
        $data = $this->storageComparer
            ->getSourceStorage($collection)
            ->read($names['new_name']);
        $new_config = new Config($names['new_name'], $this->storageComparer
            ->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
        if ($data !== FALSE) {
            $new_config->setData($data);
        }
        $entity_storage = $this->configManager
            ->getEntityTypeManager()
            ->getStorage($entity_type_id);
        // Call to the configuration entity's storage to handle the configuration
        // change.
        if (!$entity_storage instanceof ImportableEntityStorageInterface) {
            throw new EntityStorageException(sprintf("The entity storage '%s' for the '%s' entity type does not support imports", get_class($entity_storage), $entity_type_id));
        }
        $entity_storage->importRename($names['old_name'], $new_config, $old_config);
        $this->setProcessedConfiguration($collection, 'rename', $rename_name);
        return TRUE;
    }
    
    /**
     * Determines if an import is already running.
     *
     * @return bool
     *   TRUE if an import is already running, FALSE if not.
     */
    public function alreadyImporting() {
        return !$this->lock
            ->lockMayBeAvailable(static::LOCK_NAME);
    }
    
    /**
     * Gets all the service dependencies from \Drupal.
     *
     * Since the ConfigImporter handles module installation the kernel and the
     * container can be rebuilt and altered during processing. It is necessary to
     * keep the services used by the importer in sync.
     */
    protected function reInjectMe() {
        // When rebuilding the container,
        // \Drupal\Core\DrupalKernel::initializeContainer() saves the hashes of the
        // old container and passes them to the new one. So __sleep() will
        // recognize the old services and then __wakeup() will restore them from
        // the new container.
        $this->__sleep();
        $this->__wakeup();
    }

}

Members

Title Sort descending Modifiers Object type Summary Overrides
ConfigImporter::$configManager protected property The configuration manager.
ConfigImporter::$errors protected property A log of any errors encountered.
ConfigImporter::$eventDispatcher protected property The event dispatcher used to notify subscribers.
ConfigImporter::$extensionChangelist protected property List of extension changes to be processed by the import().
ConfigImporter::$lock protected property The used lock backend instance.
ConfigImporter::$moduleExtensionList protected property The module extension list.
ConfigImporter::$moduleHandler protected property The module handler.
ConfigImporter::$moduleInstaller protected property The module installer.
ConfigImporter::$processedConfiguration protected property List of configuration file changes processed by the import().
ConfigImporter::$processedExtensions protected property List of extension changes processed by the import().
ConfigImporter::$processedSystemTheme protected property Flag set to import system.theme during processing theme install and uninstalls.
ConfigImporter::$storageComparer protected property The storage comparer used to discover configuration changes.
ConfigImporter::$themeExtensionList protected property The theme extension list.
ConfigImporter::$themeHandler protected property The theme handler.
ConfigImporter::$totalConfigurationToProcess protected property The total number of configuration objects to process.
ConfigImporter::$totalExtensionsToProcess protected property The total number of extensions to process.
ConfigImporter::$typedConfigManager protected property The typed config manager.
ConfigImporter::$validated protected property Indicates changes to import have been validated.
ConfigImporter::alreadyImporting public function Determines if an import is already running.
ConfigImporter::checkOp protected function Checks that the operation is still valid.
ConfigImporter::createExtensionChangelist protected function Populates the extension change list.
ConfigImporter::doSyncStep public function Calls a config import step.
ConfigImporter::finish protected function Finishes the batch.
ConfigImporter::getEmptyExtensionsProcessedList protected function Gets an empty list of extensions to process.
ConfigImporter::getErrors public function Returns error messages created while running the import.
ConfigImporter::getExtensionChangelist public function Gets a list changes for extensions.
ConfigImporter::getNextConfigurationOperation protected function Gets the next configuration operation to perform.
ConfigImporter::getNextExtensionOperation protected function Gets the next extension operation to perform.
ConfigImporter::getProcessedConfiguration public function Gets list of processed changes.
ConfigImporter::getProcessedExtensions public function Gets list of processed extension changes.
ConfigImporter::getStorageComparer public function Gets the configuration storage comparer.
ConfigImporter::getUnprocessedConfiguration public function Gets a list of unprocessed changes for a given operation.
ConfigImporter::getUnprocessedExtensions protected function Gets a list of unprocessed changes for extensions.
ConfigImporter::hasUnprocessedConfigurationChanges public function Checks if there are any unprocessed configuration changes.
ConfigImporter::import public function Imports the changelist to the target storage.
ConfigImporter::importConfig protected function Writes a configuration change from the source to the target storage.
ConfigImporter::importInvokeOwner protected function Invokes import* methods on configuration entity storage.
ConfigImporter::importInvokeRename protected function Imports a configuration entity rename.
ConfigImporter::initialize public function Initializes the config importer in preparation for processing a batch.
ConfigImporter::LOCK_NAME constant The name used to identify the lock.
ConfigImporter::logError public function Logs an error message.
ConfigImporter::processConfiguration protected function Processes a configuration change.
ConfigImporter::processConfigurations protected function Processes configuration as a batch operation.
ConfigImporter::processExtension protected function Processes an extension change.
ConfigImporter::processExtensions protected function Processes extensions as a batch operation.
ConfigImporter::processMissingContent protected function Handles processing of missing content.
ConfigImporter::reInjectMe protected function Gets all the service dependencies from \Drupal.
ConfigImporter::reset public function Resets the storage comparer and processed list.
ConfigImporter::setProcessedConfiguration protected function Sets a change as processed.
ConfigImporter::setProcessedExtension protected function Sets an extension change as processed.
ConfigImporter::validate public function Dispatches validate event for a ConfigImporter object.
ConfigImporter::__construct public function Constructs a configuration import object.
DependencySerializationTrait::$_entityStorages protected property
DependencySerializationTrait::$_serviceIds protected property
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
StringTranslationTrait::$stringTranslation protected property The string translation service. 3
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.