ConfigImporter.php 36.8 KB
Newer Older
1 2 3 4 5 6 7 8 9
<?php

/**
 * @file
 * Contains \Drupal\Core\Config\ConfigImporter.
 */

namespace Drupal\Core\Config;

10 11
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
12 13
use Drupal\Component\Utility\String;
use Drupal\Core\Config\Entity\ImportableEntityStorageInterface;
14
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
15
use Drupal\Core\Entity\EntityStorageException;
16
use Drupal\Core\Lock\LockBackendInterface;
17 18
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
19 20 21 22 23 24 25 26 27 28 29 30
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Defines a configuration importer.
 *
 * A config importer imports the changes into the configuration system. To
 * determine which changes to import a StorageComparer in used.
 *
 * @see \Drupal\Core\Config\StorageComparerInterface
 *
 * The ConfigImporter has a identifier which is used to construct event names.
 * The events fired during an import are:
31
 * - ConfigEvents::IMPORT_VALIDATE: Events listening can throw a
32 33 34
 *   \Drupal\Core\Config\ConfigImporterException to prevent an import from
 *   occurring.
 *   @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
35
 * - ConfigEvents::IMPORT: Events listening can react to a successful import.
36 37 38 39
 *   @see \Drupal\Core\EventSubscriber\ConfigSnapshotSubscriber
 *
 * @see \Drupal\Core\Config\ConfigImporterEvent
 */
40
class ConfigImporter {
41
  use StringTranslationTrait;
42
  use DependencySerializationTrait;
43 44

  /**
45
   * The name used to identify the lock.
46
   */
47
  const LOCK_ID = 'config_importer';
48 49 50 51 52 53 54 55 56 57 58

  /**
   * The storage comparer used to discover configuration changes.
   *
   * @var \Drupal\Core\Config\StorageComparerInterface
   */
  protected $storageComparer;

  /**
   * The event dispatcher used to notify subscribers.
   *
59
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
60 61 62 63
   */
  protected $eventDispatcher;

  /**
64
   * The configuration manager.
65
   *
66
   * @var \Drupal\Core\Config\ConfigManagerInterface
67
   */
68
  protected $configManager;
69 70

  /**
71
   * The used lock backend instance.
72
   *
73
   * @var \Drupal\Core\Lock\LockBackendInterface
74
   */
75
  protected $lock;
76 77

  /**
78
   * The typed config manager.
79
   *
80
   * @var \Drupal\Core\Config\TypedConfigManagerInterface
81
   */
82
  protected $typedConfigManager;
83 84

  /**
85
   * List of configuration file changes processed by the import().
86 87 88
   *
   * @var array
   */
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
  protected $processedConfiguration;

  /**
   * List of extension changes processed by the import().
   *
   * @var array
   */
  protected $processedExtensions;

  /**
   * List of extension changes to be processed by the import().
   *
   * @var array
   */
  protected $extensionChangelist;
104 105 106 107 108 109 110 111

  /**
   * Indicates changes to import have been validated.
   *
   * @var bool
   */
  protected $validated;

112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The theme handler.
   *
   * @var \Drupal\Core\Extension\ThemeHandlerInterface
   */
  protected $themeHandler;

  /**
127
   * Flag set to import system.theme during processing theme install and uninstalls.
128 129 130 131 132
   *
   * @var bool
   */
  protected $processedSystemTheme = FALSE;

133
  /**
134 135 136 137 138
   * A log of any errors encountered.
   *
   * If errors are logged during the validation event the configuration
   * synchronization will not occur. If errors occur during an import then best
   * efforts are made to complete the synchronization.
139 140 141 142 143
   *
   * @var array
   */
  protected $errors = array();

144 145 146 147 148 149 150 151 152 153 154 155 156 157
  /**
   * The total number of extensions to process.
   *
   * @var int
   */
  protected $totalExtensionsToProcess = 0;

