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