06fed4b1b04ee5e3fb5e2a1dc7eeb4fcaa34f415
[yaffs-website] / web / core / lib / Drupal / Core / Config / ConfigImporter.php
1 <?php
2
3 namespace Drupal\Core\Config;
4
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;
16
17 /**
18  * Defines a configuration importer.
19  *
20  * A config importer imports the changes into the configuration system. To
21  * determine which changes to import a StorageComparer in used.
22  *
23  * @see \Drupal\Core\Config\StorageComparerInterface
24  *
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
29  *   occurring.
30  *   @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
31  * - ConfigEvents::IMPORT: Events listening can react to a successful import.
32  *   @see \Drupal\Core\EventSubscriber\ConfigSnapshotSubscriber
33  *
34  * @see \Drupal\Core\Config\ConfigImporterEvent
35  */
36 class ConfigImporter {
37   use StringTranslationTrait;
38   use DependencySerializationTrait;
39
40   /**
41    * The name used to identify the lock.
42    */
43   const LOCK_NAME = 'config_importer';
44
45   /**
46    * The storage comparer used to discover configuration changes.
47    *
48    * @var \Drupal\Core\Config\StorageComparerInterface
49    */
50   protected $storageComparer;
51
52   /**
53    * The event dispatcher used to notify subscribers.
54    *
55    * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
56    */
57   protected $eventDispatcher;
58
59   /**
60    * The configuration manager.
61    *
62    * @var \Drupal\Core\Config\ConfigManagerInterface
63    */
64   protected $configManager;
65
66   /**
67    * The used lock backend instance.
68    *
69    * @var \Drupal\Core\Lock\LockBackendInterface
70    */
71   protected $lock;
72
73   /**
74    * The typed config manager.
75    *
76    * @var \Drupal\Core\Config\TypedConfigManagerInterface
77    */
78   protected $typedConfigManager;
79
80   /**
81    * List of configuration file changes processed by the import().
82    *
83    * @var array
84    */
85   protected $processedConfiguration;
86
87   /**
88    * List of extension changes processed by the import().
89    *
90    * @var array
91    */
92   protected $processedExtensions;
93
94   /**
95    * List of extension changes to be processed by the import().
96    *
97    * @var array
98    */
99   protected $extensionChangelist;
100
101   /**
102    * Indicates changes to import have been validated.
103    *
104    * @var bool
105    */
106   protected $validated;
107
108   /**
109    * The module handler.
110    *
111    * @var \Drupal\Core\Extension\ModuleHandlerInterface
112    */
113   protected $moduleHandler;
114
115   /**
116    * The theme handler.
117    *
118    * @var \Drupal\Core\Extension\ThemeHandlerInterface
119    */
120   protected $themeHandler;
121
122   /**
123    * Flag set to import system.theme during processing theme install and uninstalls.
124    *
125    * @var bool
126    */
127   protected $processedSystemTheme = FALSE;
128
129   /**
130    * A log of any errors encountered.
131    *
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.
135    *
136    * @var array
137    */
138   protected $errors = [];
139
140   /**
141    * The total number of extensions to process.
142    *
143    * @var int
144    */
145   protected $totalExtensionsToProcess = 0;
146
147   /**
148    * The total number of configuration objects to process.
149    *
150    * @var int
151    */
152   protected $totalConfigurationToProcess = 0;
153
154   /**
155    * The module installer.
156    *
157    * @var \Drupal\Core\Extension\ModuleInstallerInterface
158    */
159   protected $moduleInstaller;
160
161   /**
162    * Constructs a configuration import object.
163    *
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
176    *   The module handler
177    * @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer
178    *   The module installer.
179    * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
180    *   The theme handler
181    * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
182    *   The string translation service.
183    */
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;
188     $this->lock = $lock;
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();
196     }
197     $this->processedExtensions = $this->getEmptyExtensionsProcessedList();
198   }
199
200   /**
201    * Logs an error message.
202    *
203    * @param string $message
204    *   The message to log.
205    */
206   public function logError($message) {
207     $this->errors[] = $message;
208   }
209
210   /**
211    * Returns error messages created while running the import.
212    *
213    * @return array
214    *   List of messages.
215    */
216   public function getErrors() {
217     return $this->errors;
218   }
219
220   /**
221    * Gets the configuration storage comparer.
222    *
223    * @return \Drupal\Core\Config\StorageComparerInterface
224    *   Storage comparer object used to calculate configuration changes.
225    */
226   public function getStorageComparer() {
227     return $this->storageComparer;
228   }
229
230   /**
231    * Resets the storage comparer and processed list.
232    *
233    * @return \Drupal\Core\Config\ConfigImporter
234    *   The ConfigImporter instance.
235    */
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();
241     }
242     $this->extensionChangelist = $this->processedExtensions = $this->getEmptyExtensionsProcessedList();
243
244     $this->validated = FALSE;
245     $this->processedSystemTheme = FALSE;
246     return $this;
247   }
248
249   /**
250    * Gets an empty list of extensions to process.
251    *
252    * @return array
253    *   An empty list of extensions to process.
254    */
255   protected function getEmptyExtensionsProcessedList() {
256     return [
257       'module' => [
258         'install' => [],
259         'uninstall' => [],
260       ],
261       'theme' => [
262         'install' => [],
263         'uninstall' => [],
264       ],
265     ];
266   }
267
268   /**
269    * Checks if there are any unprocessed configuration changes.
270    *
271    * @return bool
272    *   TRUE if there are changes to process and FALSE if not.
273    */
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))) {
278           return TRUE;
279         }
280       }
281     }
282     return FALSE;
283   }
284
285   /**
286    * Gets list of processed changes.
287    *
288    * @param string $collection
289    *   (optional) The configuration collection to get processed changes for.
290    *   Defaults to the default collection.
291    *
292    * @return array
293    *   An array containing a list of processed changes.
294    */
295   public function getProcessedConfiguration($collection = StorageInterface::DEFAULT_COLLECTION) {
296     return $this->processedConfiguration[$collection];
297   }
298
299   /**
300    * Sets a change as processed.
301    *
302    * @param string $collection
303    *   The configuration collection to set a change as processed for.
304    * @param string $op
305    *   The change operation performed, either delete, create, rename, or update.
306    * @param string $name
307    *   The name of the configuration processed.
308    */
309   protected function setProcessedConfiguration($collection, $op, $name) {
310     $this->processedConfiguration[$collection][$op][] = $name;
311   }
312
313   /**
314    * Gets a list of unprocessed changes for a given operation.
315    *
316    * @param string $op
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.
322    *
323    * @return array
324    *   An array of configuration names.
325    */
326   public function getUnprocessedConfiguration($op, $collection = StorageInterface::DEFAULT_COLLECTION) {
327     return array_diff($this->storageComparer->getChangelist($op, $collection), $this->processedConfiguration[$collection][$op]);
328   }
329
330   /**
331    * Gets list of processed extension changes.
332    *
333    * @return array
334    *   An array containing a list of processed extension changes.
335    */
336   public function getProcessedExtensions() {
337     return $this->processedExtensions;
338   }
339
340   /**
341    * Sets an extension change as processed.
342    *
343    * @param string $type
344    *   The type of extension, either 'theme' or 'module'.
345    * @param string $op
346    *   The change operation performed, either install or uninstall.
347    * @param string $name
348    *   The name of the extension processed.
349    */
350   protected function setProcessedExtension($type, $op, $name) {
351     $this->processedExtensions[$type][$op][] = $name;
352   }
353
354   /**
355    * Populates the extension change list.
356    */
357   protected function createExtensionChangelist() {
358     // Create an empty changelist.
359     $this->extensionChangelist = $this->getEmptyExtensionsProcessedList();
360
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');
364
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) {
368       return;
369     }
370
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;
377     }, $module_list);
378
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:
385     // array(
386     //   'actions' => 0,
387     //   'ban' => 0,
388     //   'options' => -2,
389     //   'text' => -1,
390     // );
391     // will result in the following sort order:
392     // -2   options
393     // -1   text
394     //  0 0 ban
395     //  0 1 actions
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);
399
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);
407
408     // Work out what themes to install and to uninstall.
409     $this->extensionChangelist['theme']['install'] = array_keys(array_diff_key($new_extensions['theme'], $current_extensions['theme']));
410     $this->extensionChangelist['theme']['uninstall'] = array_keys(array_diff_key($current_extensions['theme'], $new_extensions['theme']));
411   }
412
413   /**
414    * Gets a list changes for extensions.
415    *
416    * @param string $type
417    *   The type of extension, either 'theme' or 'module'.
418    * @param string $op
419    *   The change operation to get the unprocessed list for, either install
420    *   or uninstall.
421    *
422    * @return array
423    *   An array of extension names.
424    */
425   public function getExtensionChangelist($type, $op = NULL) {
426     if ($op) {
427       return $this->extensionChangelist[$type][$op];
428     }
429     return $this->extensionChangelist[$type];
430   }
431
432   /**
433    * Gets a list of unprocessed changes for extensions.
434    *
435    * @param string $type
436    *   The type of extension, either 'theme' or 'module'.
437    *
438    * @return array
439    *   An array of extension names.
440    */
441   protected function getUnprocessedExtensions($type) {
442     $changelist = $this->getExtensionChangelist($type);
443     return [
444       'install' => array_diff($changelist['install'], $this->processedExtensions[$type]['install']),
445       'uninstall' => array_diff($changelist['uninstall'], $this->processedExtensions[$type]['uninstall']),
446     ];
447   }
448
449   /**
450    * Imports the changelist to the target storage.
451    *
452    * @return \Drupal\Core\Config\ConfigImporter
453    *   The ConfigImporter instance.
454    *
455    * @throws \Drupal\Core\Config\ConfigException
456    */
457   public function import() {
458     if ($this->hasUnprocessedConfigurationChanges()) {
459       $sync_steps = $this->initialize();
460
461       foreach ($sync_steps as $step) {
462         $context = [];
463         do {
464           $this->doSyncStep($step, $context);
465         } while ($context['finished'] < 1);
466       }
467     }
468     return $this;
469   }
470
471   /**
472    * Calls a config import step.
473    *
474    * @param string|callable $sync_step
475    *   The step to do. Either a method on the ConfigImporter class or a
476    *   callable.
477    * @param array $context
478    *   A batch context array. If the config importer is not running in a batch
479    *   the only array key that is used is $context['finished']. A process needs
480    *   to set $context['finished'] = 1 when it is done.
481    *
482    * @throws \InvalidArgumentException
483    *   Exception thrown if the $sync_step can not be called.
484    */
485   public function doSyncStep($sync_step, &$context) {
486     if (!is_array($sync_step) && method_exists($this, $sync_step)) {
487       \Drupal::service('config.installer')->setSyncing(TRUE);
488       $this->$sync_step($context);
489     }
490     elseif (is_callable($sync_step)) {
491       \Drupal::service('config.installer')->setSyncing(TRUE);
492       call_user_func_array($sync_step, [&$context, $this]);
493     }
494     else {
495       throw new \InvalidArgumentException('Invalid configuration synchronization step');
496     }
497     \Drupal::service('config.installer')->setSyncing(FALSE);
498   }
499
500   /**
501    * Initializes the config importer in preparation for processing a batch.
502    *
503    * @return array
504    *   An array of \Drupal\Core\Config\ConfigImporter method names and callables
505    *   that are invoked to complete the import. If there are modules or themes
506    *   to process then an extra step is added.
507    *
508    * @throws \Drupal\Core\Config\ConfigImporterException
509    *   If the configuration is already importing.
510    */
511   public function initialize() {
512     // Ensure that the changes have been validated.
513     $this->validate();
514
515     if (!$this->lock->acquire(static::LOCK_NAME)) {
516       // Another process is synchronizing configuration.
517       throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_NAME));
518     }
519
520     $sync_steps = [];
521     $modules = $this->getUnprocessedExtensions('module');
522     foreach (['install', 'uninstall'] as $op) {
523       $this->totalExtensionsToProcess += count($modules[$op]);
524     }
525     $themes = $this->getUnprocessedExtensions('theme');
526     foreach (['install', 'uninstall'] as $op) {
527       $this->totalExtensionsToProcess += count($themes[$op]);
528     }
529
530     // We have extensions to process.
531     if ($this->totalExtensionsToProcess > 0) {
532       $sync_steps[] = 'processExtensions';
533     }
534     $sync_steps[] = 'processConfigurations';
535     $sync_steps[] = 'processMissingContent';
536     // Allow modules to add new steps to configuration synchronization.
537     $this->moduleHandler->alter('config_import_steps', $sync_steps, $this);
538     $sync_steps[] = 'finish';
539     return $sync_steps;
540   }
541
542   /**
543    * Processes extensions as a batch operation.
544    *
545    * @param array|\ArrayAccess $context
546    *   The batch context.
547    */
548   protected function processExtensions(&$context) {
549     $operation = $this->getNextExtensionOperation();
550     if (!empty($operation)) {
551       $this->processExtension($operation['type'], $operation['op'], $operation['name']);
552       $context['message'] = t('Synchronizing extensions: @op @name.', ['@op' => $operation['op'], '@name' => $operation['name']]);
553       $processed_count = count($this->processedExtensions['module']['install']) + count($this->processedExtensions['module']['uninstall']);
554       $processed_count += count($this->processedExtensions['theme']['uninstall']) + count($this->processedExtensions['theme']['install']);
555       $context['finished'] = $processed_count / $this->totalExtensionsToProcess;
556     }
557     else {
558       $context['finished'] = 1;
559     }
560   }
561
562   /**
563    * Processes configuration as a batch operation.
564    *
565    * @param array|\ArrayAccess $context
566    *   The batch context.
567    */
568   protected function processConfigurations(&$context) {
569     // The first time this is called we need to calculate the total to process.
570     // This involves recalculating the changelist which will ensure that if
571     // extensions have been processed any configuration affected will be taken
572     // into account.
573     if ($this->totalConfigurationToProcess == 0) {
574       $this->storageComparer->reset();
575       foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
576         foreach (['delete', 'create', 'rename', 'update'] as $op) {
577           $this->totalConfigurationToProcess += count($this->getUnprocessedConfiguration($op, $collection));
578         }
579       }
580     }
581     $operation = $this->getNextConfigurationOperation();
582     if (!empty($operation)) {
583       if ($this->checkOp($operation['collection'], $operation['op'], $operation['name'])) {
584         $this->processConfiguration($operation['collection'], $operation['op'], $operation['name']);
585       }
586       if ($operation['collection'] == StorageInterface::DEFAULT_COLLECTION) {
587         $context['message'] = $this->t('Synchronizing configuration: @op @name.', ['@op' => $operation['op'], '@name' => $operation['name']]);
588       }
589       else {
590         $context['message'] = $this->t('Synchronizing configuration: @op @name in @collection.', ['@op' => $operation['op'], '@name' => $operation['name'], '@collection' => $operation['collection']]);
591       }
592       $processed_count = 0;
593       foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
594         foreach (['delete', 'create', 'rename', 'update'] as $op) {
595           $processed_count += count($this->processedConfiguration[$collection][$op]);
596         }
597       }
598       $context['finished'] = $processed_count / $this->totalConfigurationToProcess;
599     }
600     else {
601       $context['finished'] = 1;
602     }
603   }
604
605   /**
606    * Handles processing of missing content.
607    *
608    * @param array|\ArrayAccess $context
609    *   Standard batch context.
610    */
611   protected function processMissingContent(&$context) {
612     $sandbox = &$context['sandbox']['config'];
613     if (!isset($sandbox['missing_content'])) {
614       $missing_content = $this->configManager->findMissingContentDependencies();
615       $sandbox['missing_content']['data'] = $missing_content;
616       $sandbox['missing_content']['total'] = count($missing_content);
617     }
618     else {
619       $missing_content = $sandbox['missing_content']['data'];
620     }
621     if (!empty($missing_content)) {
622       $event = new MissingContentEvent($missing_content);
623       // Fire an event to allow listeners to create the missing content.
624       $this->eventDispatcher->dispatch(ConfigEvents::IMPORT_MISSING_CONTENT, $event);
625       $sandbox['missing_content']['data'] = $event->getMissingContent();
626     }
627     $current_count = count($sandbox['missing_content']['data']);
628     if ($current_count) {
629       $context['message'] = $this->t('Resolving missing content');
630       $context['finished'] = ($sandbox['missing_content']['total'] - $current_count) / $sandbox['missing_content']['total'];
631     }
632     else {
633       $context['finished'] = 1;
634     }
635   }
636
637   /**
638    * Finishes the batch.
639    *
640    * @param array|\ArrayAccess $context
641    *   The batch context.
642    */
643   protected function finish(&$context) {
644     $this->eventDispatcher->dispatch(ConfigEvents::IMPORT, new ConfigImporterEvent($this));
645     // The import is now complete.
646     $this->lock->release(static::LOCK_NAME);
647     $this->reset();
648     $context['message'] = t('Finalizing configuration synchronization.');
649     $context['finished'] = 1;
650   }
651
652   /**
653    * Gets the next extension operation to perform.
654    *
655    * @return array|bool
656    *   An array containing the next operation and extension name to perform it
657    *   on. If there is nothing left to do returns FALSE;
658    */
659   protected function getNextExtensionOperation() {
660     foreach (['module', 'theme'] as $type) {
661       foreach (['install', 'uninstall'] as $op) {
662         $unprocessed = $this->getUnprocessedExtensions($type);
663         if (!empty($unprocessed[$op])) {
664           return [
665             'op' => $op,
666             'type' => $type,
667             'name' => array_shift($unprocessed[$op]),
668           ];
669         }
670       }
671     }
672     return FALSE;
673   }
674
675   /**
676    * Gets the next configuration operation to perform.
677    *
678    * @return array|bool
679    *   An array containing the next operation and configuration name to perform
680    *   it on. If there is nothing left to do returns FALSE;
681    */
682   protected function getNextConfigurationOperation() {
683     // The order configuration operations is processed is important. Deletes
684     // have to come first so that recreates can work.
685     foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
686       foreach (['delete', 'create', 'rename', 'update'] as $op) {
687         $config_names = $this->getUnprocessedConfiguration($op, $collection);
688         if (!empty($config_names)) {
689           return [
690             'op' => $op,
691             'name' => array_shift($config_names),
692             'collection' => $collection,
693           ];
694         }
695       }
696     }
697     return FALSE;
698   }
699
700   /**
701    * Dispatches validate event for a ConfigImporter object.
702    *
703    * Events should throw a \Drupal\Core\Config\ConfigImporterException to
704    * prevent an import from occurring.
705    *
706    * @throws \Drupal\Core\Config\ConfigImporterException
707    *   Exception thrown if the validate event logged any errors.
708    */
709   public function validate() {
710     if (!$this->validated) {
711       // Create the list of installs and uninstalls.
712       $this->createExtensionChangelist();
713       // Validate renames.
714       foreach ($this->getUnprocessedConfiguration('rename') as $name) {
715         $names = $this->storageComparer->extractRenameNames($name);
716         $old_entity_type_id = $this->configManager->getEntityTypeIdByName($names['old_name']);
717         $new_entity_type_id = $this->configManager->getEntityTypeIdByName($names['new_name']);
718         if ($old_entity_type_id != $new_entity_type_id) {
719           $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']]));
720         }
721         // Has to be a configuration entity.
722         if (!$old_entity_type_id) {
723           $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']]));
724         }
725       }
726       $this->eventDispatcher->dispatch(ConfigEvents::IMPORT_VALIDATE, new ConfigImporterEvent($this));
727       if (count($this->getErrors())) {
728         throw new ConfigImporterException('There were errors validating the config synchronization.');
729       }
730       else {
731         $this->validated = TRUE;
732       }
733     }
734     return $this;
735   }
736
737   /**
738    * Processes a configuration change.
739    *
740    * @param string $collection
741    *   The configuration collection to process changes for.
742    * @param string $op
743    *   The change operation.
744    * @param string $name
745    *   The name of the configuration to process.
746    *
747    * @throws \Exception
748    *   Thrown when the import process fails, only thrown when no importer log is
749    *   set, otherwise the exception message is logged and the configuration
750    *   is skipped.
751    */
752   protected function processConfiguration($collection, $op, $name) {
753     try {
754       $processed = FALSE;
755       if ($collection == StorageInterface::DEFAULT_COLLECTION) {
756         $processed = $this->importInvokeOwner($collection, $op, $name);
757       }
758       if (!$processed) {
759         $this->importConfig($collection, $op, $name);
760       }
761     }
762     catch (\Exception $e) {
763       $this->logError($this->t('Unexpected error during import with operation @op for @name: @message', ['@op' => $op, '@name' => $name, '@message' => $e->getMessage()]));
764       // Error for that operation was logged, mark it as processed so that
765       // the import can continue.
766       $this->setProcessedConfiguration($collection, $op, $name);
767     }
768   }
769
770   /**
771    * Processes an extension change.
772    *
773    * @param string $type
774    *   The type of extension, either 'module' or 'theme'.
775    * @param string $op
776    *   The change operation.
777    * @param string $name
778    *   The name of the extension to process.
779    */
780   protected function processExtension($type, $op, $name) {
781     // Set the config installer to use the sync directory instead of the
782     // extensions own default config directories.
783     \Drupal::service('config.installer')
784       ->setSourceStorage($this->storageComparer->getSourceStorage());
785     if ($type == 'module') {
786       $this->moduleInstaller->$op([$name], FALSE);
787       // Installing a module can cause a kernel boot therefore reinject all the
788       // services.
789       $this->reInjectMe();
790       // During a module install or uninstall the container is rebuilt and the
791       // module handler is called from drupal_get_complete_schema(). This causes
792       // the container's instance of the module handler not to have loaded all
793       // the enabled modules.
794       $this->moduleHandler->loadAll();
795     }
796     if ($type == 'theme') {
797       // Theme uninstalls possible remove default or admin themes therefore we
798       // need to import this before doing any. If there are no uninstalls and
799       // the default or admin theme is changing this will be picked up whilst
800       // processing configuration.
801       if ($op == 'uninstall' && $this->processedSystemTheme === FALSE) {
802         $this->importConfig(StorageInterface::DEFAULT_COLLECTION, 'update', 'system.theme');
803         $this->configManager->getConfigFactory()->reset('system.theme');
804         $this->processedSystemTheme = TRUE;
805       }
806       $this->themeHandler->$op([$name]);
807     }
808
809     $this->setProcessedExtension($type, $op, $name);
810   }
811
812   /**
813    * Checks that the operation is still valid.
814    *
815    * During a configuration import secondary writes and deletes are possible.
816    * This method checks that the operation is still valid before processing a
817    * configuration change.
818    *
819    * @param string $collection
820    *   The configuration collection.
821    * @param string $op
822    *   The change operation.
823    * @param string $name
824    *   The name of the configuration to process.
825    *
826    * @return bool
827    *   TRUE is to continue processing, FALSE otherwise.
828    *
829    * @throws \Drupal\Core\Config\ConfigImporterException
830    */
831   protected function checkOp($collection, $op, $name) {
832     if ($op == 'rename') {
833       $names = $this->storageComparer->extractRenameNames($name);
834       $target_exists = $this->storageComparer->getTargetStorage($collection)->exists($names['new_name']);
835       if ($target_exists) {
836         // If the target exists, the rename has already occurred as the
837         // result of a secondary configuration write. Change the operation
838         // into an update. This is the desired behavior since renames often
839         // have to occur together. For example, renaming a node type must
840         // also result in renaming its fields and entity displays.
841         $this->storageComparer->moveRenameToUpdate($name);
842         return FALSE;
843       }
844       return TRUE;
845     }
846     $target_exists = $this->storageComparer->getTargetStorage($collection)->exists($name);
847     switch ($op) {
848       case 'delete':
849         if (!$target_exists) {
850           // The configuration has already been deleted. For example, a field
851           // is automatically deleted if all the instances are.
852           $this->setProcessedConfiguration($collection, $op, $name);
853           return FALSE;
854         }
855         break;
856
857       case 'create':
858         if ($target_exists) {
859           // If the target already exists, use the entity storage to delete it
860           // again, if is a simple config, delete it directly.
861           if ($entity_type_id = $this->configManager->getEntityTypeIdByName($name)) {
862             $entity_storage = $this->configManager->getEntityManager()->getStorage($entity_type_id);
863             $entity_type = $this->configManager->getEntityManager()->getDefinition($entity_type_id);
864             $entity = $entity_storage->load($entity_storage->getIDFromConfigName($name, $entity_type->getConfigPrefix()));
865             $entity->delete();
866             $this->logError($this->t('Deleted and replaced configuration entity "@name"', ['@name' => $name]));
867           }
868           else {
869             $this->storageComparer->getTargetStorage($collection)->delete($name);
870             $this->logError($this->t('Deleted and replaced configuration "@name"', ['@name' => $name]));
871           }
872           return TRUE;
873         }
874         break;
875
876       case 'update':
877         if (!$target_exists) {
878           $this->logError($this->t('Update target "@name" is missing.', ['@name' => $name]));
879           // Mark as processed so that the synchronization continues. Once the
880           // the current synchronization is complete it will show up as a
881           // create.
882           $this->setProcessedConfiguration($collection, $op, $name);
883           return FALSE;
884         }
885         break;
886     }
887     return TRUE;
888   }
889
890   /**
891    * Writes a configuration change from the source to the target storage.
892    *
893    * @param string $collection
894    *   The configuration collection.
895    * @param string $op
896    *   The change operation.
897    * @param string $name
898    *   The name of the configuration to process.
899    */
900   protected function importConfig($collection, $op, $name) {
901     // Allow config factory overriders to use a custom configuration object if
902     // they are responsible for the collection.
903     $overrider = $this->configManager->getConfigCollectionInfo()->getOverrideService($collection);
904     if ($overrider) {
905       $config = $overrider->createConfigObject($name, $collection);
906     }
907     else {
908       $config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
909     }
910     if ($op == 'delete') {
911       $config->delete();
912     }
913     else {
914       $data = $this->storageComparer->getSourceStorage($collection)->read($name);
915       $config->setData($data ? $data : []);
916       $config->save();
917     }
918     $this->setProcessedConfiguration($collection, $op, $name);
919   }
920
921   /**
922    * Invokes import* methods on configuration entity storage.
923    *
924    * Allow modules to take over configuration change operations for higher-level
925    * configuration data.
926    *
927    * @todo Add support for other extension types; e.g., themes etc.
928    *
929    * @param string $collection
930    *   The configuration collection.
931    * @param string $op
932    *   The change operation to get the unprocessed list for, either delete,
933    *   create, rename, or update.
934    * @param string $name
935    *   The name of the configuration to process.
936    *
937    * @return bool
938    *   TRUE if the configuration was imported as a configuration entity. FALSE
939    *   otherwise.
940    *
941    * @throws \Drupal\Core\Entity\EntityStorageException
942    *   Thrown if the data is owned by an entity type, but the entity storage
943    *   does not support imports.
944    */
945   protected function importInvokeOwner($collection, $op, $name) {
946     // Renames are handled separately.
947     if ($op == 'rename') {
948       return $this->importInvokeRename($collection, $name);
949     }
950     // Validate the configuration object name before importing it.
951     // Config::validateName($name);
952     if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) {
953       $old_config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
954       if ($old_data = $this->storageComparer->getTargetStorage($collection)->read($name)) {
955         $old_config->initWithData($old_data);
956       }
957
958       $data = $this->storageComparer->getSourceStorage($collection)->read($name);
959       $new_config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
960       if ($data !== FALSE) {
961         $new_config->setData($data);
962       }
963
964       $method = 'import' . ucfirst($op);
965       $entity_storage = $this->configManager->getEntityManager()->getStorage($entity_type);
966       // Call to the configuration entity's storage to handle the configuration
967       // change.
968       if (!($entity_storage instanceof ImportableEntityStorageInterface)) {
969         throw new EntityStorageException(sprintf('The entity storage "%s" for the "%s" entity type does not support imports', get_class($entity_storage), $entity_type));
970       }
971       $entity_storage->$method($name, $new_config, $old_config);
972       $this->setProcessedConfiguration($collection, $op, $name);
973       return TRUE;
974     }
975     return FALSE;
976   }
977
978   /**
979    * Imports a configuration entity rename.
980    *
981    * @param string $collection
982    *   The configuration collection.
983    * @param string $rename_name
984    *   The rename configuration name, as provided by
985    *   \Drupal\Core\Config\StorageComparer::createRenameName().
986    *
987    * @return bool
988    *   TRUE if the configuration was imported as a configuration entity. FALSE
989    *   otherwise.
990    *
991    * @throws \Drupal\Core\Entity\EntityStorageException
992    *   Thrown if the data is owned by an entity type, but the entity storage
993    *   does not support imports.
994    *
995    * @see \Drupal\Core\Config\ConfigImporter::createRenameName()
996    */
997   protected function importInvokeRename($collection, $rename_name) {
998     $names = $this->storageComparer->extractRenameNames($rename_name);
999     $entity_type_id = $this->configManager->getEntityTypeIdByName($names['old_name']);
1000     $old_config = new Config($names['old_name'], $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
1001     if ($old_data = $this->storageComparer->getTargetStorage($collection)->read($names['old_name'])) {
1002       $old_config->initWithData($old_data);
1003     }
1004
1005     $data = $this->storageComparer->getSourceStorage($collection)->read($names['new_name']);
1006     $new_config = new Config($names['new_name'], $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
1007     if ($data !== FALSE) {
1008       $new_config->setData($data);
1009     }
1010
1011     $entity_storage = $this->configManager->getEntityManager()->getStorage($entity_type_id);
1012     // Call to the configuration entity's storage to handle the configuration
1013     // change.
1014     if (!($entity_storage instanceof ImportableEntityStorageInterface)) {
1015       throw new EntityStorageException(sprintf("The entity storage '%s' for the '%s' entity type does not support imports", get_class($entity_storage), $entity_type_id));
1016     }
1017     $entity_storage->importRename($names['old_name'], $new_config, $old_config);
1018     $this->setProcessedConfiguration($collection, 'rename', $rename_name);
1019     return TRUE;
1020   }
1021
1022   /**
1023    * Determines if a import is already running.
1024    *
1025    * @return bool
1026    *   TRUE if an import is already running, FALSE if not.
1027    */
1028   public function alreadyImporting() {
1029     return !$this->lock->lockMayBeAvailable(static::LOCK_NAME);
1030   }
1031
1032   /**
1033    * Gets all the service dependencies from \Drupal.
1034    *
1035    * Since the ConfigImporter handles module installation the kernel and the
1036    * container can be rebuilt and altered during processing. It is necessary to
1037    * keep the services used by the importer in sync.
1038    */
1039   protected function reInjectMe() {
1040     $this->_serviceIds = [];
1041     $vars = get_object_vars($this);
1042     foreach ($vars as $key => $value) {
1043       if (is_object($value) && isset($value->_serviceId)) {
1044         $this->$key = \Drupal::service($value->_serviceId);
1045       }
1046     }
1047   }
1048
1049 }