  /**
   * The total number of configuration objects to process.
   *
   * @var int
   */
  protected $totalConfigurationToProcess = 0;

158 159 160 161
  /**
   * Constructs a configuration import object.
   *
   * @param \Drupal\Core\Config\StorageComparerInterface $storage_comparer
162
   *   A storage comparer object used to determine configuration changes and
163 164 165
   *   access the source and target storage objects.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   The event dispatcher used to notify subscribers of config import events.
166 167
   * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
   *   The configuration manager.
168
   * @param \Drupal\Core\Lock\LockBackendInterface $lock
169
   *   The lock backend to ensure multiple imports do not occur at the same time.
170
   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
171
   *   The typed configuration manager.
172 173 174 175
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler
   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
   *   The theme handler
176
   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
177
   *   The string translation service.
178
   */
179
  public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, LockBackendInterface $lock, TypedConfigManagerInterface $typed_config, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, TranslationInterface $string_translation) {
180 181
    $this->storageComparer = $storage_comparer;
    $this->eventDispatcher = $event_dispatcher;
182
    $this->configManager = $config_manager;
183
    $this->lock = $lock;
184
    $this->typedConfigManager = $typed_config;
185 186
    $this->moduleHandler = $module_handler;
    $this->themeHandler = $theme_handler;
187
    $this->stringTranslation = $string_translation;
188 189 190
    foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
      $this->processedConfiguration[$collection] = $this->storageComparer->getEmptyChangelist();
    }
191
    $this->processedExtensions = $this->getEmptyExtensionsProcessedList();
192 193
  }

194 195 196 197 198 199
  /**
   * Logs an error message.
   *
   * @param string $message
   *   The message to log.
   */
200
  public function logError($message) {
201 202 203 204 205 206 207 208 209 210 211 212 213
    $this->errors[] = $message;
  }

  /**
   * Returns error messages created while running the import.
   *
   * @return array
   *   List of messages.
   */
  public function getErrors() {
    return $this->errors;
  }

214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
  /**
   * Gets the configuration storage comparer.
   *
   * @return \Drupal\Core\Config\StorageComparerInterface
   *   Storage comparer object used to calculate configuration changes.
   */
  public function getStorageComparer() {
    return $this->storageComparer;
  }

  /**
   * Resets the storage comparer and processed list.
   *
   * @return \Drupal\Core\Config\ConfigImporter
   *   The ConfigImporter instance.
   */
  public function reset() {
    $this->storageComparer->reset();
232 233 234
    foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
      $this->processedConfiguration[$collection] = $this->storageComparer->getEmptyChangelist();
    }
235 236
    $this->processedExtensions = $this->getEmptyExtensionsProcessedList();
    $this->createExtensionChangelist();
237
    $this->validated = FALSE;
238
    $this->processedSystemTheme = FALSE;
239 240 241 242
    return $this;
  }

  /**
243 244 245 246 247 248 249 250 251 252 253 254
   * Gets an empty list of extensions to process.
   *
   * @return array
   *   An empty list of extensions to process.
   */
  protected function getEmptyExtensionsProcessedList() {
    return array(
      'module' => array(
        'install' => array(),
        'uninstall' => array(),
      ),
      'theme' => array(
255 256
        'install' => array(),
        'uninstall' => array(),
257 258 259 260 261 262
      ),
    );
  }

  /**
   * Checks if there are any unprocessed configuration changes.
263 264 265 266
   *
   * @return bool
   *   TRUE if there are changes to process and FALSE if not.
   */
267
  public function hasUnprocessedConfigurationChanges() {
268 269 270 271 272
    foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
      foreach (array('delete', 'create', 'rename', 'update') as $op) {
        if (count($this->getUnprocessedConfiguration($op, $collection))) {
          return TRUE;
        }
273 274 275 276 277 278 279 280
      }
    }
    return FALSE;
  }

  /**
   * Gets list of processed changes.
   *
281 282 283 284
   * @param string $collection
   *   (optional) The configuration collection to get processed changes for.
   *   Defaults to the default collection.
   *
285 286 287
   * @return array
   *   An array containing a list of processed changes.
   */
288 289
  public function getProcessedConfiguration($collection = StorageInterface::DEFAULT_COLLECTION) {
    return $this->processedConfiguration[$collection];
290 291 292 293 294
  }

  /**
   * Sets a change as processed.
   *
295 296
   * @param string $collection
   *   The configuration collection to set a change as processed for.
297
   * @param string $op
298
   *   The change operation performed, either delete, create, rename, or update.
299 300 301
   * @param string $name
   *   The name of the configuration processed.
   */
302 303
  protected function setProcessedConfiguration($collection, $op, $name) {
    $this->processedConfiguration[$collection][$op][] = $name;
304 305 306 307 308 309 310
  }

  /**
   * Gets a list of unprocessed changes for a given operation.
   *
   * @param string $op
   *   The change operation to get the unprocessed list for, either delete,
311
   *   create, rename, or update.
312 313 314
   * @param string $collection
   *   (optional) The configuration collection to get unprocessed changes for.
   *   Defaults to the default collection.
315 316 317 318
   *
   * @return array
   *   An array of configuration names.
   */
