diff --git a/core/core.services.yml b/core/core.services.yml index 6f8cce6..1bff04e 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -58,6 +58,13 @@ services: factory_method: get factory_service: cache_factory arguments: [path] + 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 @@ -232,9 +239,12 @@ 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', '@config.factory', '@settings'] + url_generator: + class: Drupal\Core\Routing\CachedUrlGenerator + arguments: ['@url_generator.uncached', '@cache.url_generator'] calls: - [setRequest, ['@?request']] tags: diff --git a/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php index a5dc955..ad5bdb5 100644 --- a/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php @@ -9,7 +9,6 @@ use Drupal\Core\CacheDecorator\AliasManagerCacheDecorator; use Drupal\Core\PathProcessor\InboundPathProcessorInterface; -use Drupal\Core\Routing\PathBasedGeneratorInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Event\GetResponseEvent; diff --git a/core/lib/Drupal/Core/Routing/CachedUrlGenerator.php b/core/lib/Drupal/Core/Routing/CachedUrlGenerator.php new file mode 100644 index 0000000..c6012a7 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/CachedUrlGenerator.php @@ -0,0 +1,198 @@ +urlGenerator = $url_generator; + $this->cache = $cache; + } + + /** + * 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(), $referenceType = self::ABSOLUTE_PATH) { + // We can only cache the url if $name is a string, not the actual route + // object. + $parameter_hash = hash('sha256', serialize($parameters)); + if (is_scalar($name) && isset($this->cachedUrls[self::ROUTE_CACHE_PREFIX . $name . $parameter_hash])) { + return $this->cachedUrls[self::ROUTE_CACHE_PREFIX . $name . $parameter_hash]; + } + $url = $this->urlGenerator->generate($name, $parameters, $referenceType); + if (is_scalar($name)) { + // Cache the url for this route. + $this->cachedUrls[self::ROUTE_CACHE_PREFIX . $name . $parameter_hash] = $url; + $this->cacheNeedsWriting = TRUE; + } + return $url; + } + + /** + * {@inheritdoc} + */ + public function generateFromPath($path = NULL, $options = array()) { + $options_hash = hash('sha256', serialize($options)); + if (isset($this->cachedUrls[self::PATH_CACHE_PREFIX . $path . $options_hash])) { + return $this->cachedUrls[self::PATH_CACHE_PREFIX . $path . $options_hash]; + } + $url = $this->urlGenerator->generateFromPath($path, $options); + // Cache the url for this route. + $this->cachedUrls[self::PATH_CACHE_PREFIX . $path . $options_hash] = $url; + $this->cacheNeedsWriting = TRUE; + return $url; + } + + /** + * {@inheritdoc} + */ + public function setRequest(Request $request) { + $this->cacheKey = $request->attributes->get('system_path'); + $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/PathBasedGeneratorInterface.php b/core/lib/Drupal/Core/Routing/PathBasedGeneratorInterface.php index 79ed373..e40fbdc 100644 --- a/core/lib/Drupal/Core/Routing/PathBasedGeneratorInterface.php +++ b/core/lib/Drupal/Core/Routing/PathBasedGeneratorInterface.php @@ -8,11 +8,12 @@ namespace Drupal\Core\Routing; use Symfony\Component\HttpFoundation\Request; +use Symfony\Cmf\Component\Routing\VersatileGeneratorInterface; /** * Defines an interface for generating a url from a path as opposed to a route. */ -interface PathBasedGeneratorInterface { +interface PathBasedGeneratorInterface extends VersatileGeneratorInterface { /** * Generates an internal or external URL. 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..0bd0a60 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Routing/CachedUrlGeneratorTest.php @@ -0,0 +1,124 @@ + '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\PathBasedGeneratorInterface'); + $this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); + + $this->cachedUrlGenerator = new CachedUrlGenerator($this->urlGenerator, $this->cache); + } + + /** + * Tests the generate method. + * + * @see \Drupal\Core\Routing\CachedUrlGenerator::generate() + */ + public function testGenerate() { + $this->urlGenerator->expects($this->once()) + ->method('generate') + ->with('test_route') + ->will($this->returnValue('test-route-1')); + $this->assertEquals('test-route-1', $this->cachedUrlGenerator->generate('test_route')); + $this->assertEquals('test-route-1', $this->cachedUrlGenerator->generate('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('generate') + ->will($this->returnValueMap(array( + array('test_route', array('key' => 'value1'), CachedUrlGenerator::ABSOLUTE_PATH, 'test-route-1/value1'), + array('test_route', array('key' => 'value2'), CachedUrlGenerator::ABSOLUTE_PATH, '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))); + } + +} + diff --git a/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php index d6006ad..bece149 100644 --- a/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php +++ b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php @@ -8,12 +8,8 @@ namespace Drupal\Tests\Core\Routing; use Drupal\Component\Utility\Settings; -use Drupal\Core\Config\ConfigFactory; -use Drupal\Core\Config\NullStorage; -use Drupal\Core\Config\Context\ConfigContextFactory; use Drupal\Core\PathProcessor\PathProcessorAlias; use Drupal\Core\PathProcessor\PathProcessorManager; -use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -22,6 +18,7 @@ use Drupal\Tests\UnitTestCase; use Drupal\Core\Routing\UrlGenerator; +use Drupal\Core\Routing\CachedUrlGenerator; /** * Basic tests for the Route. @@ -206,6 +203,50 @@ public function testUrlGenerationWithHttpsRequirement() { } /** + * Tests the Cached URL Generator. + */ + public function testCachedUrlGenerator() { + $system_path = 'some_path'; + + // Mock a cache backend to pass to the CachedUrlGenerator. + $cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); + // Set an expectation that the get() method will be called on the + // mock cache backend and make it return a cache object with one + // cached URL. + $cache->expects($this->at(0)) + ->method('get') + ->with($this->equalTo($system_path)) + ->will($this->returnValue((object) array('data' => array(CachedUrlGenerator::PATH_CACHE_PREFIX . 'first_path' => 'foo')))); + // Set an expectation that the set() method will be called on the + // mock cache backend with an array containing two URLS. + $cache->expects($this->at(1)) + ->method('set') + ->with($this->equalTo($system_path), $this->equalTo(array(CachedUrlGenerator::PATH_CACHE_PREFIX . 'first_path' => 'foo', CachedUrlGenerator::PATH_CACHE_PREFIX . 'second_path' => 'bar'))); + + // Mock a UrlGenerator to pass to the CachedUrlGenerator. + $url_generator = $this->getMockBuilder('Drupal\Core\Routing\UrlGenerator') + ->disableOriginalConstructor() + ->getMock(); + + // The decorated url generator should only be consulted + // once, for the second path, which was not in the cache. + $url_generator->expects($this->once()) + ->method('generateFromPath') + ->with($this->equalTo('second_path'), $this->anything()) + ->will($this->returnValue('bar')); + + // Instantiate a CachedUrlGenerator and call various methods + // on it to ensure the expectations we set on our mocks are met. + $cached_url_generator = new CachedUrlGenerator($url_generator, $cache); + $request = Request::create('some/path'); + $request->attributes->set('system_path', $system_path); + $cached_url_generator->setRequest($request); + $url = $cached_url_generator->generateFromPath('first_path'); + $url = $cached_url_generator->generateFromPath('second_path'); + $cached_url_generator->destruct(); + } + + /** * Tests path-based URL generation. */ public function testPathBasedURLGeneration() {