Issue #2361423 by alexpott, larowlan, dixon_, swentel: Add a step to config import to allow contrib to create content based on config dependencies
parent
7a72d33bbc
commit
66c9f700d9
|
@ -234,6 +234,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\FinalMissingContentSubscriber
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
config.installer:
|
||||
class: Drupal\Core\Config\ConfigInstaller
|
||||
arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher']
|
||||
|
|
|
@ -98,6 +98,23 @@ final class ConfigEvents {
|
|||
*/
|
||||
const IMPORT = 'config.importer.import';
|
||||
|
||||
/**
|
||||
* Name of event fired when missing content dependencies are detected.
|
||||
*
|
||||
* Events subscribers are fired as part of the configuration import batch.
|
||||
* Each subscribe should call
|
||||
* \Drupal\Core\Config\MissingContentEvent::resolveMissingContent() when they
|
||||
* address a missing dependency. To address large amounts of dependencies
|
||||
* subscribers can call
|
||||
* \Drupal\Core\Config\MissingContentEvent::stopPropagation() which will stop
|
||||
* calling other events and guarantee that the configuration import batch will
|
||||
* fire the event again to continue processing missing content dependencies.
|
||||
*
|
||||
* @see \Drupal\Core\Config\ConfigImporter::processMissingContent()
|
||||
* @see \Drupal\Core\Config\MissingContentEvent
|
||||
*/
|
||||
const IMPORT_MISSING_CONTENT = 'config.importer.missing_content';
|
||||
|
||||
/**
|
||||
* Name of event fired to collect information on all config collections.
|
||||
*
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
namespace Drupal\Core\Config;
|
||||
|
||||
use Drupal\Core\Config\Importer\MissingContentEvent;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Extension\ModuleInstallerInterface;
|
||||
use Drupal\Core\Extension\ThemeHandlerInterface;
|
||||
|
@ -536,7 +537,7 @@ class ConfigImporter {
|
|||
$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';
|
||||
|
@ -606,6 +607,38 @@ class ConfigImporter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles processing of missing content.
|
||||
*
|
||||
* @param array $context
|
||||
* Standard batch context.
|
||||
*/
|
||||
protected function processMissingContent(array &$context) {
|
||||
$sandbox = &$context['sandbox']['config'];
|
||||
if (!isset($sandbox['missing_content'])) {
|
||||
$missing_content = $this->configManager->findMissingContentDependencies();
|
||||
$sandbox['missing_content']['data'] = $missing_content;
|
||||
$sandbox['missing_content']['total'] = count($missing_content);
|
||||
}
|
||||
else {
|
||||
$missing_content = $sandbox['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);
|
||||
$sandbox['missing_content']['data'] = $event->getMissingContent();
|
||||
}
|
||||
$current_count = count($sandbox['missing_content']['data']);
|
||||
if ($current_count) {
|
||||
$context['message'] = $this->t('Resolving missing content');
|
||||
$context['finished'] = ($sandbox['missing_content']['total'] - $current_count) / $sandbox['missing_content']['total'];
|
||||
}
|
||||
else {
|
||||
$context['finished'] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishes the batch.
|
||||
*
|
||||
|
|
|
@ -447,4 +447,29 @@ class ConfigManager implements ConfigManagerInterface {
|
|||
return $entity->onDependencyRemoval($affected_dependencies);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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 dependency is entity_type:bundle:uuid.
|
||||
list($entity_type, $bundle, $uuid) = explode(':', $content_dependency, 3);
|
||||
if (!$this->entityManager->loadEntityByUuid($entity_type, $uuid)) {
|
||||
$missing_dependencies[$uuid] = array(
|
||||
'entity_type' => $entity_type,
|
||||
'bundle' => $bundle,
|
||||
'uuid' => $uuid,
|
||||
);
|
||||
}
|
||||
}
|
||||
return $missing_dependencies;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -180,4 +180,14 @@ interface ConfigManagerInterface {
|
|||
*/
|
||||
public function getConfigCollectionInfo();
|
||||
|
||||
/**
|
||||
* Finds missing content dependencies declared in configuration entities.
|
||||
*
|
||||
* @return array
|
||||
* A list of missing content dependencies. The array is keyed by UUID. Each
|
||||
* value is an array with the following keys: 'entity_type', 'bundle' and
|
||||
* 'uuid'.
|
||||
*/
|
||||
public function findMissingContentDependencies();
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Config\Importer\FinalMissingContentSubscriber.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Config\Importer;
|
||||
|
||||
use Drupal\Core\Config\ConfigEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* Final event subscriber to the missing content event.
|
||||
*
|
||||
* Ensure that all missing content dependencies are removed from the event so
|
||||
* the importer can complete.
|
||||
*
|
||||
* @see \Drupal\Core\Config\ConfigImporter::processMissingContent()
|
||||
*/
|
||||
class FinalMissingContentSubscriber implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* Handles the missing content event.
|
||||
*
|
||||
* @param \Drupal\Core\Config\Importer\MissingContentEvent $event
|
||||
* The missing content event.
|
||||
*/
|
||||
public function onMissingContent(MissingContentEvent $event) {
|
||||
foreach (array_keys($event->getMissingContent()) as $uuid) {
|
||||
$event->resolveMissingContent($uuid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Config\MissingContentEvent.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Config\Importer;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Wraps a configuration event for event listeners.
|
||||
*
|
||||
* @see \Drupal\Core\Config\Config\ConfigEvents::IMPORT_MISSING_CONTENT
|
||||
*/
|
||||
class MissingContentEvent extends Event {
|
||||
|
||||
/**
|
||||
* A list of missing content dependencies.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $missingContent;
|
||||
|
||||
/**
|
||||
* Constructs a configuration import missing content event object.
|
||||
*
|
||||
* @param array $missing_content
|
||||
* Missing content information.
|
||||
*/
|
||||
public function __construct(array $missing_content) {
|
||||
$this->missingContent = $missing_content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets missing content information.
|
||||
*
|
||||
* @return array
|
||||
* A list of missing content dependencies. The array is keyed by UUID. Each
|
||||
* value is an array with the following keys: 'entity_type', 'bundle' and
|
||||
* 'uuid'.
|
||||
*/
|
||||
public function getMissingContent() {
|
||||
return $this->missingContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the missing content by removing it from the list.
|
||||
*
|
||||
* @param string $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]);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
|
@ -8,21 +8,23 @@
|
|||
namespace Drupal\config\Tests;
|
||||
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\simpletest\KernelTestBase;
|
||||
use Drupal\system\Tests\Entity\EntityUnitTestBase;
|
||||
|
||||
/**
|
||||
* Tests for configuration dependencies.
|
||||
*
|
||||
* @group config
|
||||
*/
|
||||
class ConfigDependencyTest extends KernelTestBase {
|
||||
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', 'entity_test', 'user');
|
||||
public static $modules = array('config_test', 'entity_test', 'user');
|
||||
|
||||
/**
|
||||
* Tests that calculating dependencies for system module.
|
||||
|
@ -41,7 +43,8 @@ class ConfigDependencyTest extends KernelTestBase {
|
|||
/**
|
||||
* Tests creating dependencies on configuration entities.
|
||||
*/
|
||||
public function testDependencyMangement() {
|
||||
public function testDependencyManagement() {
|
||||
/** @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.
|
||||
|
@ -110,9 +113,14 @@ class ConfigDependencyTest extends KernelTestBase {
|
|||
$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).');
|
||||
|
@ -151,6 +159,30 @@ class ConfigDependencyTest extends KernelTestBase {
|
|||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\config\Tests\ConfigImporterMissingContentTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\config\Tests;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Config\ConfigImporter;
|
||||
use Drupal\Core\Config\ConfigImporterException;
|
||||
use Drupal\Core\Config\StorageComparer;
|
||||
use Drupal\simpletest\KernelTestBase;
|
||||
|
||||
/**
|
||||
* Tests importing configuration which has missing content dependencies.
|
||||
*
|
||||
* @group config
|
||||
*/
|
||||
class ConfigImporterMissingContentTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Config Importer object used for testing.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigImporter
|
||||
*/
|
||||
protected $configImporter;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('system', 'user', 'entity_test', 'config_test', 'config_import_test');
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->installSchema('system', 'sequences');
|
||||
$this->installEntitySchema('entity_test');
|
||||
$this->installEntitySchema('user');
|
||||
$this->installConfig(array('config_test'));
|
||||
// Installing config_test's default configuration pollutes the global
|
||||
// variable being used for recording hook invocations by this test already,
|
||||
// so it has to be cleared out manually.
|
||||
unset($GLOBALS['hook_config_test']);
|
||||
|
||||
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
|
||||
|
||||
// Set up the ConfigImporter object for testing.
|
||||
$storage_comparer = new StorageComparer(
|
||||
$this->container->get('config.storage.staging'),
|
||||
$this->container->get('config.storage'),
|
||||
$this->container->get('config.manager')
|
||||
);
|
||||
$this->configImporter = new ConfigImporter(
|
||||
$storage_comparer->createChangelist(),
|
||||
$this->container->get('event_dispatcher'),
|
||||
$this->container->get('config.manager'),
|
||||
$this->container->get('lock'),
|
||||
$this->container->get('config.typed'),
|
||||
$this->container->get('module_handler'),
|
||||
$this->container->get('module_installer'),
|
||||
$this->container->get('theme_handler'),
|
||||
$this->container->get('string_translation')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the missing content event is fired.
|
||||
*
|
||||
* @see \Drupal\Core\Config\ConfigImporter::processMissingContent()
|
||||
* @see \Drupal\config_import_test\EventSubscriber
|
||||
*/
|
||||
function testMissingContent() {
|
||||
\Drupal::state()->set('config_import_test.config_import_missing_content', TRUE);
|
||||
|
||||
// Update a configuration entity in the staging directory to have a
|
||||
// dependency on two content entities that do not exist.
|
||||
$storage = $this->container->get('config.storage');
|
||||
$staging = $this->container->get('config.storage.staging');
|
||||
$entity_one = entity_create('entity_test', array('name' => 'one'));
|
||||
$entity_two = entity_create('entity_test', array('name' => 'two'));
|
||||
$entity_three = entity_create('entity_test', array('name' => 'three'));
|
||||
$dynamic_name = 'config_test.dynamic.dotted.default';
|
||||
$original_dynamic_data = $storage->read($dynamic_name);
|
||||
// Entity one will be resolved by
|
||||
// \Drupal\config_import_test\EventSubscriber::onConfigImporterMissingContentOne().
|
||||
$original_dynamic_data['dependencies']['content'][] = $entity_one->getConfigDependencyName();
|
||||
// Entity two will be resolved by
|
||||
// \Drupal\config_import_test\EventSubscriber::onConfigImporterMissingContentTwo().
|
||||
$original_dynamic_data['dependencies']['content'][] = $entity_two->getConfigDependencyName();
|
||||
// Entity three will be resolved by
|
||||
// \Drupal\Core\Config\Importer\FinalMissingContentSubscriber.
|
||||
$original_dynamic_data['dependencies']['content'][] = $entity_three->getConfigDependencyName();
|
||||
$staging->write($dynamic_name, $original_dynamic_data);
|
||||
|
||||
// Import.
|
||||
$this->configImporter->reset()->import();
|
||||
$this->assertEqual([], $this->configImporter->getErrors(), 'There were no errors during the import.');
|
||||
$this->assertEqual($entity_one->uuid(), \Drupal::state()->get('config_import_test.config_import_missing_content_one'), 'The missing content event is fired during configuration import.');
|
||||
$this->assertEqual($entity_two->uuid(), \Drupal::state()->get('config_import_test.config_import_missing_content_two'), 'The missing content event is fired during configuration import.');
|
||||
$original_dynamic_data = $storage->read($dynamic_name);
|
||||
$this->assertEqual([$entity_one->getConfigDependencyName(), $entity_two->getConfigDependencyName(), $entity_three->getConfigDependencyName()], $original_dynamic_data['dependencies']['content'], 'The imported configuration entity has the missing content entity dependency.');
|
||||
}
|
||||
|
||||
}
|
|
@ -10,6 +10,7 @@ namespace Drupal\config_import_test;
|
|||
use Drupal\Core\Config\ConfigCrudEvent;
|
||||
use Drupal\Core\Config\ConfigEvents;
|
||||
use Drupal\Core\Config\ConfigImporterEvent;
|
||||
use Drupal\Core\Config\Importer\MissingContentEvent;
|
||||
use Drupal\Core\State\StateInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
|
@ -51,6 +52,39 @@ class EventSubscriber implements EventSubscriberInterface {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the missing content event.
|
||||
*
|
||||
* @param \Drupal\Core\Config\Importer\MissingContentEvent $event
|
||||
* The missing content event.
|
||||
*/
|
||||
public function onConfigImporterMissingContentOne(MissingContentEvent $event) {
|
||||
if ($this->state->get('config_import_test.config_import_missing_content', FALSE) && $this->state->get('config_import_test.config_import_missing_content_one', FALSE) === FALSE) {
|
||||
$missing = $event->getMissingContent();
|
||||
$uuid = key($missing);
|
||||
$this->state->set('config_import_test.config_import_missing_content_one', key($missing));
|
||||
$event->resolveMissingContent($uuid);
|
||||
// Stopping propagation ensures that onConfigImporterMissingContentTwo
|
||||
// will be fired on the next batch step.
|
||||
$event->stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the missing content event.
|
||||
*
|
||||
* @param \Drupal\Core\Config\Importer\MissingContentEvent $event
|
||||
* The missing content event.
|
||||
*/
|
||||
public function onConfigImporterMissingContentTwo(MissingContentEvent $event) {
|
||||
if ($this->state->get('config_import_test.config_import_missing_content', FALSE) && $this->state->get('config_import_test.config_import_missing_content_two', FALSE) === FALSE) {
|
||||
$missing = $event->getMissingContent();
|
||||
$uuid = key($missing);
|
||||
$this->state->set('config_import_test.config_import_missing_content_two', key($missing));
|
||||
$event->resolveMissingContent($uuid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reacts to a config save and records information in state for testing.
|
||||
*
|
||||
|
@ -106,6 +140,7 @@ class EventSubscriber implements EventSubscriberInterface {
|
|||
$events[ConfigEvents::SAVE][] = array('onConfigSave', 40);
|
||||
$events[ConfigEvents::DELETE][] = array('onConfigDelete', 40);
|
||||
$events[ConfigEvents::IMPORT_VALIDATE] = array('onConfigImporterValidate');
|
||||
$events[ConfigEvents::IMPORT_MISSING_CONTENT] = array(array('onConfigImporterMissingContentOne'), array('onConfigImporterMissingContentTwo', -100));
|
||||
return $events;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue