diff --git a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php index 1c745ed6a2620f3915317bb0012fc514f1353bf0..0a75387dd1d83b6781dc93322b28b9c6477c3c2b 100644 --- a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php +++ b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php @@ -83,16 +83,18 @@ public function build(RouteMatchInterface $route_match) { } $breadcrumb = $builder->build($route_match); - if ($breadcrumb instanceof Breadcrumb) { $context['builder'] = $builder; - $breadcrumb->addCacheableDependency($cacheable_metadata); break; } else { throw new \UnexpectedValueException('Invalid breadcrumb returned by ' . get_class($builder) . '::build().'); } } + + // Ensure all collected cacheability is applied. + $breadcrumb->addCacheableDependency($cacheable_metadata); + // Allow modules to alter the breadcrumb. $this->moduleHandler->alter('system_breadcrumb', $breadcrumb, $route_match, $context); diff --git a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml index 4f3c9e28b1c5a7ce9d7b1ef64e07f6bedad34127..f427fe5fe87e2b2a8950255b451793b0c8781419 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml +++ b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml @@ -577,3 +577,11 @@ menu_test.breadcrumb3: _title: 'Normal title' requirements: _access: 'TRUE' + +menu_test.skippable-breadcrumb: + path: '/menu-test/skippable-breadcrumb' + defaults: + _controller: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback' + _title: 'Normal title' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/menu_test/src/MenuTestServiceProvider.php b/core/modules/system/tests/modules/menu_test/src/MenuTestServiceProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..f6aad34ecf1834fba039782fa86a2f49dcb95c07 --- /dev/null +++ b/core/modules/system/tests/modules/menu_test/src/MenuTestServiceProvider.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\menu_test; + +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\DependencyInjection\ServiceModifierInterface; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Decorate core's default path-based breadcrumb builder when it is available. + */ +class MenuTestServiceProvider implements ServiceModifierInterface { + + /** + * {@inheritdoc} + */ + public function alter(ContainerBuilder $container): void { + if ($container->has('system.breadcrumb.default')) { + $container->register('menu_test.breadcrumb.default', SkippablePathBasedBreadcrumbBuilder::class) + ->setDecoratedService('system.breadcrumb.default') + ->addArgument(new Reference('menu_test.breadcrumb.default.inner')) + ->addArgument(new Reference('request_stack')); + } + } + +} diff --git a/core/modules/system/tests/modules/menu_test/src/SkippablePathBasedBreadcrumbBuilder.php b/core/modules/system/tests/modules/menu_test/src/SkippablePathBasedBreadcrumbBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..fc075042ad6edaac4ae907fb97673c5d55acb932 --- /dev/null +++ b/core/modules/system/tests/modules/menu_test/src/SkippablePathBasedBreadcrumbBuilder.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\menu_test; + +use Drupal\Core\Breadcrumb\Breadcrumb; +use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface; +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Routing\RouteMatchInterface; +use Symfony\Component\HttpFoundation\RequestStack; + +/** + * A path-based breadcrumb builder can be skipped from applying. + */ +class SkippablePathBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface { + + public function __construct( + protected BreadcrumbBuilderInterface $pathBasedBreadcrumbBuilder, + protected RequestStack $requestStack, + ) {} + + /** + * {@inheritdoc} + */ + public function applies(RouteMatchInterface $route_match, ?CacheableMetadata $cacheable_metadata = NULL): bool { + $query_arg = 'menu_test_skip_breadcrumbs'; + $cacheable_metadata?->addCacheContexts(['url.query_args:' . $query_arg]); + // Apply unless the query argument is present. + return !$this->requestStack->getCurrentRequest()->query->has($query_arg); + } + + /** + * {@inheritdoc} + */ + public function build(RouteMatchInterface $route_match): Breadcrumb { + return $this->pathBasedBreadcrumbBuilder->build($route_match); + } + +} diff --git a/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php b/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php index 1e8b89fff2f251a438a789231e02c16510a310c5..4b2fb36ab24c10c4ee74ffe5e1fcb197e1d08fea 100644 --- a/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php +++ b/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php @@ -388,6 +388,15 @@ public function testBreadCrumbs(): void { $this->drupalGet('menu-test/breadcrumb1/breadcrumb2/breadcrumb3'); $this->assertSession()->responseContains('<script>alert(12);</script>'); $this->assertSession()->assertEscaped('<script>alert(123);</script>'); + + // Assert that the breadcrumb cacheability is respected after not applying. + $this->assertBreadcrumb(Url::fromRoute('menu_test.skippable-breadcrumb', [], [ + 'query' => [ + 'menu_test_skip_breadcrumbs' => 'yes', + ], + ]), []); + $trail = $home + ['menu-test' => 'Menu test root']; + $this->assertBreadcrumb(Url::fromRoute('menu_test.skippable-breadcrumb'), $trail); } /**