Issue #2863986 by bircher, pfrenssen, alexpott, claudiu.cristea, Adita, dawehner, gambry, chr.fritsch: Allow updating modules with new service dependencies
parent
8f6cc82a4a
commit
8f430db9e6
|
@ -5,6 +5,7 @@ namespace Drupal\Core\Update;
|
||||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||||
use Drupal\Core\DependencyInjection\ServiceModifierInterface;
|
use Drupal\Core\DependencyInjection\ServiceModifierInterface;
|
||||||
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
|
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
use Symfony\Component\DependencyInjection\Definition;
|
use Symfony\Component\DependencyInjection\Definition;
|
||||||
use Symfony\Component\DependencyInjection\Reference;
|
use Symfony\Component\DependencyInjection\Reference;
|
||||||
|
|
||||||
|
@ -52,6 +53,33 @@ class UpdateServiceProvider implements ServiceProviderInterface, ServiceModifier
|
||||||
->clearTag('path_processor_inbound')
|
->clearTag('path_processor_inbound')
|
||||||
->clearTag('path_processor_outbound');
|
->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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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']);
|
||||||
|
}
|
|
@ -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
|
|
@ -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.';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
||||||
|
services:
|
||||||
|
new_dependency_test_with_service.service:
|
||||||
|
class: Drupal\new_dependency_test_with_service\NewService
|
|
@ -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';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue