Commit 14b0c7c0 authored by andypost's avatar andypost

Issue #2614644 by aaronbauman, andypost, vprocessor, Sam152, larowlan: Split...

Issue #2614644 by aaronbauman, andypost, vprocessor, Sam152, larowlan: Split DefaultContentManager into Exporter and Importer
parent 2c9aad85
......@@ -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);
}
}
}
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', '@rest.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', '@rest.link_manager', '@event_dispatcher', '@module_handler', '@info_parser', '%default_content.link_domain%']
<?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);
}
<?php
namespace Drupal\default_content;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/**
* Adds customized normalizer to handle taxonomy hierarchy.
*/
class DefaultContentServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
$modules = $container->getParameter('container.modules');
// @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'),
new Reference('entity.manager'),
new Reference('module_handler'),
]);
// The priority must be higher than that of
// serializer.normalizer.entity.hal in hal.services.yml.
$service_definition->addTag('normalizer', ['priority' => 30]);
$container->setDefinition('default_content.normalizer.taxonomy_term.halt', $service_definition);
}
}
}
--- 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;
- }
-
-}
......@@ -3,28 +3,9 @@
namespace Drupal\default_content;
/**
* An interface defining a default content importer.
* An interface defining a default content exporter.
*/
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);
interface ExporterInterface {
/**
* Exports a single entity as importContent expects it.
......@@ -71,6 +52,6 @@ interface DefaultContentManagerInterface {
* @param string $folder
* The folder to write files into.
*/
public function writeDefaultContent($serialized_by_type, $folder);
public function writeDefaultContent(array $serialized_by_type, $folder);
}
<?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\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 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\rest\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\rest\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];
}
}
<?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);
}
......@@ -5,16 +5,10 @@ namespace Drupal\default_content;
/**
* A scanner to find YAML files in a given folder.
*/
class DefaultContentScanner {
class Scanner implements 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.
* {@inheritdoc}
*/
public function scan($directory) {
// Use Unix paths regardless of platform, skip dot directories, follow
......@@ -27,7 +21,7 @@ class DefaultContentScanner {
$directory_iterator = new \RecursiveDirectoryIterator($directory, $flags);
$iterator = new \RecursiveIteratorIterator($directory_iterator);
$files = array();
$files = [];
foreach ($iterator as $fileinfo) {
/* @var \SplFileInfo $fileinfo */
......
<?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);
}
......@@ -2,7 +2,7 @@
namespace Drupal\Tests\default_content\Kernel;
use Drupal\default_content\DefaultContentManager;