3 namespace Drupal\Core\Config;
5 use Drupal\Core\Config\Importer\MissingContentEvent;
6 use Drupal\Core\Extension\ModuleHandlerInterface;
7 use Drupal\Core\Extension\ModuleInstallerInterface;
8 use Drupal\Core\Extension\ThemeHandlerInterface;
9 use Drupal\Core\Config\Entity\ImportableEntityStorageInterface;
10 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
11 use Drupal\Core\Entity\EntityStorageException;
12 use Drupal\Core\Lock\LockBackendInterface;
13 use Drupal\Core\StringTranslation\StringTranslationTrait;
14 use Drupal\Core\StringTranslation\TranslationInterface;
15 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
18 * Defines a configuration importer.
20 * A config importer imports the changes into the configuration system. To
21 * determine which changes to import a StorageComparer in used.
23 * @see \Drupal\Core\Config\StorageComparerInterface
25 * The ConfigImporter has a identifier which is used to construct event names.
26 * The events fired during an import are:
27 * - ConfigEvents::IMPORT_VALIDATE: Events listening can throw a
28 * \Drupal\Core\Config\ConfigImporterException to prevent an import from
30 * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
31 * - ConfigEvents::IMPORT: Events listening can react to a successful import.
32 * @see \Drupal\Core\EventSubscriber\ConfigSnapshotSubscriber
34 * @see \Drupal\Core\Config\ConfigImporterEvent
36 class ConfigImporter {
37 use StringTranslationTrait;
38 use DependencySerializationTrait;
41 * The name used to identify the lock.
43 const LOCK_NAME = 'config_importer';
46 * The storage comparer used to discover configuration changes.
48 * @var \Drupal\Core\Config\StorageComparerInterface
50 protected $storageComparer;
53 * The event dispatcher used to notify subscribers.
55 * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
57 protected $eventDispatcher;
60 * The configuration manager.
62 * @var \Drupal\Core\Config\ConfigManagerInterface
64 protected $configManager;
67 * The used lock backend instance.
69 * @var \Drupal\Core\Lock\LockBackendInterface
74 * The typed config manager.
76 * @var \Drupal\Core\Config\TypedConfigManagerInterface
78 protected $typedConfigManager;
81 * List of configuration file changes processed by the import().
85 protected $processedConfiguration;
88 * List of extension changes processed by the import().
92 protected $processedExtensions;
95 * List of extension changes to be processed by the import().
99 protected $extensionChangelist;
102 * Indicates changes to import have been validated.
106 protected $validated;
109 * The module handler.
111 * @var \Drupal\Core\Extension\ModuleHandlerInterface
113 protected $moduleHandler;
118 * @var \Drupal\Core\Extension\ThemeHandlerInterface
120 protected $themeHandler;
123 * Flag set to import system.theme during processing theme install and uninstalls.
127 protected $processedSystemTheme = FALSE;
130 * A log of any errors encountered.
132 * If errors are logged during the validation event the configuration
133 * synchronization will not occur. If errors occur during an import then best
134 * efforts are made to complete the synchronization.
138 protected $errors = [];
141 * The total number of extensions to process.
145 protected $totalExtensionsToProcess = 0;
148 * The total number of configuration objects to process.
152 protected $totalConfigurationToProcess = 0;
155 * The module installer.
157 * @var \Drupal\Core\Extension\ModuleInstallerInterface
159 protected $moduleInstaller;
162 * Constructs a configuration import object.
164 * @param \Drupal\Core\Config\StorageComparerInterface $storage_comparer
165 * A storage comparer object used to determine configuration changes and
166 * access the source and target storage objects.
167 * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
168 * The event dispatcher used to notify subscribers of config import events.
169 * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
170 * The configuration manager.
171 * @param \Drupal\Core\Lock\LockBackendInterface $lock
172 * The lock backend to ensure multiple imports do not occur at the same time.
173 * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
174 * The typed configuration manager.
175 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
177 * @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer
178 * The module installer.
179 * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
181 * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
182 * The string translation service.
184 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) {
185 $this->storageComparer = $storage_comparer;
186 $this->eventDispatcher = $event_dispatcher;
187 $this->configManager = $config_manager;
189 $this->typedConfigManager = $typed_config;
190 $this->moduleHandler = $module_handler;
191 $this->moduleInstaller = $module_installer;
192 $this->themeHandler = $theme_handler;
193 $this->stringTranslation = $string_translation;
194 foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
195 $this->processedConfiguration[$collection] = $this->storageComparer->getEmptyChangelist();
197 $this->processedExtensions = $this->getEmptyExtensionsProcessedList();
201 * Logs an error message.
203 * @param string $message
204 * The message to log.
206 public function logError($message) {
207 $this->errors[] = $message;
211 * Returns error messages created while running the import.
216 public function getErrors() {
217 return $this->errors;
221 * Gets the configuration storage comparer.
223 * @return \Drupal\Core\Config\StorageComparerInterface
224 * Storage comparer object used to calculate configuration changes.
226 public function getStorageComparer() {
227 return $this->storageComparer;
231 * Resets the storage comparer and processed list.
233 * @return \Drupal\Core\Config\ConfigImporter
234 * The ConfigImporter instance.
236 public function reset() {
237 $this->storageComparer->reset();
238 // Empty all the lists.
239 foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
240 $this->processedConfiguration[$collection] = $this->storageComparer->getEmptyChangelist();
242 $this->extensionChangelist = $this->processedExtensions = $this->getEmptyExtensionsProcessedList();
244 $this->validated = FALSE;
245 $this->processedSystemTheme = FALSE;
250 * Gets an empty list of extensions to process.
253 * An empty list of extensions to process.
255 protected function getEmptyExtensionsProcessedList() {
269 * Checks if there are any unprocessed configuration changes.
272 * TRUE if there are changes to process and FALSE if not.
274 public function hasUnprocessedConfigurationChanges() {
275 foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
276 foreach (['delete', 'create', 'rename', 'update'] as $op) {
277 if (count($this->getUnprocessedConfiguration($op, $collection))) {
286 * Gets list of processed changes.
288 * @param string $collection
289 * (optional) The configuration collection to get processed changes for.
290 * Defaults to the default collection.
293 * An array containing a list of processed changes.
295 public function getProcessedConfiguration($collection = StorageInterface::DEFAULT_COLLECTION) {
296 return $this->processedConfiguration[$collection];
300 * Sets a change as processed.
302 * @param string $collection
303 * The configuration collection to set a change as processed for.
305 * The change operation performed, either delete, create, rename, or update.
306 * @param string $name
307 * The name of the configuration processed.
309 protected function setProcessedConfiguration($collection, $op, $name) {
310 $this->processedConfiguration[$collection][$op][] = $name;
314 * Gets a list of unprocessed changes for a given operation.
317 * The change operation to get the unprocessed list for, either delete,
318 * create, rename, or update.
319 * @param string $collection
320 * (optional) The configuration collection to get unprocessed changes for.
321 * Defaults to the default collection.
324 * An array of configuration names.
326 public function getUnprocessedConfiguration($op, $collection = StorageInterface::DEFAULT_COLLECTION) {
327 return array_diff($this->storageComparer->getChangelist($op, $collection), $this->processedConfiguration[$collection][$op]);
331 * Gets list of processed extension changes.
334 * An array containing a list of processed extension changes.
336 public function getProcessedExtensions() {
337 return $this->processedExtensions;
341 * Sets an extension change as processed.
343 * @param string $type
344 * The type of extension, either 'theme' or 'module'.
346 * The change operation performed, either install or uninstall.
347 * @param string $name
348 * The name of the extension processed.
350 protected function setProcessedExtension($type, $op, $name) {
351 $this->processedExtensions[$type][$op][] = $name;
355 * Populates the extension change list.
357 protected function createExtensionChangelist() {
358 // Create an empty changelist.
359 $this->extensionChangelist = $this->getEmptyExtensionsProcessedList();
361 // Read the extensions information to determine changes.
362 $current_extensions = $this->storageComparer->getTargetStorage()->read('core.extension');
363 $new_extensions = $this->storageComparer->getSourceStorage()->read('core.extension');
365 // If there is no extension information in sync then exit. This is probably
366 // due to an empty sync directory.
367 if (!$new_extensions) {
371 // Get a list of modules with dependency weights as values.
372 $module_data = system_rebuild_module_data();
373 // Set the actual module weights.
374 $module_list = array_combine(array_keys($module_data), array_keys($module_data));
375 $module_list = array_map(function ($module) use ($module_data) {
376 return $module_data[$module]->sort;
379 // Determine which modules to uninstall.
380 $uninstall = array_keys(array_diff_key($current_extensions['module'], $new_extensions['module']));
381 // Sort the list of newly uninstalled extensions by their weights, so that
382 // dependencies are uninstalled last. Extensions of the same weight are
383 // sorted in reverse alphabetical order, to ensure the order is exactly
384 // opposite from installation. For example, this module list:
391 // will result in the following sort order:
396 // @todo Move this sorting functionality to the extension system.
397 array_multisort(array_values($module_list), SORT_ASC, array_keys($module_list), SORT_DESC, $module_list);
398 $this->extensionChangelist['module']['uninstall'] = array_intersect(array_keys($module_list), $uninstall);
400 // Determine which modules to install.
401 $install = array_keys(array_diff_key($new_extensions['module'], $current_extensions['module']));
402 // Ensure that installed modules are sorted in exactly the reverse order
403 // (with dependencies installed first, and modules of the same weight sorted
404 // in alphabetical order).
405 $module_list = array_reverse($module_list);
406 $this->extensionChangelist['module']['install'] = array_intersect(array_keys($module_list), $install);
408 // If we're installing the install profile ensure it comes last. This will
409 // occur when installing a site from configuration.
410 $install_profile_key = array_search($new_extensions['profile'], $this->extensionChangelist['module']['install'], TRUE);
411 if ($install_profile_key !== FALSE) {
412 unset($this->extensionChangelist['module']['install'][$install_profile_key]);
413 $this->extensionChangelist['module']['install'][] = $new_extensions['profile'];
416 // Work out what themes to install and to uninstall.
417 $this->extensionChangelist['theme']['install'] = array_keys(array_diff_key($new_extensions['theme'], $current_extensions['theme']));
418 $this->extensionChangelist['theme']['uninstall'] = array_keys(array_diff_key($current_extensions['theme'], $new_extensions['theme']));
422 * Gets a list changes for extensions.
424 * @param string $type
425 * The type of extension, either 'theme' or 'module'.
427 * The change operation to get the unprocessed list for, either install
431 * An array of extension names.
433 public function getExtensionChangelist($type, $op = NULL) {
435 return $this->extensionChangelist[$type][$op];
437 return $this->extensionChangelist[$type];
441 * Gets a list of unprocessed changes for extensions.
443 * @param string $type
444 * The type of extension, either 'theme' or 'module'.
447 * An array of extension names.
449 protected function getUnprocessedExtensions($type) {
450 $changelist = $this->getExtensionChangelist($type);
452 'install' => array_diff($changelist['install'], $this->processedExtensions[$type]['install']),
453 'uninstall' => array_diff($changelist['uninstall'], $this->processedExtensions[$type]['uninstall']),
458 * Imports the changelist to the target storage.
460 * @return \Drupal\Core\Config\ConfigImporter
461 * The ConfigImporter instance.
463 * @throws \Drupal\Core\Config\ConfigException
465 public function import() {
466 if ($this->hasUnprocessedConfigurationChanges()) {
467 $sync_steps = $this->initialize();
469 foreach ($sync_steps as $step) {
472 $this->doSyncStep($step, $context);
473 } while ($context['finished'] < 1);
480 * Calls a config import step.
482 * @param string|callable $sync_step
483 * The step to do. Either a method on the ConfigImporter class or a
485 * @param array $context
486 * A batch context array. If the config importer is not running in a batch
487 * the only array key that is used is $context['finished']. A process needs
488 * to set $context['finished'] = 1 when it is done.
490 * @throws \InvalidArgumentException
491 * Exception thrown if the $sync_step can not be called.
493 public function doSyncStep($sync_step, &$context) {
494 if (!is_array($sync_step) && method_exists($this, $sync_step)) {
495 \Drupal::service('config.installer')->setSyncing(TRUE);
496 $this->$sync_step($context);
498 elseif (is_callable($sync_step)) {
499 \Drupal::service('config.installer')->setSyncing(TRUE);
500 call_user_func_array($sync_step, [&$context, $this]);
503 throw new \InvalidArgumentException('Invalid configuration synchronization step');
505 \Drupal::service('config.installer')->setSyncing(FALSE);
509 * Initializes the config importer in preparation for processing a batch.
512 * An array of \Drupal\Core\Config\ConfigImporter method names and callables
513 * that are invoked to complete the import. If there are modules or themes
514 * to process then an extra step is added.
516 * @throws \Drupal\Core\Config\ConfigImporterException
517 * If the configuration is already importing.
519 public function initialize() {
520 // Ensure that the changes have been validated.
523 if (!$this->lock->acquire(static::LOCK_NAME)) {
524 // Another process is synchronizing configuration.
525 throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_NAME));
529 $modules = $this->getUnprocessedExtensions('module');
530 foreach (['install', 'uninstall'] as $op) {
531 $this->totalExtensionsToProcess += count($modules[$op]);
533 $themes = $this->getUnprocessedExtensions('theme');
534 foreach (['install', 'uninstall'] as $op) {
535 $this->totalExtensionsToProcess += count($themes[$op]);
538 // We have extensions to process.
539 if ($this->totalExtensionsToProcess > 0) {
540 $sync_steps[] = 'processExtensions';
542 $sync_steps[] = 'processConfigurations';
543 $sync_steps[] = 'processMissingContent';
544 // Allow modules to add new steps to configuration synchronization.
545 $this->moduleHandler->alter('config_import_steps', $sync_steps, $this);
546 $sync_steps[] = 'finish';
551 * Processes extensions as a batch operation.
553 * @param array|\ArrayAccess $context
556 protected function processExtensions(&$context) {
557 $operation = $this->getNextExtensionOperation();
558 if (!empty($operation)) {
559 $this->processExtension($operation['type'], $operation['op'], $operation['name']);
560 $context['message'] = t('Synchronizing extensions: @op @name.', ['@op' => $operation['op'], '@name' => $operation['name']]);
561 $processed_count = count($this->processedExtensions['module']['install']) + count($this->processedExtensions['module']['uninstall']);
562 $processed_count += count($this->processedExtensions['theme']['uninstall']) + count($this->processedExtensions['theme']['install']);
563 $context['finished'] = $processed_count / $this->totalExtensionsToProcess;
566 $context['finished'] = 1;
571 * Processes configuration as a batch operation.
573 * @param array|\ArrayAccess $context
576 protected function processConfigurations(&$context) {
577 // The first time this is called we need to calculate the total to process.
578 // This involves recalculating the changelist which will ensure that if
579 // extensions have been processed any configuration affected will be taken
581 if ($this->totalConfigurationToProcess == 0) {
582 $this->storageComparer->reset();
583 foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
584 foreach (['delete', 'create', 'rename', 'update'] as $op) {
585 $this->totalConfigurationToProcess += count($this->getUnprocessedConfiguration($op, $collection));
589 $operation = $this->getNextConfigurationOperation();
590 if (!empty($operation)) {
591 if ($this->checkOp($operation['collection'], $operation['op'], $operation['name'])) {
592 $this->processConfiguration($operation['collection'], $operation['op'], $operation['name']);
594 if ($operation['collection'] == StorageInterface::DEFAULT_COLLECTION) {
595 $context['message'] = $this->t('Synchronizing configuration: @op @name.', ['@op' => $operation['op'], '@name' => $operation['name']]);
598 $context['message'] = $this->t('Synchronizing configuration: @op @name in @collection.', ['@op' => $operation['op'], '@name' => $operation['name'], '@collection' => $operation['collection']]);
600 $processed_count = 0;
601 foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
602 foreach (['delete', 'create', 'rename', 'update'] as $op) {
603 $processed_count += count($this->processedConfiguration[$collection][$op]);
606 $context['finished'] = $processed_count / $this->totalConfigurationToProcess;
609 $context['finished'] = 1;
614 * Handles processing of missing content.
616 * @param array|\ArrayAccess $context
617 * Standard batch context.
619 protected function processMissingContent(&$context) {
620 $sandbox = &$context['sandbox']['config'];
621 if (!isset($sandbox['missing_content'])) {
622 $missing_content = $this->configManager->findMissingContentDependencies();
623 $sandbox['missing_content']['data'] = $missing_content;
624 $sandbox['missing_content']['total'] = count($missing_content);
627 $missing_content = $sandbox['missing_content']['data'];
629 if (!empty($missing_content)) {
630 $event = new MissingContentEvent($missing_content);
631 // Fire an event to allow listeners to create the missing content.
632 $this->eventDispatcher->dispatch(ConfigEvents::IMPORT_MISSING_CONTENT, $event);
633 $sandbox['missing_content']['data'] = $event->getMissingContent();
635 $current_count = count($sandbox['missing_content']['data']);
636 if ($current_count) {
637 $context['message'] = $this->t('Resolving missing content');
638 $context['finished'] = ($sandbox['missing_content']['total'] - $current_count) / $sandbox['missing_content']['total'];
641 $context['finished'] = 1;
646 * Finishes the batch.
648 * @param array|\ArrayAccess $context
651 protected function finish(&$context) {
652 $this->eventDispatcher->dispatch(ConfigEvents::IMPORT, new ConfigImporterEvent($this));
653 // The import is now complete.
654 $this->lock->release(static::LOCK_NAME);
656 $context['message'] = t('Finalizing configuration synchronization.');
657 $context['finished'] = 1;
661 * Gets the next extension operation to perform.
664 * An array containing the next operation and extension name to perform it
665 * on. If there is nothing left to do returns FALSE;
667 protected function getNextExtensionOperation() {
668 foreach (['module', 'theme'] as $type) {
669 foreach (['install', 'uninstall'] as $op) {
670 $unprocessed = $this->getUnprocessedExtensions($type);
671 if (!empty($unprocessed[$op])) {
675 'name' => array_shift($unprocessed[$op]),
684 * Gets the next configuration operation to perform.
687 * An array containing the next operation and configuration name to perform
688 * it on. If there is nothing left to do returns FALSE;
690 protected function getNextConfigurationOperation() {
691 // The order configuration operations is processed is important. Deletes
692 // have to come first so that recreates can work.
693 foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
694 foreach (['delete', 'create', 'rename', 'update'] as $op) {
695 $config_names = $this->getUnprocessedConfiguration($op, $collection);
696 if (!empty($config_names)) {
699 'name' => array_shift($config_names),
700 'collection' => $collection,
709 * Dispatches validate event for a ConfigImporter object.
711 * Events should throw a \Drupal\Core\Config\ConfigImporterException to
712 * prevent an import from occurring.
714 * @throws \Drupal\Core\Config\ConfigImporterException
715 * Exception thrown if the validate event logged any errors.
717 public function validate() {
718 if (!$this->validated) {
719 // Create the list of installs and uninstalls.
720 $this->createExtensionChangelist();
722 foreach ($this->getUnprocessedConfiguration('rename') as $name) {
723 $names = $this->storageComparer->extractRenameNames($name);
724 $old_entity_type_id = $this->configManager->getEntityTypeIdByName($names['old_name']);
725 $new_entity_type_id = $this->configManager->getEntityTypeIdByName($names['new_name']);
726 if ($old_entity_type_id != $new_entity_type_id) {
727 $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']]));
729 // Has to be a configuration entity.
730 if (!$old_entity_type_id) {
731 $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']]));
734 $this->eventDispatcher->dispatch(ConfigEvents::IMPORT_VALIDATE, new ConfigImporterEvent($this));
735 if (count($this->getErrors())) {
736 $errors = array_merge(['There were errors validating the config synchronization.'], $this->getErrors());
737 throw new ConfigImporterException(implode(PHP_EOL, $errors));
740 $this->validated = TRUE;
747 * Processes a configuration change.
749 * @param string $collection
750 * The configuration collection to process changes for.
752 * The change operation.
753 * @param string $name
754 * The name of the configuration to process.
757 * Thrown when the import process fails, only thrown when no importer log is
758 * set, otherwise the exception message is logged and the configuration
761 protected function processConfiguration($collection, $op, $name) {
764 if ($collection == StorageInterface::DEFAULT_COLLECTION) {
765 $processed = $this->importInvokeOwner($collection, $op, $name);
768 $this->importConfig($collection, $op, $name);
771 catch (\Exception $e) {
772 $this->logError($this->t('Unexpected error during import with operation @op for @name: @message', ['@op' => $op, '@name' => $name, '@message' => $e->getMessage()]));
773 // Error for that operation was logged, mark it as processed so that
774 // the import can continue.
775 $this->setProcessedConfiguration($collection, $op, $name);
780 * Processes an extension change.
782 * @param string $type
783 * The type of extension, either 'module' or 'theme'.
785 * The change operation.
786 * @param string $name
787 * The name of the extension to process.
789 protected function processExtension($type, $op, $name) {
790 // Set the config installer to use the sync directory instead of the
791 // extensions own default config directories.
792 \Drupal::service('config.installer')
793 ->setSourceStorage($this->storageComparer->getSourceStorage());
794 if ($type == 'module') {
795 $this->moduleInstaller->$op([$name], FALSE);
796 // Installing a module can cause a kernel boot therefore reinject all the
799 // During a module install or uninstall the container is rebuilt and the
800 // module handler is called. This causes the container's instance of the
801 // module handler not to have loaded all the enabled modules.
802 $this->moduleHandler->loadAll();
804 if ($type == 'theme') {
805 // Theme uninstalls possible remove default or admin themes therefore we
806 // need to import this before doing any. If there are no uninstalls and
807 // the default or admin theme is changing this will be picked up whilst
808 // processing configuration.
809 if ($op == 'uninstall' && $this->processedSystemTheme === FALSE) {
810 $this->importConfig(StorageInterface::DEFAULT_COLLECTION, 'update', 'system.theme');
811 $this->configManager->getConfigFactory()->reset('system.theme');
812 $this->processedSystemTheme = TRUE;
814 $this->themeHandler->$op([$name]);
817 $this->setProcessedExtension($type, $op, $name);
821 * Checks that the operation is still valid.
823 * During a configuration import secondary writes and deletes are possible.
824 * This method checks that the operation is still valid before processing a
825 * configuration change.
827 * @param string $collection
828 * The configuration collection.
830 * The change operation.
831 * @param string $name
832 * The name of the configuration to process.
835 * TRUE is to continue processing, FALSE otherwise.
837 * @throws \Drupal\Core\Config\ConfigImporterException
839 protected function checkOp($collection, $op, $name) {
840 if ($op == 'rename') {
841 $names = $this->storageComparer->extractRenameNames($name);
842 $target_exists = $this->storageComparer->getTargetStorage($collection)->exists($names['new_name']);
843 if ($target_exists) {
844 // If the target exists, the rename has already occurred as the
845 // result of a secondary configuration write. Change the operation
846 // into an update. This is the desired behavior since renames often
847 // have to occur together. For example, renaming a node type must
848 // also result in renaming its fields and entity displays.
849 $this->storageComparer->moveRenameToUpdate($name);
854 $target_exists = $this->storageComparer->getTargetStorage($collection)->exists($name);
857 if (!$target_exists) {
858 // The configuration has already been deleted. For example, a field
859 // is automatically deleted if all the instances are.
860 $this->setProcessedConfiguration($collection, $op, $name);
866 if ($target_exists) {
867 // If the target already exists, use the entity storage to delete it
868 // again, if is a simple config, delete it directly.
869 if ($entity_type_id = $this->configManager->getEntityTypeIdByName($name)) {
870 $entity_storage = $this->configManager->getEntityManager()->getStorage($entity_type_id);
871 $entity_type = $this->configManager->getEntityManager()->getDefinition($entity_type_id);
872 $entity = $entity_storage->load($entity_storage->getIDFromConfigName($name, $entity_type->getConfigPrefix()));
874 $this->logError($this->t('Deleted and replaced configuration entity "@name"', ['@name' => $name]));
877 $this->storageComparer->getTargetStorage($collection)->delete($name);
878 $this->logError($this->t('Deleted and replaced configuration "@name"', ['@name' => $name]));
885 if (!$target_exists) {
886 $this->logError($this->t('Update target "@name" is missing.', ['@name' => $name]));
887 // Mark as processed so that the synchronization continues. Once the
888 // the current synchronization is complete it will show up as a
890 $this->setProcessedConfiguration($collection, $op, $name);
899 * Writes a configuration change from the source to the target storage.
901 * @param string $collection
902 * The configuration collection.
904 * The change operation.
905 * @param string $name
906 * The name of the configuration to process.
908 protected function importConfig($collection, $op, $name) {
909 // Allow config factory overriders to use a custom configuration object if
910 // they are responsible for the collection.
911 $overrider = $this->configManager->getConfigCollectionInfo()->getOverrideService($collection);
913 $config = $overrider->createConfigObject($name, $collection);
916 $config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
918 if ($op == 'delete') {
922 $data = $this->storageComparer->getSourceStorage($collection)->read($name);
923 $config->setData($data ? $data : []);
926 $this->setProcessedConfiguration($collection, $op, $name);
930 * Invokes import* methods on configuration entity storage.
932 * Allow modules to take over configuration change operations for higher-level
933 * configuration data.
935 * @todo Add support for other extension types; e.g., themes etc.
937 * @param string $collection
938 * The configuration collection.
940 * The change operation to get the unprocessed list for, either delete,
941 * create, rename, or update.
942 * @param string $name
943 * The name of the configuration to process.
946 * TRUE if the configuration was imported as a configuration entity. FALSE
949 * @throws \Drupal\Core\Entity\EntityStorageException
950 * Thrown if the data is owned by an entity type, but the entity storage
951 * does not support imports.
953 protected function importInvokeOwner($collection, $op, $name) {
954 // Renames are handled separately.
955 if ($op == 'rename') {
956 return $this->importInvokeRename($collection, $name);
958 // Validate the configuration object name before importing it.
959 // Config::validateName($name);
960 if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) {
961 $old_config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
962 if ($old_data = $this->storageComparer->getTargetStorage($collection)->read($name)) {
963 $old_config->initWithData($old_data);
966 $data = $this->storageComparer->getSourceStorage($collection)->read($name);
967 $new_config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
968 if ($data !== FALSE) {
969 $new_config->setData($data);
972 $method = 'import' . ucfirst($op);
973 $entity_storage = $this->configManager->getEntityManager()->getStorage($entity_type);
974 // Call to the configuration entity's storage to handle the configuration
976 if (!($entity_storage instanceof ImportableEntityStorageInterface)) {
977 throw new EntityStorageException(sprintf('The entity storage "%s" for the "%s" entity type does not support imports', get_class($entity_storage), $entity_type));
979 $entity_storage->$method($name, $new_config, $old_config);
980 $this->setProcessedConfiguration($collection, $op, $name);
987 * Imports a configuration entity rename.
989 * @param string $collection
990 * The configuration collection.
991 * @param string $rename_name
992 * The rename configuration name, as provided by
993 * \Drupal\Core\Config\StorageComparer::createRenameName().
996 * TRUE if the configuration was imported as a configuration entity. FALSE
999 * @throws \Drupal\Core\Entity\EntityStorageException
1000 * Thrown if the data is owned by an entity type, but the entity storage
1001 * does not support imports.
1003 * @see \Drupal\Core\Config\ConfigImporter::createRenameName()
1005 protected function importInvokeRename($collection, $rename_name) {
1006 $names = $this->storageComparer->extractRenameNames($rename_name);
1007 $entity_type_id = $this->configManager->getEntityTypeIdByName($names['old_name']);
1008 $old_config = new Config($names['old_name'], $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
1009 if ($old_data = $this->storageComparer->getTargetStorage($collection)->read($names['old_name'])) {
1010 $old_config->initWithData($old_data);
1013 $data = $this->storageComparer->getSourceStorage($collection)->read($names['new_name']);
1014 $new_config = new Config($names['new_name'], $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
1015 if ($data !== FALSE) {
1016 $new_config->setData($data);
1019 $entity_storage = $this->configManager->getEntityManager()->getStorage($entity_type_id);
1020 // Call to the configuration entity's storage to handle the configuration
1022 if (!($entity_storage instanceof ImportableEntityStorageInterface)) {
1023 throw new EntityStorageException(sprintf("The entity storage '%s' for the '%s' entity type does not support imports", get_class($entity_storage), $entity_type_id));
1025 $entity_storage->importRename($names['old_name'], $new_config, $old_config);
1026 $this->setProcessedConfiguration($collection, 'rename', $rename_name);
1031 * Determines if a import is already running.
1034 * TRUE if an import is already running, FALSE if not.
1036 public function alreadyImporting() {
1037 return !$this->lock->lockMayBeAvailable(static::LOCK_NAME);
1041 * Gets all the service dependencies from \Drupal.
1043 * Since the ConfigImporter handles module installation the kernel and the
1044 * container can be rebuilt and altered during processing. It is necessary to
1045 * keep the services used by the importer in sync.
1047 protected function reInjectMe() {
1048 $this->_serviceIds = [];
1049 $vars = get_object_vars($this);
1050 foreach ($vars as $key => $value) {
1051 if (is_object($value) && isset($value->_serviceId)) {
1052 $this->$key = \Drupal::service($value->_serviceId);