diff --git a/core/core.services.yml b/core/core.services.yml index f3c6cd9..8de5f30 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -74,6 +74,13 @@ services: factory_method: get factory_service: cache_factory arguments: [discovery] + cache.url_generator: + class: Drupal\Core\Cache\CacheBackendInterface + tags: + - { name: cache.bin } + factory_method: get + factory_service: cache_factory + arguments: [url_generator] config.cachedstorage.storage: class: Drupal\Core\Config\FileStorage factory_class: Drupal\Core\Config\FileStorageFactory @@ -301,12 +308,15 @@ services: arguments: ['@router.route_provider'] calls: - [setFinalMatcher, ['@router.matcher.final_matcher']] - url_generator: + url_generator.uncached: class: Drupal\Core\Routing\UrlGenerator arguments: ['@router.route_provider', '@path_processor_manager', '@route_processor_manager', '@config.factory', '@settings'] calls: - [setRequest, ['@?request']] - [setContext, ['@?router.request_context']] + url_generator: + class: Drupal\Core\Routing\CachedUrlGenerator + arguments: ['@url_generator.uncached', '@cache.url_generator', '@language_manager'] link_generator: class: Drupal\Core\Utility\LinkGenerator arguments: ['@url_generator', '@module_handler'] diff --git a/core/lib/Drupal/Core/Routing/CachedUrlGenerator.php b/core/lib/Drupal/Core/Routing/CachedUrlGenerator.php new file mode 100644 index 0000000..d73b769 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/CachedUrlGenerator.php @@ -0,0 +1,227 @@ +urlGenerator = $url_generator; + $this->cache = $cache; + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public function clearCache() { + $this->cachedUrls = array(); + $this->cache->delete($this->cacheKey); + } + + /** + * Writes the cache of generated URLs. + */ + protected function writeCache() { + if ($this->cacheNeedsWriting && !empty($this->cachedUrls) && !empty($this->cacheKey)) { + // Set the URL cache to expire in 24 hours. + $expire = REQUEST_TIME + (60 * 60 * 24); + $this->cache->set($this->cacheKey, $this->cachedUrls, $expire); + } + } + + /** + * {@inheritdoc} + */ + public function generate($name, $parameters = array(), $absolute = FALSE) { + $options = array(); + // We essentially inline the implentation from the Drupal UrlGenerator + // and avoid setting $options so that we increase the liklihood of caching. + if ($absolute) { + $options['absolute'] = $absolute; + } + return $this->generateFromRoute($name, $parameters, $options); + } + + /** + * {@inheritdoc} + */ + public function generateFromPath($path = NULL, $options = array()) { + $key = self::PATH_CACHE_PREFIX . hash('sha256', $path . serialize($options)); + if (!isset($this->cachedUrls[$key])) { + $this->cachedUrls[$key] = $this->urlGenerator->generateFromPath($path, $options); + $this->cacheNeedsWriting = TRUE; + } + return $this->cachedUrls[$key]; + } + + /** + * {@inheritdoc} + */ + public function generateFromRoute($name, $parameters = array(), $options = array()) { + // In some cases $name may be a Route object, rather than a string. + $key = self::ROUTE_CACHE_PREFIX . hash('sha256', serialize($name) . serialize($options) . serialize($parameters)); + if (!isset($this->cachedUrls[$key])) { + $this->cachedUrls[$key] = $this->urlGenerator->generateFromRoute($name, $parameters, $options); + $this->cacheNeedsWriting = TRUE; + } + return $this->cachedUrls[$key]; + } + + /** + * {@inheritdoc} + */ + public function setRequest(Request $request) { + $this->cacheKey = $request->attributes->get('_system_path'); + // Only multilingual sites have language dependant URLs. + if ($this->languageManager->isMultilingual()) { + $this->cacheKey .= '::' . $this->languageManager->getLanguage(Language::TYPE_URL)->getId(); + } + $cached = $this->cache->get($this->cacheKey); + if ($cached) { + $this->cachedUrls = $cached->data; + } + $this->urlGenerator->setRequest($request); + } + + /** + * {@inheritdoc} + */ + public function setBaseUrl($url) { + $this->urlGenerator->setBaseUrl($url); + } + + /** + * {@inheritdoc} + */ + public function setBasePath($path) { + $this->urlGenerator->setBasePath($path); + } + + /** + * {@inheritdoc} + */ + public function setScriptPath($path) { + $this->urlGenerator->setScriptPath($path); + } + + /** + * {@inheritdoc} + */ + public function supports($name) { + return $this->urlGenerator->supports($name); + } + + /** + * {@inheritdoc} + */ + public function getRouteDebugMessage($name, array $parameters = array()) { + return $this->urlGenerator->getRouteDebugMessage($name, $parameters); + } + + /** + * {@inheritdoc} + */ + public function destruct() { + $this->writeCache(); + } + + /** + * {@inheritdoc} + */ + public function setContext(RequestContext $context) { + $this->urlGenerator->setContext($context); + } + + /** + * {@inheritdoc} + */ + public function getContext() { + return $this->urlGenerator->getContext(); + } + + /** + * {@inheritdoc} + */ + public function getPathFromRoute($name, $parameters = array()) { + return $this->urlGenerator->getPathFromRoute($name, $parameters); + } + + +} diff --git a/core/lib/Drupal/Core/Routing/CachedUrlGeneratorInterface.php b/core/lib/Drupal/Core/Routing/CachedUrlGeneratorInterface.php new file mode 100644 index 0000000..d9bbbf3 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/CachedUrlGeneratorInterface.php @@ -0,0 +1,22 @@ + 'fr', ); $this->drupalPostForm(NULL, $edit, t('Save configuration')); + // Clear cached urls in the local process too. + $this->container->get('url_generator')->clearCache(); $this->assertOptionSelected('edit-site-default-language', 'fr', 'Default language updated.'); $this->assertEqual($this->getUrl(), url('fr/admin/config/regional/settings', array('absolute' => TRUE)), 'Correct page redirection.'); diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageListTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageListTest.php index 273850b..77ff83f 100644 --- a/core/modules/language/lib/Drupal/language/Tests/LanguageListTest.php +++ b/core/modules/language/lib/Drupal/language/Tests/LanguageListTest.php @@ -71,6 +71,8 @@ function testLanguageList() { 'site_default_language' => $langcode, ); $this->drupalPostForm(NULL, $edit, t('Save configuration')); + // Clear cached urls in the local process too. + $this->container->get('url_generator')->clearCache(); $this->assertNoOptionSelected('edit-site-default-language', 'en', 'Default language updated.'); $this->assertEqual($this->getUrl(), url($langcode . '/' . $path, array('absolute' => TRUE)), 'Correct page redirection.'); @@ -96,6 +98,8 @@ function testLanguageList() { 'site_default_language' => 'en', ); $this->drupalPostForm($path, $edit, t('Save configuration')); + // Clear cached urls in the local process too. + $this->container->get('url_generator')->clearCache(); // Ensure 'delete' link works. $this->drupalGet('admin/config/regional/language'); $this->clickLink(t('Delete')); @@ -119,6 +123,10 @@ function testLanguageList() { // Make sure the "language_count" state has been updated correctly. $this->container->get('language_manager')->reset(); $languages = $this->container->get('language_manager')->getLanguages(); + $language_count = $this->container->get('state')->get('language_count') ?: 1; + $this->assertEqual($language_count, count($languages), 'Language count is correct.'); + // Clear cached urls in the local process too. + $this->container->get('url_generator')->clearCache(); // Delete French. $this->drupalPostForm('admin/config/regional/language/delete/fr', array(), t('Delete')); // Get the count of languages. @@ -145,6 +153,8 @@ function testLanguageList() { 'direction' => '0', ); $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language')); + // Clear cached urls in the local process too. + $this->container->get('url_generator')->clearCache(); $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.'); $this->assertText($name, 'Name found.'); @@ -157,6 +167,8 @@ function testLanguageList() { 'site_default_language' => $langcode, ); $this->drupalPostForm(NULL, $edit, t('Save configuration')); + // Clear cached urls in the local process too. + $this->container->get('url_generator')->clearCache(); $this->assertNoOptionSelected('edit-site-default-language', 'en', 'Default language updated.'); $this->assertEqual($this->getUrl(), url($langcode . '/' . $path, array('absolute' => TRUE)), 'Correct page redirection.'); diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkFormController.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkFormController.php index 0c4ac19..8d2fbca 100644 --- a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkFormController.php +++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkFormController.php @@ -10,7 +10,7 @@ use Drupal\Core\Entity\EntityFormController; use Drupal\Core\Language\Language; use Drupal\Core\Path\AliasManagerInterface; -use Drupal\Core\Routing\UrlGenerator; +use Drupal\Core\Routing\UrlGeneratorInterface; use Drupal\menu_link\MenuLinkStorageInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -36,7 +36,7 @@ class MenuLinkFormController extends EntityFormController { /** * The URL generator. * - * @var \Drupal\Core\Routing\UrlGenerator + * @var \Drupal\Core\Routing\UrlGeneratorInterface */ protected $urlGenerator; @@ -47,10 +47,10 @@ class MenuLinkFormController extends EntityFormController { * The menu link storage. * @param \Drupal\Core\Path\AliasManagerInterface $path_alias_manager * The path alias manager. - * @param \Drupal\Core\Routing\UrlGenerator $url_generator + * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator * The URL generator. */ - public function __construct(MenuLinkStorageInterface $menu_link_storage, AliasManagerInterface $path_alias_manager, UrlGenerator $url_generator) { + public function __construct(MenuLinkStorageInterface $menu_link_storage, AliasManagerInterface $path_alias_manager, UrlGeneratorInterface $url_generator) { $this->menuLinkStorage = $menu_link_storage; $this->pathAliasManager = $path_alias_manager; $this->urlGenerator = $url_generator; diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index b46e747..49e490a 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -16,6 +16,8 @@ use Drupal\Core\Database\Database; use Drupal\Core\Database\ConnectionNotDefinedException; use Drupal\Core\Language\Language; +use Drupal\Core\Routing\CachedUrlGenerator; +use Drupal\Core\Routing\CachedUrlGeneratorInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AnonymousUserSession; use Drupal\Core\Session\UserSession; @@ -3696,6 +3698,12 @@ protected function prepareRequestForGenerator($clean_urls = TRUE, $override_serv $request = Request::create($request_path, 'GET', array(), array(), array(), $server); $generator->setRequest($request); + + // Ensure any internal caches of the URL generator are cleared. + if ($generator instanceof CachedUrlGeneratorInterface) { + $generator->clearCache(); + } + return $request; } } diff --git a/core/modules/system/lib/Drupal/system/Form/RegionalForm.php b/core/modules/system/lib/Drupal/system/Form/RegionalForm.php index bf4ebaf..c998bce 100644 --- a/core/modules/system/lib/Drupal/system/Form/RegionalForm.php +++ b/core/modules/system/lib/Drupal/system/Form/RegionalForm.php @@ -10,6 +10,8 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Locale\CountryManagerInterface; use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Routing\CachedUrlGeneratorInterface; +use Drupal\Core\Routing\UrlGeneratorInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -25,16 +27,26 @@ class RegionalForm extends ConfigFormBase { protected $countryManager; /** + * The URL generator. + * + * @var \Drupal\Core\Routing\UrlGeneratorInterface + */ + protected $urlGenerator; + + /** * Constructs a RegionalForm object. * * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The factory for configuration objects. * @param \Drupal\Core\Locale\CountryManagerInterface $country_manager * The country manager. + * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator + * The url generator. */ - public function __construct(ConfigFactoryInterface $config_factory, CountryManagerInterface $country_manager) { + public function __construct(ConfigFactoryInterface $config_factory, CountryManagerInterface $country_manager, UrlGeneratorInterface $url_generator) { parent::__construct($config_factory); $this->countryManager = $country_manager; + $this->urlGenerator = $url_generator; } /** @@ -43,7 +55,8 @@ public function __construct(ConfigFactoryInterface $config_factory, CountryManag public static function create(ContainerInterface $container) { return new static( $container->get('config.factory'), - $container->get('country_manager') + $container->get('country_manager'), + $container->get('url_generator') ); } @@ -151,6 +164,10 @@ public function submitForm(array &$form, array &$form_state) { ->set('timezone.user.default', $form_state['values']['user_default_timezone']) ->save(); + if ($this->urlGenerator instanceof CachedUrlGeneratorInterface) { + $this->urlGenerator->clearCache(); + } + parent::submitForm($form, $form_state); } diff --git a/core/tests/Drupal/Tests/Core/Routing/CachedUrlGeneratorTest.php b/core/tests/Drupal/Tests/Core/Routing/CachedUrlGeneratorTest.php new file mode 100644 index 0000000..907c0c1 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Routing/CachedUrlGeneratorTest.php @@ -0,0 +1,171 @@ + 'Cached UrlGenerator', + 'description' => 'Confirm that the cached UrlGenerator is functioning properly.', + 'group' => 'Routing', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->urlGenerator = $this->getMock('Drupal\Core\Routing\UrlGeneratorInterface'); + $this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); + $this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManager'); + + $this->cachedUrlGenerator = new CachedUrlGenerator($this->urlGenerator, $this->cache, $this->languageManager); + } + + /** + * Tests the generate method. + * + * @see \Drupal\Core\Routing\CachedUrlGenerator::generate() + */ + public function testGenerate() { + $this->urlGenerator->expects($this->once()) + ->method('generateFromRoute') + ->with('test_route') + ->will($this->returnValue('test-route-1')); + $this->assertEquals('test-route-1', $this->cachedUrlGenerator->generateFromRoute('test_route')); + $this->assertEquals('test-route-1', $this->cachedUrlGenerator->generateFromRoute('test_route')); + } + + /** + * Tests the generate method with the same route name but different parameters. + * + * @see \Drupal\Core\Routing\CachedUrlGenerator::generate() + */ + public function testGenerateWithDifferentParameters() { + $this->urlGenerator->expects($this->exactly(2)) + ->method('generateFromRoute') + ->will($this->returnValueMap(array( + array('test_route', array('key' => 'value1'), array(), 'test-route-1/value1'), + array('test_route', array('key' => 'value2'), array(), 'test-route-1/value2'), + ))); + $this->assertEquals('test-route-1/value1', $this->cachedUrlGenerator->generate('test_route', array('key' => 'value1'))); + $this->assertEquals('test-route-1/value1', $this->cachedUrlGenerator->generate('test_route', array('key' => 'value1'))); + $this->assertEquals('test-route-1/value2', $this->cachedUrlGenerator->generate('test_route', array('key' => 'value2'))); + $this->assertEquals('test-route-1/value2', $this->cachedUrlGenerator->generate('test_route', array('key' => 'value2'))); + } + + /** + * Tests the generateFromPath method. + * + * @see \Drupal\Core\Routing\CachedUrlGenerator::generateFromPath() + */ + public function testGenerateFromPath() { + $this->urlGenerator->expects($this->once()) + ->method('generateFromPath') + ->with('test-route-1') + ->will($this->returnValue('test-route-1')); + $this->assertEquals('test-route-1', $this->cachedUrlGenerator->generateFromPath('test-route-1')); + $this->assertEquals('test-route-1', $this->cachedUrlGenerator->generateFromPath('test-route-1')); + } + + /** + * Tests the generate method with the same path but different options + * + * @see \Drupal\Core\Routing\CachedUrlGenerator::generateFromPath() + */ + public function testGenerateFromPathWithDifferentParameters() { + $this->urlGenerator->expects($this->exactly(2)) + ->method('generateFromPath') + ->will($this->returnValueMap(array( + array('test-route-1', array('absolute' => TRUE), 'http://localhost/test-route-1'), + array('test-route-1', array('absolute' => FALSE), 'test-route-1'), + ))); + $this->assertEquals('http://localhost/test-route-1', $this->cachedUrlGenerator->generateFromPath('test-route-1', array('absolute' => TRUE))); + $this->assertEquals('http://localhost/test-route-1', $this->cachedUrlGenerator->generateFromPath('test-route-1', array('absolute' => TRUE))); + $this->assertEquals('test-route-1', $this->cachedUrlGenerator->generateFromPath('test-route-1', array('absolute' => FALSE))); + $this->assertEquals('test-route-1', $this->cachedUrlGenerator->generateFromPath('test-route-1', array('absolute' => FALSE))); + } + + + /** + * Tests the generateFromRoute method. + * + * @see \Drupal\Core\Routing\CachedUrlGenerator::generateFromRoute() + */ + public function testGenerateFromRoute() { + $this->urlGenerator->expects($this->once()) + ->method('generateFromRoute') + ->with('test_route') + ->will($this->returnValue('test-route-1')); + $this->assertEquals('test-route-1', $this->cachedUrlGenerator->generateFromRoute('test_route')); + $this->assertEquals('test-route-1', $this->cachedUrlGenerator->generateFromRoute('test_route')); + } + + /** + * Tests the generateFromRoute method with the same path, different options. + * + * @see \Drupal\Core\Routing\CachedUrlGenerator::generateFromRoute() + */ + public function testGenerateFromRouteWithDifferentParameters() { + $this->urlGenerator->expects($this->exactly(4)) + ->method('generateFromRoute') + ->will($this->returnValueMap(array( + array('test_route', array('key' => 'value1'), array(), 'test-route-1/value1'), + array('test_route', array('key' => 'value1'), array('absolute' => TRUE), 'http://localhost/test-route-1/value1'), + array('test_route', array('key' => 'value2'), array(), 'test-route-1/value2'), + array('test_route', array('key' => 'value2'), array('absolute' => TRUE), 'http://localhost/test-route-1/value2'), + ))); + $this->assertEquals('test-route-1/value1', $this->cachedUrlGenerator->generateFromRoute('test_route', array('key' => 'value1'))); + $this->assertEquals('test-route-1/value1', $this->cachedUrlGenerator->generateFromRoute('test_route', array('key' => 'value1'))); + $this->assertEquals('http://localhost/test-route-1/value1', $this->cachedUrlGenerator->generateFromRoute('test_route', array('key' => 'value1'), array('absolute' => TRUE))); + $this->assertEquals('http://localhost/test-route-1/value1', $this->cachedUrlGenerator->generateFromRoute('test_route', array('key' => 'value1'), array('absolute' => TRUE))); + $this->assertEquals('test-route-1/value2', $this->cachedUrlGenerator->generateFromRoute('test_route', array('key' => 'value2'))); + $this->assertEquals('test-route-1/value2', $this->cachedUrlGenerator->generateFromRoute('test_route', array('key' => 'value2'))); + $this->assertEquals('http://localhost/test-route-1/value2', $this->cachedUrlGenerator->generateFromRoute('test_route', array('key' => 'value2'), array('absolute' => TRUE))); + $this->assertEquals('http://localhost/test-route-1/value2', $this->cachedUrlGenerator->generateFromRoute('test_route', array('key' => 'value2'), array('absolute' => TRUE))); + } + +} +