319 320
  public function getUnprocessedConfiguration($op, $collection = StorageInterface::DEFAULT_COLLECTION) {
    return array_diff($this->storageComparer->getChangelist($op, $collection), $this->processedConfiguration[$collection][$op]);
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
  }

  /**
   * Gets list of processed extension changes.
   *
   * @return array
   *   An array containing a list of processed extension changes.
   */
  public function getProcessedExtensions() {
    return $this->processedExtensions;
  }

  /**
   * Sets an extension change as processed.
   *
   * @param string $type
   *   The type of extension, either 'theme' or 'module'.
   * @param string $op
   *   The change operation performed, either install or uninstall.
   * @param string $name
   *   The name of the extension processed.
   */
  protected function setProcessedExtension($type, $op, $name) {
    $this->processedExtensions[$type][$op][] = $name;
  }

  /**
   * Populates the extension change list.
   */
  protected function createExtensionChangelist() {
    // Read the extensions information to determine changes.
    $current_extensions = $this->storageComparer->getTargetStorage()->read('core.extension');
    $new_extensions = $this->storageComparer->getSourceStorage()->read('core.extension');

    // If there is no extension information in staging then exit. This is
    // probably due to an empty staging directory.
    if (!$new_extensions) {
      return;
    }

    // Get a list of modules with dependency weights as values.
    $module_data = system_rebuild_module_data();
    // Set the actual module weights.
    $module_list = array_combine(array_keys($module_data), array_keys($module_data));
    $module_list = array_map(function ($module) use ($module_data) {
      return $module_data[$module]->sort;
    }, $module_list);

369
    // Determine which modules to uninstall.
370
    $uninstall = array_diff(array_keys($current_extensions['module']), array_keys($new_extensions['module']));
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
    // Sort the list of newly uninstalled extensions by their weights, so that
    // dependencies are uninstalled last. Extensions of the same weight are
    // sorted in reverse alphabetical order, to ensure the order is exactly
    // opposite from installation. For example, this module list:
    // array(
    //   'actions' => 0,
    //   'ban' => 0,
    //   'options' => -2,
    //   'text' => -1,
    // );
    // will result in the following sort order:
    // -2   options
    // -1   text
    //  0 0 ban
    //  0 1 actions
    // @todo Move this sorting functionality to the extension system.
    array_multisort(array_values($module_list), SORT_ASC, array_keys($module_list), SORT_DESC, $module_list);
388
    $uninstall = array_intersect(array_keys($module_list), $uninstall);
389 390 391 392 393 394 395

    // Determine which modules to install.
    $install = array_diff(array_keys($new_extensions['module']), array_keys($current_extensions['module']));
    // Ensure that installed modules are sorted in exactly the reverse order
    // (with dependencies installed first, and modules of the same weight sorted
    // in alphabetical order).
    $module_list = array_reverse($module_list);
396 397
    $install = array_intersect(array_keys($module_list), $install);

398 399 400
    // Work out what themes to install and to uninstall.
    $theme_install = array_diff(array_keys($new_extensions['theme']), array_keys($current_extensions['theme']));
    $theme_uninstall = array_diff(array_keys($current_extensions['theme']), array_keys($new_extensions['theme']));
401 402 403 404 405 406 407

    $this->extensionChangelist = array(
      'module' => array(
        'uninstall' => $uninstall,
        'install' => $install,
      ),
      'theme' => array(
408 409
        'install' => $theme_install,
        'uninstall' => $theme_uninstall,
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441
      ),
    );
  }

  /**
   * Gets a list changes for extensions.
   *
   * @param string $type
   *   The type of extension, either 'theme' or 'module'.
   * @param string $op
   *   The change operation to get the unprocessed list for, either install
   *   or uninstall.
   *
   * @return array
   *   An array of extension names.
   */
  protected function getExtensionChangelist($type, $op = NULL) {
    if ($op) {
      return $this->extensionChangelist[$type][$op];
    }
    return $this->extensionChangelist[$type];
  }

  /**
   * Gets a list of unprocessed changes for extensions.
   *
   * @param string $type
   *   The type of extension, either 'theme' or 'module'.
   *
   * @return array
   *   An array of extension names.
   */
