diff --git a/core/core.services.yml b/core/core.services.yml index d9b1839..5877a38 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -80,6 +80,11 @@ services: arguments: ['@request_stack'] tags: - { name: cache.context } + cache_context.url.path.parent: + class: Drupal\Core\Cache\Context\PathParentCacheContext + arguments: ['@request_stack'] + tags: + - { name: cache.context } cache_context.url.query_args: class: Drupal\Core\Cache\Context\QueryArgsCacheContext arguments: ['@request_stack'] diff --git a/core/lib/Drupal/Core/Cache/Context/PathParentCacheContext.php b/core/lib/Drupal/Core/Cache/Context/PathParentCacheContext.php new file mode 100644 index 0000000..76c22fb --- /dev/null +++ b/core/lib/Drupal/Core/Cache/Context/PathParentCacheContext.php @@ -0,0 +1,41 @@ +requestStack->getCurrentRequest(); + $path_elements = explode('/', trim($request->getPathInfo(), '/')); + array_pop($path_elements); + return implode('/', $path_elements); + } + + /** + * {@inheritdoc} + */ + public function getCacheableMetadata() { + return new CacheableMetadata(); + } + +} diff --git a/core/modules/node/src/Tests/NodeTranslationUITest.php b/core/modules/node/src/Tests/NodeTranslationUITest.php index b4871b7..b4dce56 100644 --- a/core/modules/node/src/Tests/NodeTranslationUITest.php +++ b/core/modules/node/src/Tests/NodeTranslationUITest.php @@ -25,7 +25,7 @@ class NodeTranslationUITest extends ContentTranslationUITestBase { 'theme', 'route', 'timezone', - 'url.path', + 'url.path.parent', 'url.query_args:_wrapper_format', 'user' ]; diff --git a/core/modules/system/src/PathBasedBreadcrumbBuilder.php b/core/modules/system/src/PathBasedBreadcrumbBuilder.php index 060f1b1..dcfc073 100644 --- a/core/modules/system/src/PathBasedBreadcrumbBuilder.php +++ b/core/modules/system/src/PathBasedBreadcrumbBuilder.php @@ -136,9 +136,9 @@ public function build(RouteMatchInterface $route_match) { // /user is just a redirect, so skip it. // @todo Find a better way to deal with /user. $exclude['/user'] = TRUE; - // Because this breadcrumb builder is entirely path-based, vary by the - // 'url.path' cache context. - $breadcrumb->addCacheContexts(['url.path']); + // Add the url.path.parent cache context. This code ignores the last path + // part so the result only depends on the path parents. + $breadcrumb->addCacheContexts(['url.path.parent']); while (count($path_elements) > 1) { array_pop($path_elements); // Copy the path elements for up-casting. diff --git a/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php b/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php index 29f0cea..eb095a7 100644 --- a/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php +++ b/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php @@ -142,7 +142,7 @@ public function testBuildOnFrontpage() { $breadcrumb = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface')); $this->assertEquals([], $breadcrumb->getLinks()); - $this->assertEquals(['url.path'], $breadcrumb->getCacheContexts()); + $this->assertEquals(['url.path.parent'], $breadcrumb->getCacheContexts()); $this->assertEquals([], $breadcrumb->getCacheTags()); $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge()); } @@ -159,7 +159,7 @@ public function testBuildWithOnePathElement() { $breadcrumb = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface')); $this->assertEquals([0 => new Link('Home', new Url(''))], $breadcrumb->getLinks()); - $this->assertEquals(['url.path'], $breadcrumb->getCacheContexts()); + $this->assertEquals(['url.path.parent'], $breadcrumb->getCacheContexts()); $this->assertEquals([], $breadcrumb->getCacheTags()); $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge()); } @@ -194,7 +194,7 @@ public function testBuildWithTwoPathElements() { $breadcrumb = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface')); $this->assertEquals([0 => new Link('Home', new Url('')), 1 => new Link('Example', new Url('example'))], $breadcrumb->getLinks()); - $this->assertEquals(['url.path', 'user.permissions'], $breadcrumb->getCacheContexts()); + $this->assertEquals(['url.path.parent', 'user.permissions'], $breadcrumb->getCacheContexts()); $this->assertEquals([], $breadcrumb->getCacheTags()); $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge()); } @@ -245,7 +245,7 @@ public function testBuildWithThreePathElements() { new Link('Example', new Url('example')), new Link('Bar', new Url('example_bar')), ], $breadcrumb->getLinks()); - $this->assertEquals(['bar', 'url.path', 'user.permissions'], $breadcrumb->getCacheContexts()); + $this->assertEquals(['bar', 'url.path.parent', 'user.permissions'], $breadcrumb->getCacheContexts()); $this->assertEquals(['example'], $breadcrumb->getCacheTags()); $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge()); } @@ -272,7 +272,7 @@ public function testBuildWithException($exception_class, $exception_argument) { // No path matched, though at least the frontpage is displayed. $this->assertEquals([0 => new Link('Home', new Url(''))], $breadcrumb->getLinks()); - $this->assertEquals(['url.path'], $breadcrumb->getCacheContexts()); + $this->assertEquals(['url.path.parent'], $breadcrumb->getCacheContexts()); $this->assertEquals([], $breadcrumb->getCacheTags()); $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge()); } @@ -316,7 +316,7 @@ public function testBuildWithNonProcessedPath() { // No path matched, though at least the frontpage is displayed. $this->assertEquals([0 => new Link('Home', new Url(''))], $breadcrumb->getLinks()); - $this->assertEquals(['url.path'], $breadcrumb->getCacheContexts()); + $this->assertEquals(['url.path.parent'], $breadcrumb->getCacheContexts()); $this->assertEquals([], $breadcrumb->getCacheTags()); $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge()); } @@ -364,7 +364,7 @@ public function testBuildWithUserPath() { $breadcrumb = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface')); $this->assertEquals([0 => new Link('Home', new Url('')), 1 => new Link('Admin', new Url('user_page'))], $breadcrumb->getLinks()); - $this->assertEquals(['url.path', 'user.permissions'], $breadcrumb->getCacheContexts()); + $this->assertEquals(['url.path.parent', 'user.permissions'], $breadcrumb->getCacheContexts()); $this->assertEquals([], $breadcrumb->getCacheTags()); $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge()); } diff --git a/core/tests/Drupal/FunctionalTests/Breadcrumb/Breadcrumb404Test.php b/core/tests/Drupal/FunctionalTests/Breadcrumb/Breadcrumb404Test.php new file mode 100644 index 0000000..7dcafbf --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Breadcrumb/Breadcrumb404Test.php @@ -0,0 +1,57 @@ +placeBlock('system_breadcrumb_block', ['id' => 'breadcrumb']); + + // Prime the cache first. + $this->drupalGet('/not-found-1'); + $base_count = count($this->getBreadcrumbCacheEntries()); + + $this->drupalGet('/not-found-2'); + $next_count = count($this->getBreadcrumbCacheEntries()); + $this->assertEquals($base_count, $next_count); + + $this->drupalGet('/not-found-3'); + $next_count = count($this->getBreadcrumbCacheEntries()); + $this->assertEquals($base_count, $next_count); + } + + /** + * Gets the breadcrumb cache entries. + * + * @return array + * The breadcrumb cache entries. + */ + protected function getBreadcrumbCacheEntries() { + $database = \Drupal::database(); + $cache_entries = $database->select('cache_render') + ->fields('cache_render') + ->condition('cid', $database->escapeLike('entity_view:block:breadcrumb') . '%', 'LIKE') + ->execute() + ->fetchAllAssoc('cid'); + return $cache_entries; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Cache/Context/PathParentCacheContextTest.php b/core/tests/Drupal/Tests/Core/Cache/Context/PathParentCacheContextTest.php new file mode 100644 index 0000000..2c44735 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Cache/Context/PathParentCacheContextTest.php @@ -0,0 +1,43 @@ +push($request); + $cache_context = new PathParentCacheContext($request_stack); + $this->assertSame($cache_context->getContext(), $context); + } + + /** + * Provides a list of paths and expected cache contexts. + */ + public function providerTestGetContext() { + return [ + ['/some/path', 'some'], + ['/some/other-path', 'some'], + ['/some/other/path', 'some/other'], + ['/some/other/path?q=foo&b=bar', 'some/other'], + ['/some', ''], + ['/', ''], + ]; + } + +}