From 8f430db9e6ddf33db14940e68e97cc64df0e50ac Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Fri, 28 Jun 2019 13:18:53 +0100 Subject: [PATCH] Issue #2863986 by bircher, pfrenssen, alexpott, claudiu.cristea, Adita, dawehner, gambry, chr.fritsch: Allow updating modules with new service dependencies --- .../Core/Update/UpdateServiceProvider.php | 28 ++++++++ .../new_dependency_test.info.yml | 8 +++ .../new_dependency_test.install | 15 +++++ .../new_dependency_test.services.yml | 12 ++++ .../src/DecoratedDependentService.php | 42 ++++++++++++ .../src/DependentService.php | 39 +++++++++++ .../new_dependency_test_with_service.info.yml | 6 ++ ..._dependency_test_with_service.services.yml | 3 + .../src/NewService.php | 20 ++++++ .../Update/UpdatePathNewDependencyTest.php | 65 +++++++++++++++++++ 10 files changed, 238 insertions(+) create mode 100644 core/modules/system/tests/modules/new_dependency_test/new_dependency_test.info.yml create mode 100644 core/modules/system/tests/modules/new_dependency_test/new_dependency_test.install create mode 100644 core/modules/system/tests/modules/new_dependency_test/new_dependency_test.services.yml create mode 100644 core/modules/system/tests/modules/new_dependency_test/src/DecoratedDependentService.php create mode 100644 core/modules/system/tests/modules/new_dependency_test/src/DependentService.php create mode 100644 core/modules/system/tests/modules/new_dependency_test_with_service/new_dependency_test_with_service.info.yml create mode 100644 core/modules/system/tests/modules/new_dependency_test_with_service/new_dependency_test_with_service.services.yml create mode 100644 core/modules/system/tests/modules/new_dependency_test_with_service/src/NewService.php create mode 100644 core/modules/system/tests/src/Functional/Update/UpdatePathNewDependencyTest.php diff --git a/core/lib/Drupal/Core/Update/UpdateServiceProvider.php b/core/lib/Drupal/Core/Update/UpdateServiceProvider.php index 1971ef260ab2..14457ceedabc 100644 --- a/core/lib/Drupal/Core/Update/UpdateServiceProvider.php +++ b/core/lib/Drupal/Core/Update/UpdateServiceProvider.php @@ -5,6 +5,7 @@ use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\DependencyInjection\ServiceModifierInterface; use Drupal\Core\DependencyInjection\ServiceProviderInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; @@ -52,6 +53,33 @@ public function alter(ContainerBuilder $container) { ->clearTag('path_processor_inbound') ->clearTag('path_processor_outbound'); } + + // 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 { + $definitions = $container->getDefinitions(); + foreach ($definitions as $key => $definition) { + foreach ($definition->getArguments() as $argument) { + if ($argument instanceof Reference) { + if (!$container->has((string) $argument) && $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); + } + } + } + } + // Remove any aliases which point to undefined services. + $aliases = $container->getAliases(); + foreach ($aliases as $key => $alias) { + if (!$container->has((string) $alias)) { + $container->removeAlias($key); + } + } + // Repeat if services or aliases have been removed. + } while (count($definitions) > count($container->getDefinitions()) || count($aliases) > count($container->getAliases())); } } diff --git a/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.info.yml b/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.info.yml new file mode 100644 index 000000000000..eab08261b53c --- /dev/null +++ b/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.info.yml @@ -0,0 +1,8 @@ +name: 'New Dependency test' +type: module +description: 'Support module for update testing.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - new_dependency_test_with_service 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 new file mode 100644 index 000000000000..e500d72b2268 --- /dev/null +++ b/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.install @@ -0,0 +1,15 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the new_dependency_test module. + */ + +/** + * Enable the new_dependency_test_with_service module. + */ +function new_dependency_test_update_8001() { + // During the update hooks the container is cleaned up to contain only + // services that have their dependencies met. Core services are available. + \Drupal::getContainer()->get('module_installer')->install(['new_dependency_test_with_service']); +} 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 new file mode 100644 index 000000000000..29a6de5b5f8a --- /dev/null +++ b/core/modules/system/tests/modules/new_dependency_test/new_dependency_test.services.yml @@ -0,0 +1,12 @@ +services: + new_dependency_test.dependent: + class: Drupal\new_dependency_test\DependentService + arguments: ['@new_dependency_test_with_service.service'] + new_dependency_test.decorated: + class: Drupal\new_dependency_test\DecoratedDependentService + arguments: ['@new_dependency_test.dependent'] + new_dependency_test.decorated_optional: + class: Drupal\new_dependency_test\DecoratedDependentService + arguments: ['@?new_dependency_test.dependent'] + new_dependency_test.alias: + alias: new_dependency_test.dependent diff --git a/core/modules/system/tests/modules/new_dependency_test/src/DecoratedDependentService.php b/core/modules/system/tests/modules/new_dependency_test/src/DecoratedDependentService.php new file mode 100644 index 000000000000..6c37f6435e61 --- /dev/null +++ b/core/modules/system/tests/modules/new_dependency_test/src/DecoratedDependentService.php @@ -0,0 +1,42 @@ +<?php + +namespace Drupal\new_dependency_test; + +/** + * Service that gets the other service of the same module injected. + * + * This service indirectly depends on a not-yet-defined service. + */ +class DecoratedDependentService { + + /** + * The injected service. + * + * @var \Drupal\new_dependency_test\DependentService + */ + protected $service; + + /** + * DecoratedDependentService constructor. + * + * @param \Drupal\new_dependency_test\DependentService|null $service + * The service of the same module which has the new dependency. + */ + public function __construct(DependentService $service = NULL) { + $this->service = $service; + } + + /** + * Get the simple greeting from the service and decorate it. + * + * @return string + * The enhanced greeting. + */ + public function greet() { + if (isset($this->service)) { + return $this->service->greet() . ' World'; + } + return 'Sorry, no service.'; + } + +} diff --git a/core/modules/system/tests/modules/new_dependency_test/src/DependentService.php b/core/modules/system/tests/modules/new_dependency_test/src/DependentService.php new file mode 100644 index 000000000000..22e1631a0958 --- /dev/null +++ b/core/modules/system/tests/modules/new_dependency_test/src/DependentService.php @@ -0,0 +1,39 @@ +<?php + +namespace Drupal\new_dependency_test; + +use Drupal\new_dependency_test_with_service\NewService; + +/** + * Generic service with a dependency on a service defined in a new module. + */ +class DependentService { + + /** + * The injected service. + * + * @var \Drupal\new_dependency_test_with_service\NewService + */ + protected $service; + + /** + * DependentService constructor. + * + * @param \Drupal\new_dependency_test_with_service\NewService $service + * The service of the new module. + */ + public function __construct(NewService $service) { + $this->service = $service; + } + + /** + * Get the simple greeting from the service. + * + * @return string + * The greeting. + */ + public function greet() { + return $this->service->greet(); + } + +} diff --git a/core/modules/system/tests/modules/new_dependency_test_with_service/new_dependency_test_with_service.info.yml b/core/modules/system/tests/modules/new_dependency_test_with_service/new_dependency_test_with_service.info.yml new file mode 100644 index 000000000000..5091a7f79d6a --- /dev/null +++ b/core/modules/system/tests/modules/new_dependency_test_with_service/new_dependency_test_with_service.info.yml @@ -0,0 +1,6 @@ +name: 'New Dependency test with service' +type: module +description: 'Support module for update testing.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/system/tests/modules/new_dependency_test_with_service/new_dependency_test_with_service.services.yml b/core/modules/system/tests/modules/new_dependency_test_with_service/new_dependency_test_with_service.services.yml new file mode 100644 index 000000000000..26ba284e29be --- /dev/null +++ b/core/modules/system/tests/modules/new_dependency_test_with_service/new_dependency_test_with_service.services.yml @@ -0,0 +1,3 @@ +services: + new_dependency_test_with_service.service: + class: Drupal\new_dependency_test_with_service\NewService diff --git a/core/modules/system/tests/modules/new_dependency_test_with_service/src/NewService.php b/core/modules/system/tests/modules/new_dependency_test_with_service/src/NewService.php new file mode 100644 index 000000000000..6137fc2ecf78 --- /dev/null +++ b/core/modules/system/tests/modules/new_dependency_test_with_service/src/NewService.php @@ -0,0 +1,20 @@ +<?php + +namespace Drupal\new_dependency_test_with_service; + +/** + * Generic service returning a greeting. + */ +class NewService { + + /** + * Get a simple greeting. + * + * @return string + * The greeting provided by the new service. + */ + public function greet() { + return 'Hello'; + } + +} diff --git a/core/modules/system/tests/src/Functional/Update/UpdatePathNewDependencyTest.php b/core/modules/system/tests/src/Functional/Update/UpdatePathNewDependencyTest.php new file mode 100644 index 000000000000..e67f54145d43 --- /dev/null +++ b/core/modules/system/tests/src/Functional/Update/UpdatePathNewDependencyTest.php @@ -0,0 +1,65 @@ +<?php + +namespace Drupal\Tests\system\Functional\Update; + +use Drupal\FunctionalTests\Update\UpdatePathTestBase; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; + +/** + * Modules can introduce new dependencies and enable them in update hooks. + * + * @group system + * @group legacy + */ +class UpdatePathNewDependencyTest extends UpdatePathTestBase { + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles() { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../tests/fixtures/update/drupal-8.6.0.bare.testing.php.gz', + ]; + } + + /** + * Test that a module can add services that depend on new modules. + */ + public function testUpdateNewDependency() { + // The new_dependency_test before the update is just an empty info.yml file. + // The code of the new_dependency_test module is after the update and + // contains the dependency on the new_dependency_test_with_service module. + $extension_config = $this->container->get('config.factory')->getEditable('core.extension'); + $extension_config + ->set('module.new_dependency_test', 0) + ->set('module', module_config_sort($extension_config->get('module'))) + ->save(TRUE); + drupal_set_installed_schema_version('new_dependency_test', \Drupal::CORE_MINIMUM_SCHEMA_VERSION); + + // Rebuild the container and test that the service with the optional unmet + // dependency is still available while the ones that fail are not. + + try { + $this->rebuildContainer(); + $this->fail('The container has services with unmet dependencies and should have failed to rebuild.'); + } + catch (ServiceNotFoundException $exception) { + $this->assertEquals('The service "new_dependency_test.dependent" has a dependency on a non-existent service "new_dependency_test_with_service.service".', $exception->getMessage()); + } + + // Running the updates enables the dependency. + $this->runUpdates(); + + $this->assertTrue(array_key_exists('new_dependency_test', $this->container->get('config.factory')->get('core.extension')->get('module'))); + $this->assertTrue(array_key_exists('new_dependency_test_with_service', $this->container->get('config.factory')->get('core.extension')->get('module'))); + + // Tests that the new services are available and working as expected. + $this->assertEquals('Hello', $this->container->get('new_dependency_test_with_service.service')->greet()); + $this->assertEquals('Hello', $this->container->get('new_dependency_test.dependent')->greet()); + $this->assertEquals('Hello', $this->container->get('new_dependency_test.alias')->greet()); + $this->assertEquals('Hello World', $this->container->get('new_dependency_test.decorated')->greet()); + $this->assertEquals('Hello World', $this->container->get('new_dependency_test.decorated_optional')->greet()); + + } + +} -- GitLab