442
  protected function getUnprocessedExtensions($type) {
443
    $changelist = $this->getExtensionChangelist($type);
444 445 446 447
    return array(
      'install' => array_diff($changelist['install'], $this->processedExtensions[$type]['install']),
      'uninstall' => array_diff($changelist['uninstall'], $this->processedExtensions[$type]['uninstall']),
    );
448 449 450 451 452 453 454 455 456 457 458
  }

  /**
   * Imports the changelist to the target storage.
   *
   * @throws \Drupal\Core\Config\ConfigException
   *
   * @return \Drupal\Core\Config\ConfigImporter
   *   The ConfigImporter instance.
   */
  public function import() {
459
    if ($this->hasUnprocessedConfigurationChanges()) {
460
      $sync_steps = $this->initialize();
461

462 463 464 465 466
      foreach ($sync_steps as $step) {
        $context = array();
        do {
          $this->doSyncStep($step, $context);
        } while ($context['finished'] < 1);
467
      }
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486
    }
    return $this;
  }

  /**
   * Calls a config import step.
   *
   * @param string|callable $sync_step
   *   The step to do. Either a method on the ConfigImporter class or a
   *   callable.
   * @param array $context
   *   A batch context array. If the config importer is not running in a batch
   *   the only array key that is used is $context['finished']. A process needs
   *   to set $context['finished'] = 1 when it is done.
   *
   * @throws \InvalidArgumentException
   *   Exception thrown if the $sync_step can not be called.
   */
  public function doSyncStep($sync_step, &$context) {
487
    if (!is_array($sync_step) && method_exists($this, $sync_step)) {
488 489 490
      $this->$sync_step($context);
    }
    elseif (is_callable($sync_step)) {
491
      call_user_func_array($sync_step, array(&$context, $this));
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525
    }
    else {
      throw new \InvalidArgumentException('Invalid configuration synchronization step');
    }
  }

  /**
   * Initializes the config importer in preparation for processing a batch.
   *
   * @return array
   *   An array of \Drupal\Core\Config\ConfigImporter method names and callables
   *   that are invoked to complete the import. If there are modules or themes
   *   to process then an extra step is added.
   *
   * @throws \Drupal\Core\Config\ConfigImporterException
   *   If the configuration is already importing.
   */
  public function initialize() {
    $this->createExtensionChangelist();

    // Ensure that the changes have been validated.
    $this->validate();

    if (!$this->lock->acquire(static::LOCK_ID)) {
      // Another process is synchronizing configuration.
      throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_ID));
    }

    $sync_steps = array();
    $modules = $this->getUnprocessedExtensions('module');
    foreach (array('install', 'uninstall') as $op) {
      $this->totalExtensionsToProcess += count($modules[$op]);
    }
    $themes = $this->getUnprocessedExtensions('theme');
526
    foreach (array('install', 'uninstall') as $op) {
527 528
      $this->totalExtensionsToProcess += count($themes[$op]);
    }
529

530 531 532
    // We have extensions to process.
    if ($this->totalExtensionsToProcess > 0) {
      $sync_steps[] = 'processExtensions';
533
      $sync_steps[] = 'flush';
534 535 536 537
    }
    $sync_steps[] = 'processConfigurations';

    // Allow modules to add new steps to configuration synchronization.
538
    $this->moduleHandler->alter('config_import_steps', $sync_steps, $this);
539 540 541
    $sync_steps[] = 'finish';
    return $sync_steps;
  }
542

543 544 545 546 547 548 549 550 551 552 553 554 555 556
  /**
   * Flushes Drupal's caches.
   */
  public function flush(array &$context) {
    // Rebuild the container and flush Drupal's caches. If the container is not
    // rebuilt first the entity types are not discovered correctly due to using
    // an entity manager that has the incorrect container namespaces injected.
    \Drupal::service('kernel')->rebuildContainer(TRUE);
    drupal_flush_all_caches();
    $this->reInjectMe();
    $context['message'] = $this->t('Flushed all caches.');
    $context['finished'] = 1;
  }

557 558 559 560 561 562
  /**
   * Processes extensions as a batch operation.
   *
   * @param array $context.
   *   The batch context.
   */
563
  protected function processExtensions(array &$context) {
564 565 566 567 568
    $operation = $this->getNextExtensionOperation();
    if (!empty($operation)) {
      $this->processExtension($operation['type'], $operation['op'], $operation['name']);
      $context['message'] = t('Synchronising extensions: @op @name.', array('@op' => $operation['op'], '@name' => $operation['name']));
      $processed_count = count($this->processedExtensions['module']['install']) + count($this->processedExtensions['module']['uninstall']);
569
      $processed_count += count($this->processedExtensions['theme']['uninstall']) + count($this->processedExtensions['theme']['install']);
570 571 572 573 574 575 576 577 578 579 580 581 582
      $context['finished'] = $processed_count / $this->totalExtensionsToProcess;
    }
    else {
      $context['finished'] = 1;
    }
  }

  /**
   * Processes configuration as a batch operation.
   *
   * @param array $context.
   *   The batch context.
   */
