diff --git a/src/DefaultContentManager.php b/src/DefaultContentManager.php index e6b5e14..45ca254 100644 --- a/src/DefaultContentManager.php +++ b/src/DefaultContentManager.php @@ -8,7 +8,7 @@ namespace Drupal\default_content; use Drupal\Component\Graph\Graph; -use Drupal\Component\Utility\SafeMarkup; +use Drupal\Component\Render\FormattableMarkup; use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -16,9 +16,11 @@ use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Extension\InfoParserInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StackMiddleware\Session; use Drupal\default_content\Event\DefaultContentEvents; use Drupal\default_content\Event\ExportEvent; -use Drupal\default_content\Event\ImportEvent; +use Drupal\default_content\Event\ImportFromFolderEvent; +use Drupal\default_content\Event\ImportFromModuleEvent; use Drupal\rest\LinkManager\LinkManagerInterface; use Drupal\rest\Plugin\Type\ResourcePluginManager; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -61,7 +63,6 @@ class DefaultContentManager implements DefaultContentManagerInterface { protected $entityManager; /** -<<<<<<< HEAD * The entity repository. * * @var \Drupal\Core\Entity\EntityRepositoryInterface @@ -69,8 +70,6 @@ class DefaultContentManager implements DefaultContentManagerInterface { protected $entityRepository; /** -======= ->>>>>>> origin/8.x-1.x * The module handler. * * @var \Drupal\Core\Extension\ModuleHandlerInterface @@ -92,33 +91,40 @@ class DefaultContentManager implements DefaultContentManagerInterface { protected $scanner; /** - * A list of vertex objects keyed by their link. + * The link manager service. * - * @var array + * @var \Drupal\rest\LinkManager\LinkManagerInterface */ - protected $vertexes = array(); + protected $linkManager; + + /** + * The event dispatcher. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + protected $eventDispatcher; + /** * The graph entries. * * @var array */ - protected $graph = []; + protected $graph = array(); /** - * The link manager service. + * File map. * - * @var \Drupal\rest\LinkManager\LinkManagerInterface + * @var array */ - protected $linkManager; + protected $fileMap = array(); /** - * The event dispatcher. + * A list of vertex objects keyed by their link. * - * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + * @var array */ - protected $eventDispatcher; - + protected $vertexes = array(); /** * Constructs the default content manager. @@ -127,7 +133,7 @@ class DefaultContentManager implements DefaultContentManagerInterface { * The serializer service. * @param \Drupal\rest\Plugin\Type\ResourcePluginManager $resource_plugin_manager * The rest resource plugin manager. - * @param \Drupal\Core\Session|AccountInterface $current_user + * @param Session|AccountInterface $current_user * The current user. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager * The entity type manager service. @@ -153,121 +159,55 @@ class DefaultContentManager implements DefaultContentManagerInterface { $this->infoParser = $info_parser; } - /** * {@inheritdoc} */ public function importContent($folder) { - $created = array(); - - $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. - $self = $decoded['_links']['self']['href']; - - // Throw an exception when this URL already exists. - if (isset($file_map[$self])) { - $args = array( - '@href' => $self, - '@first' => $file_map[$self]->uri, - '@second' => $file->uri, - ); - // Reset link domain. - $this->linkManager->setLinkDomain(FALSE); - throw new \Exception(SafeMarkup::format('Default content with href @href 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[$self] = $file; - // Create a vertex for the graph. - $vertex = $this->getVertex($self); - $this->graph[$vertex->link]['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) { - $edge = $this->getVertex($item['_links']['self']['href']); - $this->graph[$vertex->link]['edges'][$edge->link] = 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; + foreach ($this->getContentEntityDefinitions() as $entity_type_id => $entity_type) { + if (file_exists($folder . '/' . $entity_type_id)) { + $this->buildGraph($folder . '/' . $entity_type_id); } } - - // Reset the tree. - $this->resetTree(); - // Reset link domain. - $this->linkManager->setLinkDomain(FALSE); - return $created; + return $this->createEntities(); } - + /** + * Import default content from given module. + * + * @param string $module + * Module machine name. + * + * @return array + * List of created entities. + */ public function importContentFromModule($module) { $folder = drupal_get_path('module', $module) . "/content"; $created = array(); if (file_exists($folder)) { $created = $this->importContent($folder); - $this->eventDispatcher->dispatch(DefaultContentEvents::IMPORT, - new ImportEvent($created, $module)); + $this->eventDispatcher->dispatch(DefaultContentEvents::IMPORT, new ImportFromModuleEvent($created, $module)); } - return $created; } - + /** + * Import default content from specified folder. + * + * @param string $folder + * Path to default content folder. + * + * @return array + * List of created entities. + */ public function importContentFromFolder($folder) { $created = array(); if (file_exists($folder)) { - $created = $this->importContent($folder); - $this->eventDispatcher->dispatch(DefaultContentEvents::IMPORT, - new ImportEvent($created, $folder)); + $this->eventDispatcher->dispatch(DefaultContentEvents::IMPORT, new ImportFromFolderEvent($created, $folder)); } - return $created; } - /** * {@inheritdoc} */ @@ -280,11 +220,9 @@ class DefaultContentManager implements DefaultContentManagerInterface { // Reset link domain. $this->linkManager->setLinkDomain(FALSE); $this->eventDispatcher->dispatch(DefaultContentEvents::EXPORT, new ExportEvent($entity)); - return $return; } - /** * {@inheritdoc} */ @@ -293,14 +231,12 @@ class DefaultContentManager implements DefaultContentManagerInterface { $entity = $storage->load($entity_id); if (!$entity) { - throw new \InvalidArgumentException(SafeMarkup::format('Entity @type with ID @id does not exist', ['@type' => $entity_type_id, '@id' => $entity_id])); + throw new \InvalidArgumentException(new FormattableMarkup('Entity @type with ID @id does not exist', ['@type' => $entity_type_id, '@id' => $entity_id])); } /** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */ $entities = [$entity]; - $entities = array_merge($entities, $this->getEntityReferencesRecursive($entity)); - $serialized_entities_per_type = []; $this->linkManager->setLinkDomain(static::LINK_DOMAIN); // Serialize all entities and key them by entity TYPE and uuid. @@ -308,11 +244,9 @@ class DefaultContentManager implements DefaultContentManagerInterface { $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} */ @@ -332,7 +266,6 @@ class DefaultContentManager implements DefaultContentManagerInterface { return $exported_content; } - /** * {@inheritdoc} */ @@ -347,6 +280,100 @@ class DefaultContentManager implements DefaultContentManagerInterface { } } + /** + * Build dependency graph and populate file map array. + * + * @param string $folder + * Default content folder. + * + * @throws \Exception + */ + protected function buildGraph($folder) { + $files = $this->scanner()->scan($folder); + // 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. + $self = $decoded['_links']['self']['href']; + + // Throw an exception when this URL already exists. + if (isset($this->fileMap[$self])) { + $args = array( + '@href' => $self, + '@first' => $this->fileMap[$self]->uri, + '@second' => $file->uri, + ); + + // Reset link domain. + $this->linkManager->setLinkDomain(FALSE); + throw new \Exception(new FormattableMarkup('Default content with href @href exists twice: @first @second', $args)); + } + + preg_match_all('/type\/(.*)\/(.*)/', $decoded['_links']['type']['href'], $matches); + $entity_type_id = isset($matches[1][0]) ? $matches[1][0] : NULL; + $entity_bundle_id = isset($matches[2][0]) ? $matches[2][0] : NULL; + + // Store the entity type with the file. + $file->entity_type_id = $entity_type_id; + + // Store the file in the file map. + $this->fileMap[$self] = $file; + // Create a vertex for the graph. + $vertex = $this->getVertex($self); + $this->graph[$vertex->link]['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) { + $edge = $this->getVertex($item['_links']['self']['href']); + $this->graph[$vertex->link]['edges'][$edge->link] = TRUE; + } + } + } + } + + /** + * Create entities given a pre-populated graph and file map. + * + * @see DefaultContentManager::buildGraph() + * + * @return \Drupal\Core\Config\Entity\ConfigEntityInterface[] + * List of created entities. + */ + public function createEntities() { + $created = array(); + + // @todo what if no dependencies? + $sorted = $this->sortTree($this->graph); + foreach ($sorted as $link => $details) { + if (!empty($this->fileMap[$link])) { + $file = $this->fileMap[$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; + } + } + + // Reset the tree. + $this->resetTree(); + // Reset link domain. + $this->linkManager->setLinkDomain(FALSE); + return $created; + } /** * Returns all referenced entities of an entity. @@ -380,11 +407,9 @@ class DefaultContentManager implements DefaultContentManagerInterface { if ($depth > 5) { return $entity_dependencies; } - return array_unique($entity_dependencies, SORT_REGULAR); } - /** * Utility to get a default content scanner * @@ -398,7 +423,6 @@ class DefaultContentManager implements DefaultContentManagerInterface { return new DefaultContentScanner(); } - /** * {@inheritdoc} */ @@ -406,21 +430,31 @@ class DefaultContentManager implements DefaultContentManagerInterface { $this->scanner = $scanner; } - /** - * Parses content files + * Parses content files. */ protected function parseFile($file) { return file_get_contents($file->uri); } - + /** + * Reset internal state. + */ protected function resetTree() { - $this->graph = []; + $this->graph = array(); $this->vertexes = array(); + $this->fileMap = array(); } - + /** + * Sort graph tree. + * + * @param array $graph + * Graph array. + * + * @return array + * Return sorted graph. + */ protected function sortTree(array $graph) { $graph_object = new Graph($graph); $sorted = $graph_object->searchAndSort(); @@ -428,7 +462,6 @@ class DefaultContentManager implements DefaultContentManagerInterface { return array_reverse($sorted); } - /** * Returns a vertex object for a given item link. * @@ -447,4 +480,21 @@ class DefaultContentManager implements DefaultContentManagerInterface { return $this->vertexes[$item_link]; } + /** + * Get only content entity definitions. + * + * @return \Drupal\Core\Config\Entity\ConfigEntityInterface[] + * List of content entity definitions. + */ + protected function getContentEntityDefinitions() { + $definitions = array(); + foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) { + $reflection = new \ReflectionClass($entity_type->getClass()); + if ($reflection->implementsInterface('\Drupal\Core\Entity\ContentEntityInterface')) { + $definitions[$entity_type_id] = $entity_type; + } + } + return $definitions; + } + } diff --git a/src/Event/DefaultContentEvents.php b/src/Event/DefaultContentEvents.php index cec2c75..86a79e3 100644 --- a/src/Event/DefaultContentEvents.php +++ b/src/Event/DefaultContentEvents.php @@ -20,7 +20,7 @@ final class DefaultContentEvents { * * This event allows modules to perform actions after the default content has * been imported. The event listener receives a - * \Drupal\default_content\Event\ImportEvent instance. + * \Drupal\default_content\Event\ImportFromModuleEvent instance. * * @Event * diff --git a/src/Event/ImportEvent.php b/src/Event/ImportEvent.php deleted file mode 100644 index 4b9aeb8..0000000 --- a/src/Event/ImportEvent.php +++ /dev/null @@ -1,61 +0,0 @@ -entities = $entities; - $this->module = $module; - } - - /** - * Get the imported entities. - * - * @return \Drupal\Core\Entity\ContentEntityInterface[] - */ - public function getImportedEntities() { - return $this->entities; - } - - /** - * Gets the module name. - * - * @return string - * The module name that provided the default content. - */ - public function getModule() { - return $this->module; - } - -} - diff --git a/src/Event/ImportFromFolderEvent.php b/src/Event/ImportFromFolderEvent.php new file mode 100644 index 0000000..da5f274 --- /dev/null +++ b/src/Event/ImportFromFolderEvent.php @@ -0,0 +1,61 @@ +entities = $entities; + $this->folder = $folder; + } + + /** + * Get the imported entities. + * + * @return \Drupal\Core\Entity\ContentEntityInterface[] + */ + public function getImportedEntities() { + return $this->entities; + } + + /** + * Get default content folder. + * + * @return string + * The folder that provided the default content. + */ + public function getFolder() { + return $this->folder; + } + +} + diff --git a/src/Event/ImportFromModuleEvent.php b/src/Event/ImportFromModuleEvent.php new file mode 100644 index 0000000..e97f9b6 --- /dev/null +++ b/src/Event/ImportFromModuleEvent.php @@ -0,0 +1,61 @@ +entities = $entities; + $this->module = $module; + } + + /** + * Get the imported entities. + * + * @return \Drupal\Core\Entity\ContentEntityInterface[] + */ + public function getImportedEntities() { + return $this->entities; + } + + /** + * Gets the module name. + * + * @return string + * The module name that provided the default content. + */ + public function getModule() { + return $this->module; + } + +} + diff --git a/src/Tests/DefaultContentTest.php b/src/Tests/DefaultContentTest.php index e34d4a0..78cd02a 100644 --- a/src/Tests/DefaultContentTest.php +++ b/src/Tests/DefaultContentTest.php @@ -4,6 +4,7 @@ * @file * Contains \Drupal\default_content\Tests\DefaultContentTest. */ + namespace Drupal\default_content\Tests; use Drupal\simpletest\WebTestBase; @@ -32,14 +33,39 @@ class DefaultContentTest extends WebTestBase { } /** - * Test importing default content. + * Test importing default content from module. */ - public function testImport() { + public function testImportFromModule() { // Login as admin. $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_import_test'), TRUE); + $this->rebuildContainer(); + $this->assertImportedNodes(); + } + + /** + * Test importing default content from folder. + */ + public function testImportFromFolder() { + // Login as admin. + $this->drupalLogin($this->drupalCreateUser(array_keys(\Drupal::moduleHandler()->invokeAll(('permission'))))); + // Enable configuration test module. \Drupal::service('module_installer')->install(array('default_content_test'), TRUE); + // Import content from a specific folder. + $folder = drupal_get_path('module', 'default_content_import_test') . '/content'; + \Drupal::service('default_content.manager')->importContentFromFolder($folder); $this->rebuildContainer(); + $this->assertImportedNodes(); + } + + /** + * Assert that imported nodes have been created correctly. + * + * @see DefaultContentTest::testImportFromModule() + * @see DefaultContentTest::testImportFromFolder() + */ + protected function assertImportedNodes() { $node = $this->drupalGetNodeByTitle('Imported node'); $this->assertEqual($node->body->value, 'Crikey it works!'); $this->assertEqual($node->getType(), 'page'); diff --git a/tests/modules/default_content_import_test/content/node/imported.json b/tests/modules/default_content_import_test/content/node/imported.json new file mode 100755 index 0000000..e3a3723 --- /dev/null +++ b/tests/modules/default_content_import_test/content/node/imported.json @@ -0,0 +1,128 @@ +{ + "_links": { + "self": { + "href": "http://drupal.org/node/1" + }, + "type": { + "href": "http://drupal.org/rest/type/node/page" + }, + "http://drupal.org/rest/relation/node/page/uid": [ + { + "href": "http://drupal.org/user/2" + } + ], + "http://drupal.org/rest/relation/node/page/revision_uid": [ + { + "href": "http://drupal.org/user/2" + } + ], + "http://drupal.org/rest/relation/node/page/field_tags":[ + { + "href":"http://drupal.org/taxonomy/term/1" + } + ] + }, + "uuid": [ + { + "value": "65c412a3-b83f-4efb-8a05-5a6ecea10ad4" + } + ], + "type": [ + { + "target_id": "page" + } + ], + "langcode": [ + { + "value": "en" + } + ], + "title": [ + { + "value": "Imported node" + } + ], + "body": [ + { + "value": "Crikey it works!" + } + ], + "_embedded": { + "http://drupal.org/rest/relation/node/page/uid": [ + { + "_links": { + "self": { + "href": "http://drupal.org/user/1" + }, + "type": { + "href": "http://drupal.org/rest/type/user/user" + } + } + } + ], + "http://drupal.org/rest/relation/node/page/revision_uid": [ + { + "_links": { + "self": { + "href": "http://drupal.org/user/1" + }, + "type": { + "href": "http://drupal.org/rest/type/user/user" + } + } + } + ], + "http://drupal.org/rest/relation/node/page/field_tags": [ + { + "_links":{ + "self":{ + "href":"http://drupal.org/taxonomy/term/1" + }, + "type":{ + "href":"http://drupal.org/rest/type/taxonomy_term/tags" + } + }, + "uuid":[ + { + "value":"550f86ad-aa11-4047-953f-636d42889f85" + } + ] + } + ] + }, + "status": [ + { + "value": "1" + } + ], + "created": [ + { + "value": "1381645976" + } + ], + "changed": [ + { + "value": "1381646540" + } + ], + "promote": [ + { + "value": "1" + } + ], + "sticky": [ + { + "value": "1" + } + ], + "revision_timestamp": [ + { + "value": "1381646540" + } + ], + "revision_log": [ + { + "value": "" + } + ] +} diff --git a/tests/modules/default_content_import_test/content/taxonomy_term/tag.json b/tests/modules/default_content_import_test/content/taxonomy_term/tag.json new file mode 100644 index 0000000..5113f07 --- /dev/null +++ b/tests/modules/default_content_import_test/content/taxonomy_term/tag.json @@ -0,0 +1,50 @@ +{ + "_links":{ + "self":{ + "href":"http://drupal.org/taxonomy/term/1" + }, + "type":{ + "href":"http://drupal.org/rest/type/taxonomy_term/tags" + } + }, + "tid":[ + { + "value":"1" + } + ], + "uuid":[ + { + "value":"550f86ad-aa11-4047-953f-636d42889f85" + } + ], + "vid":[ + { + "value":"tags" + } + ], + "langcode":[ + { + "value":"und" + } + ], + "name":[ + { + "value":"A tag" + } + ], + "description":[ + { + "value":"" + } + ], + "weight":[ + { + "value":"0" + } + ], + "changed":[ + { + "value":"1381645869" + } + ] +} diff --git a/tests/modules/default_content_import_test/default_content_import_test.info.yml b/tests/modules/default_content_import_test/default_content_import_test.info.yml new file mode 100644 index 0000000..c362bf8 --- /dev/null +++ b/tests/modules/default_content_import_test/default_content_import_test.info.yml @@ -0,0 +1,8 @@ +name: 'Default content import tests' +type: module +hidden: TRUE +description: 'Default content import tests' +package: Web services +core: 8.x +dependencies: + - default_content_test diff --git a/tests/modules/default_content_import_test/default_content_import_test.module b/tests/modules/default_content_import_test/default_content_import_test.module new file mode 100644 index 0000000..171929f --- /dev/null +++ b/tests/modules/default_content_import_test/default_content_import_test.module @@ -0,0 +1,6 @@ +