From 105ded63a12a825aa01d3ed27d4e041e4fee1d3a Mon Sep 17 00:00:00 2001
From: Kevin Wenger <wenger.kev@gmail.com>
Date: Fri, 21 Apr 2017 09:33:53 +0200
Subject: [PATCH 1/1] Compatibility D8.3 from alpha5

---
 default_content.module                             |   2 +-
 default_content.services.yml                       |  13 +-
 drush/default_content.drush.inc                    |  29 +-
 src/DefaultContentManager.php                      | 430 ---------------------
 src/DefaultContentManagerInterface.php             |  76 ----
 src/DefaultContentScanner.php                      |  49 ---
 src/DefaultContentServiceProvider.php              |   5 +-
 src/Exporter.php                                   | 254 ++++++++++++
 src/Exporter.php.rej                               | 257 ++++++++++++
 src/ExporterInterface.php                          |  57 +++
 src/Importer.php                                   | 258 +++++++++++++
 src/ImporterInterface.php                          |  21 +
 src/Scanner.php                                    |  43 +++
 src/ScannerInterface.php                           |  21 +
 tests/src/Functional/DefaultContentTest.php        |  14 +-
 .../DefaultContentManagerIntegrationTest.php       |  40 +-
 16 files changed, 969 insertions(+), 600 deletions(-)
 delete mode 100644 src/DefaultContentManager.php
 delete mode 100644 src/DefaultContentManagerInterface.php
 delete mode 100644 src/DefaultContentScanner.php
 create mode 100644 src/Exporter.php
 create mode 100644 src/Exporter.php.rej
 create mode 100644 src/ExporterInterface.php
 create mode 100644 src/Importer.php
 create mode 100644 src/ImporterInterface.php
 create mode 100644 src/Scanner.php
 create mode 100644 src/ScannerInterface.php

diff --git a/default_content.module b/default_content.module
index 0e307ca..ce3422a 100644
--- a/default_content.module
+++ b/default_content.module
@@ -12,7 +12,7 @@ function default_content_modules_installed($modules) {
   // @todo Move this to an event once we have HookEvent.
   foreach ($modules as $module) {
     if (!\Drupal::isConfigSyncing()) {
-      \Drupal::service('default_content.manager')->importContent($module);
+      \Drupal::service('default_content.importer')->importContent($module);
     }
   }
 }
diff --git a/default_content.services.yml b/default_content.services.yml
index b2fd96e..aa745b9 100644
--- a/default_content.services.yml
+++ b/default_content.services.yml
@@ -1,4 +1,11 @@
+parameters:
+  default_content.link_domain: 'http://drupal.org'
 services:
-  default_content.manager:
-    class: Drupal\default_content\DefaultContentManager
-    arguments: ['@serializer', '@plugin.manager.rest', '@current_user', '@entity_type.manager', '@entity.repository', '@rest.link_manager', '@event_dispatcher', '@module_handler', '@info_parser']
+  default_content.scanner:
+    class: Drupal\default_content\Scanner
+  default_content.importer:
+    class: Drupal\default_content\Importer
+    arguments: ['@serializer', '@plugin.manager.rest', '@entity_type.manager', '@hal.link_manager', '@event_dispatcher', '@default_content.scanner', '%default_content.link_domain%']
+  default_content.exporter:
+    class: Drupal\default_content\Exporter
+    arguments: ['@serializer', '@entity_type.manager', '@entity.repository', '@hal.link_manager', '@event_dispatcher', '@module_handler', '@info_parser', '%default_content.link_domain%']
diff --git a/drush/default_content.drush.inc b/drush/default_content.drush.inc
index 65375f9..6c4ace0 100644
--- a/drush/default_content.drush.inc
+++ b/drush/default_content.drush.inc
@@ -1,6 +1,5 @@
 <?php
 
