diff --git a/core/core.services.yml b/core/core.services.yml index 19967f6..3705fde 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -121,6 +121,10 @@ services: - { name: event_subscriber } - { name: service_collector, tag: 'config.factory.override', call: addOverride } arguments: ['@config.storage', '@event_dispatcher', '@config.typed'] + config.importer_subscriber: + class: Drupal\Core\Config\Importer\EventSubscriber + tags: + - { name: event_subscriber } config.installer: class: Drupal\Core\Config\ConfigInstaller arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher'] diff --git a/core/lib/Drupal/Core/Config/ConfigEvents.php b/core/lib/Drupal/Core/Config/ConfigEvents.php index 7fa22a5..35c1ab0 100644 --- a/core/lib/Drupal/Core/Config/ConfigEvents.php +++ b/core/lib/Drupal/Core/Config/ConfigEvents.php @@ -51,6 +51,13 @@ const IMPORT = 'config.importer.import'; /** + * Name of event fired when missing content dependencies at detected. + * + * @see \Drupal\Core\Config\ConfigImporter::processMissingContentDependencies(). + */ + const IMPORT_MISSING_CONTENT = 'config.importer.missing_content'; + + /** * Name of event fired to collect information on all collections. * * @see \Drupal\Core\Config\ConfigManager::getConfigCollectionInfo() diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index a13a414..1da9eaa 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Config; +use Drupal\Core\Config\Importer\MissingContentEvent; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Component\Utility\String; @@ -532,7 +533,7 @@ public function initialize() { $sync_steps[] = 'processExtensions'; } $sync_steps[] = 'processConfigurations'; - + $sync_steps[] = 'processMissingContent'; // Allow modules to add new steps to configuration synchronization. $this->moduleHandler->alter('config_import_steps', $sync_steps, $this); $sync_steps[] = 'finish'; @@ -602,6 +603,31 @@ protected function processConfigurations(array &$context) { } } + protected function processMissingContent(array &$context) { + if (!isset($context['sandbox']['config']['missing_content'])) { + $missing_content = $this->configManager->findMissingContentDependencies(); + $context['sandbox']['config']['missing_content']['data'] = $missing_content; + $context['sandbox']['config']['missing_content']['total'] = count($missing_content); + } + else { + $missing_content = $context['sandbox']['config']['missing_content']['data']; + } + if (!empty($missing_content)) { + $event = new MissingContentEvent($missing_content); + // Fire an event to allow listeners to create the missing content. + $this->eventDispatcher->dispatch(ConfigEvents::IMPORT_MISSING_CONTENT, $event); + $context['sandbox']['config']['missing_content']['data'] = $event->getMissingContent(); + } + $current_count = count($context['sandbox']['config']['missing_content']['data']); + if ($current_count) { + $context['message'] = $this->t('Resolving missing content'); + $context['finished'] = ($context['sandbox']['config']['missing_content']['total'] - $current_count) / $context['sandbox']['config']['missing_content']['total']; + } + else { + $context['finished'] = 1; + } + } + /** * Finishes the batch. * diff --git a/core/lib/Drupal/Core/Config/ConfigManager.php b/core/lib/Drupal/Core/Config/ConfigManager.php index a97cf72..1fec419 100644 --- a/core/lib/Drupal/Core/Config/ConfigManager.php +++ b/core/lib/Drupal/Core/Config/ConfigManager.php @@ -311,4 +311,31 @@ public function getConfigCollectionInfo() { return $this->configCollectionInfo; } + /** + * {@inheritdoc} + */ + public function findMissingContentDependencies() { + $content_dependencies = array(); + $missing_dependencies = array(); + foreach ($this->activeStorage->readMultiple($this->activeStorage->listAll()) as $config_data) { + if (isset($config_data['dependencies']['content'])) { + $content_dependencies = array_merge($content_dependencies, $config_data['dependencies']['content']); + } + } + foreach (array_unique($content_dependencies) as $content_dependency) { + // Format of the name is entity_type:bundle:uuid + list($entity_type, $bundle, $uuid) = explode(':', $content_dependency, 3); + // Can we do some multiple loading? + if (!$this->entityManager->loadEntityByUuid($entity_type, $uuid)) { + $missing_dependencies[$uuid] = array( + 'entity_type' => $entity_type, + 'bundle' => $bundle, + 'uuid' => $uuid, + ); + } + } + return $missing_dependencies; + } + + } diff --git a/core/lib/Drupal/Core/Config/ConfigManagerInterface.php b/core/lib/Drupal/Core/Config/ConfigManagerInterface.php index c5fdec1..05c33b3 100644 --- a/core/lib/Drupal/Core/Config/ConfigManagerInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigManagerInterface.php @@ -133,4 +133,9 @@ public function supportsConfigurationEntities($collection); */ public function getConfigCollectionInfo(); + /** + * @return array + * A list of missing dependencies() + */ + public function findMissingContentDependencies(); } diff --git a/core/lib/Drupal/Core/Config/Importer/EventSubscriber.php b/core/lib/Drupal/Core/Config/Importer/EventSubscriber.php new file mode 100644 index 0000000..9cce5ce --- /dev/null +++ b/core/lib/Drupal/Core/Config/Importer/EventSubscriber.php @@ -0,0 +1,39 @@ +getMissingContent()) as $uuid) { + $event->resolveMissingContent($uuid); + } + } + + /** + * {@inheritdoc} + */ + static function getSubscribedEvents() { + // This should always be the final event as it will mark all content + // dependencies as resolved. + $events[ConfigEvents::IMPORT_MISSING_CONTENT][] = array('onMissingContent', 1024); + return $events; + } + +} diff --git a/core/lib/Drupal/Core/Config/Importer/MissingContentEvent.php b/core/lib/Drupal/Core/Config/Importer/MissingContentEvent.php new file mode 100644 index 0000000..1bbe849 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Importer/MissingContentEvent.php @@ -0,0 +1,73 @@ +missingContent = $missing_content; + } + + /** + * Gets missing content information. + * + * @return array + */ + public function getMissingContent() { + return $this->missingContent; + } + + /** + * Resolves the missing content by removing it from the list. + * + * Calling this also stops event propagation so that if the event subscriber + * does actually creates content it will work nicely with the batch system. + * + * @param $uuid + * The UUID of the content entity to mark resolved. + * + * @return $this + * The MissingContentEvent object. + */ + public function resolveMissingContent($uuid) { + if (isset($this->missingContent[$uuid])) { + unset($this->missingContent[$uuid]); + // Stop propagation if the subscriber resolves any conflicts. This allows + // multiple events to listen to the missing content event and interact + // with the config importer in a batch. + // \Drupal\Core\Config\Importer\EventSubscriber implements an event to + // catch any unresolved content dependencies and mark them all as resolved + // so that config importing can complete. + $this->stopPropagation(); + } + else { + // @todo throw an exception? Atm I don't think so. + } + return $this; + } + +} + diff --git a/core/modules/config/src/Tests/ConfigDependencyTest.php b/core/modules/config/src/Tests/ConfigDependencyTest.php index fa903b9..75a5839 100644 --- a/core/modules/config/src/Tests/ConfigDependencyTest.php +++ b/core/modules/config/src/Tests/ConfigDependencyTest.php @@ -7,21 +7,23 @@ namespace Drupal\config\Tests; -use Drupal\simpletest\DrupalUnitTestBase; +use Drupal\system\Tests\Entity\EntityUnitTestBase; /** * Tests for configuration dependencies. * * @group config */ -class ConfigDependencyTest extends DrupalUnitTestBase { +class ConfigDependencyTest extends EntityUnitTestBase { /** * Modules to enable. * + * The entity_test module is enabled to provide content entity types. + * * @var array */ - public static $modules = array('system', 'config_test'); + public static $modules = array('config_test'); /** * Tests that calculating dependencies for system module. @@ -41,6 +43,7 @@ public function testNonEntity() { * Tests creating dependencies on configuration entities. */ public function testDependencyMangement() { + /** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */ $config_manager = \Drupal::service('config.manager'); $storage = $this->container->get('entity.manager')->getStorage('config_test'); // Test dependencies between modules. @@ -109,9 +112,14 @@ public function testDependencyMangement() { $this->assertTrue(isset($dependents['config_test.dynamic.entity3']), 'config_test.dynamic.entity3 has a dependency on the Node module.'); $this->assertTrue(isset($dependents['config_test.dynamic.entity4']), 'config_test.dynamic.entity4 has a dependency on the Node module.'); - // Test dependency on a fake content entity. - $entity2->setEnforcedDependencies(['config' => [$entity1->getConfigDependencyName()], 'content' => ['node:page:uuid']])->save();; - $dependents = $config_manager->findConfigEntityDependents('content', array('node:page:uuid')); + // Test dependency on a content entity. + $entity_test = entity_create('entity_test', array( + 'name' => $this->randomString(), + 'type' => 'entity_test', + )); + $entity_test->save(); + $entity2->setEnforcedDependencies(['config' => [$entity1->getConfigDependencyName()], 'content' => [$entity_test->getConfigDependencyName()]])->save();; + $dependents = $config_manager->findConfigEntityDependents('content', array($entity_test->getConfigDependencyName())); $this->assertFalse(isset($dependents['config_test.dynamic.entity1']), 'config_test.dynamic.entity1 does not have a dependency on the content entity.'); $this->assertTrue(isset($dependents['config_test.dynamic.entity2']), 'config_test.dynamic.entity2 has a dependency on the content entity.'); $this->assertTrue(isset($dependents['config_test.dynamic.entity3']), 'config_test.dynamic.entity3 has a dependency on the content entity (via entity2).'); @@ -150,6 +158,30 @@ public function testDependencyMangement() { $this->assertTrue(in_array('config_query_test:entity1', $dependent_ids), 'config_test.query.entity1 has a dependency on config_test module.'); $this->assertTrue(in_array('config_query_test:entity2', $dependent_ids), 'config_test.query.entity2 has a dependency on config_test module.'); + // Test the ability to find missing content dependencies. + $missing_dependencies = $config_manager->findMissingContentDependencies(); + $this->assertEqual([], $missing_dependencies); + + $expected = [$entity_test->uuid() => [ + 'entity_type' => 'entity_test', + 'bundle' => $entity_test->bundle(), + 'uuid' => $entity_test->uuid(), + ]]; + // Delete the content entity so that is it now missing. + $entity_test->delete(); + $missing_dependencies = $config_manager->findMissingContentDependencies(); + $this->assertEqual($expected, $missing_dependencies); + + // Add a fake missing dependency to ensure multiple missing dependencies + // work. + $entity1->setEnforcedDependencies(['content' => [$entity_test->getConfigDependencyName(), 'entity_test:bundle:uuid']])->save();; + $expected['uuid'] = [ + 'entity_type' => 'entity_test', + 'bundle' => 'bundle', + 'uuid' => 'uuid', + ]; + $missing_dependencies = $config_manager->findMissingContentDependencies(); + $this->assertEqual($expected, $missing_dependencies); } /**