0f9eae967ce07f6edf9f5c13dd27ec90b7ed437e
[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     // 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'];
414     }
415
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']));
419   }
420
421   /**
422    * Gets a list changes for extensions.
423    *
424    * @param string $type
425    *   The type of extension, either 'theme' or 'module'.
426    * @param string $op
427    *   The change operation to get the unprocessed list for, either install
428    *   or uninstall.
429    *
430    * @return array
431    *   An array of extension names.
432    */
433   public function getExtensionChangelist($type, $op = NULL) {
434     if ($op) {
435       return $this->extensionChangelist[$type][$op];
436     }
437     return $this->extensionChangelist[$type];
438   }
439
440   /**
441    * Gets a list of unprocessed changes for extensions.
442    *
443    * @param string $type
444    *   The type of extension, either 'theme' or 'module'.
445    *
446    * @return array
447    *   An array of extension names.
448    */
449   protected function getUnprocessedExtensions($type) {
450     $changelist = $this->getExtensionChangelist($type);
451     return [
452       'install' => array_diff($changelist['install'], $this->processedExtensions[$type]['install']),
453       'uninstall' => array_diff($changelist['uninstall'], $this->processedExtensions[$type]['uninstall']),
454     ];
455   }
456
457   /**
458    * Imports the changelist to the target storage.
459    *
460    * @return \Drupal\Core\Config\ConfigImporter
461    *   The ConfigImporter instance.
462    *
463    * @throws \Drupal\Core\Config\ConfigException
464    */
465   public function import() {
466     if ($this->hasUnprocessedConfigurationChanges()) {
467       $sync_steps = $this->initialize();
468
469       foreach ($sync_steps as $step) {
470         $context = [];
471         do {
472           $this->doSyncStep($step, $context);
473         } while ($context['finished'] < 1);
474       }
475     }
476     return $this;
477   }
478
479   /**
480    * Calls a config import step.
481    *
482    * @param string|callable $sync_step
483    *   The step to do. Either a method on the ConfigImporter class or a
484    *   callable.
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.
489    *
490    * @throws \InvalidArgumentException
491    *   Exception thrown if the $sync_step can not be called.
492    */
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);
497     }
498     elseif (is_callable($sync_step)) {
499       \Drupal::service('config.installer')->setSyncing(TRUE);
500       call_user_func_array($sync_step, [&$context, $this]);
501     }
502     else {
503       throw new \InvalidArgumentException('Invalid configuration synchronization step');
504     }
505     \Drupal::service('config.installer')->setSyncing(FALSE);
506   }
507
508   /**
509    * Initializes the config importer in preparation for processing a batch.
510    *
511    * @return array
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.
515    *
516    * @throws \Drupal\Core\Config\ConfigImporterException
517    *   If the configuration is already importing.
518    */
519   public function initialize() {
520     // Ensure that the changes have been validated.
521     $this->validate();
522
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));
526     }
527
528     $sync_steps = [];
529     $modules = $this->getUnprocessedExtensions('module');
530     foreach (['install', 'uninstall'] as $op) {
531       $this->totalExtensionsToProcess += count($modules[$op]);
532     }
533     $themes = $this->getUnprocessedExtensions('theme');
534     foreach (['install', 'uninstall'] as $op) {
535       $this->totalExtensionsToProcess += count($themes[$op]);
536     }
537
538     // We have extensions to process.
539     if ($this->totalExtensionsToProcess > 0) {
540       $sync_steps[] = 'processExtensions';
541     }
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';
547     return $sync_steps;
548   }
549
550   /**
551    * Processes extensions as a batch operation.
552    *
553    * @param array|\ArrayAccess $context
554    *   The batch context.
555    */
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;
564     }
565     else {
566       $context['finished'] = 1;
567     }
568   }
569
570   /**
571    * Processes configuration as a batch operation.
572    *
573    * @param array|\ArrayAccess $context
574    *   The batch context.
575    */
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
580     // into account.
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));
586         }
587       }
588     }
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']);
593       }
594       if ($operation['collection'] == StorageInterface::DEFAULT_COLLECTION) {
595         $context['message'] = $this->t('Synchronizing configuration: @op @name.', ['@op' => $operation['op'], '@name' => $operation['name']]);
596       }
597       else {
598         $context['message'] = $this->t('Synchronizing configuration: @op @name in @collection.', ['@op' => $operation['op'], '@name' => $operation['name'], '@collection' => $operation['collection']]);
599       }
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]);
604         }
605       }
606       $context['finished'] = $processed_count / $this->totalConfigurationToProcess;
607     }
608     else {
609       $context['finished'] = 1;
610     }
611   }
612
613   /**
614    * Handles processing of missing content.
615    *
616    * @param array|\ArrayAccess $context
617    *   Standard batch context.
618    */
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);
625     }
626     else {
627       $missing_content = $sandbox['missing_content']['data'];
628     }
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();
634     }
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'];
639     }
640     else {
641       $context['finished'] = 1;
642     }
643   }
644
645   /**
646    * Finishes the batch.
647    *
648    * @param array|\ArrayAccess $context
649    *   The batch context.
650    */
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);
655     $this->reset();
656     $context['message'] = t('Finalizing configuration synchronization.');
657     $context['finished'] = 1;
658   }
659
660   /**
661    * Gets the next extension operation to perform.
662    *
663    * @return array|bool
664    *   An array containing the next operation and extension name to perform it
665    *   on. If there is nothing left to do returns FALSE;
666    */
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])) {
672           return [
673             'op' => $op,
674             'type' => $type,
675             'name' => array_shift($unprocessed[$op]),
676           ];
677         }
678       }
679     }
680     return FALSE;
681   }
682
683   /**
684    * Gets the next configuration operation to perform.
685    *
686    * @return array|bool
687    *   An array containing the next operation and configuration name to perform
688    *   it on. If there is nothing left to do returns FALSE;
689    */
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)) {
697           return [
698             'op' => $op,
699             'name' => array_shift($config_names),
700             'collection' => $collection,
701           ];
702         }
703       }
704     }
705     return FALSE;
706   }
707
708   /**
709    * Dispatches validate event for a ConfigImporter object.
710    *
711    * Events should throw a \Drupal\Core\Config\ConfigImporterException to
712    * prevent an import from occurring.
713    *
714    * @throws \Drupal\Core\Config\ConfigImporterException
715    *   Exception thrown if the validate event logged any errors.
716    */
717   public function validate() {
718     if (!$this->validated) {
719       // Create the list of installs and uninstalls.
720       $this->createExtensionChangelist();
721       // Validate renames.
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']]));
728         }
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']]));
732         }
733       }
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));
738       }
739       else {
740         $this->validated = TRUE;
741       }
742     }
743     return $this;
744   }
745
746   /**
747    * Processes a configuration change.
748    *
749    * @param string $collection
750    *   The configuration collection to process changes for.
751    * @param string $op
752    *   The change operation.
753    * @param string $name
754    *   The name of the configuration to process.
755    *
756    * @throws \Exception
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
759    *   is skipped.
760    */
761   protected function processConfiguration($collection, $op, $name) {
762     try {
763       $processed = FALSE;
764       if ($collection == StorageInterface::DEFAULT_COLLECTION) {
765         $processed = $this->importInvokeOwner($collection, $op, $name);
766       }
767       if (!$processed) {
768         $this->importConfig($collection, $op, $name);
769       }
770     }
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);
776     }
777   }
778
779   /**
780    * Processes an extension change.
781    *
782    * @param string $type
783    *   The type of extension, either 'module' or 'theme'.
784    * @param string $op
785    *   The change operation.
786    * @param string $name
787    *   The name of the extension to process.
788    */
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
797       // services.
798       $this->reInjectMe();
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();
803     }
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;
813       }
814       $this->themeHandler->$op([$name]);
815     }
816
817     $this->setProcessedExtension($type, $op, $name);
818   }
819
820   /**
821    * Checks that the operation is still valid.
822    *
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.
826    *
827    * @param string $collection
828    *   The configuration collection.
829    * @param string $op
830    *   The change operation.
831    * @param string $name
832    *   The name of the configuration to process.
833    *
834    * @return bool
835    *   TRUE is to continue processing, FALSE otherwise.
836    *
837    * @throws \Drupal\Core\Config\ConfigImporterException
838    */
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);
850         return FALSE;
851       }
852       return TRUE;
853     }
854     $target_exists = $this->storageComparer->getTargetStorage($collection)->exists($name);
855     switch ($op) {
856       case 'delete':
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);
861           return FALSE;
862         }
863         break;
864
865       case 'create':
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()));
873             $entity->delete();
874             $this->logError($this->t('Deleted and replaced configuration entity "@name"', ['@name' => $name]));
875           }
876           else {
877             $this->storageComparer->getTargetStorage($collection)->delete($name);
878             $this->logError($this->t('Deleted and replaced configuration "@name"', ['@name' => $name]));
879           }
880           return TRUE;
881         }
882         break;
883
884       case 'update':
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
889           // create.
890           $this->setProcessedConfiguration($collection, $op, $name);
891           return FALSE;
892         }
893         break;
894     }
895     return TRUE;
896   }
897
898   /**
899    * Writes a configuration change from the source to the target storage.
900    *
901    * @param string $collection
902    *   The configuration collection.
903    * @param string $op
904    *   The change operation.
905    * @param string $name
906    *   The name of the configuration to process.
907    */
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);
912     if ($overrider) {
913       $config = $overrider->createConfigObject($name, $collection);
914     }
915     else {
916       $config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
917     }
918     if ($op == 'delete') {
919       $config->delete();
920     }
921     else {
922       $data = $this->storageComparer->getSourceStorage($collection)->read($name);
923       $config->setData($data ? $data : []);
924       $config->save();
925     }
926     $this->setProcessedConfiguration($collection, $op, $name);
927   }
928
929   /**
930    * Invokes import* methods on configuration entity storage.
931    *
932    * Allow modules to take over configuration change operations for higher-level
933    * configuration data.
934    *
935    * @todo Add support for other extension types; e.g., themes etc.
936    *
937    * @param string $collection
938    *   The configuration collection.
939    * @param string $op
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.
944    *
945    * @return bool
946    *   TRUE if the configuration was imported as a configuration entity. FALSE
947    *   otherwise.
948    *
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.
952    */
953   protected function importInvokeOwner($collection, $op, $name) {
954     // Renames are handled separately.
955     if ($op == 'rename') {
956       return $this->importInvokeRename($collection, $name);
957     }
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);
964       }
965
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);
970       }
971
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
975       // change.
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));
978       }
979       $entity_storage->$method($name, $new_config, $old_config);
980       $this->setProcessedConfiguration($collection, $op, $name);
981       return TRUE;
982     }
983     return FALSE;
984   }
985
986   /**
987    * Imports a configuration entity rename.
988    *
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().
994    *
995    * @return bool
996    *   TRUE if the configuration was imported as a configuration entity. FALSE
997    *   otherwise.
998    *
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.
1002    *
1003    * @see \Drupal\Core\Config\ConfigImporter::createRenameName()
1004    */
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);
1011     }
1012
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);
1017     }
1018
1019     $entity_storage = $this->configManager->getEntityManager()->getStorage($entity_type_id);
1020     // Call to the configuration entity's storage to handle the configuration
1021     // change.
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));
1024     }
1025     $entity_storage->importRename($names['old_name'], $new_config, $old_config);
1026     $this->setProcessedConfiguration($collection, 'rename', $rename_name);
1027     return TRUE;
1028   }
1029
1030   /**
1031    * Determines if a import is already running.
1032    *
1033    * @return bool
1034    *   TRUE if an import is already running, FALSE if not.
1035    */
1036   public function alreadyImporting() {
1037     return !$this->lock->lockMayBeAvailable(static::LOCK_NAME);
1038   }
1039
1040   /**
1041    * Gets all the service dependencies from \Drupal.
1042    *
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.
1046    */
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);
1053       }
1054     }
1055   }
1056
1057 }