583
  protected function processConfigurations(array &$context) {
584 585 586 587 588 589
    // The first time this is called we need to calculate the total to process.
    // This involves recalculating the changelist which will ensure that if
    // extensions have been processed any configuration affected will be taken
    // into account.
    if ($this->totalConfigurationToProcess == 0) {
      $this->storageComparer->reset();
590 591 592
      foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
        foreach (array('delete', 'create', 'rename', 'update') as $op) {
          $this->totalConfigurationToProcess += count($this->getUnprocessedConfiguration($op, $collection));
593 594
        }
      }
595 596 597
    }
    $operation = $this->getNextConfigurationOperation();
    if (!empty($operation)) {
598 599 600 601 602 603 604 605 606 607 608 609 610 611
      if ($this->checkOp($operation['collection'], $operation['op'], $operation['name'])) {
        $this->processConfiguration($operation['collection'], $operation['op'], $operation['name']);
      }
      if ($operation['collection'] == StorageInterface::DEFAULT_COLLECTION) {
        $context['message'] = $this->t('Synchronizing configuration: @op @name.', array('@op' => $operation['op'], '@name' => $operation['name']));
      }
      else {
        $context['message'] = $this->t('Synchronizing configuration: @op @name in @collection.', array('@op' => $operation['op'], '@name' => $operation['name'], '@collection' => $operation['collection']));
      }
      $processed_count = 0;
      foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
        foreach (array('delete', 'create', 'rename', 'update') as $op) {
          $processed_count += count($this->processedConfiguration[$collection][$op]);
        }
612 613 614 615 616 617 618 619 620 621 622 623 624 625
      }
      $context['finished'] = $processed_count / $this->totalConfigurationToProcess;
    }
    else {
      $context['finished'] = 1;
    }
  }

  /**
   * Finishes the batch.
   *
   * @param array $context.
   *   The batch context.
   */
626
  protected function finish(array &$context) {
627 628 629 630
    $this->eventDispatcher->dispatch(ConfigEvents::IMPORT, new ConfigImporterEvent($this));
    // The import is now complete.
    $this->lock->release(static::LOCK_ID);
    $this->reset();
631
    $context['message'] = t('Finalizing configuration synchronization.');
632 633
    $context['finished'] = 1;
  }
634

635 636 637 638 639 640 641 642
  /**
   * Gets the next extension operation to perform.
   *
   * @return array|bool
   *   An array containing the next operation and extension name to perform it
   *   on. If there is nothing left to do returns FALSE;
   */
  protected function getNextExtensionOperation() {
643 644 645 646 647 648 649 650 651 652
    foreach (array('module', 'theme') as $type) {
      foreach (array('install', 'uninstall') as $op) {
        $unprocessed = $this->getUnprocessedExtensions($type);
        if (!empty($unprocessed[$op])) {
          return array(
            'op' => $op,
            'type' => $type,
            'name' => array_shift($unprocessed[$op]),
          );
        }
653 654 655 656 657 658 659 660 661 662 663 664 665 666 667
      }
    }
    return FALSE;
  }

  /**
   * Gets the next configuration operation to perform.
   *
   * @return array|bool
   *   An array containing the next operation and configuration name to perform
   *   it on. If there is nothing left to do returns FALSE;
   */
  protected function getNextConfigurationOperation() {
    // The order configuration operations is processed is important. Deletes
    // have to come first so that recreates can work.
668 669 670 671 672 673 674 675 676 677
    foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
      foreach (array('delete', 'create', 'rename', 'update') as $op) {
        $config_names = $this->getUnprocessedConfiguration($op, $collection);
        if (!empty($config_names)) {
          return array(
            'op' => $op,
            'name' => array_shift($config_names),
            'collection' => $collection,
          );
        }
678 679 680
      }
    }
    return FALSE;
681 682 683 684 685 686 687
  }

  /**
   * Dispatches validate event for a ConfigImporter object.
   *
   * Events should throw a \Drupal\Core\Config\ConfigImporterException to
   * prevent an import from occurring.
688 689 690
   *
   * @throws \Drupal\Core\Config\ConfigImporterException
   *   Exception thrown if the validate event logged any errors.
691
   */
