diff --git a/core/lib/Drupal/Component/DependencyInjection/Container.php b/core/lib/Drupal/Component/DependencyInjection/Container.php index e291a8ff05575c97e181c10edcb171bb072c5c68..26b848896430eb95f6b1b830474d8d319e80ca67 100644 --- a/core/lib/Drupal/Component/DependencyInjection/Container.php +++ b/core/lib/Drupal/Component/DependencyInjection/Container.php @@ -120,9 +120,6 @@ public function __construct(array $container_definition = []) { $this->parameters = $container_definition['parameters'] ?? []; $this->serviceDefinitions = $container_definition['services'] ?? []; $this->frozen = $container_definition['frozen'] ?? FALSE; - - // Register the service_container with itself. - $this->services['service_container'] = $this; } /** @@ -143,6 +140,10 @@ public function get($id, $invalid_behavior = ContainerInterface::EXCEPTION_ON_IN return $this->services[$id]; } + if ($id === 'service_container') { + return $this; + } + if (isset($this->loading[$id])) { throw new ServiceCircularReferenceException($id, array_keys($this->loading)); } @@ -313,7 +314,7 @@ public function set($id, $service): void { * {@inheritdoc} */ public function has($id): bool { - return isset($this->aliases[$id]) || isset($this->services[$id]) || isset($this->serviceDefinitions[$id]); + return isset($this->aliases[$id]) || isset($this->services[$id]) || isset($this->serviceDefinitions[$id]) || $id === 'service_container'; } /** @@ -543,7 +544,7 @@ protected function getParameterAlternatives($name) { * {@inheritdoc} */ public function getServiceIds() { - return array_keys($this->serviceDefinitions + $this->services); + return array_merge(['service_container'], array_keys($this->serviceDefinitions + $this->services)); } /** diff --git a/core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php b/core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php index 2b72dfb5d4845cc4fc94aaecd2636b5c3954f10e..fdb74e33c3515e00998998b42b9caeeac13a119e 100644 --- a/core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php +++ b/core/lib/Drupal/Component/DependencyInjection/PhpArrayContainer.php @@ -38,9 +38,6 @@ public function __construct(array $container_definition = []) { $this->parameters = $container_definition['parameters'] ?? []; $this->serviceDefinitions = $container_definition['services'] ?? []; $this->frozen = $container_definition['frozen'] ?? FALSE; - - // Register the service_container with itself. - $this->services['service_container'] = $this; } /** diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index f8bbaf8699b907a0a93dde8513662386c735a469..6a05941bde80f906b324e5051b1d294de15e709f 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -1185,6 +1185,90 @@ public function rebuildContainer() { return $this->initializeContainer(); } + /** + * {@inheritdoc} + */ + public function resetContainer(): ContainerInterface { + $session_started = FALSE; + $subrequest = FALSE; + $reload_module_handler = FALSE; + + // Save the id of the currently logged in user. + if ($this->container->initialized('current_user')) { + $current_user_id = $this->container->get('current_user')->id(); + } + + if ($this->container->initialized('module_handler') && $this->container->get('module_handler')->isLoaded()) { + $reload_module_handler = TRUE; + } + + // After rebuilding the container some objects will have stale services. + // Record a map of objects to service IDs prior to rebuilding the + // container in order to ensure + // \Drupal\Core\DependencyInjection\DependencySerializationTrait works as + // expected. + $this->container->get(ReverseContainer::class)->recordContainer(); + + // If there is a session, close and save it. + if ($this->container->initialized('session')) { + $session = $this->container->get('session'); + if ($session->isStarted()) { + $session_started = TRUE; + $session->save(); + } + unset($session); + } + + $all_messages = $this->container->get('messenger')->all(); + + $persist = $this->getServicesToPersist($this->container); + $this->container->reset(); + $this->persistServices($this->container, $persist); + + $this->container->set('kernel', $this); + + // Set the class loader which was registered as a synthetic service. + $this->container->set('class_loader', $this->classLoader); + + if ($reload_module_handler) { + $this->container->get('module_handler')->reload(); + } + + if ($session_started) { + $this->container->get('session')->start(); + } + + // The request stack is preserved across container rebuilds. Re-inject the + // new session into the main request if one was present before. + if (($request_stack = $this->container->get('request_stack', ContainerInterface::NULL_ON_INVALID_REFERENCE))) { + if ($request = $request_stack->getMainRequest()) { + $subrequest = TRUE; + if ($request->hasSession()) { + $request->setSession($this->container->get('session')); + } + } + } + + if (!empty($current_user_id)) { + $this->container->get('current_user')->setInitialAccountId($current_user_id); + } + + // Re-add messages. + foreach ($all_messages as $type => $messages) { + foreach ($messages as $message) { + $this->container->get('messenger')->addMessage($message, $type); + } + } + + // Allow other parts of the codebase to react on container reset in + // subrequest. + if (!empty($subrequest)) { + $this->container->get('event_dispatcher')->dispatch(new Event(), self::CONTAINER_INITIALIZE_SUBREQUEST_FINISHED); + } + + return $this->container; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/DrupalKernelInterface.php b/core/lib/Drupal/Core/DrupalKernelInterface.php index dd7864cd7b45213757cefeec9066c9b8f3d6a4f0..3e52f763843e7db61e38247c235408ff99258e1f 100644 --- a/core/lib/Drupal/Core/DrupalKernelInterface.php +++ b/core/lib/Drupal/Core/DrupalKernelInterface.php @@ -2,6 +2,7 @@ namespace Drupal\Core; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request; @@ -120,6 +121,13 @@ public function updateModules(array $module_list, array $module_filenames = []); */ public function rebuildContainer(); + /** + * Force a container reset. + * + * @return \Symfony\Component\DependencyInjection\ContainerInterface + */ + public function resetContainer(): ContainerInterface; + /** * Invalidate the service container for the next request. */ diff --git a/core/modules/system/tests/modules/container_rebuild_test/container_rebuild_test.routing.yml b/core/modules/system/tests/modules/container_rebuild_test/container_rebuild_test.routing.yml index 0382c21dd25c3d0e398272cdab77c019332d8331..e395b8386021a3edbca0edac73de7932ae14f50a 100644 --- a/core/modules/system/tests/modules/container_rebuild_test/container_rebuild_test.routing.yml +++ b/core/modules/system/tests/modules/container_rebuild_test/container_rebuild_test.routing.yml @@ -4,3 +4,11 @@ container_rebuild_test.module_path: _controller: '\Drupal\container_rebuild_test\TestController::showModuleInfo' requirements: _access: 'TRUE' + + +container_rebuild_test.container_reset: + path: '/container_rebuild_test/container_reset' + defaults: + _controller: '\Drupal\container_rebuild_test\TestController::containerReset' + requirements: + _user_is_logged_in: 'TRUE' diff --git a/core/modules/system/tests/modules/container_rebuild_test/src/TestController.php b/core/modules/system/tests/modules/container_rebuild_test/src/TestController.php index 7fc722faccc7eb78a38d6961f9752d86a014bb56..92a8bb5a8b686fc9974884bcb40082aa68e9074a 100644 --- a/core/modules/system/tests/modules/container_rebuild_test/src/TestController.php +++ b/core/modules/system/tests/modules/container_rebuild_test/src/TestController.php @@ -3,9 +3,19 @@ namespace Drupal\container_rebuild_test; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\DrupalKernelInterface; class TestController extends ControllerBase { + /** + * Constructs a TestController. + * + * @param \Drupal\Core\DrupalKernelInterface $kernel + * The Drupal kernel. + */ + public function __construct(protected DrupalKernelInterface $kernel) { + } + /** * Displays the path to a module. * @@ -34,4 +44,19 @@ public function showModuleInfo(string $module, string $function) { ]; } + /** + * Resets the container. + * + * @return array + * A render array. + */ + public function containerReset() { + $this->messenger()->addMessage(t('Before the container was reset.')); + $this->kernel->resetContainer(); + // The container has been reset, therefore we need to get the new service. + $this->messenger = NULL; + $this->messenger()->addMessage(t('After the container was reset.')); + return []; + } + } diff --git a/core/modules/system/tests/src/Functional/DrupalKernel/ContainerResetWebTest.php b/core/modules/system/tests/src/Functional/DrupalKernel/ContainerResetWebTest.php new file mode 100644 index 0000000000000000000000000000000000000000..575e5edc74a65be5ef08b479493ec32387d8f5c8 --- /dev/null +++ b/core/modules/system/tests/src/Functional/DrupalKernel/ContainerResetWebTest.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\system\Functional\DrupalKernel; + +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\Tests\BrowserTestBase; + +// cspell:ignore contenedor fuera reiniciado Después + +/** + * Ensures that the container rebuild works as expected. + * + * @group DrupalKernel + */ +class ContainerResetWebTest extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['container_rebuild_test', 'locale']; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + protected function setUp(): void { + parent::setUp(); + ConfigurableLanguage::createFromLangcode('es')->save(); + // Create translations for testing. + $locale_storage = $this->container->get('locale.storage'); + $langcode = 'es'; + $source = $locale_storage->createString(['source' => 'Before the container was reset.'])->save(); + $locale_storage->createTranslation([ + 'lid' => $source->lid, + 'language' => $langcode, + 'translation' => 'Antes de que el contenedor fuera reiniciado.', + ])->save(); + $source = $locale_storage->createString(['source' => 'After the container was reset.'])->save(); + $locale_storage->createTranslation([ + 'lid' => $source->lid, + 'language' => $langcode, + 'translation' => 'Después de que el contenedor fue reiniciado.', + ])->save(); + } + + /** + * Sets a different deployment identifier. + */ + public function testContainerRebuild() { + $this->drupalLogin($this->drupalCreateUser()); + + $this->drupalGet('container_rebuild_test/container_reset'); + $this->assertSession()->pageTextContains('Before the container was reset'); + $this->assertSession()->pageTextContains('After the container was reset'); + $this->drupalGet('es/container_rebuild_test/container_reset'); + $this->assertSession()->pageTextContains('Antes de que el contenedor fuera reiniciado.'); + $this->assertSession()->pageTextContains('Después de que el contenedor fue reiniciado.'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php b/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php index ff1d2d5d55165034ab0f798e3746fd7dce2880e0..b6cc246569f2b5a44182025e79654af2a9ed66a3 100644 --- a/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php +++ b/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php @@ -245,4 +245,52 @@ public function testClassLoaderAutoDetect($value) { $kernel->boot(); } + /** + * @covers ::resetContainer + */ + public function testResetContainer() { + $modules_enabled = [ + 'system' => 'system', + 'user' => 'user', + ]; + + $request = Request::createFromGlobals(); + $kernel = $this->getTestKernel($request, $modules_enabled); + $container = $kernel->getContainer(); + + // Ensure services are reset when ::resetContainer is called. + $this->assertFalse($container->initialized('renderer')); + $renderer = $container->get('renderer'); + $this->assertTrue($container->initialized('renderer')); + + // Ensure the current user is maintained through a container reset. + $this->assertSame(0, $container->get('current_user')->id()); + $container->get('current_user')->setInitialAccountId(2); + + // Ensure messages are maintained through a container reset. + $this->assertEmpty($container->get('messenger')->messagesByType('Container reset')); + $container->get('messenger')->addMessage('Test reset', 'Container reset'); + $this->assertSame(['Test reset'], $container->get('messenger')->messagesByType('Container reset')); + + // Ensure persisted services are persisted. + $request_stack = $container->get('request_stack'); + + $kernel->resetContainer(); + + // Ensure services are reset when ::resetContainer is called. + $this->assertFalse($container->initialized('renderer')); + $this->assertNotSame($renderer, $container->get('renderer')); + $this->assertTrue($container->initialized('renderer')); + $this->assertSame($kernel, $container->get('kernel')); + + // Ensure the current user is maintained through a container reset. + $this->assertSame(2, $container->get('current_user')->id()); + + // Ensure messages are maintained through a container reset. + $this->assertSame(['Test reset'], $container->get('messenger')->messagesByType('Container reset')); + + // Ensure persisted services are persisted. + $this->assertSame($request_stack, $container->get('request_stack')); + } + } diff --git a/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php index 5dfc90fea553d82e3f732f2bd7204ae5a38326b0..53dae9704e004b393982dfe3d6f1c8b60a17e04f 100644 --- a/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php +++ b/core/tests/Drupal/Tests/Component/DependencyInjection/ContainerTest.php @@ -675,7 +675,7 @@ public function testInitializedForAliases() { * @covers ::getServiceIds */ public function testGetServiceIds() { - $service_definition_keys = array_keys($this->containerDefinition['services']); + $service_definition_keys = array_merge(['service_container'], array_keys($this->containerDefinition['services'])); $this->assertEquals($service_definition_keys, $this->container->getServiceIds(), 'Retrieved service IDs match definition.'); $mock_service = new MockService(); @@ -715,6 +715,25 @@ public function testGetServiceIdMappings() { ], $this->container->getServiceIdMappings()); } + /** + * Tests Container::reset(). + * + * @covers ::reset + */ + public function testReset() { + $this->assertFalse($this->container->initialized('late.service'), 'Late service is not initialized.'); + $this->container->get('late.service'); + $this->assertTrue($this->container->initialized('late.service'), 'Late service is initialized after it was retrieved once.'); + + // Reset the container. All initialized services will be reset. + $this->container->reset(); + + $this->assertFalse($this->container->initialized('late.service'), 'Late service is not initialized.'); + $this->container->get('late.service'); + $this->assertTrue($this->container->initialized('late.service'), 'Late service is initialized after it was retrieved once.'); + $this->assertSame($this->container, $this->container->get('service_container')); + } + /** * Gets a mock container definition. * @@ -733,9 +752,6 @@ protected function getMockContainerDefinition() { $parameters['service_from_parameter'] = $this->getServiceCall('service.provider_alias'); $services = []; - $services['service_container'] = [ - 'class' => '\Drupal\service_container\DependencyInjection\Container', - ]; $services['other.service'] = [ 'class' => get_class($fake_service), ];