-use Drupal\user\Entity\User;
 /**
  * @file
  * Drush integration for the default_content module.
@@ -55,9 +54,9 @@ function default_content_drush_command() {
  *   The entity ID to export.
  */
 function drush_default_content_export($entity_type_id, $entity_id) {
-  /** @var \Drupal\default_content\DefaultContentManagerInterface $manager */
-  $manager = \Drupal::service('default_content.manager');
-  $export = $manager->exportContent($entity_type_id, $entity_id);
+  /** @var \Drupal\default_content\DefaultContentExporterInterface $exporter */
+  $exporter = \Drupal::service('default_content.exporter');
+  $export = $exporter->exportContent($entity_type_id, $entity_id);
 
   if ($file = drush_get_option('file')) {
     file_put_contents($file, $export);
@@ -76,20 +75,20 @@ function drush_default_content_export($entity_type_id, $entity_id) {
  *   (Optional) The entity ID to export or all entities will be exported.
  */
 function drush_default_content_export_references($entity_type_id, $entity_id = NULL) {
-  /** @var \Drupal\default_content\DefaultContentManagerInterface $manager */
-  $manager = \Drupal::service('default_content.manager');
+  /** @var \Drupal\default_content\DefaultContentExporterInterface $exporter */
+  $exporter = \Drupal::service('default_content.exporter');
 
   $folder = drush_get_option('folder', '.');
   if (is_null($entity_id)) {
     $entities = \Drupal::entityQuery($entity_type_id)->execute();
   }
   else {
-    $entities = array($entity_id);
+    $entities = [$entity_id];
   }
   // @todo Add paging.
   foreach ($entities as $entity_id) {
-    $serialized_by_type = $manager->exportContentWithReferences($entity_type_id, $entity_id);
-    $manager->writeDefaultContent($serialized_by_type, $folder);
+    $serialized_by_type = $exporter->exportContentWithReferences($entity_type_id, $entity_id);
+    $exporter->writeDefaultContent($serialized_by_type, $folder);
   }
 }
 
@@ -100,9 +99,11 @@ function drush_default_content_export_references($entity_type_id, $entity_id = N
  *   The module name to export.
  */
 function drush_default_content_export_module($module_name) {
-  /** @var \Drupal\default_content\DefaultContentManagerInterface $manager */
-  $manager = \Drupal::service('default_content.manager');
-  $serialized_by_type = $manager->exportModuleContent($module_name);
-  $module_folder = \Drupal::moduleHandler()->getModule($module_name)->getPath() . '/content';
-  $manager->writeDefaultContent($serialized_by_type, $module_folder);
+  /** @var \Drupal\default_content\DefaultContentExporterInterface $exporter */
+  $exporter = \Drupal::service('default_content.exporter');
+  $serialized_by_type = $exporter->exportModuleContent($module_name);
+  $module_folder = \Drupal::moduleHandler()
+    ->getModule($module_name)
+    ->getPath() . '/content';
+  $exporter->writeDefaultContent($serialized_by_type, $module_folder);
 }
diff --git a/src/DefaultContentManager.php b/src/DefaultContentManager.php
deleted file mode 100644
index 40acca2..0000000
--- a/src/DefaultContentManager.php
+++ /dev/null
@@ -1,430 +0,0 @@
-<?php
-
-namespace Drupal\default_content;
-
-use Drupal\Component\Graph\Graph;
-use Drupal\Component\Render\FormattableMarkup;
-use Drupal\Core\Entity\ContentEntityInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Entity\EntityRepositoryInterface;
-use Drupal\Core\Extension\InfoParserInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\default_content\Event\DefaultContentEvents;
-use Drupal\default_content\Event\ExportEvent;
-use Drupal\default_content\Event\ImportEvent;
-use Drupal\rest\LinkManager\LinkManagerInterface;
-use Drupal\rest\Plugin\Type\ResourcePluginManager;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Symfony\Component\Serializer\Serializer;
-
-/**
- * A service for handling import of default content.
- *
- * @todo throw useful exceptions
- */
-class DefaultContentManager implements DefaultContentManagerInterface {
-
-  const LINK_DOMAIN = 'http://drupal.org';
-
-  /**
-   * The serializer service.
-   *
-   * @var \Symfony\Component\Serializer\Serializer
-   */
-  protected $serializer;
-
-  /**
-   * The rest resource plugin manager.
-   *
-   * @var \Drupal\rest\Plugin\Type\ResourcePluginManager
-   */
-  protected $resourcePluginManager;
-
-  /**
-   * The current user.
-   *
-   * @var \Drupal\Core\Session\AccountInterface
-   */
-  protected $currentUser;
-
-  /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected $entityManager;
-
-  /**
-   * The entity repository.
-   *
-   * @var \Drupal\Core\Entity\EntityRepositoryInterface
-   */
-  protected $entityRepository;
-
-  /**
-   * The module handler.
-   *
-   * @var \Drupal\Core\Extension\ModuleHandlerInterface
-   */
-  protected $moduleHandler;
-
-  /**
-   * The info file parser.
-   *
-   * @var \Drupal\Core\Extension\InfoParserInterface
-   */
-  protected $infoParser;
-
-  /**
-   * The file system scanner.
-   *
-   * @var \Drupal\default_content\DefaultContentScanner
-   */
-  protected $scanner;
-
-  /**
-   * A list of vertex objects keyed by their link.
-   *
-   * @var array
-   */
-  protected $vertexes = array();
-
-  /**
-   * The graph entries.
-   *
-   * @var array
-   */
-  protected $graph = [];
-
-  /**
-   * The link manager service.
-   *
-   * @var \Drupal\rest\LinkManager\LinkManagerInterface
-   */
-  protected $linkManager;
-
-  /**
-   * The event dispatcher.
-   *
-   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
-   */
-  protected $eventDispatcher;
-
-  /**
-   * Constructs the default content manager.
-   *
-   * @param \Symfony\Component\Serializer\Serializer $serializer
-   *   The serializer service.
-   * @param \Drupal\rest\Plugin\Type\ResourcePluginManager $resource_plugin_manager
-   *   The rest resource plugin manager.
-   * @param \Drupal\Core\Session|AccountInterface $current_user
-   *   The current user.
-   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager
-   *   The entity type manager service.
-   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
-   *   The entity repository service.
-   * @param \Drupal\rest\LinkManager\LinkManagerInterface $link_manager
-   *   The link manager service.
-   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
-   *   The event dispatcher.
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   *   The module handler.
-   * @param \Drupal\Core\Extension\InfoParserInterface $info_parser
-   *   The info file parser.
-   */
-  public function __construct(Serializer $serializer, ResourcePluginManager $resource_plugin_manager, AccountInterface $current_user, EntityTypeManagerInterface $entity_manager, EntityRepositoryInterface $entity_repository, LinkManagerInterface $link_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, InfoParserInterface $info_parser) {
-    $this->serializer = $serializer;
-    $this->resourcePluginManager = $resource_plugin_manager;
-    $this->entityManager = $entity_manager;
-    $this->entityRepository = $entity_repository;
-    $this->linkManager = $link_manager;
-    $this->eventDispatcher = $event_dispatcher;
-    $this->moduleHandler = $module_handler;
-    $this->infoParser = $info_parser;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function importContent($module) {
-    $created = array();
-    $folder = drupal_get_path('module', $module) . "/content";
-
-    if (file_exists($folder)) {
-      $file_map = array();
-      foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
-        $reflection = new \ReflectionClass($entity_type->getClass());
-        // We are only interested in importing content entities.
-        if ($reflection->implementsInterface('\Drupal\Core\Config\Entity\ConfigEntityInterface')) {
-          continue;
-        }
-        if (!file_exists($folder . '/' . $entity_type_id)) {
-          continue;
-        }
-        $files = $this->scanner()->scan($folder . '/' . $entity_type_id);
-        // Default content uses drupal.org as domain.
-        // @todo Make this use a uri like default-content:.
-        $this->linkManager->setLinkDomain(static::LINK_DOMAIN);
-        // Parse all of the files and sort them in order of dependency.
-        foreach ($files as $file) {
-          $contents = $this->parseFile($file);
-          // Decode the file contents.
-          $decoded = $this->serializer->decode($contents, 'hal_json');
-          // Get the link to this entity.
-          $item_uuid = $decoded['uuid'][0]['value'];
-
-          // Throw an exception when this UUID already exists.
-          if (isset($file_map[$item_uuid])) {
-            $args = array(
-              '@uuid' => $item_uuid,
-              '@first' => $file_map[$item_uuid]->uri,
-              '@second' => $file->uri,
-            );
-            // Reset link domain.
-            $this->linkManager->setLinkDomain(FALSE);
-            throw new \Exception(new FormattableMarkup('Default content with uuid @uuid exists twice: @first @second', $args));
-          }
-
-          // Store the entity type with the file.
-          $file->entity_type_id = $entity_type_id;
-          // Store the file in the file map.
-          $file_map[$item_uuid] = $file;
-          // Create a vertex for the graph.
-          $vertex = $this->getVertex($item_uuid);
-          $this->graph[$vertex->id]['edges'] = [];
-          if (empty($decoded['_embedded'])) {
-            // No dependencies to resolve.
-            continue;
-          }
-          // Here we need to resolve our dependencies:
-          foreach ($decoded['_embedded'] as $embedded) {
-            foreach ($embedded as $item) {
-              $uuid = $item['uuid'][0]['value'];
-              $edge = $this->getVertex($uuid);
-              $this->graph[$vertex->id]['edges'][$edge->id] = TRUE;
-            }
-          }
-        }
-      }
-
-      // @todo what if no dependencies?
-      $sorted = $this->sortTree($this->graph);
-      foreach ($sorted as $link => $details) {
-        if (!empty($file_map[$link])) {
-          $file = $file_map[$link];
-          $entity_type_id = $file->entity_type_id;
-          $resource = $this->resourcePluginManager->getInstance(array('id' => 'entity:' . $entity_type_id));
-          $definition = $resource->getPluginDefinition();
-          $contents = $this->parseFile($file);
-          $class = $definition['serialization_class'];
-          $entity = $this->serializer->deserialize($contents, $class, 'hal_json', array('request_method' => 'POST'));
-          $entity->enforceIsNew(TRUE);
-          $entity->save();
-          $created[$entity->uuid()] = $entity;
-        }
-      }
-      $this->eventDispatcher->dispatch(DefaultContentEvents::IMPORT, new ImportEvent($created, $module));
-    }
-    // Reset the tree.
-    $this->resetTree();
-    // Reset link domain.
-    $this->linkManager->setLinkDomain(FALSE);
-    return $created;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function exportContent($entity_type_id, $entity_id) {
-    $storage = $this->entityManager->getStorage($entity_type_id);
-    $entity = $storage->load($entity_id);
-
-    $this->linkManager->setLinkDomain(static::LINK_DOMAIN);
-    $return = $this->serializer->serialize($entity, 'hal_json', ['json_encode_options' => JSON_PRETTY_PRINT]);
-    // Reset link domain.
-    $this->linkManager->setLinkDomain(FALSE);
-    $this->eventDispatcher->dispatch(DefaultContentEvents::EXPORT, new ExportEvent($entity));
-
-    return $return;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function exportContentWithReferences($entity_type_id, $entity_id) {
-    $storage = $this->entityManager->getStorage($entity_type_id);
-    $entity = $storage->load($entity_id);
-
-    if (!$entity) {
-      throw new \InvalidArgumentException(new FormattableMarkup('Entity @type with ID @id does not exist', ['@type' => $entity_type_id, '@id' => $entity_id]));
-    }
-    if (!($entity instanceof ContentEntityInterface)) {
-      throw new \InvalidArgumentException(new FormattableMarkup('Entity @type with ID @id should be a content entity', ['@type' => $entity_type_id, '@id' => $entity_id]));
-    }
-
-    $entities = [$entity->uuid() => $entity];
-    $entities = $this->getEntityReferencesRecursive($entity, 0, $entities);
-
-    $serialized_entities_per_type = [];
-    $this->linkManager->setLinkDomain(static::LINK_DOMAIN);
-    // Serialize all entities and key them by entity TYPE and uuid.
-    foreach ($entities as $entity) {
-      $serialized_entities_per_type[$entity->getEntityTypeId()][$entity->uuid()] = $this->serializer->serialize($entity, 'hal_json', ['json_encode_options' => JSON_PRETTY_PRINT]);
-    }
-    $this->linkManager->setLinkDomain(FALSE);
-
-    return $serialized_entities_per_type;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function exportModuleContent($module_name) {
-    $info_file = $this->moduleHandler->getModule($module_name)->getPathname();
-    $info = $this->infoParser->parse($info_file);
-    $exported_content = [];
-    if (empty($info['default_content'])) {
-      return $exported_content;
-    }
-    foreach ($info['default_content'] as $entity_type => $uuids) {
-      foreach ($uuids as $uuid) {
-        $entity = $this->entityRepository->loadEntityByUuid($entity_type, $uuid);
-        $exported_content[$entity_type][$uuid] = $this->exportContent($entity_type, $entity->id());
-      }
-    }
-    return $exported_content;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function writeDefaultContent($serialized_by_type, $folder) {
-    foreach ($serialized_by_type as $entity_type => $serialized_entities) {
-      // Ensure that the folder per entity type exists.
-      $entity_type_folder = "$folder/$entity_type";
-      file_prepare_directory($entity_type_folder, FILE_CREATE_DIRECTORY);
-      foreach ($serialized_entities as $uuid => $serialized_entity) {
-        file_put_contents($entity_type_folder . '/' . $uuid . '.json', $serialized_entity);
-      }
-    }
-  }
-
-  /**
-   * Returns all referenced entities of an entity.
-   *
-   * This method is also recursive to support use-cases like a node -> media
-   * -> file.
-   *
-   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
-   *   The entity.
-   * @param int $depth
-   *   Guard against infinite recursion.
-   * @param \Drupal\Core\Entity\ContentEntityInterface[] $indexed_dependencies
-   *   Previously discovered dependencies.
-   *
-   * @return \Drupal\Core\Entity\ContentEntityInterface[]
-   *   Keyed array of entities indexed by entity type and ID.
-   */
-  protected function getEntityReferencesRecursive(ContentEntityInterface $entity, $depth = 0, array &$indexed_dependencies = []) {
-    $entity_dependencies = $entity->referencedEntities();
-
-    foreach ($entity_dependencies as $dependent_entity) {
-      // Config entities should not be exported but rather provided by default
-      // config.
-      if (!($dependent_entity instanceof ContentEntityInterface)) {
-        continue;
-      }
-      // Using UUID to keep dependencies unique to prevent recursion.
-      $key = $dependent_entity->uuid();
-      if (isset($indexed_dependencies[$key])) {
-        // Do not add already indexed dependencies.
-        continue;
-      }
-      $indexed_dependencies[$key] = $dependent_entity;
-      // Build in some support against infinite recursion.
-      if ($depth < 6) {
-        // @todo Make $depth configurable.
-        $indexed_dependencies += $this->getEntityReferencesRecursive($dependent_entity, $depth + 1, $indexed_dependencies);
-      }
-    }
-
-    return $indexed_dependencies;
-  }
-
-  /**
-   * Utility to get a default content scanner.
-   *
-   * @return \Drupal\default_content\DefaultContentScanner
-   *   A system listing implementation.
-   */
-  protected function scanner() {
-    if ($this->scanner) {
-      return $this->scanner;
-    }
-    return new DefaultContentScanner();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setScanner(DefaultContentScanner $scanner) {
-    $this->scanner = $scanner;
-  }
-
-  /**
-   * Parses content files.
-   *
-   * @param object $file
-   *   The scanned file.
-   *
-   * @return string
-   *   Contents of the file.
-   */
-  protected function parseFile($file) {
-    return file_get_contents($file->uri);
-  }
-
-  /**
-   * Resets tree properties.
-   */
-  protected function resetTree() {
-    $this->graph = [];
-    $this->vertexes = array();
-  }
-
-  /**
-   * Sorts dependencies tree.
-   *
-   * @param array $graph
-   *   Array of dependencies.
-   *
-   * @return array
-   *   Array of sorted dependencies.
-   */
-  protected function sortTree(array $graph) {
-    $graph_object = new Graph($graph);
-    $sorted = $graph_object->searchAndSort();
-    uasort($sorted, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
-    return array_reverse($sorted);
-  }
-
-  /**
-   * Returns a vertex object for a given item link.
-   *
-   * Ensures that the same object is returned for the same item link.
-   *
-   * @param string $item_link
-   *   The item link as a string.
-   *
-   * @return object
-   *   The vertex object.
-   */
-  protected function getVertex($item_link) {
-    if (!isset($this->vertexes[$item_link])) {
-      $this->vertexes[$item_link] = (object) array('id' => $item_link);
-    }
-    return $this->vertexes[$item_link];
-  }
-
-}
diff --git a/src/DefaultContentManagerInterface.php b/src/DefaultContentManagerInterface.php
deleted file mode 100644
index fdf67a9..0000000
--- a/src/DefaultContentManagerInterface.php
+++ /dev/null
@@ -1,76 +0,0 @@
-<?php
-
-namespace Drupal\default_content;
-
-/**
- * An interface defining a default content importer.
- */
-interface DefaultContentManagerInterface {
-
-  /**
-   * Set the scanner.
-   *
-   * @param \Drupal\default_content\DefaultContentScanner $scanner
-   *   The system scanner.
-   */
-  public function setScanner(DefaultContentScanner $scanner);
-
-  /**
-   * Imports default content for a given module.
-   *
-   * @param string $module
-   *   The module to create the default content for.
-   *
-   * @return array[\Drupal\Core\Entity\EntityInterface]
-   *   The created entities.
-   */
-  public function importContent($module);
-
-  /**
-   * Exports a single entity as importContent expects it.
-   *
-   * @param string $entity_type_id
-   *   The entity type ID.
-   * @param mixed $entity_id
-   *   The entity ID to export.
-   *
-   * @return string
-   *   The rendered export as hal.
-   */
-  public function exportContent($entity_type_id, $entity_id);
-
-  /**
-   * Exports a single entity and all its referenced entity.
-   *
-   * @param string $entity_type_id
-   *   The entity type ID.
-   * @param mixed $entity_id
-   *   The entity ID to export.
-   *
-   * @return string[][]
-   *   The serialized entities keyed by entity type and UUID.
-   */
-  public function exportContentWithReferences($entity_type_id, $entity_id);
-
-  /**
-   * Exports all of the content defined in a module's info file.
-   *
-   * @param string $module_name
-   *   The name of the module.
-   *
-   * @return string[][]
-   *   The serialized entities keyed by entity type and UUID.
-   */
-  public function exportModuleContent($module_name);
-
-  /**
-   * Writes an array of serialized entities to a given folder.
-   *
-   * @param string[][] $serialized_by_type
-   *   An array of serialized entities keyed by entity type and UUID.
-   * @param string $folder
-   *   The folder to write files into.
-   */
-  public function writeDefaultContent($serialized_by_type, $folder);
-
-}
diff --git a/src/DefaultContentScanner.php b/src/DefaultContentScanner.php
deleted file mode 100644
index e2bc6ed..0000000
--- a/src/DefaultContentScanner.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-namespace Drupal\default_content;
-
-/**
- * A scanner to find YAML files in a given folder.
- */
-class DefaultContentScanner {
-
-  /**
-   * Returns a list of file objects.
-   *
-   * @param string $directory
-   *   Absolute path to the directory to search.
-   *
-   * @return object[]
-   *   List of stdClass objects with name and uri properties.
-   */
-  public function scan($directory) {
-    // Use Unix paths regardless of platform, skip dot directories, follow
-    // symlinks (to allow extensions to be linked from elsewhere), and return
-    // the RecursiveDirectoryIterator instance to have access to getSubPath(),
-    // since SplFileInfo does not support relative paths.
-    $flags = \FilesystemIterator::UNIX_PATHS;
-    $flags |= \FilesystemIterator::SKIP_DOTS;
-    $flags |= \FilesystemIterator::CURRENT_AS_SELF;
-    $directory_iterator = new \RecursiveDirectoryIterator($directory, $flags);
-    $iterator = new \RecursiveIteratorIterator($directory_iterator);
-
-    $files = array();
-    foreach ($iterator as $fileinfo) {
-      /* @var \SplFileInfo $fileinfo */
-
-      // Skip directories and non-json files.
-      if ($fileinfo->isDir() || $fileinfo->getExtension() != 'json') {
-        continue;
-      }
-
-      // @todo Use a typed class?
-      $file = new \stdClass();
-      $file->name = $fileinfo->getFilename();
-      $file->uri = $fileinfo->getPathname();
-      $files[$file->uri] = $file;
-    }
-
-    return $files;
-  }
-
-}
diff --git a/src/DefaultContentServiceProvider.php b/src/DefaultContentServiceProvider.php
index a58d61a..60ee329 100644
--- a/src/DefaultContentServiceProvider.php
+++ b/src/DefaultContentServiceProvider.php
@@ -4,6 +4,7 @@ namespace Drupal\default_content;
 
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\DependencyInjection\ServiceProviderBase;
+use Drupal\default_content\Normalizer\TermEntityNormalizer;
 use Symfony\Component\DependencyInjection\Definition;
 use Symfony\Component\DependencyInjection\Reference;
 
@@ -20,8 +21,8 @@ class DefaultContentServiceProvider extends ServiceProviderBase {
     // @todo Get rid of after https://www.drupal.org/node/2543726
     if (isset($modules['taxonomy'])) {
       // Add a normalizer service for term entities.
-      $service_definition = new Definition('Drupal\default_content\Normalizer\TermEntityNormalizer', [
-        new Reference('rest.link_manager'),
+      $service_definition = new Definition(TermEntityNormalizer::class, [
+        new Reference('hal.link_manager'),
         new Reference('entity.manager'),
         new Reference('module_handler'),
       ]);
diff --git a/src/Exporter.php b/src/Exporter.php
new file mode 100644
index 0000000..298affa
--- /dev/null
+++ b/src/Exporter.php
@@ -0,0 +1,254 @@
+<?php
+
+namespace Drupal\default_content;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityRepositoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Extension\InfoParserInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\default_content\Event\DefaultContentEvents;
+use Drupal\default_content\Event\ExportEvent;
+use Drupal\hal\LinkManager\LinkManagerInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\Serializer\Serializer;
+
+/**
+ * A service for handling import of default content.
+ *
+ * @todo throw useful exceptions
+ */
+class Exporter implements ExporterInterface {
+
+  /**
+   * Defines relation domain URI for entity links.
+   *
+   * @var string
+   */
+  protected $linkDomain;
+
+  /**
+   * The serializer service.
+   *
+   * @var \Symfony\Component\Serializer\Serializer
+   */
+  protected $serializer;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The entity repository.
+   *
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+   */
+  protected $entityRepository;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The info file parser.
+   *
+   * @var \Drupal\Core\Extension\InfoParserInterface
+   */
+  protected $infoParser;
+
+  /**
+   * The link manager service.
+   *
+   * @var \Drupal\hal\LinkManager\LinkManagerInterface
+   */
+  protected $linkManager;
+
+  /**
+   * The event dispatcher.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
+  /**
+   * Constructs the default content manager.
+   *
+   * @param \Symfony\Component\Serializer\Serializer $serializer
+   *   The serializer service.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager service.
+   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
+   *   The entity repository service.
+   * @param \Drupal\hal\LinkManager\LinkManagerInterface $link_manager
+   *   The link manager service.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   The event dispatcher.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Extension\InfoParserInterface $info_parser
+   *   The info file parser.
+   * @param string $link_domain
+   *   Defines relation domain URI for entity links.
+   */
+  public function __construct(Serializer $serializer, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, LinkManagerInterface $link_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, InfoParserInterface $info_parser, $link_domain) {
+    $this->serializer = $serializer;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->entityRepository = $entity_repository;
+    $this->linkManager = $link_manager;
+    $this->eventDispatcher = $event_dispatcher;
+    $this->moduleHandler = $module_handler;
+    $this->infoParser = $info_parser;
+    $this->linkDomain = $link_domain;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function exportContent($entity_type_id, $entity_id) {
+    $storage = $this->entityTypeManager->getStorage($entity_type_id);
+    $entity = $storage->load($entity_id);
+
+    $this->linkManager->setLinkDomain($this->linkDomain);
+    $return = $this->serializer->serialize($entity, 'hal_json', ['json_encode_options' => JSON_PRETTY_PRINT]);
+    // Reset link domain.
+    $this->linkManager->setLinkDomain(FALSE);
+    $this->eventDispatcher->dispatch(DefaultContentEvents::EXPORT, new ExportEvent($entity));
+
+    return $return;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function exportContentWithReferences($entity_type_id, $entity_id) {
+    $storage = $this->entityTypeManager->getStorage($entity_type_id);
+    $entity = $storage->load($entity_id);
+
+    if (!$entity) {
+      throw new \InvalidArgumentException(new FormattableMarkup('Entity @type with ID @id does not exist', ['@type' => $entity_type_id, '@id' => $entity_id]));
+    }
+    if (!($entity instanceof ContentEntityInterface)) {
+      throw new \InvalidArgumentException(new FormattableMarkup('Entity @type with ID @id should be a content entity', ['@type' => $entity_type_id, '@id' => $entity_id]));
+    }
+
+    $entities = [$entity->uuid() => $entity];
+    $entities = $this->getEntityReferencesRecursive($entity, 0, $entities);
+
+    $serialized_entities_per_type = [];
+    $this->linkManager->setLinkDomain($this->linkDomain);
+    // Serialize all entities and key them by entity TYPE and uuid.
+    foreach ($entities as $entity) {
+      $serialized_entities_per_type[$entity->getEntityTypeId()][$entity->uuid()] = $this->serializer->serialize($entity, 'hal_json', ['json_encode_options' => JSON_PRETTY_PRINT]);
+    }
+    $this->linkManager->setLinkDomain(FALSE);
+
+    return $serialized_entities_per_type;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function exportModuleContent($module_name) {
+    $info_file = $this->moduleHandler->getModule($module_name)->getPathname();
+    $info = $this->infoParser->parse($info_file);
+    $exported_content = [];
+    if (empty($info['default_content'])) {
+      return $exported_content;
+    }
+    foreach ($info['default_content'] as $entity_type => $uuids) {
+      foreach ($uuids as $uuid) {
+        $entity = $this->entityRepository->loadEntityByUuid($entity_type, $uuid);
+        $exported_content[$entity_type][$uuid] = $this->exportContent($entity_type, $entity->id());
+      }
+    }
+    return $exported_content;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function writeDefaultContent(array $serialized_by_type, $folder) {
+    foreach ($serialized_by_type as $entity_type => $serialized_entities) {
+      // Ensure that the folder per entity type exists.
+      $entity_type_folder = "$folder/$entity_type";
+      $this->prepareDirectory($entity_type_folder);
+      foreach ($serialized_entities as $uuid => $serialized_entity) {
+        $this->putFile($entity_type_folder, $uuid, $serialized_entity);
+      }
+    }
+  }
+
+  /**
+   * Helper for ::writeDefaultContent to wrap file_prepare_directory().
+   *
+   * @param string $path
+   *   Content directory + entity directory to prepare.
+   */
+  protected function prepareDirectory($path) {
+    file_prepare_directory($path, FILE_CREATE_DIRECTORY);
+  }
+
+  /**
+   * Helper for ::writeDefaultContent to wrap file_put_contents.
+   *
+   * @param string $path
+   *   Content directory + entity directory to which to write the file.
+   * @param string $uuid
+   *   Entity UUID, to be used as filename.
+   * @param string $serialized_entity
+   *   The serialized entity to write.
+   */
+  protected function putFile($path, $uuid, $serialized_entity) {
+    file_put_contents($path . '/' . $uuid . '.json', $serialized_entity);
+  }
+
+  /**
+   * Returns all referenced entities of an entity.
+   *
+   * This method is also recursive to support use-cases like a node -> media
+   * -> file.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   The entity.
+   * @param int $depth
+   *   Guard against infinite recursion.
+   * @param \Drupal\Core\Entity\ContentEntityInterface[] $indexed_dependencies
+   *   Previously discovered dependencies.
+   *
+   * @return \Drupal\Core\Entity\ContentEntityInterface[]
+   *   Keyed array of entities indexed by entity type and ID.
+   */
+  protected function getEntityReferencesRecursive(ContentEntityInterface $entity, $depth = 0, array &$indexed_dependencies = []) {
+    $entity_dependencies = $entity->referencedEntities();
+
+    foreach ($entity_dependencies as $dependent_entity) {
+      // Config entities should not be exported but rather provided by default
+      // config.
+      if (!($dependent_entity instanceof ContentEntityInterface)) {
+        continue;
+      }
+      // Using UUID to keep dependencies unique to prevent recursion.
+      $key = $dependent_entity->uuid();
+      if (isset($indexed_dependencies[$key])) {
+        // Do not add already indexed dependencies.
+        continue;
+      }
+      $indexed_dependencies[$key] = $dependent_entity;
+      // Build in some support against infinite recursion.
+      if ($depth < 6) {
+        // @todo Make $depth configurable.
+        $indexed_dependencies += $this->getEntityReferencesRecursive($dependent_entity, $depth + 1, $indexed_dependencies);
+      }
+    }
+
+    return $indexed_dependencies;
+  }
+
+}
diff --git a/src/Exporter.php.rej b/src/Exporter.php.rej
new file mode 100644
index 0000000..2781758
--- /dev/null
+++ b/src/Exporter.php.rej
@@ -0,0 +1,257 @@
+--- src/Exporter.php
++++ src/Exporter.php
+@@ -1,254 +0,0 @@
+-<?php
+-
+-namespace Drupal\default_content;
+-
+-use Drupal\Component\Render\FormattableMarkup;
+-use Drupal\Core\Entity\ContentEntityInterface;
+-use Drupal\Core\Entity\EntityRepositoryInterface;
+-use Drupal\Core\Entity\EntityTypeManagerInterface;
+-use Drupal\Core\Extension\InfoParserInterface;
+-use Drupal\Core\Extension\ModuleHandlerInterface;
+-use Drupal\default_content\Event\DefaultContentEvents;
+-use Drupal\default_content\Event\ExportEvent;
+-use Drupal\rest\LinkManager\LinkManagerInterface;
+-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+-use Symfony\Component\Serializer\Serializer;
+-
+-/**
+- * A service for handling import of default content.
+- *
+- * @todo throw useful exceptions
+- */
+-class Exporter implements ExporterInterface {
+-
+-  /**
+-   * Defines relation domain URI for entity links.
+-   *
+-   * @var string
+-   */
+-  protected $linkDomain;
+-
+-  /**
+-   * The serializer service.
+-   *
+-   * @var \Symfony\Component\Serializer\Serializer
+-   */
+-  protected $serializer;
+-
+-  /**
+-   * The entity type manager.
+-   *
+-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+-   */
+-  protected $entityTypeManager;
+-
+-  /**
+-   * The entity repository.
+-   *
+-   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+-   */
+-  protected $entityRepository;
+-
+-  /**
+-   * The module handler.
+-   *
+-   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+-   */
+-  protected $moduleHandler;
+-
+-  /**
+-   * The info file parser.
+-   *
+-   * @var \Drupal\Core\Extension\InfoParserInterface
+-   */
+-  protected $infoParser;
+-
+-  /**
+-   * The link manager service.
+-   *
+-   * @var \Drupal\rest\LinkManager\LinkManagerInterface
+-   */
+-  protected $linkManager;
+-
+-  /**
+-   * The event dispatcher.
+-   *
+-   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+-   */
+-  protected $eventDispatcher;
+-
+-  /**
+-   * Constructs the default content manager.
+-   *
+-   * @param \Symfony\Component\Serializer\Serializer $serializer
+-   *   The serializer service.
+-   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+-   *   The entity type manager service.
+-   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
+-   *   The entity repository service.
+-   * @param \Drupal\rest\LinkManager\LinkManagerInterface $link_manager
+-   *   The link manager service.
+-   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+-   *   The event dispatcher.
+-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+-   *   The module handler.
+-   * @param \Drupal\Core\Extension\InfoParserInterface $info_parser
+-   *   The info file parser.
+-   * @param string $link_domain
+-   *   Defines relation domain URI for entity links.
+-   */
+-  public function __construct(Serializer $serializer, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, LinkManagerInterface $link_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, InfoParserInterface $info_parser, $link_domain) {
+-    $this->serializer = $serializer;
+-    $this->entityTypeManager = $entity_type_manager;
+-    $this->entityRepository = $entity_repository;
+-    $this->linkManager = $link_manager;
+-    $this->eventDispatcher = $event_dispatcher;
+-    $this->moduleHandler = $module_handler;
+-    $this->infoParser = $info_parser;
+-    $this->linkDomain = $link_domain;
+-  }
+-
+-  /**
+-   * {@inheritdoc}
+-   */
+-  public function exportContent($entity_type_id, $entity_id) {
+-    $storage = $this->entityTypeManager->getStorage($entity_type_id);
+-    $entity = $storage->load($entity_id);
+-
+-    $this->linkManager->setLinkDomain($this->linkDomain);
+-    $return = $this->serializer->serialize($entity, 'hal_json', ['json_encode_options' => JSON_PRETTY_PRINT]);
+-    // Reset link domain.
+-    $this->linkManager->setLinkDomain(FALSE);
+-    $this->eventDispatcher->dispatch(DefaultContentEvents::EXPORT, new ExportEvent($entity));
+-
+-    return $return;
+-  }
+-
+-  /**
+-   * {@inheritdoc}
+-   */
+-  public function exportContentWithReferences($entity_type_id, $entity_id) {
+-    $storage = $this->entityTypeManager->getStorage($entity_type_id);
+-    $entity = $storage->load($entity_id);
+-
+-    if (!$entity) {
+-      throw new \InvalidArgumentException(new FormattableMarkup('Entity @type with ID @id does not exist', ['@type' => $entity_type_id, '@id' => $entity_id]));
+-    }
+-    if (!($entity instanceof ContentEntityInterface)) {
+-      throw new \InvalidArgumentException(new FormattableMarkup('Entity @type with ID @id should be a content entity', ['@type' => $entity_type_id, '@id' => $entity_id]));
+-    }
+-
+-    $entities = [$entity->uuid() => $entity];
+-    $entities = $this->getEntityReferencesRecursive($entity, 0, $entities);
+-
+-    $serialized_entities_per_type = [];
+-    $this->linkManager->setLinkDomain($this->linkDomain);
+-    // Serialize all entities and key them by entity TYPE and uuid.
+-    foreach ($entities as $entity) {
+-      $serialized_entities_per_type[$entity->getEntityTypeId()][$entity->uuid()] = $this->serializer->serialize($entity, 'hal_json', ['json_encode_options' => JSON_PRETTY_PRINT]);
+-    }
+-    $this->linkManager->setLinkDomain(FALSE);
+-
+-    return $serialized_entities_per_type;
+-  }
+-
+-  /**
+-   * {@inheritdoc}
+-   */
+-  public function exportModuleContent($module_name) {
+-    $info_file = $this->moduleHandler->getModule($module_name)->getPathname();
+-    $info = $this->infoParser->parse($info_file);
+-    $exported_content = [];
+-    if (empty($info['default_content'])) {
+-      return $exported_content;
+-    }
+-    foreach ($info['default_content'] as $entity_type => $uuids) {
+-      foreach ($uuids as $uuid) {
+-        $entity = $this->entityRepository->loadEntityByUuid($entity_type, $uuid);
+-        $exported_content[$entity_type][$uuid] = $this->exportContent($entity_type, $entity->id());
+-      }
+-    }
+-    return $exported_content;
+-  }
+-
+-  /**
+-   * {@inheritdoc}
+-   */
+-  public function writeDefaultContent(array $serialized_by_type, $folder) {
+-    foreach ($serialized_by_type as $entity_type => $serialized_entities) {
+-      // Ensure that the folder per entity type exists.
+-      $entity_type_folder = "$folder/$entity_type";
+-      $this->prepareDirectory($entity_type_folder);
+-      foreach ($serialized_entities as $uuid => $serialized_entity) {
+-        $this->putFile($entity_type_folder, $uuid, $serialized_entity);
+-      }
+-    }
+-  }
+-
+-  /**
+-   * Helper for ::writeDefaultContent to wrap file_prepare_directory();
+-   *
+-   * @param string $path
+-   *   Content directory + entity directory to prepare.
+-   */
+-  protected function prepareDirectory($path) {
+-    file_prepare_directory($path, FILE_CREATE_DIRECTORY);
+-  }
+-
+-  /**
+-   * Helper for ::writeDefaultContent to wrap file_put_contents
+-   *
+-   * @param string $path
+-   *   Content directory + entity directory to which to write the file.
+-   * @param string $uuid
+-   *   Entity UUID, to be used as filename.
+-   * @param string $serialized_entity
+-   *   The serialized entity to write.
+-   */
+-  protected function putFile($path, $uuid, $serialized_entity) {
+-    file_put_contents($entity_type_folder . '/' . $uuid . '.json', $serialized_entity);
+-  }
+-
+-  /**
+-   * Returns all referenced entities of an entity.
+-   *
+-   * This method is also recursive to support use-cases like a node -> media
+-   * -> file.
+-   *
+-   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+-   *   The entity.
+-   * @param int $depth
+-   *   Guard against infinite recursion.
+-   * @param \Drupal\Core\Entity\ContentEntityInterface[] $indexed_dependencies
+-   *   Previously discovered dependencies.
+-   *
+-   * @return \Drupal\Core\Entity\ContentEntityInterface[]
+-   *   Keyed array of entities indexed by entity type and ID.
+-   */
+-  protected function getEntityReferencesRecursive(ContentEntityInterface $entity, $depth = 0, array &$indexed_dependencies = []) {
+-    $entity_dependencies = $entity->referencedEntities();
+-
+-    foreach ($entity_dependencies as $dependent_entity) {
+-      // Config entities should not be exported but rather provided by default
+-      // config.
+-      if (!($dependent_entity instanceof ContentEntityInterface)) {
+-        continue;
+-      }
+-      // Using UUID to keep dependencies unique to prevent recursion.
+-      $key = $dependent_entity->uuid();
+-      if (isset($indexed_dependencies[$key])) {
+-        // Do not add already indexed dependencies.
+-        continue;
+-      }
+-      $indexed_dependencies[$key] = $dependent_entity;
+-      // Build in some support against infinite recursion.
+-      if ($depth < 6) {
+-        // @todo Make $depth configurable.
+-        $indexed_dependencies += $this->getEntityReferencesRecursive($dependent_entity, $depth + 1, $indexed_dependencies);
+-      }
+-    }
+-
+-    return $indexed_dependencies;
+-  }
+-
+-}
diff --git a/src/ExporterInterface.php b/src/ExporterInterface.php
new file mode 100644
index 0000000..7f2d94d
--- /dev/null
+++ b/src/ExporterInterface.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\default_content;
+
+/**
+ * An interface defining a default content exporter.
+ */
+interface ExporterInterface {
+
+  /**
+   * Exports a single entity as importContent expects it.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   * @param mixed $entity_id
+   *   The entity ID to export.
+   *
+   * @return string
+   *   The rendered export as hal.
+   */
+  public function exportContent($entity_type_id, $entity_id);
+
+  /**
+   * Exports a single entity and all its referenced entity.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   * @param mixed $entity_id
+   *   The entity ID to export.
+   *
+   * @return string[][]
+   *   The serialized entities keyed by entity type and UUID.
+   */
+  public function exportContentWithReferences($entity_type_id, $entity_id);
+
+  /**
+   * Exports all of the content defined in a module's info file.
+   *
+   * @param string $module_name
+   *   The name of the module.
+   *
+   * @return string[][]
+   *   The serialized entities keyed by entity type and UUID.
+   */
+  public function exportModuleContent($module_name);
+
+  /**
+   * Writes an array of serialized entities to a given folder.
+   *
+   * @param string[][] $serialized_by_type
+   *   An array of serialized entities keyed by entity type and UUID.
+   * @param string $folder
+   *   The folder to write files into.
+   */
+  public function writeDefaultContent(array $serialized_by_type, $folder);
+
+}
diff --git a/src/Importer.php b/src/Importer.php
new file mode 100644
index 0000000..553045b
--- /dev/null
+++ b/src/Importer.php
@@ -0,0 +1,258 @@
+<?php
+
+namespace Drupal\default_content;
+
+use Drupal\Component\Graph\Graph;
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\default_content\Event\DefaultContentEvents;
+use Drupal\default_content\Event\ImportEvent;
+use Drupal\hal\LinkManager\LinkManagerInterface;
+use Drupal\rest\Plugin\Type\ResourcePluginManager;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\Serializer\Serializer;
+
+/**
+ * A service for handling import of default content.
+ *
+ * @todo throw useful exceptions
+ */
+class Importer implements ImporterInterface {
+
+  /**
+   * Defines relation domain URI for entity links.
+   *
+   * @var string
+   */
+  protected $linkDomain;
+
+  /**
+   * The serializer service.
+   *
+   * @var \Symfony\Component\Serializer\Serializer
+   */
+  protected $serializer;
+
+  /**
+   * The rest resource plugin manager.
+   *
+   * @var \Drupal\rest\Plugin\Type\ResourcePluginManager
+   */
+  protected $resourcePluginManager;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * A list of vertex objects keyed by their link.
+   *
+   * @var array
+   */
+  protected $vertexes = [];
+
+  /**
+   * The graph entries.
+   *
+   * @var array
+   */
+  protected $graph = [];
+
+  /**
+   * The link manager service.
+   *
+   * @var \Drupal\hal\LinkManager\LinkManagerInterface
+   */
+  protected $linkManager;
+
+  /**
+   * The event dispatcher.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
+  /**
+   * The file system scanner.
+   *
+   * @var \Drupal\default_content\ScannerInterface
+   */
+  protected $scanner;
+
+  /**
+   * Constructs the default content manager.
+   *
+   * @param \Symfony\Component\Serializer\Serializer $serializer
+   *   The serializer service.
+   * @param \Drupal\rest\Plugin\Type\ResourcePluginManager $resource_plugin_manager
+   *   The rest resource plugin manager.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager service.
+   * @param \Drupal\hal\LinkManager\LinkManagerInterface $link_manager
+   *   The link manager service.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   The event dispatcher.
+   * @param \Drupal\default_content\ScannerInterface $scanner
+   *   The file scanner.
+   * @param string $link_domain
+   *   Defines relation domain URI for entity links.
+   */
+  public function __construct(Serializer $serializer, ResourcePluginManager $resource_plugin_manager, EntityTypeManagerInterface $entity_type_manager, LinkManagerInterface $link_manager, EventDispatcherInterface $event_dispatcher, ScannerInterface $scanner, $link_domain) {
+    $this->serializer = $serializer;
+    $this->resourcePluginManager = $resource_plugin_manager;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->linkManager = $link_manager;
+    $this->eventDispatcher = $event_dispatcher;
+    $this->scanner = $scanner;
+    $this->linkDomain = $link_domain;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function importContent($module) {
+    $created = [];
+    $folder = drupal_get_path('module', $module) . "/content";
+
+    if (file_exists($folder)) {
+      $file_map = [];
+      foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
+        $reflection = new \ReflectionClass($entity_type->getClass());
+        // We are only interested in importing content entities.
+        if ($reflection->implementsInterface(ConfigEntityInterface::class)) {
+          continue;
+        }
+        if (!file_exists($folder . '/' . $entity_type_id)) {
+          continue;
+        }
+        $files = $this->scanner->scan($folder . '/' . $entity_type_id);
+        // Default content uses drupal.org as domain.
+        // @todo Make this use a uri like default-content:.
+        $this->linkManager->setLinkDomain($this->linkDomain);
+        // Parse all of the files and sort them in order of dependency.
+        foreach ($files as $file) {
+          $contents = $this->parseFile($file);
+          // Decode the file contents.
+          $decoded = $this->serializer->decode($contents, 'hal_json');
+          // Get the link to this entity.
+          $item_uuid = $decoded['uuid'][0]['value'];
+
+          // Throw an exception when this UUID already exists.
+          if (isset($file_map[$item_uuid])) {
+            $args = [
+              '@uuid' => $item_uuid,
+              '@first' => $file_map[$item_uuid]->uri,
+              '@second' => $file->uri,
+            ];
+            // Reset link domain.
+            $this->linkManager->setLinkDomain(FALSE);
+            throw new \Exception(new FormattableMarkup('Default content with uuid @uuid exists twice: @first @second', $args));
+          }
+
+          // Store the entity type with the file.
+          $file->entity_type_id = $entity_type_id;
+          // Store the file in the file map.
+          $file_map[$item_uuid] = $file;
+          // Create a vertex for the graph.
+          $vertex = $this->getVertex($item_uuid);
+          $this->graph[$vertex->id]['edges'] = [];
+          if (empty($decoded['_embedded'])) {
+            // No dependencies to resolve.
+            continue;
+          }
+          // Here we need to resolve our dependencies:
+          foreach ($decoded['_embedded'] as $embedded) {
+            foreach ($embedded as $item) {
+              $uuid = $item['uuid'][0]['value'];
+              $edge = $this->getVertex($uuid);
+              $this->graph[$vertex->id]['edges'][$edge->id] = TRUE;
+            }
+          }
+        }
+      }
+
+      // @todo what if no dependencies?
+      $sorted = $this->sortTree($this->graph);
+      foreach ($sorted as $link => $details) {
+        if (!empty($file_map[$link])) {
+          $file = $file_map[$link];
+          $entity_type_id = $file->entity_type_id;
+          $resource = $this->resourcePluginManager->createInstance('entity:' . $entity_type_id);
+          $definition = $resource->getPluginDefinition();
+          $contents = $this->parseFile($file);
+          $class = $definition['serialization_class'];
+          $entity = $this->serializer->deserialize($contents, $class, 'hal_json', ['request_method' => 'POST']);
+          $entity->enforceIsNew(TRUE);
+          $entity->save();
+          $created[$entity->uuid()] = $entity;
+        }
+      }
+      $this->eventDispatcher->dispatch(DefaultContentEvents::IMPORT, new ImportEvent($created, $module));
+    }
+    // Reset the tree.
+    $this->resetTree();
+    // Reset link domain.
+    $this->linkManager->setLinkDomain(FALSE);
+    return $created;
+  }
+
+  /**
+   * Parses content files.
+   *
+   * @param object $file
+   *   The scanned file.
+   *
+   * @return string
+   *   Contents of the file.
+   */
+  protected function parseFile($file) {
+    return file_get_contents($file->uri);
+  }
+
+  /**
+   * Resets tree properties.
+   */
+  protected function resetTree() {
+    $this->graph = [];
+    $this->vertexes = [];
+  }
+
+  /**
+   * Sorts dependencies tree.
+   *
+   * @param array $graph
+   *   Array of dependencies.
+   *
+   * @return array
+   *   Array of sorted dependencies.
+   */
+  protected function sortTree(array $graph) {
+    $graph_object = new Graph($graph);
+    $sorted = $graph_object->searchAndSort();
+    uasort($sorted, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
+    return array_reverse($sorted);
+  }
+
+  /**
+   * Returns a vertex object for a given item link.
+   *
+   * Ensures that the same object is returned for the same item link.
+   *
+   * @param string $item_link
+   *   The item link as a string.
+   *
+   * @return object
+   *   The vertex object.
+   */
+  protected function getVertex($item_link) {
+    if (!isset($this->vertexes[$item_link])) {
+      $this->vertexes[$item_link] = (object) ['id' => $item_link];
+    }
+    return $this->vertexes[$item_link];
+  }
+
+}
diff --git a/src/ImporterInterface.php b/src/ImporterInterface.php
new file mode 100644
index 0000000..0d300a3
--- /dev/null
+++ b/src/ImporterInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\default_content;
+
+/**
+ * An interface defining a default content importer.
+ */
+interface ImporterInterface {
+
+  /**
+   * Imports default content from a given module.
+   *
+   * @param string $module
+   *   The module to create the default content from.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface[]
+   *   An array of created entities keyed by their UUIDs.
+   */
+  public function importContent($module);
+
+}
diff --git a/src/Scanner.php b/src/Scanner.php
new file mode 100644
index 0000000..6643227
--- /dev/null
+++ b/src/Scanner.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\default_content;
+
+/**
+ * A scanner to find YAML files in a given folder.
+ */
+class Scanner implements ScannerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function scan($directory) {
+    // Use Unix paths regardless of platform, skip dot directories, follow
+    // symlinks (to allow extensions to be linked from elsewhere), and return
+    // the RecursiveDirectoryIterator instance to have access to getSubPath(),
+    // since SplFileInfo does not support relative paths.
+    $flags = \FilesystemIterator::UNIX_PATHS;
+    $flags |= \FilesystemIterator::SKIP_DOTS;
+    $flags |= \FilesystemIterator::CURRENT_AS_SELF;
+    $directory_iterator = new \RecursiveDirectoryIterator($directory, $flags);
+    $iterator = new \RecursiveIteratorIterator($directory_iterator);
+
+    $files = [];
+    foreach ($iterator as $fileinfo) {
+      /* @var \SplFileInfo $fileinfo */
+
+      // Skip directories and non-json files.
+      if ($fileinfo->isDir() || $fileinfo->getExtension() != 'json') {
+        continue;
+      }
+
+      // @todo Use a typed class?
+      $file = new \stdClass();
+      $file->name = $fileinfo->getFilename();
+      $file->uri = $fileinfo->getPathname();
+      $files[$file->uri] = $file;
+    }
+
+    return $files;
+  }
+
+}
diff --git a/src/ScannerInterface.php b/src/ScannerInterface.php
new file mode 100644
index 0000000..7e88d62
--- /dev/null
+++ b/src/ScannerInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\default_content;
+
+/**
+ * A scanner to find YAML files in a given folder.
+ */
+interface ScannerInterface {
+
+  /**
+   * Returns a list of file objects.
+   *
+   * @param string $directory
+   *   Absolute path to the directory to search.
+   *
+   * @return object[]
+   *   List of stdClass objects with name and uri properties.
+   */
+  public function scan($directory);
+
+}
diff --git a/tests/src/Functional/DefaultContentTest.php b/tests/src/Functional/DefaultContentTest.php
index caeaf15..149e6e9 100644
--- a/tests/src/Functional/DefaultContentTest.php
+++ b/tests/src/Functional/DefaultContentTest.php
@@ -21,14 +21,14 @@ class DefaultContentTest extends BrowserTestBase {
    *
    * @var array
    */
-  public static $modules = array('rest', 'taxonomy', 'hal', 'default_content');
+  public static $modules = ['rest', 'taxonomy', 'hal', 'default_content'];
 
   /**
    * {@inheritdoc}
    */
   protected function setUp() {
     parent::setUp();
-    $this->createContentType(array('type' => 'page'));
+    $this->createContentType(['type' => 'page']);
   }
 
   /**
@@ -36,14 +36,18 @@ class DefaultContentTest extends BrowserTestBase {
    */
   public function testImport() {
     // Login as admin.
-    $this->drupalLogin($this->drupalCreateUser(array_keys(\Drupal::moduleHandler()->invokeAll(('permission')))));
+    $this->drupalLogin($this->drupalCreateUser(array_keys(\Drupal::moduleHandler()
+      ->invokeAll(('permission')))));
     // Enable the module and import the content.
-    \Drupal::service('module_installer')->install(array('default_content_test'), TRUE);
+    \Drupal::service('module_installer')
+      ->install(['default_content_test'], TRUE);
     $this->rebuildContainer();
     $node = $this->getNodeByTitle('Imported node');
     $this->assertEquals($node->body->value, 'Crikey it works!');
     $this->assertEquals($node->getType(), 'page');
-    $terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadMultiple();
+    $terms = \Drupal::entityTypeManager()
+      ->getStorage('taxonomy_term')
+      ->loadMultiple();
     $term = reset($terms);
     $this->assertTrue(!empty($term));
     $this->assertEquals($term->name->value, 'A tag');
diff --git a/tests/src/Kernel/DefaultContentManagerIntegrationTest.php b/tests/src/Kernel/DefaultContentManagerIntegrationTest.php
index 6ccc77f..de0deeb 100644
--- a/tests/src/Kernel/DefaultContentManagerIntegrationTest.php
+++ b/tests/src/Kernel/DefaultContentManagerIntegrationTest.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\Tests\default_content\Kernel;
 
-use Drupal\default_content\DefaultContentManager;
+use Drupal\default_content\Exporter;
 use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\node\Entity\Node;
@@ -14,10 +14,10 @@ use Drupal\user\Entity\User;
 /**
  * Tests export functionality.
  *
- * @coversDefaultClass \Drupal\default_content\DefaultContentManager
+ * @coversDefaultClass \Drupal\default_content\Exporter
  * @group default_content
  */
-class DefaultContentManagerIntegrationTest extends KernelTestBase {
+class ExporterIntegrationTest extends KernelTestBase {
 
   use EntityReferenceTestTrait;
 
@@ -27,11 +27,11 @@ class DefaultContentManagerIntegrationTest extends KernelTestBase {
   public static $modules = ['system'];
 
   /**
-   * The tested default content manager.
+   * The tested default content exporter.
    *
-   * @var \Drupal\default_content\DefaultContentManager
+   * @var \Drupal\default_content\Exporter
    */
-  protected $defaultContentManager;
+  protected $exporter;
 
   /**
    * {@inheritdoc}
@@ -51,7 +51,7 @@ class DefaultContentManagerIntegrationTest extends KernelTestBase {
       'default_content',
     ]);
     \Drupal::service('router.builder')->rebuild();
-    $this->defaultContentManager = \Drupal::service('default_content.manager');
+    $this->exporter = \Drupal::service('default_content.exporter');
 
     $vocabulary = Vocabulary::create(['vid' => 'test']);
     $vocabulary->save();
@@ -61,11 +61,11 @@ class DefaultContentManagerIntegrationTest extends KernelTestBase {
 
     /** @var \Symfony\Component\Serializer\Serializer $serializer */
     $serializer = \Drupal::service('serializer');
-    \Drupal::service('rest.link_manager')
-      ->setLinkDomain(DefaultContentManager::LINK_DOMAIN);
+    \Drupal::service('hal.link_manager')
+      ->setLinkDomain($this->container->getParameter('default_content.link_domain'));
     $expected = $serializer->serialize($term, 'hal_json', ['json_encode_options' => JSON_PRETTY_PRINT]);
 
-    $exported = $this->defaultContentManager->exportContent('taxonomy_term', $term->id());
+    $exported = $this->exporter->exportContent('taxonomy_term', $term->id());
     $exported_decoded = json_decode($exported);
 
     // Ensure the proper UUID is part of it.
@@ -81,7 +81,7 @@ class DefaultContentManagerIntegrationTest extends KernelTestBase {
     ]);
     $child_term->save();
     // Make sure parent relation is exported.
-    $exported = $this->defaultContentManager->exportContent('taxonomy_term', $child_term->id());
+    $exported = $this->exporter->exportContent('taxonomy_term', $child_term->id());
     $relation_uri = 'http://drupal.org/rest/relation/taxonomy_term/test/parent';
     $exported_decoded = json_decode($exported);
     $this->assertFalse(empty($exported_decoded->_links->{$relation_uri}));
@@ -94,7 +94,7 @@ class DefaultContentManagerIntegrationTest extends KernelTestBase {
   public function testExportWithReferences() {
     \Drupal::service('module_installer')->install(['node', 'default_content']);
     \Drupal::service('router.builder')->rebuild();
-    $this->defaultContentManager = \Drupal::service('default_content.manager');
+    $this->exporter = \Drupal::service('default_content.exporter');
 
     $user = User::create(['name' => 'my username']);
     $user->save();
@@ -114,12 +114,12 @@ class DefaultContentManagerIntegrationTest extends KernelTestBase {
 
     /** @var \Symfony\Component\Serializer\Serializer $serializer */
     $serializer = \Drupal::service('serializer');
-    \Drupal::service('rest.link_manager')
-      ->setLinkDomain(DefaultContentManager::LINK_DOMAIN);
+    \Drupal::service('hal.link_manager')
+      ->setLinkDomain($this->container->getParameter('default_content.link_domain'));
     $expected_node = $serializer->serialize($node, 'hal_json', ['json_encode_options' => JSON_PRETTY_PRINT]);
     $expected_user = $serializer->serialize($user, 'hal_json', ['json_encode_options' => JSON_PRETTY_PRINT]);
 
-    $exported_by_entity_type = $this->defaultContentManager->exportContentWithReferences('node', $node->id());
+    $exported_by_entity_type = $this->exporter->exportContentWithReferences('node', $node->id());
 
     // Ensure that the node type is not tryed to be exported.
     $this->assertEqual(array_keys($exported_by_entity_type), ['node', 'user']);
@@ -153,7 +153,7 @@ class DefaultContentManagerIntegrationTest extends KernelTestBase {
     // Loop reference.
     $node1->{$field_name}->target_id = $node3->id();
     $node1->save();
-    $exported_by_entity_type = $this->defaultContentManager->exportContentWithReferences('node', $node3->id());
+    $exported_by_entity_type = $this->exporter->exportContentWithReferences('node', $node3->id());
     // Ensure all 3 nodes are exported.
     $this->assertEquals(3, count($exported_by_entity_type['node']));
   }
@@ -168,7 +168,7 @@ class DefaultContentManagerIntegrationTest extends KernelTestBase {
       'default_content_export_test',
     ]);
     \Drupal::service('router.builder')->rebuild();
-    $this->defaultContentManager = \Drupal::service('default_content.manager');
+    $this->exporter = \Drupal::service('default_content.exporter');
 
     $test_uuid = '0e45d92f-1919-47cd-8b60-964a8a761292';
     $node_type = NodeType::create(['type' => 'test']);
@@ -178,11 +178,11 @@ class DefaultContentManagerIntegrationTest extends KernelTestBase {
     $node->save();
     $node = Node::load($node->id());
     $serializer = \Drupal::service('serializer');
-    \Drupal::service('rest.link_manager')
-      ->setLinkDomain(DefaultContentManager::LINK_DOMAIN);
+    \Drupal::service('hal.link_manager')
+      ->setLinkDomain($this->container->getParameter('default_content.link_domain'));
     $expected_node = $serializer->serialize($node, 'hal_json', ['json_encode_options' => JSON_PRETTY_PRINT]);
 
-    $content = $this->defaultContentManager->exportModuleContent('default_content_export_test');
+    $content = $this->exporter->exportModuleContent('default_content_export_test');
     $this->assertEqual($content['node'][$test_uuid], $expected_node);
   }
 
-- 
2.8.1

