From 4eaa775b102440020a771b4ca54c7c6953224eb6 Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Fri, 17 May 2013 15:16:16 +0100
Subject: [PATCH] Issue #1890784 by alexpott, YesCT, heyrocker, tim.plunkett,
 EllaTheHarpy, beejeebus: Refactor configuration import and sync functions.

---
 core/core.services.yml                        |   9 +
 core/includes/config.inc                      | 274 ++------------
 core/lib/Drupal/Core/Config/ConfigFactory.php |   8 +-
 .../lib/Drupal/Core/Config/ConfigImporter.php | 338 ++++++++++++++++++
 .../Core/Config/ConfigImporterEvent.php       |  40 +++
 .../Core/Config/ConfigImporterException.php   |  13 +
 .../Drupal/Core/Config/ConfigInstaller.php    |  33 ++
 .../Config/Entity/ConfigStorageController.php |   4 +-
 .../Drupal/Core/Config/StorageComparer.php    | 201 +++++++++++
 .../Core/Config/StorageComparerInterface.php  | 120 +++++++
 .../Core/Config/StorageComparerManifest.php   | 105 ++++++
 .../ConfigImportSubscriber.php                |  48 +++
 .../ConfigSnapshotSubscriber.php              |  68 ++++
 core/modules/config/config.admin.inc          |  71 ++--
 .../Drupal/config/Tests/ConfigCRUDTest.php    |  19 +-
 .../Drupal/config/Tests/ConfigImportTest.php  |  37 +-
 .../config/Tests/ConfigImportUITest.php       |   5 +-
 .../config/Tests/ConfigOverrideTest.php       |   3 +-
 .../config/Tests/ConfigSnapshotTest.php       |  21 +-
 .../ConfigTestStorageController.php           |   6 +-
 .../field/Tests/FieldImportChangeTest.php     |   2 +-
 .../field/Tests/FieldImportCreateTest.php     |   2 +-
 .../field/Tests/FieldImportDeleteTest.php     |   2 +-
 .../Drupal/field/Tests/FieldUnitTestBase.php  |   4 +-
 .../image/Tests/ImageAdminStylesTest.php      |   2 +-
 .../lib/Drupal/simpletest/TestBase.php        |  33 ++
 .../system/Tests/Database/RegressionTest.php  |   7 +
 .../Tests/Formatter/TextPlainUnitTest.php     |   5 +-
 .../views/Tests/ViewTestConfigInstaller.php   |  27 ++
 .../lib/Drupal/views/Tests/ViewTestData.php   |  29 +-
 .../Drupal/views/Tests/ViewUnitTestBase.php   |   5 +-
 31 files changed, 1218 insertions(+), 323 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Config/ConfigImporter.php
 create mode 100644 core/lib/Drupal/Core/Config/ConfigImporterEvent.php
 create mode 100644 core/lib/Drupal/Core/Config/ConfigImporterException.php
 create mode 100644 core/lib/Drupal/Core/Config/ConfigInstaller.php
 create mode 100644 core/lib/Drupal/Core/Config/StorageComparer.php
 create mode 100644 core/lib/Drupal/Core/Config/StorageComparerInterface.php
 create mode 100644 core/lib/Drupal/Core/Config/StorageComparerManifest.php
 create mode 100644 core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
 create mode 100644 core/lib/Drupal/Core/EventSubscriber/ConfigSnapshotSubscriber.php
 create mode 100644 core/modules/views/lib/Drupal/views/Tests/ViewTestConfigInstaller.php

diff --git a/core/core.services.yml b/core/core.services.yml
index 42b56145d3a3..9a361399bc41 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -356,6 +356,15 @@ services:
     class: Drupal\Core\EventSubscriber\ConfigGlobalOverrideSubscriber
     tags:
       - { name: event_subscriber }
+  config_import_subscriber:
+    class: Drupal\Core\EventSubscriber\ConfigImportSubscriber
+    tags:
+      - { name: event_subscriber }
+  config_snapshot_subscriber:
+    class: Drupal\Core\EventSubscriber\ConfigSnapshotSubscriber
+    tags:
+      - { name: event_subscriber }
+    arguments: ['@config.storage', '@config.storage.snapshot']
   language_request_subscriber:
     class: Drupal\Core\EventSubscriber\LanguageRequestSubscriber
     tags:
diff --git a/core/includes/config.inc b/core/includes/config.inc
index f4148456627e..31f6e5b07a0e 100644
--- a/core/includes/config.inc
+++ b/core/includes/config.inc
@@ -2,8 +2,10 @@
 
 use Drupal\Core\Config\Config;
 use Drupal\Core\Config\ConfigException;
+use Drupal\Core\Config\ConfigInstaller;
 use Drupal\Core\Config\FileStorage;
 use Drupal\Core\Config\StorageInterface;
+use Drupal\Core\Config\StorageComparer;
 use Symfony\Component\Yaml\Dumper;
 
 /**
@@ -11,11 +13,6 @@
  * This is the API for configuration storage.
  */
 
