diff --git a/core/core.services.yml b/core/core.services.yml index 6a31a44f6177ac0ecd0f869225cf4a578803fbb9..d9b1839cdbc411402c3c1d10f7e5f35f4600c6da 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -473,7 +473,7 @@ services: class: Drupal\Core\Extension\ModuleInstaller tags: - { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator } - arguments: ['@app.root', '@module_handler', '@kernel'] + arguments: ['@app.root', '@module_handler', '@kernel', '@router.builder'] lazy: true content_uninstall_validator: class: Drupal\Core\Entity\ContentUninstallValidator @@ -763,6 +763,9 @@ services: tags: - { name: event_subscriber } - { name: backend_overridable } + router.route_provider.lazy_builder: + class: Drupal\Core\Routing\RouteProviderLazyBuilder + arguments: ['@router.route_provider', '@router.builder'] router.route_preloader: class: Drupal\Core\Routing\RoutePreloader arguments: ['@router.route_provider', '@state', '@cache.bootstrap'] diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php index b2223b735942665d18d7b65e1557ee8bfbce620c..a903b8af8309301af00517e23a4e93df4811b7b8 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -279,6 +279,10 @@ public function install(array $module_list, $enable_dependencies = TRUE) { // @see https://www.drupal.org/node/2208429 \Drupal::service('theme_handler')->refreshInfo(); + // In order to make uninstalling transactional if anything uses routes. + \Drupal::getContainer()->set('router.route_provider.old', \Drupal::service('router.route_provider')); + \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.lazy_builder')); + // Allow the module to perform install tasks. $this->moduleHandler->invoke($module, 'install'); @@ -289,7 +293,15 @@ public function install(array $module_list, $enable_dependencies = TRUE) { // If any modules were newly installed, invoke hook_modules_installed(). if (!empty($modules_installed)) { - \Drupal::service('router.builder')->setRebuildNeeded(); + \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.old')); + if (!\Drupal::service('router.route_provider.lazy_builder')->hasRebuilt()) { + // Rebuild routes after installing module. This is done here on top of + // \Drupal\Core\Routing\RouteBuilder::destruct to not run into errors on + // fastCGI which executes ::destruct() after the module installation + // page was sent already. + \Drupal::service('router.builder')->rebuild(); + } + $this->moduleHandler->invokeAll('modules_installed', array($modules_installed)); } @@ -378,6 +390,10 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { // Remove all configuration belonging to the module. \Drupal::service('config.manager')->uninstall('module', $module); + // In order to make uninstalling transactional if anything uses routes. + \Drupal::getContainer()->set('router.route_provider.old', \Drupal::service('router.route_provider')); + \Drupal::getContainer()->set('router.route_provider', \Drupal::service('router.route_provider.lazy_builder')); + // Notify interested components that this module's entity types are being // deleted. For example, a SQL-based storage handler can use this as an // opportunity to drop the corresponding database tables. @@ -450,7 +466,11 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { $post_update_registry = \Drupal::service('update.post_update_registry'); $post_update_registry->filterOutInvokedUpdatesByModule($module); } - \Drupal::service('router.builder')->setRebuildNeeded(); + // Rebuild routes after installing module. This is done here on top of + // \Drupal\Core\Routing\RouteBuilder::destruct to not run into errors on + // fastCGI which executes ::destruct() after the Module uninstallation page + // was sent already. + \Drupal::service('router.builder')->rebuild(); drupal_get_installed_schema_version(NULL, TRUE); // Let other modules react. diff --git a/core/lib/Drupal/Core/Extension/module.api.php b/core/lib/Drupal/Core/Extension/module.api.php index 6dda3cc081dfde59e132364a4e6461c86d6fdf98..9abea30b9fc8a5bbadfed0fd7db0b1d568c1c490 100644 --- a/core/lib/Drupal/Core/Extension/module.api.php +++ b/core/lib/Drupal/Core/Extension/module.api.php @@ -198,6 +198,12 @@ function hook_modules_installed($modules) { * If the module implements hook_schema(), the database tables will * be created before this hook is fired. * + * If the module provides a MODULE.routing.yml or alters routing information + * these changes will not be available when this hook is fired. If up-to-date + * router information is required, for example to use \Drupal\Core\Url, then + * (preferably) use hook_modules_installed() or rebuild the router in the + * hook_install() implementation. + * * Implementations of this hook are by convention declared in the module's * .install file. The implementation can rely on the .module file being loaded. * The hook will only be called when a module is installed. The module's schema diff --git a/core/lib/Drupal/Core/Routing/RouteProviderLazyBuilder.php b/core/lib/Drupal/Core/Routing/RouteProviderLazyBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..8c59900c82ff38b5f9deed5f97b50ffafd63e922 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/RouteProviderLazyBuilder.php @@ -0,0 +1,135 @@ +<?php + +namespace Drupal\Core\Routing; + +use Symfony\Cmf\Component\Routing\PagedRouteProviderInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * A Route Provider front-end for all Drupal-stored routes. + */ +class RouteProviderLazyBuilder implements PreloadableRouteProviderInterface, PagedRouteProviderInterface { + + /** + * The route provider service. + * + * @var \Drupal\Core\Routing\RouteProviderInterface + */ + protected $routeProvider; + + /** + * The route building service. + * + * @var \Drupal\Core\Routing\RouteBuilderInterface + */ + protected $routeBuilder; + + /** + * Flag to determine if the router has been rebuilt. + * + * @var bool + */ + protected $rebuilt = FALSE; + + /** + * RouteProviderLazyBuilder constructor. + * + * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider + * The route provider service. + * @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder + * The route building service. + */ + public function __construct(RouteProviderInterface $route_provider, RouteBuilderInterface $route_builder) { + $this->routeProvider = $route_provider; + $this->routeBuilder = $route_builder; + } + + /** + * Gets the real route provider service and rebuilds the router id necessary. + * + * @return \Drupal\Core\Routing\RouteProviderInterface + * The route provider service. + */ + protected function getRouteProvider() { + if (!$this->rebuilt) { + $this->routeBuilder->rebuild(); + $this->rebuilt = TRUE; + } + return $this->routeProvider; + } + + /** + * {@inheritdoc} + */ + public function getRouteCollectionForRequest(Request $request) { + return $this->getRouteProvider()->getRouteCollectionForRequest($request); + } + + /** + * {@inheritdoc} + */ + public function getRouteByName($name) { + return $this->getRouteProvider()->getRouteByName($name); + } + + /** + * {@inheritdoc} + */ + public function preLoadRoutes($names) { + return $this->getRouteProvider()->preLoadRoutes($names); + } + + /** + * {@inheritdoc} + */ + public function getRoutesByNames($names) { + return $this->getRouteProvider()->getRoutesByNames($names); + } + + /** + * {@inheritdoc} + */ + public function getRoutesByPattern($pattern) { + return $this->getRouteProvider()->getRoutesByPattern($pattern); + } + + /** + * {@inheritdoc} + */ + public function getAllRoutes() { + return $this->getRouteProvider()->getAllRoutes(); + } + + /** + * {@inheritdoc} + */ + public function reset() { + // Don't call getRouteProvider as this is results in recursive rebuilds. + return $this->routeProvider->reset(); + } + + /** + * {@inheritdoc} + */ + public function getRoutesPaged($offset, $length = NULL) { + return $this->getRouteProvider()->getRoutesPaged($offset, $length); + } + + /** + * {@inheritdoc} + */ + public function getRoutesCount() { + return $this->getRouteProvider()->getRoutesCount(); + } + + /** + * Determines if the router has been rebuilt. + * + * @return bool + * TRUE is the router has been rebuilt, FALSE if not. + */ + public function hasRebuilt() { + return $this->rebuilt; + } + +} diff --git a/core/modules/content_translation/content_translation.install b/core/modules/content_translation/content_translation.install index 690ad2ba29609a5ec649b340339b16d9b966f8d7..0f8a184651a9c8cfcbc678c2d7e96db14965e216 100644 --- a/core/modules/content_translation/content_translation.install +++ b/core/modules/content_translation/content_translation.install @@ -14,15 +14,19 @@ function content_translation_install() { // Assign a fairly low weight to ensure our implementation of // hook_module_implements_alter() is run among the last ones. module_set_weight('content_translation', 10); + // Translation works when at least two languages are added. if (count(\Drupal::languageManager()->getLanguages()) < 2) { - // @todo: Switch to Url::fromRoute() once https://www.drupal.org/node/2589967 is resolved. - $t_args = [':language_url' => Url::fromUri('internal:/admin/config/regional/language')->toString()]; + $t_args = [ + ':language_url' => Url::fromRoute('entity.configurable_language.collection')->toString() + ]; $message = t('This site has only a single language enabled. <a href=":language_url">Add at least one more language</a> in order to translate content.', $t_args); drupal_set_message($message, 'warning'); } // Point the user to the content translation settings. - $t_args = [':settings_url' => Url::fromUri('internal:/admin/config/regional/content-language')->toString()]; + $t_args = [ + ':settings_url' => Url::fromRoute('language.content_settings_page')->toString() + ]; $message = t('<a href=":settings_url">Enable translation</a> for <em>content types</em>, <em>taxonomy vocabularies</em>, <em>accounts</em>, or any other element you wish to translate.', $t_args); drupal_set_message($message, 'warning'); } diff --git a/core/tests/Drupal/KernelTests/Config/DefaultConfigTest.php b/core/tests/Drupal/KernelTests/Config/DefaultConfigTest.php index aed57e595016edac87f6617f2c6510946982e45b..3d6c0b9ca23d6209e564c24cf0b0ae1d167b2dbb 100644 --- a/core/tests/Drupal/KernelTests/Config/DefaultConfigTest.php +++ b/core/tests/Drupal/KernelTests/Config/DefaultConfigTest.php @@ -56,6 +56,11 @@ public function testModuleConfig($module) { /** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */ $config_manager = $this->container->get('config.manager'); + // @todo https://www.drupal.org/node/2308745 Rest has an implicit dependency + // on the Node module remove once solved. + if (in_array($module, ['rest', 'hal'])) { + $module_installer->install(['node']); + } $module_installer->install([$module]); // System and user are required in order to be able to install some of the diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c15e7fba7c0b5c961aa2028b33a125ea784f676d --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerTest.php @@ -0,0 +1,47 @@ +<?php + +namespace Drupal\KernelTests\Core\Extension; + +use Drupal\Core\Database\Database; +use Drupal\KernelTests\KernelTestBase; +use Symfony\Component\Routing\Exception\RouteNotFoundException; + +/** + * Tests the ModuleInstaller class. + * + * @coversDefaultClass \Drupal\Core\Extension\ModuleInstaller + * + * @group Extension + */ +class ModuleInstallerTest extends KernelTestBase { + + /** + * Modules to install. + * + * The System module is required because system_rebuild_module_data() is used. + * + * @var array + */ + public static $modules = ['system']; + + /** + * Tests that routes are rebuilt during install and uninstall of modules. + * + * @covers ::install + * @covers ::uninstall + */ + public function testRouteRebuild() { + // Remove the routing table manually to ensure it can be created lazily + // properly. + Database::getConnection()->schema()->dropTable('router'); + + $this->container->get('module_installer')->install(['router_test']); + $route = $this->container->get('router.route_provider')->getRouteByName('router_test.1'); + $this->assertEquals('/router_test/test1', $route->getPath()); + + $this->container->get('module_installer')->uninstall(['router_test']); + $this->setExpectedException(RouteNotFoundException::class); + $this->container->get('router.route_provider')->getRouteByName('router_test.1'); + } + +}