692
  protected function validate() {
693
    if (!$this->validated) {
694 695 696 697 698 699 700 701 702 703 704 705 706
      // Validate renames.
      foreach ($this->getUnprocessedConfiguration('rename') as $name) {
        $names = $this->storageComparer->extractRenameNames($name);
        $old_entity_type_id = $this->configManager->getEntityTypeIdByName($names['old_name']);
        $new_entity_type_id = $this->configManager->getEntityTypeIdByName($names['new_name']);
        if ($old_entity_type_id != $new_entity_type_id) {
          $this->logError($this->t('Entity type mismatch on rename. !old_type not equal to !new_type for existing configuration !old_name and staged configuration !new_name.', array('old_type' => $old_entity_type_id, 'new_type' => $new_entity_type_id, 'old_name' => $names['old_name'], 'new_name' => $names['new_name'])));
        }
        // Has to be a configuration entity.
        if (!$old_entity_type_id) {
          $this->logError($this->t('Rename operation for simple configuration. Existing configuration !old_name and staged configuration !new_name.', array('old_name' => $names['old_name'], 'new_name' => $names['new_name'])));
        }
      }
707
      $this->eventDispatcher->dispatch(ConfigEvents::IMPORT_VALIDATE, new ConfigImporterEvent($this));
708 709 710 711 712 713
      if (count($this->getErrors())) {
        throw new ConfigImporterException('There were errors validating the config synchronization.');
      }
      else {
        $this->validated = TRUE;
      }
714 715 716 717 718
    }
    return $this;
  }

  /**
719 720
   * Processes a configuration change.
   *
721 722
   * @param string $collection
   *   The configuration collection to process changes for.
723 724 725 726
   * @param string $op
   *   The change operation.
   * @param string $name
   *   The name of the configuration to process.
727 728 729 730 731
   *
   * @throws \Exception
   *   Thrown when the import process fails, only thrown when no importer log is
   *   set, otherwise the exception message is logged and the configuration
   *   is skipped.
732
   */
733
  protected function processConfiguration($collection, $op, $name) {
734
    try {
735 736 737 738 739 740
      $processed = FALSE;
      if ($this->configManager->supportsConfigurationEntities($collection)) {
        $processed = $this->importInvokeOwner($collection, $op, $name);
      }
      if (!$processed) {
        $this->importConfig($collection, $op, $name);
741 742 743 744 745 746
      }
    }
    catch (\Exception $e) {
      $this->logError($this->t('Unexpected error during import with operation @op for @name: @message', array('@op' => $op, '@name' => $name, '@message' => $e->getMessage())));
      // Error for that operation was logged, mark it as processed so that
      // the import can continue.
747
      $this->setProcessedConfiguration($collection, $op, $name);
748 749 750
    }
  }

751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778
  /**
   * Processes an extension change.
   *
   * @param string $type
   *   The type of extension, either 'module' or 'theme'.
   * @param string $op
   *   The change operation.
   * @param string $name
   *   The name of the extension to process.
   */
  protected function processExtension($type, $op, $name) {
    // Set the config installer to use the staging directory instead of the
    // extensions own default config directories.
    \Drupal::service('config.installer')
      ->setSyncing(TRUE)
      ->setSourceStorage($this->storageComparer->getSourceStorage());
    if ($type == 'module') {
      $this->moduleHandler->$op(array($name), FALSE);
      // Installing a module can cause a kernel boot therefore reinject all the
      // services.
      $this->reInjectMe();
      // During a module install or uninstall the container is rebuilt and the
      // module handler is called from drupal_get_complete_schema(). This causes
      // the container's instance of the module handler not to have loaded all
      // the enabled modules.
      $this->moduleHandler->loadAll();
    }
    if ($type == 'theme') {
779 780 781
      // Theme uninstalls possible remove default or admin themes therefore we
      // need to import this before doing any. If there are no uninstalls and
      // the default or admin theme is changing this will be picked up whilst
782
      // processing configuration.
783
      if ($op == 'uninstall' && $this->processedSystemTheme === FALSE) {
784
        $this->importConfig(StorageInterface::DEFAULT_COLLECTION, 'update', 'system.theme');
785 786 787 788 789 790 791 792 793 794 795 796
        $this->configManager->getConfigFactory()->reset('system.theme');
        $this->processedSystemTheme = TRUE;
      }
      $this->themeHandler->$op(array($name));
    }

    $this->setProcessedExtension($type, $op, $name);
    \Drupal::service('config.installer')
      ->setSyncing(FALSE)
      ->resetSourceStorage();
  }

797 798 799 800 801 802 803
  /**
   * Checks that the operation is still valid.
   *
   * During a configuration import secondary writes and deletes are possible.
   * This method checks that the operation is still valid before processing a
   * configuration change.
   *
804 805
   * @param string $collection
   *   The configuration collection.
806 807 808 809 810 811 812 813 814 815
   * @param string $op
   *   The change operation.
   * @param string $name
   *   The name of the configuration to process.
   *
   * @throws \Drupal\Core\Config\ConfigImporterException
   *
   * @return bool
   *   TRUE is to continue processing, FALSE otherwise.
   */
