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\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 @@ class UpdateServiceProvider implements ServiceProviderInterface, ServiceModifier
|
|||
->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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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