diff --git a/core/lib/Drupal/Core/Update/UpdateCompilerPass.php b/core/lib/Drupal/Core/Update/UpdateCompilerPass.php index f0f6f5b0c2..746832be5d 100644 --- a/core/lib/Drupal/Core/Update/UpdateCompilerPass.php +++ b/core/lib/Drupal/Core/Update/UpdateCompilerPass.php @@ -3,8 +3,6 @@ namespace Drupal\Core\Update; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\Compiler\RepeatablePassInterface; -use Symfony\Component\DependencyInjection\Compiler\RepeatedPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; @@ -15,50 +13,44 @@ * Updates can install new modules that add services that existing services now * depend on. This compiler pass allows the update system to work in such cases. */ -class UpdateCompilerPass implements CompilerPassInterface, RepeatablePassInterface { - - /** - * @var \Symfony\Component\DependencyInjection\Compiler\RepeatedPass - */ - private $repeatedPass; - - /** - * {@inheritdoc} - */ - public function setRepeatedPass(RepeatedPass $repeatedPass) { - $this->repeatedPass = $repeatedPass; - } +class UpdateCompilerPass implements CompilerPassInterface { /** * {@inheritdoc} */ public function process(ContainerBuilder $container) { - $has_changed = FALSE; - foreach ($container->getDefinitions() as $key => $definition) { - foreach ($definition->getArguments() as $argument) { - if ($argument instanceof Reference) { - $argument_id = (string) $argument; - if (!$container->has($argument_id) && $argument->getInvalidBehavior() === ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) { - // If the container does not have the argument and would throw an - // exception, remove the service. - $container->removeDefinition($key); - $has_changed = TRUE; + // Loop over the defined services and remove any with unmet dependencies. + // The kernel cannot be booted if the container has such services. This + // allows modules to run their update hooks to enable newly added + // dependencies. + do { + $has_changed = FALSE; + foreach ($container->getDefinitions() as $key => $definition) { + foreach ($definition->getArguments() as $argument) { + if ($argument instanceof Reference) { + $argument_id = (string) $argument; + if (!$container->has($argument_id) && $argument->getInvalidBehavior() === ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) { + // If the container does not have the argument and would throw an + // exception, remove the service. + $container->removeDefinition($key); + $container->log($this, sprintf('Removed service "%s"; reason: depends on non-existent service "%s".', $key, $argument_id)); + $has_changed = TRUE; + } } } } - } - // Remove any aliases which point to undefined services. - $aliases = $container->getAliases(); - foreach ($aliases as $key => $alias) { - if (!$container->has((string) $alias)) { - $container->removeAlias($key); - $has_changed = TRUE; + if ($has_changed) { + // Remove any aliases which point to undefined services. + foreach ($container->getAliases() as $key => $alias) { + $id = (string) $alias; + if (!$container->has($id)) { + $container->removeAlias($key); + $container->log($this, sprintf('Removed alias "%s"; reason: alias to non-existent service "%s".', $key, $id)); + } + } } - } - - if ($has_changed) { - $this->repeatedPass->setRepeat(); - } + // Repeat if services or aliases have been removed. + } while ($has_changed); } } diff --git a/core/lib/Drupal/Core/Update/UpdateServiceProvider.php b/core/lib/Drupal/Core/Update/UpdateServiceProvider.php index 379f85fb8b..22c9131eb3 100644 --- a/core/lib/Drupal/Core/Update/UpdateServiceProvider.php +++ b/core/lib/Drupal/Core/Update/UpdateServiceProvider.php @@ -6,7 +6,6 @@ use Drupal\Core\DependencyInjection\ServiceModifierInterface; use Drupal\Core\DependencyInjection\ServiceProviderInterface; use Symfony\Component\DependencyInjection\Compiler\PassConfig; -use Symfony\Component\DependencyInjection\Compiler\RepeatedPass; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; @@ -22,9 +21,7 @@ public function register(ContainerBuilder $container) { $definition = new Definition('Drupal\Core\Cache\NullBackend', ['null']); $container->setDefinition('cache.null', $definition); - // This has to come before CheckExceptionOnInvalidReferenceBehaviorPass and - // after DecoratorServicePass. - $container->addCompilerPass(new RepeatedPass([new UpdateCompilerPass()]), PassConfig::TYPE_REMOVE, 128); + $container->addCompilerPass(new UpdateCompilerPass(), PassConfig::TYPE_REMOVE, 128); } /** diff --git a/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.install b/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.install index 326618be5c..cdf0c03664 100644 --- a/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.install +++ b/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.install @@ -9,6 +9,14 @@ * Enable the new_dependency_test_with_service module. */ function new_dependency_test_update_8001() { + $services = [ + 'new_dependency_test.hard_dependency', + 'new_dependency_test.optional_dependency', + 'new_dependency_test.recursion', + 'new_dependency_test.alias', + 'new_dependency_test.alias_dependency', + ]; + // Gather the state of the services prior to installing the // new_dependency_test_with_service module. \Drupal::state()->set( @@ -21,15 +29,11 @@ function new_dependency_test_update_8001() { \Drupal::service('new_dependency_test.another_service_two')->isDecorated() ); - \Drupal::state()->set( - 'new_dependency_test_update_8001.has_before_install:new_dependency_test.hard_dependency', - \Drupal::hasService('new_dependency_test.hard_dependency') - ); - - \Drupal::state()->set( - 'new_dependency_test_update_8001.has_before_install:new_dependency_test.optional_dependency', - \Drupal::hasService('new_dependency_test.optional_dependency') - ); + $map = []; + foreach ($services as $id) { + $map[$id] = \Drupal::hasService($id); + } + \Drupal::state()->set('new_dependency_test_update_8001.has_before_install', $map); // During the update hooks the container is cleaned up to contain only // services that have their dependencies met. Core services are available. @@ -37,13 +41,9 @@ function new_dependency_test_update_8001() { // Gather the state of the services after installing the // new_dependency_test_with_service module. - \Drupal::state()->set( - 'new_dependency_test_update_8001.has_after_install:new_dependency_test.hard_dependency', - \Drupal::hasService('new_dependency_test.hard_dependency') - ); - - \Drupal::state()->set( - 'new_dependency_test_update_8001.has_after_install:new_dependency_test.optional_dependency', - \Drupal::hasService('new_dependency_test.optional_dependency') - ); + $map = []; + foreach ($services as $id) { + $map[$id] = \Drupal::hasService($id); + } + \Drupal::state()->set('new_dependency_test_update_8001.has_after_install', $map); } diff --git a/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.services.yml b/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.services.yml index a0f045e852..4e43a3c79e 100644 --- a/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.services.yml +++ b/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.services.yml @@ -1,4 +1,12 @@ services: + new_dependency_test.alias_dependency: + class: Drupal\new_dependency_test\ServiceWithDependency + arguments: ['@new_dependency_test.alias'] + new_dependency_test.recursion: + class: Drupal\new_dependency_test\ServiceWithDependency + arguments: ['@new_dependency_test.hard_dependency'] + new_dependency_test.alias: + alias: new_dependency_test.dependent new_dependency_test.dependent: class: Drupal\new_dependency_test\InjectedService arguments: ['@new_dependency_test_with_service.service'] @@ -8,8 +16,6 @@ services: new_dependency_test.optional_dependency: class: Drupal\new_dependency_test\ServiceWithDependency arguments: ['@?new_dependency_test.dependent'] - new_dependency_test.alias: - alias: new_dependency_test.dependent new_dependency_test.another_service: class: Drupal\new_dependency_test\Service new_dependency_test.another_service.decorated: diff --git a/core/modules/system/tests/src/Functional/Update/UpdatePathNewDependencyTest.php b/core/modules/system/tests/src/Functional/Update/UpdatePathNewDependencyTest.php index bc52535dbd..6e1c947c2a 100644 --- a/core/modules/system/tests/src/Functional/Update/UpdatePathNewDependencyTest.php +++ b/core/modules/system/tests/src/Functional/Update/UpdatePathNewDependencyTest.php @@ -65,10 +65,23 @@ public function testUpdateNewDependency() { $this->assertTrue(\Drupal::state()->get('new_dependency_test_update_8001.decorated_service_custom_inner'), 'The new_dependency_test.another_service_two service is decorated'); // Tests that services are available as expected. - $this->assertFalse(\Drupal::state()->get('new_dependency_test_update_8001.has_before_install:new_dependency_test.hard_dependency'), 'The new_dependency_test.hard_dependency is not available prior to install'); - $this->assertTrue(\Drupal::state()->get('new_dependency_test_update_8001.has_before_install:new_dependency_test.optional_dependency'), 'The new_dependency_test.optional_dependency is available prior to install'); - $this->assertTrue(\Drupal::state()->get('new_dependency_test_update_8001.has_after_install:new_dependency_test.hard_dependency'), 'The new_dependency_test.hard_dependency is available after install'); - $this->assertTrue(\Drupal::state()->get('new_dependency_test_update_8001.has_after_install:new_dependency_test.optional_dependency'), 'The new_dependency_test.optional_dependency is available after install'); + $before_install = \Drupal::state()->get('new_dependency_test_update_8001.has_before_install', []); + $this->assertSame([ + 'new_dependency_test.hard_dependency' => FALSE, + 'new_dependency_test.optional_dependency' => TRUE, + 'new_dependency_test.recursion' => FALSE, + 'new_dependency_test.alias' => FALSE, + 'new_dependency_test.alias_dependency' => FALSE, + ], $before_install); + + $after_install = \Drupal::state()->get('new_dependency_test_update_8001.has_after_install', []); + $this->assertSame([ + 'new_dependency_test.hard_dependency' => TRUE, + 'new_dependency_test.optional_dependency' => TRUE, + 'new_dependency_test.recursion' => TRUE, + 'new_dependency_test.alias' => TRUE, + 'new_dependency_test.alias_dependency' => TRUE, + ], $after_install); } }