816
  protected function checkOp($collection, $op, $name) {
817 818
    if ($op == 'rename') {
      $names = $this->storageComparer->extractRenameNames($name);
819
      $target_exists = $this->storageComparer->getTargetStorage($collection)->exists($names['new_name']);
820 821 822 823 824
      if ($target_exists) {
        // If the target exists, the rename has already occurred as the
        // result of a secondary configuration write. Change the operation
        // into an update. This is the desired behavior since renames often
        // have to occur together. For example, renaming a node type must
825
        // also result in renaming its fields and entity displays.
826 827 828 829 830
        $this->storageComparer->moveRenameToUpdate($name);
        return FALSE;
      }
      return TRUE;
    }
831
    $target_exists = $this->storageComparer->getTargetStorage($collection)->exists($name);
832 833 834 835 836
    switch ($op) {
      case 'delete':
        if (!$target_exists) {
          // The configuration has already been deleted. For example, a field
          // is automatically deleted if all the instances are.
837
          $this->setProcessedConfiguration($collection, $op, $name);
838 839 840 841 842 843 844 845 846 847 848 849 850
          return FALSE;
        }
        break;

      case 'create':
        if ($target_exists) {
          // If the target already exists, use the entity storage to delete it
          // again, if is a simple config, delete it directly.
          if ($entity_type_id = $this->configManager->getEntityTypeIdByName($name)) {
            $entity_storage = $this->configManager->getEntityManager()->getStorage($entity_type_id);
            $entity_type = $this->configManager->getEntityManager()->getDefinition($entity_type_id);
            $entity = $entity_storage->load($entity_storage->getIDFromConfigName($name, $entity_type->getConfigPrefix()));
            $entity->delete();
851
            $this->logError($this->t('Deleted and replaced configuration entity "@name"', array('@name' => $name)));
852 853
          }
          else {
854
            $this->storageComparer->getTargetStorage($collection)->delete($name);
855 856 857 858 859 860 861 862 863 864 865 866
            $this->logError($this->t('Deleted and replaced configuration "@name"', array('@name' => $name)));
          }
          return TRUE;
        }
        break;

      case 'update':
        if (!$target_exists) {
          $this->logError($this->t('Update target "@name" is missing.', array('@name' => $name)));
          // Mark as processed so that the synchronisation continues. Once the
          // the current synchronisation is complete it will show up as a
          // create.
867
          $this->setProcessedConfiguration($collection, $op, $name);
868 869 870 871 872 873 874
          return FALSE;
        }
        break;
    }
    return TRUE;
  }

875 876 877
  /**
   * Writes a configuration change from the source to the target storage.
   *
878 879
   * @param string $collection
   *   The configuration collection.
880 881 882 883 884
   * @param string $op
   *   The change operation.
   * @param string $name
   *   The name of the configuration to process.
   */
885
  protected function importConfig($collection, $op, $name) {
886 887 888 889 890 891 892 893 894
    // Allow config factory overriders to use a custom configuration object if
    // they are responsible for the collection.
    $overrider = $this->configManager->getConfigCollectionInfo()->getOverrideService($collection);
    if ($overrider) {
      $config = $overrider->createConfigObject($name, $collection);
    }
    else {
      $config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
    }
895 896 897 898
    if ($op == 'delete') {
      $config->delete();
    }
    else {
899
      $data = $this->storageComparer->getSourceStorage($collection)->read($name);
900 901 902
      $config->setData($data ? $data : array());
      $config->save();
    }
903
    $this->setProcessedConfiguration($collection, $op, $name);
904 905
  }

906
  /**
907
   * Invokes import* methods on configuration entity storage.
908 909 910 911 912
   *
   * Allow modules to take over configuration change operations for higher-level
   * configuration data.
   *
   * @todo Add support for other extension types; e.g., themes etc.
913
   *
914 915
   * @param string $collection
   *   The configuration collection.
916 917
   * @param string $op
   *   The change operation to get the unprocessed list for, either delete,
918
   *   create, rename, or update.
919 920 921
   * @param string $name
   *   The name of the configuration to process.
   *
922 923 924 925
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   Thrown if the data is owned by an entity type, but the entity storage
   *   does not support imports.
   *
926 927 928
   * @return bool
   *   TRUE if the configuration was imported as a configuration entity. FALSE
   *   otherwise.
929
   */