-/**
- * Config import lock name used to prevent concurrent synchronizations.
- */
-const CONFIG_IMPORT_LOCK = 'config_import';
-
 /**
  * Installs the default configuration of a given extension.
  *
@@ -25,10 +22,6 @@
  *   The name of the module or theme to install default configuration for.
  */
 function config_install_default_config($type, $name) {
-  // Use the override free context for config importing so that any overrides do
-  // not change the data on import.
-  config_context_enter('config.context.free');
-
   // If this module defines any ConfigEntity types then create an empty
   // manifest file for each of them.
   foreach (config_get_module_config_entities($name) as $entity_info) {
@@ -38,22 +31,19 @@ function config_install_default_config($type, $name) {
   $config_dir = drupal_get_path($type, $name) . '/config';
   if (is_dir($config_dir)) {
     $source_storage = new FileStorage($config_dir);
-    $target_storage = drupal_container()->get('config.storage');
-
-    // Ignore manifest files.
-    $config_changes = config_sync_get_changes($source_storage, $target_storage, FALSE);
-    if (empty($config_changes['create'])) {
-      return;
-    }
-
-    // Do not overwrite or delete pre-existing configuration.
-    $config_changes['change'] = array();
-    $config_changes['delete'] = array();
-    $remaining_changes = config_import_invoke_owner($config_changes, $source_storage, $target_storage);
-    config_sync_changes($remaining_changes, $source_storage, $target_storage);
+    $storage_comparer = new StorageComparer($source_storage, Drupal::service('config.storage'));
+    // Only import new config. Changed config is from previous enables and
+    // should not be overwritten.
+    $storage_comparer->addChangelistCreate();
+    $installer = new ConfigInstaller(
+      $storage_comparer,
+      Drupal::service('event_dispatcher'),
+      Drupal::service('config.factory'),
+      Drupal::entityManager(),
+      Drupal::lock()
+    );
+    $installer->import();
   }
-  // Exit the override free context.
-  config_context_leave();
 }
 
 /**
@@ -154,227 +144,6 @@ function config_context_leave() {
     ->leaveContext();
 }
 
-/**
- * Returns a list of differences between configuration storages.
- *
- * @param Drupal\Core\Config\StorageInterface $source_storage
- *   The storage to synchronize configuration from.
- * @param Drupal\Core\Config\StorageInterface $target_storage
- *   The storage to synchronize configuration to.
- * @param bool $use_manifest
- *   (optional) Whether to determine changes based on manifest files. Defaults
- *   to TRUE.
- *
- * @return array|bool
- *   An associative array containing the differences between source and target
- *   storage, or FALSE if there are no differences.
- */
-function config_sync_get_changes(StorageInterface $source_storage, StorageInterface $target_storage, $use_manifest = TRUE) {
-  $config_changes = array(
-    'create' => array(),
-    'change' => array(),
-    'delete' => array(),
-  );
-  $all_source_names = $source_storage->listAll();
-  $all_target_names = $target_storage->listAll();
-
-  // Config entities maintain 'manifest' files that list the objects they
-  // are currently handling. Each file is a simple indexed array of config
-  // object names. In order to generate a list of objects that have been
-  // created or deleted we need to open these files in both the source and
-  // target storage, generate an array of the objects, and compare them.
-  if ($use_manifest) {
-    $source_config_data = array();
-    $target_config_data = array();
-    foreach ($source_storage->listAll('manifest') as $name) {
-      if ($source_manifest_data = $source_storage->read($name)) {
-        $source_config_data = array_merge($source_config_data, $source_manifest_data);
-      }
-
-      if ($target_manifest_data = $target_storage->read($name)) {
-        $target_config_data = array_merge($target_config_data, $target_manifest_data);
-      }
-    }
-
-    foreach (array_diff_key($target_config_data, $source_config_data) as $name => $value) {
-      $config_changes['delete'][] = $value['name'];
-    }
-
-    foreach (array_diff_key($source_config_data, $target_config_data) as $name => $value) {
-      $config_changes['create'][] = $value['name'];
-    }
-  }
-  else {
-    $config_changes['delete'] = array_diff($all_target_names, $all_source_names);
-    $config_changes['create'] = array_diff($all_source_names, $all_target_names);
-  }
-
-  foreach (array_intersect($all_source_names, $all_target_names) as $name) {
-    // Ignore manifest files
-    if (substr($name, 0, 9) != 'manifest.') {
-      $source_config_data = $source_storage->read($name);
-      $target_config_data = $target_storage->read($name);
-      if ($source_config_data !== $target_config_data) {
-        $config_changes['change'][] = $name;
-      }
-    }
-  }
-
-  // Do not trigger subsequent synchronization operations if there are no
-  // changes in any category.
-  if (empty($config_changes['create']) && empty($config_changes['change']) && empty($config_changes['delete'])) {
-    return FALSE;
-  }
-  return $config_changes;
-}
-
-/**
- * Writes an array of config file changes from a source storage to a target storage.
- *
- * @param array $config_changes
- *   An array of changes to be written.
- * @param Drupal\Core\Config\StorageInterface $source_storage
- *   The storage to synchronize configuration from.
- * @param Drupal\Core\Config\StorageInterface $target_storage
- *   The storage to synchronize configuration to.
- */
-function config_sync_changes(array $config_changes, StorageInterface $source_storage, StorageInterface $target_storage) {
-  $target_context = drupal_container()->get('config.context.free');
-  $factory = drupal_container()->get('config.factory');
-  foreach (array('delete', 'create', 'change') as $op) {
-    foreach ($config_changes[$op] as $name) {
-      $config = new Config($name, $target_storage, $target_context);
-      if ($op == 'delete') {
-        $config->delete();
-      }
-      else {
-        $data = $source_storage->read($name);
-        $config->setData($data ? $data : array());
-        $config->save();
-      }
-      $factory->reset($name);
-    }
-  }
-}
-
-/**
- * Imports configuration into the active configuration.
- *
- * @return bool|null
- *   TRUE if configuration was imported successfully, FALSE in case of a
- *   synchronization error, or NULL if there are no changes to synchronize.
- */
-function config_import() {
-  // Retrieve a list of differences between staging and the active configuration.
-  $source_storage = drupal_container()->get('config.storage.staging');
-  $snapshot_storage = drupal_container()->get('config.storage.snapshot');
-  $target_storage = drupal_container()->get('config.storage');
-
-  $config_changes = config_sync_get_changes($source_storage, $target_storage);
-  if (empty($config_changes)) {
-    return;
-  }
-
-  if (!lock()->acquire(CONFIG_IMPORT_LOCK)) {
-    // Another request is synchronizing configuration.
-    // Return a negative result for UI purposes. We do not differentiate between
-    // an actual synchronization error and a failed lock, because concurrent
-    // synchronizations are an edge-case happening only when multiple developers
-    // or site builders attempt to do it without coordinating.
-    return FALSE;
-  }
-
-  $success = TRUE;
-  try {
-    // Use the override free context for config importing so that any overrides do
-    // not change the data on import.
-    config_context_enter('config.context.free');
-
-    $remaining_changes = config_import_invoke_owner($config_changes, $source_storage, $target_storage);
-    config_sync_changes($remaining_changes, $source_storage, $target_storage);
-    config_import_create_snapshot($target_storage, $snapshot_storage);
-
-    // Exit the override free context.
-    config_context_leave();
-  }
-  catch (ConfigException $e) {
-    watchdog_exception('config_import', $e);
-    $success = FALSE;
-  }
-  lock()->release(CONFIG_IMPORT_LOCK);
-
-  return $success;
-}
-
-/**
- * Creates a configuration snapshot following a successful import.
- *
- * @param Drupal\Core\Config\StorageInterface $source_storage
- *   The storage to synchronize configuration from.
- * @param Drupal\Core\Config\StorageInterface $target_storage
- *   The storage to synchronize configuration to.
- */
-function config_import_create_snapshot(StorageInterface $source_storage, StorageInterface $snapshot_storage) {
-  $snapshot_storage->deleteAll();
-  foreach ($source_storage->listAll() as $name) {
-    $snapshot_storage->write($name, $source_storage->read($name));
-  }
-}
-
-/**
- * Invokes MODULE_config_import() callbacks for configuration changes.
- *
- * @param array $config_changes
- *   An array of changes to be loaded.
- * @param Drupal\Core\Config\StorageInterface $source_storage
- *   The storage to synchronize configuration from.
- * @param Drupal\Core\Config\StorageInterface $target_storage
- *   The storage to synchronize configuration to.
- *
- * @todo Add support for other extension types; e.g., themes etc.
- */
-function config_import_invoke_owner(array $config_changes, StorageInterface $source_storage, StorageInterface $target_storage) {
-  $factory = drupal_container()->get('config.factory');
-  // Use the admin context for config importing so that any overrides do not
-  // change the data on import.
-  $free_context = drupal_container()->get('config.context.free');
-  // Allow modules to take over configuration change operations for
-  // higher-level configuration data.
-  // First pass deleted, then new, and lastly changed configuration, in order to
-  // handle dependencies correctly.
-  $manager = Drupal::entityManager();
-  foreach (array('delete', 'create', 'change') as $op) {
-    foreach ($config_changes[$op] as $key => $name) {
-      // Call to the configuration entity's storage controller to handle the
-      // configuration change.
-      $handled_by_module = FALSE;
-      // Validate the configuration object name before importing it.
-      Config::validateName($name);
-      if ($entity_type = config_get_entity_type_by_name($name)) {
-        $old_config = new Config($name, $target_storage, $free_context);
-        $old_config->load();
-
-        $data = $source_storage->read($name);
-        $new_config = new Config($name, $source_storage, $free_context);
-        if ($data !== FALSE) {
-          $new_config->setData($data);
-        }
-
-        $method = 'import' . ucfirst($op);
-        $handled_by_module = $manager->getStorageController($entity_type)->$method($name, $new_config, $old_config);
-      }
-      if (!empty($handled_by_module)) {
-        $factory->reset($name);
-        // Reset the manifest config object for the config entity.
-        $entity_info = Drupal::entityManager()->getDefinition($entity_type);
-        $factory->reset('manifest.' . $entity_info['config_prefix']);
-        unset($config_changes[$op][$key]);
-      }
-    }
-  }
-  return $config_changes;
-}
-
 /**
  * Return a list of all config entity types provided by a module.
  *
@@ -424,6 +193,21 @@ function config_typed() {
   return drupal_container()->get('config.typed');
 }
 
+/**
+ * Creates a configuration snapshot following a successful import.
+ *
+ * @param Drupal\Core\Config\StorageInterface $source_storage
+ *   The storage to synchronize configuration from.
+ * @param Drupal\Core\Config\StorageInterface $snapshot_storage
+ *   The storage to synchronize configuration to.
+ */
+function config_import_create_snapshot(StorageInterface $source_storage, StorageInterface $snapshot_storage) {
+  $snapshot_storage->deleteAll();
+  foreach ($source_storage->listAll() as $name) {
+    $snapshot_storage->write($name, $source_storage->read($name));
+  }
+}
+
 /**
  * Return a formatted diff of a named config between two storages.
  *
diff --git a/core/lib/Drupal/Core/Config/ConfigFactory.php b/core/lib/Drupal/Core/Config/ConfigFactory.php
index 0c3785cd3ee4..dd5df1e6ca0d 100644
--- a/core/lib/Drupal/Core/Config/ConfigFactory.php
+++ b/core/lib/Drupal/Core/Config/ConfigFactory.php
@@ -96,15 +96,13 @@ public function get($name) {
    */
   public function reset($name = NULL) {
     if ($name) {
-      // Reinitialize the configuration object in all contexts.
+      // Clear the cached configuration object in all contexts.
       foreach ($this->getCacheKeys($name) as $cache_key) {
-        $this->cache[$cache_key]->init();
+        unset($this->cache[$cache_key]);
       }
     }
     else {
-      foreach ($this->cache as $config) {
-        $config->init();
-      }
+      $this->cache = array();
     }
     return $this;
   }
diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php
new file mode 100644
index 000000000000..d496fe78fd48
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/ConfigImporter.php
@@ -0,0 +1,338 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Config\ConfigImporter.
+ */
+
+namespace Drupal\Core\Config;
+
+use Drupal\Core\Config\Context\FreeConfigContext;
+use Drupal\Core\Entity\EntityManager;
+use Drupal\Core\Lock\LockBackendInterface;
+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:
+ * - 'config.importer.validate': Events listening can throw a
+ *   \Drupal\Core\Config\ConfigImporterException to prevent an import from
+ *   occurring.
+ *   @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
+ * - 'config.importer.import': Events listening can react to a successful import.
+ *   @see \Drupal\Core\EventSubscriber\ConfigSnapshotSubscriber
+ *
+ * @see \Drupal\Core\Config\ConfigImporterEvent
+ */
+class ConfigImporter {
+
+  /**
+   * The name used to identify events and the lock.
+   */
+  const ID = 'config.importer';
+
+  /**
+   * The storage comparer used to discover configuration changes.
+   *
+   * @var \Drupal\Core\Config\StorageComparerInterface
+   */
+  protected $storageComparer;
+
+  /**
+   * The event dispatcher used to notify subscribers.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcher
+   */
+  protected $eventDispatcher;
+
+  /**
+   * The configuration context.
+   *
+   * @var \Drupal\Core\Config\Context\ContextInterface
+   */
+  protected $context;
+
+  /**
+   * The configuration factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactory
+   */
+  protected $configFactory;
+
+  /**
+   * The plugin manager for entities.
+   *
+   * @var \Drupal\Core\Entity\EntityManager
+   */
+  protected $entityManager;
+
+  /**
+   * The used lock backend instance.
+   *
+   * @var \Drupal\Core\Lock\LockBackendInterface
+   */
+  protected $lock;
+
+  /**
+   * List of changes processed by the import().
+   *
+   * @var array
+   */
+  protected $processed;
+
+  /**
+   * Indicates changes to import have been validated.
+   *
+   * @var bool
+   */
+  protected $validated;
+
+  /**
+   * Constructs a configuration import object.
+   *
+   * @param \Drupal\Core\Config\StorageComparerInterface $storage_comparer
+   *   A storage comparer object used to determin configuration changes and
+   *   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.
+   * @param \Drupal\Core\Config\ConfigFactory $config_factory
+   *   The config factory that statically caches config objects.
+   * @param \Drupal\Core\Entity\EntityManager $entity_manager
+   *   The entity manager used to import config entities.
+   * @param \Drupal\Core\Lock\LockBackendInterface
+   *   The lock backend to ensure multiple imports do not occur at the same time.
+   */
+  public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigFactory $config_factory, EntityManager $entity_manager, LockBackendInterface $lock) {
+    $this->storageComparer = $storage_comparer;
+    $this->eventDispatcher = $event_dispatcher;
+    $this->configFactory = $config_factory;
+    $this->entityManager = $entity_manager;
+    $this->lock = $lock;
+    $this->processed = $this->storageComparer->getEmptyChangelist();
+    // Use an override free context for importing so that overrides to do not
+    // pollute the imported data. The context is hard coded to ensure this is
+    // the case.
+    $this->context = new FreeConfigContext($this->eventDispatcher);
+  }
+
+  /**
+   * 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();
+    $this->processed = $this->storageComparer->getEmptyChangelist();
+    $this->validated = FALSE;
+    return $this;
+  }
+
+  /**
+   * Checks if there are any unprocessed changes.
+   *
+   * @param array $ops
+   *   The operations to check for changes. Defaults to all operations, i.e.
+   *   array('delete', 'create', 'update').
+   *
+   * @return bool
+   *   TRUE if there are changes to process and FALSE if not.
+   */
+  public function hasUnprocessedChanges($ops = array('delete', 'create', 'update')) {
+    foreach ($ops as $op) {
+      if (count($this->getUnprocessed($op))) {
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Gets list of processed changes.
+   *
+   * @return array
+   *   An array containing a list of processed changes.
+   */
+  public function getProcessed() {
+    return $this->processed;
+  }
+
+  /**
+   * Sets a change as processed.
+   *
+   * @param string $op
+   *   The change operation performed, either delete, create or update.
+   * @param string $name
+   *   The name of the configuration processed.
+   */
+  protected function setProcessed($op, $name) {
+    $this->processed[$op][] = $name;
+  }
+
+  /**
+   * Gets a list of unprocessed changes for a given operation.
+   *
+   * @param string $op
+   *   The change operation to get the unprocessed list for, either delete,
+   *   create or update.
+   *
+   * @return array
+   *   An array of configuration names.
+   */
+  public function getUnprocessed($op) {
+    return array_diff($this->storageComparer->getChangelist($op), $this->processed[$op]);
+  }
+
+  /**
+   * Imports the changelist to the target storage.
+   *
+   * @throws \Drupal\Core\Config\ConfigException
+   *
+   * @return \Drupal\Core\Config\ConfigImporter
+   *   The ConfigImporter instance.
+   */
+  public function import() {
+    if ($this->hasUnprocessedChanges()) {
+      // Ensure that the changes have been validated.
+      $this->validate();
+
+      $this->configFactory->enterContext($this->context);
+      if (!$this->lock->acquire(static::ID)) {
+        // Another process is synchronizing configuration.
+        throw new ConfigImporterException(sprintf('%s is already importing', static::ID));
+      }
+      $this->importInvokeOwner();
+      $this->importConfig();
+      // Allow modules to react to a import.
+      $this->notify('import');
+
+      // The import is now complete.
+      $this->lock->release(static::ID);
+      $this->reset();
+      // Leave the context used during import and clear the ConfigFactory's
+      // static cache.
+      $this->configFactory->leaveContext()->reset();
+    }
+    return $this;
+  }
+
+  /**
+   * Dispatches validate event for a ConfigImporter object.
+   *
+   * Events should throw a \Drupal\Core\Config\ConfigImporterException to
+   * prevent an import from occurring.
+   */
+  public function validate() {
+    if (!$this->validated) {
+      $this->notify('validate');
+      $this->validated = TRUE;
+    }
+    return $this;
+  }
+
+  /**
+   * Writes an array of config changes from the source to the target storage.
+   */
+  protected function importConfig() {
+    foreach (array('delete', 'create', 'update') as $op) {
+      foreach ($this->getUnprocessed($op) as $name) {
+        $config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context);
+        if ($op == 'delete') {
+          $config->delete();
+        }
+        else {
+          $data = $this->storageComparer->getSourceStorage()->read($name);
+          $config->setData($data ? $data : array());
+          $config->save();
+        }
+        $this->setProcessed($op, $name);
+      }
+    }
+  }
+
+  /**
+   * Invokes import* methods on configuration entity storage controllers.
+   *
+   * Allow modules to take over configuration change operations for higher-level
+   * configuration data.
+   *
+   * @todo Add support for other extension types; e.g., themes etc.
+   */
+  protected function importInvokeOwner() {
+    // First pass deleted, then new, and lastly changed configuration, in order
+    // to handle dependencies correctly.
+    foreach (array('delete', 'create', 'update') as $op) {
+      foreach ($this->getUnprocessed($op) as $name) {
+        // Call to the configuration entity's storage controller to handle the
+        // configuration change.
+        $handled_by_module = FALSE;
+        // Validate the configuration object name before importing it.
+        // Config::validateName($name);
+        if ($entity_type = config_get_entity_type_by_name($name)) {
+          $old_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context);
+          $old_config->load();
+
+          $data = $this->storageComparer->getSourceStorage()->read($name);
+          $new_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context);
+          if ($data !== FALSE) {
+            $new_config->setData($data);
+          }
+
+          $method = 'import' . ucfirst($op);
+          $handled_by_module = $this->entityManager->getStorageController($entity_type)->$method($name, $new_config, $old_config);
+        }
+        if (!empty($handled_by_module)) {
+          $this->setProcessed($op, $name);
+        }
+      }
+    }
+  }
+
+  /**
+   * Dispatches a config importer event.
+   *
+   * @param string $event_name
+   *   The name of the config importer event to dispatch.
+   */
+  protected function notify($event_name) {
+    $this->eventDispatcher->dispatch(static::ID . '.' . $event_name, new ConfigImporterEvent($this));
+  }
+
+  /**
+   * Determines if a import is already running.
+   *
+   * @return bool
+   *   TRUE if an import is already running, FALSE if not.
+   */
+  public function alreadyImporting() {
+    return !$this->lock->lockMayBeAvailable(static::ID);
+  }
+
+  /**
+   * Returns the identifier for events and locks.
+   *
+   * @return string
+   *   The identifier for events and locks.
+   */
+  public function getId() {
+    return static::ID;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Config/ConfigImporterEvent.php b/core/lib/Drupal/Core/Config/ConfigImporterEvent.php
new file mode 100644
index 000000000000..caef42e930f1
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/ConfigImporterEvent.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Config\ConfigImporterEvent.
+ */
+
+namespace Drupal\Core\Config;
+
+use Symfony\Component\EventDispatcher\Event;
+
+class ConfigImporterEvent extends Event {
+  /**
+   * Configuration import object.
+   *
+   * @var \Drupal\Core\Config\ConfigImporter
+   */
+  protected $configImporter;
+
+  /**
+   * Constructs ConfigImporterEvent.
+   *
+   * @param \Drupal\Core\Config\ConfigImporter $config_importer
+   *   A config import object to notify listeners about.
+   */
+  public function __construct(ConfigImporter $config_importer) {
+    $this->configImporter = $config_importer;
+  }
+
+  /**
+   * Gets the config import object.
+   *
+   * @return \Drupal\Core\Config\ConfigImporter
+   *   The ConfigImporter object.
+   */
+  public function getConfigImporter() {
+    return $this->configImporter;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Config/ConfigImporterException.php b/core/lib/Drupal/Core/Config/ConfigImporterException.php
new file mode 100644
index 000000000000..fd18c4d10163
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/ConfigImporterException.php
@@ -0,0 +1,13 @@
+<?php
+
+/**
+* @file
+* Contains \Drupal\Core\Config\ConfigImporterException.
+*/
+
+namespace Drupal\Core\Config;
+
+/**
+* Exception thrown when a config import fails.
+*/
+class ConfigImporterException extends ConfigException {}
diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php
new file mode 100644
index 000000000000..11622b2dc577
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Config\ConfigInstaller.
+ */
+
+namespace Drupal\Core\Config;
+
+/**
+ * Defines a configuration installer.
+ *
+ * A config installer imports the changes into the configuration system during
+ * module installs.
+ *
+ * The ConfigInstaller has a identifier which is used to construct event names.
+ * The events fired during an import are:
+ * - 'config.installer.validate': Events listening can throw a
+ *   \Drupal\Core\Config\ConfigImporterException to prevent an import from
+ *   occurring.
+ *   @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
+ * - 'config.installer.import': Events listening can react to a successful import.
+ *
+ * @see \Drupal\Core\Config\ConfigImporter
+ */
+class ConfigInstaller extends ConfigImporter {
+
+  /**
+   * The name used to identify events and the lock.
+   */
+  const ID = 'config.installer';
+
+}
diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php
index 5487e04d0a62..fbf4fdcb08dc 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php
@@ -521,7 +521,7 @@ public function importCreate($name, Config $new_config, Config $old_config) {
   }
 
   /**
-   * Update configuration upon synchronizing configuration changes.
+   * Updates configuration upon synchronizing configuration changes.
    *
    * This callback is invoked when configuration is synchronized between storages
    * and allows a module to take over the synchronization of configuration data.
@@ -533,7 +533,7 @@ public function importCreate($name, Config $new_config, Config $old_config) {
    * @param \Drupal\Core\Config\Config $old_config
    *   A configuration object containing the old configuration data.
    */
-  public function importChange($name, Config $new_config, Config $old_config) {
+  public function importUpdate($name, Config $new_config, Config $old_config) {
     $id = static::getIDFromConfigName($name, $this->entityInfo['config_prefix']);
     $entities = $this->load(array($id));
     $entity = $entities[$id];
diff --git a/core/lib/Drupal/Core/Config/StorageComparer.php b/core/lib/Drupal/Core/Config/StorageComparer.php
new file mode 100644
index 000000000000..c7fa957bccb5
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/StorageComparer.php
@@ -0,0 +1,201 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Config\StorageComparer.
+ */
+
+namespace Drupal\Core\Config;
+
+/**
+ * Defines a config storage comparer.
+ */
+class StorageComparer implements StorageComparerInterface {
+
+  /**
+   * The source storage used to discover configuration changes.
+   *
+   * @var \Drupal\Core\Config\StorageInterface
+   */
+  protected $sourceStorage;
+
+  /**
+   * The target storage used to write configuration changes.
+   *
+   * @var \Drupal\Core\Config\StorageInterface
+   */
+  protected $targetStorage;
+
+  /**
+   * List of changes to between the source storage and the target storage.
+   *
+   * @var array
+   */
+  protected $changelist;
+
+  /**
+   * Lists all the configuration object names in the source storage.
+   *
+   * @see \Drupal\Core\Config\StorageComparer::getSourceNames()
+   *
+   * @var array
+   */
+  protected $sourceNames = array();
+
+  /**
+   * Lists all the configuration object names in the target storage.
+   *
+   * @see \Drupal\Core\Config\StorageComparer::getTargetNames()
+   *
+   * @var array
+   */
+  protected $targetNames = array();
+
+  /**
+   * Constructs the Configuration storage comparer.
+   *
+   * @param \Drupal\Core\Config\StorageInterface $source_storage
+   *   Storage controller object used to read configuration.
+   * @param \Drupal\Core\Config\StorageInterface $target_storage
+   *   Storage controller object used to write configuration.
+   */
+  public function __construct(StorageInterface $source_storage, StorageInterface $target_storage) {
+    $this->sourceStorage = $source_storage;
+    $this->targetStorage = $target_storage;
+    $this->changelist = $this->getEmptyChangelist();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSourceStorage() {
+    return $this->sourceStorage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTargetStorage() {
+    return $this->targetStorage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEmptyChangelist() {
+    return array(
+      'create' => array(),
+      'update' => array(),
+      'delete' => array(),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChangelist($op = NULL) {
+    if ($op) {
+      return $this->changelist[$op];
+    }
+    return $this->changelist;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addChangeList($op, array $changes) {
+    // Only add changes that aren't already listed.
+    $changes = array_diff($changes, $this->changelist[$op]);
+    $this->changelist[$op] = array_merge($this->changelist[$op], $changes);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createChangelist() {
+    return $this
+      ->addChangelistCreate()
+      ->addChangelistUpdate()
+      ->addChangelistDelete();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addChangelistDelete() {
+    return $this->addChangeList('delete', array_diff($this->getTargetNames(), $this->getSourceNames()));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addChangelistCreate() {
+    return $this->addChangeList('create', array_diff($this->getSourceNames(), $this->getTargetNames()));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addChangelistUpdate() {
+    foreach (array_intersect($this->getSourceNames(), $this->getTargetNames()) as $name) {
+      // Ignore manifest files.
+      if (substr($name, 0, 9) != 'manifest.') {
+        $source_config_data = $this->sourceStorage->read($name);
+        $target_config_data = $this->targetStorage->read($name);
+        if ($source_config_data !== $target_config_data) {
+          $this->addChangeList('update', array($name));
+        }
+      }
+    }
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function reset() {
+    $this->changelist = $this->getEmptyChangelist();
+    $this->sourceNames = $this->targetNames = array();
+    return $this->createChangelist();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasChanges($ops = array('delete', 'create', 'update')) {
+    foreach ($ops as $op) {
+      if (!empty($this->changelist[$op])) {
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Gets all the configuration names in the source storage.
+   *
+   * @return array
+   *   List of all the configuration names in the source storage.
+   */
+  protected function getSourceNames() {
+    if (empty($this->sourceNames)) {
+      $this->sourceNames = $this->sourceStorage->listAll();
+    }
+    return $this->sourceNames;
+  }
+
+  /**
+   * Gets all the configuration names in the target storage.
+   *
+   * @return array
+   *   List of all the configuration names in the target storage.
+   */
+  protected function getTargetNames() {
+    if (empty($this->targetNames)) {
+      $this->targetNames = $this->targetStorage->listAll();
+    }
+    return $this->targetNames;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Config/StorageComparerInterface.php b/core/lib/Drupal/Core/Config/StorageComparerInterface.php
new file mode 100644
index 000000000000..5ca0f3e04370
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/StorageComparerInterface.php
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Config\StorageComparerInterface.
+ */
+
+namespace Drupal\Core\Config;
+
+/**
+ * Defines an interface for comparison of configuration storage objects.
+ */
+interface StorageComparerInterface {
+
+  /**
+   * Gets the configuration source storage.
+   *
+   * @return \Drupal\Core\Config\StorageInterface
+   *   Storage controller object used to read configuration.
+   */
+  public function getSourceStorage();
+
+  /**
+   * Gets the configuration target storage.
+   *
+   * @return \Drupal\Core\Config\StorageInterface
+   *   Storage controller object used to write configuration.
+   */
+  public function getTargetStorage();
+
+  /**
+   * Gets an empty changelist.
+   *
+   * @return array
+   *   An empty changelist array.
+   */
+  public function getEmptyChangelist();
+
+  /**
+   * Gets the list of differences to import.
+   *
+   * @param string $op
+   *   (optional) A change operation. Either delete, create or update. If
+   *   supplied the returned list will be limited to this operation.
+   *
+   * @return array
+   *   An array of config changes that are yet to be imported.
+   */
+  public function getChangelist($op = NULL);
+
+  /**
+   * Adds changes to the changelist.
+   *
+   * @param string $op
+   *   The change operation performed. Either delete, create or update.
+   * @param array $changes
+   *   Array of changes to add the changelist.
+   *
+   * @return \Drupal\Core\Config\StorageComparerInterface
+   *   An object which implements the StorageComparerInterface.
+   */
+  public function addChangeList($op, array $changes);
+
+  /**
+   * Add differences between source and target configuration storage to changelist.
+   *
+   * @return \Drupal\Core\Config\StorageComparerInterface
+   *   An object which implements the StorageComparerInterface.
+   */
+  public function createChangelist();
+
+  /**
+   * Creates the delete changelist.
+   *
+   * @return \Drupal\Core\Config\StorageComparerInterface
+   *   An object which implements the StorageComparerInterface.
+   */
+  public function addChangelistDelete();
+
+  /**
+   * Creates the create changelist.
+   *
+   * @return \Drupal\Core\Config\StorageComparerInterface
+   *   An object which implements the StorageComparerInterface.
+   */
+  public function addChangelistCreate();
+
+  /**
+   * Creates the update changelist.
+   *
+   * @return \Drupal\Core\Config\StorageComparerInterface
+   *   An object which implements the StorageComparerInterface.
+   */
+  public function addChangelistUpdate();
+
+  /**
+   * Recalculates the differences.
+   *
+   * @return \Drupal\Core\Config\StorageComparerInterface
+   *   An object which implements the StorageComparerInterface.
+   */
+  public function reset();
+
+  /**
+   * Checks if there are any operations with changes to process.
+   *
+   * Until the changelist has been calculated this will always be FALSE.
+   *
+   * @see \Drupal\Core\Config\StorageComparerInterface::createChangelist().
+   *
+   * @param array $ops
+   *   The operations to check for changes. Defaults to all operations, i.e.
+   *   array('delete', 'create', 'update').
+   *
+   * @return bool
+   *   TRUE if there are changes to process and FALSE if not.
+   */
+  public function hasChanges($ops = array('delete', 'create', 'update'));
+
+}
diff --git a/core/lib/Drupal/Core/Config/StorageComparerManifest.php b/core/lib/Drupal/Core/Config/StorageComparerManifest.php
new file mode 100644
index 000000000000..2f8836d200b7
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/StorageComparerManifest.php
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Config\StorageComparerManifest.
+ */
+
+namespace Drupal\Core\Config;
+
+/**
+ * Defines a config storage comparer that uses config entity manifests.
+ *
+ * Config entities maintain 'manifest' files that list the objects they are
+ * currently handling. Each file is a simple indexed array of config object
+ * names. In order to generate a list of objects that have been created or
+ * deleted we need to open these files in both the source and target storage,
+ * generate an array of the objects, and compare them.
+ */
+class StorageComparerManifest extends StorageComparer {
+
+  /**
+   * List of config entities managed by manifests in the source storage.
+   *
+   * @see \Drupal\Core\Config\StorageComparerManifest::getSourceManifestData()
+   *
+   * @var array
+   */
+  protected $sourceManifestData = array();
+
+  /**
+   * List of config entities managed by manifests in the target storage.
+   *
+   * @see \Drupal\Core\Config\StorageComparerManifest::getTargetManifestData()
+   *
+   * @var array
+   */
+  protected $targetManifestData = array();
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addChangelistDelete() {
+    foreach (array_diff_key($this->getTargetManifestData(), $this->getSourceManifestData()) as $value) {
+      $this->addChangeList('delete', array($value['name']));
+    }
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addChangelistCreate() {
+    foreach (array_diff_key($this->getSourceManifestData(), $this->getTargetManifestData()) as $value) {
+      $this->addChangeList('create', array($value['name']));
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the list of config entities from the source storage's manifest files.
+   *
+   * @return array
+   *   The list of config entities in the source storage whose entity type has a
+   *   manifest in the source storage.
+   */
+  protected function getSourceManifestData() {
+    if (empty($this->sourceManifestData)) {
+      foreach ($this->getSourceStorage()->listAll('manifest') as $name) {
+        if ($source_manifest_data = $this->getSourceStorage()->read($name)) {
+          $this->sourceManifestData = array_merge($this->sourceManifestData, $source_manifest_data);
+        }
+      }
+    }
+    return $this->sourceManifestData;
+  }
+
+  /**
+   * Gets the list of config entities from the target storage's manifest files.
+   *
+   * @see \Drupal\Core\Config\ConfigImporter::getSourceManifestData()
+   *
+   * @return array
+   *   The list of config entities in the target storage whose entity type has a
+   *   manifest in the source storage.
+   */
+  protected function getTargetManifestData() {
+    if (empty($this->targetManifestData)) {
+      foreach ($this->getSourceStorage()->listAll('manifest') as $name) {
+        if ($target_manifest_data = $this->targetStorage->read($name)) {
+          $this->targetManifestData = array_merge($this->targetManifestData, $target_manifest_data);
+        }
+      }
+    }
+    return $this->targetManifestData;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function reset() {
+    $this->sourceManifestData = $this->targetManifestData = array();
+    return parent::reset();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
new file mode 100644
index 000000000000..ba54de3e2927
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\ConfigImportSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Config\Config;
+use Drupal\Core\Config\ConfigImporterEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+
+/**
+ * Config import subscriber for config import events.
+ */
+class ConfigImportSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Validates the configuration to be imported.
+   *
+   * @param \Drupal\Core\Config\ConfigImporterEvent $event
+   *   The Event to process.
+   *
+   * @throws \Drupal\Core\Config\ConfigNameException
+   */
+  public function onConfigImporterValidate(ConfigImporterEvent $event) {
+    foreach (array('delete', 'create', 'update') as $op) {
+      foreach ($event->getConfigImporter()->getUnprocessed($op) as $name) {
+        Config::validateName($name);
+      }
+    }
+  }
+
+  /**
+   * Registers the methods in this class that should be listeners.
+   *
+   * @return array
+   *   An array of event listener definitions.
+   */
+  static function getSubscribedEvents() {
+    $events['config.importer.validate'][] = array('onConfigImporterValidate', 40);
+    $events['config.installer.validate'][] = array('onConfigImporterValidate', 40);
+    return $events;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/EventSubscriber/ConfigSnapshotSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigSnapshotSubscriber.php
new file mode 100644
index 000000000000..54c60dc1ba49
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/ConfigSnapshotSubscriber.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\ConfigSnapshotSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Config\Config;
+use Drupal\Core\Config\StorageInterface;
+use Drupal\Core\Config\ConfigImporterEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Create a snapshot when config is imported.
+ */
+class ConfigSnapshotSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The source storage used to discover configuration changes.
+   *
+   * @var \Drupal\Core\Config\StorageInterface
+   */
+  protected $sourceStorage;
+
+  /**
+   * The snapshot storage used to write configuration changes.
+   *
+   * @var \Drupal\Core\Config\StorageInterface
+   */
+  protected $snapshotStorage;
+
+  /**
+   * Constructs the ConfigSnapshotSubscriber object.
+   *
+   * @param StorageInterface $source_storage
+   *   The source storage used to discover configuration changes.
+   * @param StorageInterface $snapshot_storage
+   *   The snapshot storage used to write configuration changes.
+   */
+  public function __construct(StorageInterface $source_storage, StorageInterface $snapshot_storage) {
+    $this->sourceStorage = $source_storage;
+    $this->snapshotStorage = $snapshot_storage;
+  }
+
+  /**
+   * Creates a config snapshot.
+   *
+   * @param \Drupal\Core\Config\ConfigImporterEvent $event
+   *   The Event to process.
+   */
+  public function onConfigImporterImport(ConfigImporterEvent $event) {
+    config_import_create_snapshot($this->sourceStorage, $this->snapshotStorage);
+  }
+
+  /**
+   * Registers the methods in this class that should be listeners.
+   *
+   * @return array
+   *   An array of event listener definitions.
+   */
+  static function getSubscribedEvents() {
+    $events['config.importer.import'][] = array('onConfigImporterImport', 40);
+    return $events;
+  }
+
+}
diff --git a/core/modules/config/config.admin.inc b/core/modules/config/config.admin.inc
index 57704de8e9ab..a9612861b8ba 100644
--- a/core/modules/config/config.admin.inc
+++ b/core/modules/config/config.admin.inc
@@ -5,9 +5,12 @@
  * Admin page callbacks for the config module.
  */
 
-use Drupal\Core\Config\StorageInterface;
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\OpenModalDialogCommand;
+use Drupal\Core\Config\ConfigException;
+use Drupal\Core\Config\ConfigImporter;
+use Drupal\Core\Config\StorageComparerManifest;
+use Drupal\Core\Config\StorageInterface;
 
 /**
  * Helper function to construct the storage changes in a configuration synchronization form.
@@ -16,12 +19,13 @@
  *   The form structure to add to. Passed by reference.
  * @param array $form_state
  *   The current state of the form. Passed by reference.
- * @param Drupal\Core\Config\StorageInterface $source_storage
+ * @param \Drupal\Core\Config\StorageInterface $source_storage
  *   The source storage to retrieve differences from.
- * @param Drupal\Core\Config\StorageInterface $target_storage
- *   The target storage to compare differences to.
+ *
+ * @return array
+ *   The form with the configuration storage changes.
  */
-function config_admin_sync_form(array &$form, array &$form_state, StorageInterface $source_storage, StorageInterface $target_storage) {
+function config_admin_sync_form(array &$form, array &$form_state, StorageInterface $source_storage) {
   $source_list = $source_storage->listAll();
   if (empty($source_list)) {
     $form['no_changes'] = array(
@@ -31,18 +35,23 @@ function config_admin_sync_form(array &$form, array &$form_state, StorageInterfa
     return $form;
   }
 
-  $config_changes = config_sync_get_changes($source_storage, $target_storage);
-  if (empty($config_changes)) {
+  $config_comparer = new StorageComparerManifest(Drupal::service('config.storage.staging'), Drupal::service('config.storage'));
+  if (!$config_comparer->createChangelist()->hasChanges()) {
     $form['no_changes'] = array(
       '#markup' => t('There are no configuration changes.'),
     );
+    $form['actions']['#access'] = FALSE;
     return $form;
   }
+  else {
+    // Store the comparer for use in the submit.
+    $form_state['storage_comparer'] = $config_comparer;
+  }
 
   // Add the AJAX library to the form for dialog support.
   $form['#attached']['library'][] = array('system', 'drupal.ajax');
 
-  foreach ($config_changes as $config_change_type => $config_files) {
+  foreach ($config_comparer->getChangelist() as $config_change_type => $config_files) {
     if (empty($config_files)) {
       continue;
     }
@@ -58,7 +67,7 @@ function config_admin_sync_form(array &$form, array &$form_state, StorageInterfa
         $form[$config_change_type]['heading']['#value'] = format_plural(count($config_files), '@count new', '@count new');
         break;
 
-      case 'change':
+      case 'update':
         $form[$config_change_type]['heading']['#value'] = format_plural(count($config_files), '@count changed', '@count changed');
         break;
 
@@ -122,23 +131,37 @@ function config_admin_import_form($form, &$form_state) {
  * Form submission handler for config_admin_import_form().
  */
 function config_admin_import_form_submit($form, &$form_state) {
-  if (!lock()->lockMayBeAvailable(CONFIG_IMPORT_LOCK)) {
+  $config_importer = new ConfigImporter(
+    $form_state['storage_comparer'],
+    Drupal::service('event_dispatcher'),
+    Drupal::service('config.factory'),
+    Drupal::entityManager(),
+    Drupal::lock()
+  );
+  if ($config_importer->alreadyImporting()) {
     drupal_set_message(t('Another request may be synchronizing configuration already.'));
   }
-  else if (config_import()) {
-    // Once a sync completes, we empty the staging directory. This prevents
-    // changes from being accidentally overwritten by stray files getting
-    // imported later.
-    $source_storage = drupal_container()->get('config.storage.staging');
-    foreach ($source_storage->listAll() as $name) {
-      $source_storage->delete($name);
+  else{
+    try {
+      $config_importer->import();
+      drupal_flush_all_caches();
+      drupal_set_message(t('The configuration was imported successfully.'));
+
+      // Once a sync completes, we empty the staging directory. This prevents
+      // changes from being accidentally overwritten by stray files getting
+      // imported later.
+      $source_storage = $config_importer->getStorageComparer()->getSourceStorage();
+      foreach ($source_storage->listAll() as $name) {
+        $source_storage->delete($name);
+      }
+    }
+    catch (ConfigException $e) {
+      // Return a negative result for UI purposes. We do not differentiate between
+      // an actual synchronization error and a failed lock, because concurrent
+      // synchronizations are an edge-case happening only when multiple developers
+      // or site builders attempt to do it without coordinating.
+      watchdog_exception('config_import', $e);
+      drupal_set_message(t('The import failed due to an error. Any errors have been logged.'), 'error');
     }
-
-    drupal_flush_all_caches();
-
-    drupal_set_message(t('The configuration was imported successfully.'));
-  }
-  else {
-    drupal_set_message(t('The import failed due to an error. Any errors have been logged.'), 'error');
   }
 }
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php
index 497fd525b9bb..7322324bd27a 100644
--- a/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php
@@ -14,6 +14,14 @@
  * Tests CRUD operations on configuration objects.
  */
 class ConfigCRUDTest extends DrupalUnitTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('system');
+
   public static function getInfo() {
     return array(
       'name' => 'CRUD operations',
@@ -193,8 +201,15 @@ function testNameValidation() {
     $manifest_data['new']['name'] = 'invalid';
     $staging->write('manifest.invalid_object_name', $manifest_data);
 
-    // Assert that config_import returns false indicating a failure.
-    $this->assertFalse(config_import(), "Config import failed when trying to importing an object with an invalid name");
+    // Verify that an exception is thrown when importing.
+    $message = 'Expected ConfigNameException was thrown when attempting to sync invalid configuration.';
+    try {
+      $this->configImporter()->import();
+      $this->fail($message);
+    }
+    catch (ConfigNameException $e) {
+      $this->pass($message);
+    }
   }
 
 }
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php
index 03fda2a77c70..9d1e6be66b04 100644
--- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php
@@ -2,17 +2,26 @@
 
 /**
  * @file
- * Definition of Drupal\config\Tests\ConfigImportTest.
+ * Contains \Drupal\config\Tests\ConfigImporterTest.
  */
 
 namespace Drupal\config\Tests;
 
+use Drupal\Core\Config\ConfigImporter;
+use Drupal\Core\Config\StorageComparerManifest;
 use Drupal\simpletest\DrupalUnitTestBase;
 
 /**
  * Tests importing configuration from files into active configuration.
  */
-class ConfigImportTest extends DrupalUnitTestBase {
+class ConfigImporterTest extends DrupalUnitTestBase {
+
+  /**
+   * Config Importer object used for testing.
+   *
+   * @var \Drupal\Core\Config\ConfigImporter
+   */
+  protected $configImporter;
 
   /**
    * Modules to enable.
@@ -39,6 +48,18 @@ function setUp() {
     // variable being used for recording hook invocations by this test already,
     // so it has to be cleared out manually.
     unset($GLOBALS['hook_config_test']);
+
+    // Set up the ConfigImporter object for testing.
+    $config_comparer = new StorageComparerManifest(
+      $this->container->get('config.storage.staging'),
+      $this->container->get('config.storage'));
+    $this->configImporter = new ConfigImporter(
+      $config_comparer->createChangelist(),
+      $this->container->get('event_dispatcher'),
+      $this->container->get('config.factory'),
+      $this->container->get('plugin.manager.entity'),
+      $this->container->get('lock')
+    );
   }
 
   /**
@@ -70,7 +91,7 @@ function testDeleted() {
     // Create an empty manifest to delete the configuration object.
     $staging->write('manifest.config_test.dynamic', array());
     // Import.
-    config_import();
+    $this->configImporter->reset()->import();
 
     // Verify the values have disappeared.
     $this->assertIdentical($storage->read($dynamic_name), FALSE);
@@ -87,7 +108,7 @@ function testDeleted() {
     $this->assertTrue(isset($GLOBALS['hook_config_test']['delete']));
 
     // Verify that there is nothing more to import.
-    $this->assertFalse(config_sync_get_changes($staging, $storage));
+    $this->assertFalse($this->configImporter->hasUnprocessedChanges());
   }
 
   /**
@@ -123,7 +144,7 @@ function testNew() {
     $this->assertIdentical($staging->exists($dynamic_name), TRUE, $dynamic_name . ' found.');
 
     // Import.
-    config_import();
+    $this->configImporter->reset()->import();
 
     // Verify the values appeared.
     $config = config($dynamic_name);
@@ -138,7 +159,7 @@ function testNew() {
     $this->assertFalse(isset($GLOBALS['hook_config_test']['delete']));
 
     // Verify that there is nothing more to import.
-    $this->assertFalse(config_sync_get_changes($staging, $storage));
+    $this->assertFalse($this->configImporter->hasUnprocessedChanges());
   }
 
   /**
@@ -174,7 +195,7 @@ function testUpdated() {
     $this->assertIdentical($config->get('label'), 'Default');
 
     // Import.
-    config_import();
+    $this->configImporter->reset()->import();
 
     // Verify the values were updated.
     $config = config($name);
@@ -195,7 +216,7 @@ function testUpdated() {
     $this->assertFalse(isset($GLOBALS['hook_config_test']['delete']));
 
     // Verify that there is nothing more to import.
-    $this->assertFalse(config_sync_get_changes($staging, $storage));
+    $this->assertFalse($this->configImporter->hasUnprocessedChanges());
   }
 
 }
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php
index ad6ac3208084..61c6e473e914 100644
--- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php
@@ -112,7 +112,8 @@ function testImportLock() {
     $this->assertNoText(t('There are no configuration changes.'));
 
     // Acquire a fake-lock on the import mechanism.
-    lock()->acquire('config_import');
+    $config_importer_lock = $this->configImporter()->getId();
+    $this->container->get('lock')->acquire($config_importer_lock);
 
     // Attempt to import configuration and verify that an error message appears.
     $this->drupalPost(NULL, array(), t('Import all'));
@@ -120,7 +121,7 @@ function testImportLock() {
     $this->assertText(t('Another request may be synchronizing configuration already.'));
 
     // Release the lock, just to keep testing sane.
-    lock()->release('config_import');
+    $this->container->get('lock')->release($config_importer_lock);
 
     // Verify site name has not changed.
     $this->assertNotEqual($new_site_name, config('system.site')->get('name'));
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php
index 6dfcf4edef92..e0748a7207b3 100644
--- a/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php
@@ -8,7 +8,6 @@
 namespace Drupal\config\Tests;
 
 use Drupal\simpletest\DrupalUnitTestBase;
-use Drupal\Core\Config\Context\ConfigContext;
 
 /**
  * Tests configuration overrides via $conf in settings.php.
@@ -118,7 +117,7 @@ function testConfOverride() {
     $staging->write('config_test.system', $expected_new_data);
 
     // Import changed data from staging to active.
-    config_import();
+    $this->configImporter()->import();
     $data = $active->read('config_test.system');
 
     // Verify that the new configuration data exists. Have to read storage
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php
index 439f1916ad9a..7315c53cb355 100644
--- a/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\config\Tests;
 
+use Drupal\Core\Config\StorageComparer;
 use Drupal\simpletest\DrupalUnitTestBase;
 
 /**
@@ -45,21 +46,24 @@ function testSnapshot() {
     $config_key = 'foo';
     $new_data = 'foobar';
 
+    $active_snapshot_comparer = new StorageComparer($active, $snapshot);
+    $staging_snapshot_comparer = new StorageComparer($staging, $snapshot);
+
     // Verify that we have an initial snapshot that matches the active
     // configuration. This has to be true as no config should be installed.
-    $this->assertFalse(config_sync_get_changes($snapshot, $active, FALSE));
+    $this->assertFalse($active_snapshot_comparer->createChangelist()->hasChanges());
 
     // Install the default config.
     config_install_default_config('module', 'config_test');
     // Although we have imported config this has not affected the snapshot.
-    $this->assertTrue(config_sync_get_changes($snapshot, $active, FALSE));
+    $this->assertTrue($active_snapshot_comparer->reset()->hasChanges());
 
     // Update the config snapshot.
     config_import_create_snapshot($active, $snapshot);
 
     // The snapshot and active config should now contain the same config
     // objects.
-    $this->assertFalse(config_sync_get_changes($snapshot, $active, FALSE));
+    $this->assertFalse($active_snapshot_comparer->reset()->hasChanges());
 
     // Change a configuration value in staging.
     $staging_data = config($config_name)->get();
@@ -67,20 +71,19 @@ function testSnapshot() {
     $staging->write($config_name, $staging_data);
 
     // Verify that active and snapshot match, and that staging doesn't match
-    // either of them.
-    $this->assertFalse(config_sync_get_changes($snapshot, $active, FALSE));
-    $this->assertTrue(config_sync_get_changes($snapshot, $staging, FALSE));
-    $this->assertTrue(config_sync_get_changes($staging, $active, FALSE));
+    // active.
+    $this->assertFalse($active_snapshot_comparer->reset()->hasChanges());
+    $this->assertTrue($staging_snapshot_comparer->createChangelist()->hasChanges());
 
     // Import changed data from staging to active.
-    config_import();
+    $this->configImporter()->import();
 
     // Verify changed config was properly imported.
     $this->assertIdentical(config($config_name)->get($config_key), $new_data);
 
     // Verify that a new snapshot was created which and that it matches
     // the active config.
-    $this->assertFalse(config_sync_get_changes($snapshot, $active, FALSE));
+    $this->assertFalse($active_snapshot_comparer->reset()->hasChanges());
   }
 
 }
diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTestStorageController.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTestStorageController.php
index e1fe4fe025e0..a73cffb551e4 100644
--- a/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTestStorageController.php
+++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTestStorageController.php
@@ -26,13 +26,13 @@ public function importCreate($name, Config $new_config, Config $old_config) {
   }
 
   /**
-   * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::importChange().
+   * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::importUpdate().
    */
-  public function importChange($name, Config $new_config, Config $old_config) {
+  public function importUpdate($name, Config $new_config, Config $old_config) {
     // Set a global value we can check in test code.
     $GLOBALS['hook_config_import'] = __METHOD__;
 
-    return parent::importChange($name, $new_config, $old_config);
+    return parent::importUpdate($name, $new_config, $old_config);
   }
 
   /**
diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldImportChangeTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportChangeTest.php
index aaf648d27711..70e7286b0286 100644
--- a/core/modules/field/lib/Drupal/field/Tests/FieldImportChangeTest.php
+++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportChangeTest.php
@@ -54,7 +54,7 @@ function testImportChange() {
     $staging->write($instance_config_name, $instance);
 
     // Import the content of the staging directory.
-    config_import();
+    $this->configImporter()->import();
 
     // Check that the updated config was correctly imported.
     $instance = entity_load('field_instance', $instance_id);
diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php
index ff21fac97bda..577de45f44a9 100644
--- a/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php
+++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php
@@ -69,7 +69,7 @@ function testImportCreate() {
     $staging->write($instance_manifest_name, $instance_manifest);
 
     // Import the content of the staging directory.
-    config_import();
+    $this->configImporter()->import();
 
     // Check that the field and instance were created.
     $field = entity_load('field_entity', $field_id);
diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php
index 0db6455947f6..c1ac59a5a241 100644
--- a/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php
+++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php
@@ -66,7 +66,7 @@ function testImportDelete() {
     $staging->write($instance_manifest_name, $instance_manifest);
 
     // Import the content of the staging directory.
-    config_import();
+    $this->configImporter()->import();
 
     // Check that the field and instance are gone.
     $field = entity_load('field_entity', $field_id, TRUE);
diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php b/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php
index 223e0acc6f8a..23f5269ff6b3 100644
--- a/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php
+++ b/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php
@@ -38,8 +38,8 @@ function setUp() {
     $this->installSchema('entity_test', 'entity_test');
     $this->installSchema('field_test', array('test_entity', 'test_entity_revision', 'test_entity_bundle'));
 
-    // Set default storage backend.
-    $this->installConfig(array('field'));
+    // Set default storage backend and configure the theme system.
+    $this->installConfig(array('field', 'system'));
   }
 
   /**
diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageAdminStylesTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageAdminStylesTest.php
index 4cb1dca043ce..f667a671ad09 100644
--- a/core/modules/image/lib/Drupal/image/Tests/ImageAdminStylesTest.php
+++ b/core/modules/image/lib/Drupal/image/Tests/ImageAdminStylesTest.php
@@ -367,7 +367,7 @@ function testConfigImport() {
     unset($manifest_data[$style_name]);
     $staging = $this->container->get('config.storage.staging');
     $staging->write('manifest.image.style', $manifest_data);
-    config_import();
+    $this->configImporter()->import();
 
     $this->assertFalse(entity_load('image_style', $style_name), 'Style deleted after config import.');
     $this->assertEqual($this->getImageCount($style), 0, 'Image style was flushed after being deleted by config import.');
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
index 259d2750990a..6e791b3fc64e 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
@@ -9,6 +9,8 @@
 
 use Drupal\Core\Database\Database;
 use Drupal\Component\Utility\Settings;
+use Drupal\Core\Config\ConfigImporter;
+use Drupal\Core\Config\StorageComparerManifest;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Database\ConnectionNotDefinedException;
 use Drupal\Core\DrupalKernel;
@@ -167,6 +169,13 @@ abstract class TestBase {
    */
   protected $container;
 
+  /**
+   * The config importer that can used in a test.
+   *
+   * @var \Drupal\Core\Config\ConfigImporter
+   */
+  protected $configImporter;
+
   /**
    * Constructor for Test.
    *
@@ -1272,4 +1281,28 @@ public static function generatePermutations($parameters) {
   public static function filePreDeleteCallback($path) {
     chmod($path, 0700);
   }
+
+  /**
+   * Returns a ConfigImporter object to import test importing of configuration.
+   *
+   * @return \Drupal\Core\Config\ConfigImporter
+   *   The ConfigImporter object.
+   */
+  public function configImporter() {
+    if (!$this->configImporter) {
+      // Set up the ConfigImporter object for testing.
+      $config_comparer = new StorageComparerManifest(
+        $this->container->get('config.storage.staging'),
+        $this->container->get('config.storage'));
+      $this->configImporter = new ConfigImporter(
+        $config_comparer,
+        $this->container->get('event_dispatcher'),
+        $this->container->get('config.factory'),
+        $this->container->get('plugin.manager.entity'),
+        $this->container->get('lock')
+      );
+    }
+    // Always recalculate the changelist when called.
+    return $this->configImporter->reset();
+  }
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Database/RegressionTest.php b/core/modules/system/lib/Drupal/system/Tests/Database/RegressionTest.php
index 4ad4417f9c4a..f38b49f0640f 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Database/RegressionTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Database/RegressionTest.php
@@ -12,6 +12,13 @@
  */
 class RegressionTest extends DatabaseTestBase {
 
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('node');
+
   public static function getInfo() {
     return array(
       'name' => 'Regression tests',
diff --git a/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php b/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php
index 712638430acf..9530fd8d0e17 100644
--- a/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php
+++ b/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php
@@ -25,7 +25,7 @@ class TextPlainUnitTest extends DrupalUnitTestBase {
    *
    * @var array
    */
-  public static $modules = array('entity', 'field', 'field_sql_storage', 'text', 'field_test');
+  public static $modules = array('system', 'entity', 'field', 'field_sql_storage', 'text', 'field_test');
 
   /**
    * Contains rendered content.
@@ -45,7 +45,8 @@ public static function getInfo() {
   function setUp() {
     parent::setUp();
 
-    $this->installConfig(array('field'));
+    // Configure the theme system.
+    $this->installConfig(array('system', 'field'));
 
     // @todo Add helper methods for all of the following.
 
diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewTestConfigInstaller.php b/core/modules/views/lib/Drupal/views/Tests/ViewTestConfigInstaller.php
new file mode 100644
index 000000000000..c514ebdd65b0
--- /dev/null
+++ b/core/modules/views/lib/Drupal/views/Tests/ViewTestConfigInstaller.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\views\Tests\ViewTestConfigInstaller.
+ */
+
+namespace Drupal\views\Tests;
+
+use Drupal\Core\Config\ConfigImporter;
+
+/**
+ * Defines a configuration installer.
+ *
+ * A config installer imports test views for views testing.
+ *
+ * @see \Drupal\Core\Config\ConfigImporter
+ * @see \Drupal\views\Tests\ViewTestData
+ */
+class ViewTestConfigInstaller extends ConfigImporter {
+
+  /**
+   * The name used to identify events and the lock.
+   */
+  const ID = 'views.test.installer';
+
+}
diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewTestData.php b/core/modules/views/lib/Drupal/views/Tests/ViewTestData.php
index cf2c0d8df3ee..3f2bfd1dd396 100644
--- a/core/modules/views/lib/Drupal/views/Tests/ViewTestData.php
+++ b/core/modules/views/lib/Drupal/views/Tests/ViewTestData.php
@@ -8,6 +8,7 @@
 namespace Drupal\views\Tests;
 
 use Drupal\Core\Config\FileStorage;
+use Drupal\Core\Config\StorageComparer;
 
 /**
  * Provides tests view data and the base test schema with sample data records.
@@ -39,13 +40,6 @@ public static function importTestViews($class, $modules = array()) {
       $class = get_parent_class($class);
     }
     if (!empty($views)) {
-      $target_storage = drupal_container()->get('config.storage');
-      $config_changes = array(
-        'delete' => array(),
-        'create' => array(),
-        'change' => array(),
-      );
-
       $module_handler = \Drupal::moduleHandler();
       foreach ($modules as $module) {
         $config_dir = drupal_get_path('module', $module) . '/test_views';
@@ -54,16 +48,27 @@ public static function importTestViews($class, $modules = array()) {
         }
 
         $source_storage = new FileStorage($config_dir);
+        // Only import views used by test.
+        $views_to_import = array();
         foreach ($source_storage->listAll('views.view.') as $config_name) {
           $id = str_replace('views.view.', '', $config_name);
           if (in_array($id, $views)) {
-            $config_changes['create'][] = $config_name;
+            $views_to_import[] = $config_name;
           }
         }
-      }
-      if (!empty($config_changes['create'])) {
-        $remaining_changes = config_import_invoke_owner($config_changes, $source_storage, $target_storage);
-        config_sync_changes($remaining_changes, $source_storage, $target_storage);
+        $storage_comparer = new StorageComparer(
+          $source_storage,
+          \Drupal::service('config.storage')
+        );
+        $storage_comparer->addChangelist('create', $views_to_import);
+        $installer = new ViewTestConfigInstaller(
+          $storage_comparer,
+          \Drupal::service('event_dispatcher'),
+          \Drupal::service('config.factory'),
+          \Drupal::entityManager(),
+          \Drupal::lock()
+        );
+        $installer->import();
       }
     }
   }
diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewUnitTestBase.php b/core/modules/views/lib/Drupal/views/Tests/ViewUnitTestBase.php
index 80b659cdfa76..134b97afd592 100644
--- a/core/modules/views/lib/Drupal/views/Tests/ViewUnitTestBase.php
+++ b/core/modules/views/lib/Drupal/views/Tests/ViewUnitTestBase.php
@@ -29,7 +29,7 @@ abstract class ViewUnitTestBase extends DrupalUnitTestBase {
    *
    * @var array
    */
-  public static $modules = array('views', 'views_test_config', 'views_test_data');
+  public static $modules = array('system', 'views', 'views_test_config', 'views_test_data');
 
   protected function setUp() {
     parent::setUp();
@@ -62,6 +62,9 @@ protected function setUpFixtures() {
     }
     $query->execute();
 
+    // Tests implementing ViewUnitTestBase depend on the theme system being
+    // properly configured.
+    $this->installConfig(array('system'));
     ViewTestData::importTestViews(get_class($this), array('views_test_config'));
   }
 
-- 
GitLab