930
  protected function importInvokeOwner($collection, $op, $name) {
931 932
    // Renames are handled separately.
    if ($op == 'rename') {
933
      return $this->importInvokeRename($collection, $name);
934
    }
935 936 937
    // Validate the configuration object name before importing it.
    // Config::validateName($name);
    if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) {
938 939
      $old_config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
      if ($old_data = $this->storageComparer->getTargetStorage($collection)->read($name)) {
940 941 942
        $old_config->initWithData($old_data);
      }

943 944
      $data = $this->storageComparer->getSourceStorage($collection)->read($name);
      $new_config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
945 946
      if ($data !== FALSE) {
        $new_config->setData($data);
947
      }
948 949

      $method = 'import' . ucfirst($op);
950 951 952
      $entity_storage = $this->configManager->getEntityManager()->getStorage($entity_type);
      // Call to the configuration entity's storage to handle the configuration
      // change.
953 954 955 956
      if (!($entity_storage instanceof ImportableEntityStorageInterface)) {
        throw new EntityStorageException(String::format('The entity storage "@storage" for the "@entity_type" entity type does not support imports', array('@storage' => get_class($entity_storage), '@entity_type' => $entity_type)));
      }
      $entity_storage->$method($name, $new_config, $old_config);
957
      $this->setProcessedConfiguration($collection, $op, $name);
958
      return TRUE;
959
    }
960 961 962 963 964 965
    return FALSE;
  }

  /**
   * Imports a configuration entity rename.
   *
966 967
   * @param string $collection
   *   The configuration collection.
968 969 970 971
   * @param string $rename_name
   *   The rename configuration name, as provided by
   *   \Drupal\Core\Config\StorageComparer::createRenameName().
   *
972 973 974 975
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   Thrown if the data is owned by an entity type, but the entity storage
   *   does not support imports.
   *
976 977 978 979 980 981
   * @return bool
   *   TRUE if the configuration was imported as a configuration entity. FALSE
   *   otherwise.
   *
   * @see \Drupal\Core\Config\ConfigImporter::createRenameName()
   */
982
  protected function importInvokeRename($collection, $rename_name) {
983 984
    $names = $this->storageComparer->extractRenameNames($rename_name);
    $entity_type_id = $this->configManager->getEntityTypeIdByName($names['old_name']);
985 986
    $old_config = new Config($names['old_name'], $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
    if ($old_data = $this->storageComparer->getTargetStorage($collection)->read($names['old_name'])) {
987 988 989
      $old_config->initWithData($old_data);
    }

990 991
    $data = $this->storageComparer->getSourceStorage($collection)->read($names['new_name']);
    $new_config = new Config($names['new_name'], $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
992 993 994 995 996 997 998 999 1000 1001 1002
    if ($data !== FALSE) {
      $new_config->setData($data);
    }

    $entity_storage = $this->configManager->getEntityManager()->getStorage($entity_type_id);
    // Call to the configuration entity's storage to handle the configuration
    // change.
    if (!($entity_storage instanceof ImportableEntityStorageInterface)) {
      throw new EntityStorageException(String::format('The entity storage "@storage" for the "@entity_type" entity type does not support imports', array('@storage' => get_class($entity_storage), '@entity_type' => $entity_type_id)));
    }
    $entity_storage->importRename($names['old_name'], $new_config, $old_config);
1003
    $this->setProcessedConfiguration($collection, 'rename', $rename_name);
1004
    return TRUE;
1005 1006 1007 1008 1009 1010 1011 1012 1013
  }

  /**
   * Determines if a import is already running.
   *
   * @return bool
   *   TRUE if an import is already running, FALSE if not.
   */
  public function alreadyImporting() {
1014
    return !$this->lock->lockMayBeAvailable(static::LOCK_ID);
1015 1016
  }

1017 1018 1019 1020 1021 1022 1023 1024 1025
  /**
   * Gets all the service dependencies from \Drupal.
   *
   * Since the ConfigImporter handles module installation the kernel and the
   * container can be rebuilt and altered during processing. It is necessary to
   * keep the services used by the importer in sync.
   */
  protected function reInjectMe() {
    $this->eventDispatcher = \Drupal::service('event_dispatcher');
1026
    $this->configManager = \Drupal::service('config.manager');
1027 1028 1029 1030
    $this->lock = \Drupal::lock();
    $this->typedConfigManager = \Drupal::service('config.typed');
    $this->moduleHandler = \Drupal::moduleHandler();
    $this->themeHandler = \Drupal::service('theme_handler');
1031
    $this->stringTranslation = \Drupal::service('string_translation');
1032
  }
1033

1034
}