diff --git a/core/lib/Drupal/Core/Breadcrumb/Breadcrumb.php b/core/lib/Drupal/Core/Breadcrumb/Breadcrumb.php new file mode 100644 index 0000000..8414154 --- /dev/null +++ b/core/lib/Drupal/Core/Breadcrumb/Breadcrumb.php @@ -0,0 +1,47 @@ +links ; + } + + /** + * Sets the breadcrumb links. + * + * @param \Drupal\Core\Link[] $links + * The breadcrumb links. + * + * @return $this + */ + public function setLinks(array $links) { + $this->links = $links; + + return $this; + } + +} diff --git a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php index ebdfa55..b16e0d3 100644 --- a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php +++ b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php @@ -32,9 +32,8 @@ public function applies(RouteMatchInterface $route_match); * @param \Drupal\Core\Routing\RouteMatchInterface $route_match * The current route match. * - * @return \Drupal\Core\Link[] - * An array of links for the breadcrumb. Returning an empty array will - * suppress all breadcrumbs. + * @return mixed[] + * A breadcrumb. */ public function build(RouteMatchInterface $route_match); diff --git a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php index 0099897..2d90160 100644 --- a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php +++ b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php @@ -76,7 +76,9 @@ public function applies(RouteMatchInterface $route_match) { * {@inheritdoc} */ public function build(RouteMatchInterface $route_match) { - $breadcrumb = array(); + $build = [ + 'links' => [], + ]; $context = array('builder' => NULL); // Call the build method of registered breadcrumb builders, // until one of them returns an array. @@ -86,11 +88,12 @@ public function build(RouteMatchInterface $route_match) { continue; } - $build = $builder->build($route_match); + $breadcrumb = $builder->build($route_match); - if (is_array($build)) { + if ($breadcrumb instanceof Breadcrumb) { // The builder returned an array of breadcrumb links. - $breadcrumb = $build; + $build['contexts'] = $breadcrumb->getCacheContexts(); + $build['links'] = $breadcrumb->getLinks(); $context['builder'] = $builder; break; } @@ -99,9 +102,9 @@ public function build(RouteMatchInterface $route_match) { } } // Allow modules to alter the breadcrumb. - $this->moduleHandler->alter('system_breadcrumb', $breadcrumb, $route_match, $context); + $this->moduleHandler->alter('system_breadcrumb', $build['links'], $route_match, $context); // Fall back to an empty breadcrumb. - return $breadcrumb; + return $build; } /** diff --git a/core/modules/book/src/BookBreadcrumbBuilder.php b/core/modules/book/src/BookBreadcrumbBuilder.php index be0e63a..b1ece44 100644 --- a/core/modules/book/src/BookBreadcrumbBuilder.php +++ b/core/modules/book/src/BookBreadcrumbBuilder.php @@ -8,6 +8,7 @@ namespace Drupal\book; use Drupal\Core\Access\AccessManagerInterface; +use Drupal\Core\Breadcrumb\Breadcrumb; use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Link; @@ -72,6 +73,8 @@ public function applies(RouteMatchInterface $route_match) { */ public function build(RouteMatchInterface $route_match) { $book_nids = array(); + $breadcrumb = new Breadcrumb(); + $links = array(Link::createFromRoute($this->t('Home'), '')); $book = $route_match->getParameter('node')->book; $depth = 1; @@ -92,7 +95,9 @@ public function build(RouteMatchInterface $route_match) { $depth++; } } - return $links; + $breadcrumb->setLinks($links); + $breadcrumb->setCacheContexts(['route.book_navigation']); + return $breadcrumb; } } diff --git a/core/modules/comment/src/CommentBreadcrumbBuilder.php b/core/modules/comment/src/CommentBreadcrumbBuilder.php index 8bc2f25..94817af 100644 --- a/core/modules/comment/src/CommentBreadcrumbBuilder.php +++ b/core/modules/comment/src/CommentBreadcrumbBuilder.php @@ -8,6 +8,7 @@ namespace Drupal\comment; use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface; +use Drupal\Core\Breadcrumb\Breadcrumb; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Link; use Drupal\Core\Routing\RouteMatchInterface; @@ -47,19 +48,21 @@ public function applies(RouteMatchInterface $route_match) { * {@inheritdoc} */ public function build(RouteMatchInterface $route_match) { - $breadcrumb = [Link::createFromRoute($this->t('Home'), '')]; + $breadcrumb = new Breadcrumb(); + $breadcrumb = $breadcrumb->setCacheContexts(['route']); + $links = [Link::createFromRoute($this->t('Home'), '')]; $entity = $route_match->getParameter('entity'); - $breadcrumb[] = new Link($entity->label(), $entity->urlInfo()); + $links[] = new Link($entity->label(), $entity->urlInfo()); if (($pid = $route_match->getParameter('pid')) && ($comment = $this->storage->load($pid))) { /** @var \Drupal\comment\CommentInterface $comment */ // Display link to parent comment. // @todo Clean-up permalink in https://www.drupal.org/node/2198041 - $breadcrumb[] = new Link($comment->getSubject(), $comment->urlInfo()); + $links[] = new Link($comment->getSubject(), $comment->urlInfo()); } - return $breadcrumb; + return $breadcrumb->setLinks($links); } } diff --git a/core/modules/forum/src/Breadcrumb/ForumBreadcrumbBuilderBase.php b/core/modules/forum/src/Breadcrumb/ForumBreadcrumbBuilderBase.php index f595ee8..d15478f 100644 --- a/core/modules/forum/src/Breadcrumb/ForumBreadcrumbBuilderBase.php +++ b/core/modules/forum/src/Breadcrumb/ForumBreadcrumbBuilderBase.php @@ -8,6 +8,7 @@ namespace Drupal\forum\Breadcrumb; use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface; +use Drupal\Core\Breadcrumb\Breadcrumb; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Link; @@ -65,14 +66,17 @@ public function __construct(EntityManagerInterface $entity_manager, ConfigFactor * {@inheritdoc} */ public function build(RouteMatchInterface $route_match) { - $breadcrumb[] = Link::createFromRoute($this->t('Home'), ''); + $breadcrumb = new Breadcrumb(); + $breadcrumb->setCacheContexts(['route']); + + $links[] = Link::createFromRoute($this->t('Home'), ''); $vocabulary = $this->entityManager ->getStorage('taxonomy_vocabulary') ->load($this->config->get('vocabulary')); - $breadcrumb[] = Link::createFromRoute($vocabulary->label(), 'forum.index'); + $links[] = Link::createFromRoute($vocabulary->label(), 'forum.index'); - return $breadcrumb; + return $breadcrumb->setLinks($links); } } diff --git a/core/modules/forum/src/Breadcrumb/ForumNodeBreadcrumbBuilder.php b/core/modules/forum/src/Breadcrumb/ForumNodeBreadcrumbBuilder.php index 0e0bd04..3fb421b 100644 --- a/core/modules/forum/src/Breadcrumb/ForumNodeBreadcrumbBuilder.php +++ b/core/modules/forum/src/Breadcrumb/ForumNodeBreadcrumbBuilder.php @@ -7,6 +7,7 @@ namespace Drupal\forum\Breadcrumb; +use Drupal\Core\Breadcrumb\Breadcrumb; use Drupal\Core\Link; use Drupal\Core\Routing\RouteMatchInterface; @@ -28,20 +29,23 @@ public function applies(RouteMatchInterface $route_match) { * {@inheritdoc} */ public function build(RouteMatchInterface $route_match) { - $breadcrumb = parent::build($route_match); + $breadcrumb = new Breadcrumb(); + $breadcrumb->setCacheContexts(['route']); + $links = parent::build($route_match); $parents = $this->forumManager->getParents($route_match->getParameter('node')->forum_tid); if ($parents) { $parents = array_reverse($parents); foreach ($parents as $parent) { - $breadcrumb[] = Link::createFromRoute($parent->label(), 'forum.page', + $links[] = Link::createFromRoute($parent->label(), 'forum.page', array( 'taxonomy_term' => $parent->id(), ) ); } } - return $breadcrumb; + + return $breadcrumb->setLinks($links); } } diff --git a/core/modules/forum/tests/src/Unit/Breadcrumb/ForumBreadcrumbBuilderBaseTest.php b/core/modules/forum/tests/src/Unit/Breadcrumb/ForumBreadcrumbBuilderBaseTest.php index da7ff11..528aceb 100644 --- a/core/modules/forum/tests/src/Unit/Breadcrumb/ForumBreadcrumbBuilderBaseTest.php +++ b/core/modules/forum/tests/src/Unit/Breadcrumb/ForumBreadcrumbBuilderBaseTest.php @@ -128,7 +128,7 @@ public function testBuild() { ); // And finally, the test. - $this->assertEquals($expected, $breadcrumb_builder->build($route_match)); + $this->assertEquals($expected, $breadcrumb_builder->build($route_match)->getLinks()); } } diff --git a/core/modules/system/src/PathBasedBreadcrumbBuilder.php b/core/modules/system/src/PathBasedBreadcrumbBuilder.php index 45418a7b..c5b1ef4 100644 --- a/core/modules/system/src/PathBasedBreadcrumbBuilder.php +++ b/core/modules/system/src/PathBasedBreadcrumbBuilder.php @@ -9,7 +9,9 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Access\AccessManagerInterface; +use Drupal\Core\Breadcrumb\Breadcrumb; use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Controller\TitleResolverInterface; use Drupal\Core\Link; @@ -125,6 +127,7 @@ public function applies(RouteMatchInterface $route_match) { * {@inheritdoc} */ public function build(RouteMatchInterface $route_match) { + $breadcrumb = new Breadcrumb(); $links = array(); // General path-based breadcrumbs. Use the actual request path, prior to @@ -139,17 +142,21 @@ 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' + // cache context. + $breadcrumb->setCacheContexts(['url']); while (count($path_elements) > 1) { array_pop($path_elements); // Copy the path elements for up-casting. $route_request = $this->getRequestForPath(implode('/', $path_elements), $exclude); if ($route_request) { $route_match = RouteMatch::createFromRequest($route_request); - $access = $this->accessManager->check($route_match, $this->currentUser); - if ($access) { + $access = $this->accessManager->check($route_match, $this->currentUser, NULL, TRUE); + // The set of breadcrumb links depends on the access result, so merge + // the access result's cacheability metadata. + $breadcrumb = $breadcrumb->merge(CacheableMetadata::createFromObject($access)); + if ($access->isAllowed()) { $title = $this->titleResolver->getTitle($route_request, $route_match->getRouteObject()); - } - if ($access) { if (!isset($title)) { // Fallback to using the raw path component as the title if the // route is missing a _title or _title_callback attribute. @@ -165,7 +172,8 @@ public function build(RouteMatchInterface $route_match) { // Add the Home link, except for the front page. $links[] = Link::createFromRoute($this->t('Home'), ''); } - return array_reverse($links); + + return $breadcrumb->setLinks(array_reverse($links)); } /** diff --git a/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php b/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php index c7629f0..8e6a102 100644 --- a/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php @@ -79,18 +79,12 @@ public function build() { // $breadcrumb is expected to be an array of rendered breadcrumb links. return array( '#theme' => 'breadcrumb', - '#links' => $breadcrumb, + '#links' => $breadcrumb['links'], + '#cache' => [ + 'contexts' => $breadcrumb['contexts'], + ], ); } } - /** - * {@inheritdoc} - * - * @todo Make cacheable in https://www.drupal.org/node/2483183 - */ - public function getCacheMaxAge() { - return 0; - } - } diff --git a/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php b/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php index 68e1abf..414568e 100644 --- a/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php +++ b/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php @@ -8,11 +8,12 @@ namespace Drupal\Tests\system\Unit\Breadcrumbs; use Drupal\Core\Link; -use Drupal\Core\Access\AccessResult; +use Drupal\Core\Access\AccessResultAllowed; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\TranslationInterface; use Drupal\Core\Url; use Drupal\Core\Utility\LinkGeneratorInterface; +use Drupal\Core\Routing\RouteMatchInterface; use Drupal\system\PathBasedBreadcrumbBuilder; use Drupal\Tests\UnitTestCase; use Symfony\Cmf\Component\Routing\RouteObjectInterface; @@ -339,7 +340,7 @@ public function testBuildWithUserPath() { public function setupAccessManagerToAllow() { $this->accessManager->expects($this->any()) ->method('check') - ->willReturn(TRUE); + ->willReturn(new AccessResultAllowed()); } protected function setupStubPathProcessor() { diff --git a/core/modules/taxonomy/src/TermBreadcrumbBuilder.php b/core/modules/taxonomy/src/TermBreadcrumbBuilder.php index 7c4cc89..818f51d 100644 --- a/core/modules/taxonomy/src/TermBreadcrumbBuilder.php +++ b/core/modules/taxonomy/src/TermBreadcrumbBuilder.php @@ -8,6 +8,7 @@ namespace Drupal\taxonomy; use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface; +use Drupal\Core\Breadcrumb\Breadcrumb; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Link; use Drupal\Core\Routing\RouteMatchInterface; @@ -67,9 +68,14 @@ public function build(RouteMatchInterface $route_match) { $breadcrumb[] = Link::createFromRoute($term->getName(), 'entity.taxonomy_term.canonical', array('taxonomy_term' => $term->id())); } $breadcrumb[] = Link::createFromRoute($this->t('Home'), ''); - $breadcrumb = array_reverse($breadcrumb); + $links = array_reverse($breadcrumb); - return $breadcrumb; + $breadcrumb = new Breadcrumb(); + // This breadcrumb builder is based on a route parameter, and hence it + // depends on the 'route' cache context. + $breadcrumb->setCacheContexts(['route']); + + return $breadcrumb->setLinks($links); } } diff --git a/core/tests/Drupal/Tests/Core/Breadcrumb/BreadcrumbManagerTest.php b/core/tests/Drupal/Tests/Core/Breadcrumb/BreadcrumbManagerTest.php index a2cbbf0..5e7add5 100644 --- a/core/tests/Drupal/Tests/Core/Breadcrumb/BreadcrumbManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Breadcrumb/BreadcrumbManagerTest.php @@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\Breadcrumb; +use Drupal\Core\Breadcrumb\Breadcrumb; use Drupal\Core\Breadcrumb\BreadcrumbManager; use Drupal\Tests\UnitTestCase; @@ -17,6 +18,13 @@ class BreadcrumbManagerTest extends UnitTestCase { /** + * The breadcrumb object. + * + * @var \Drupal\Core\Breadcrumb\Breadcrumb + */ + protected $breadcrumb; + + /** * The tested breadcrumb manager. * * @var \Drupal\Core\Breadcrumb\BreadcrumbManager @@ -36,6 +44,7 @@ class BreadcrumbManagerTest extends UnitTestCase { protected function setUp() { $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); $this->breadcrumbManager = new BreadcrumbManager($this->moduleHandler); + $this->breadcrumb = new Breadcrumb(); } /** @@ -43,7 +52,7 @@ protected function setUp() { */ public function testBuildWithoutBuilder() { $result = $this->breadcrumbManager->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface')); - $this->assertEquals(array(), $result); + $this->assertEquals(array(), $result['links']); } /** @@ -51,7 +60,8 @@ public function testBuildWithoutBuilder() { */ public function testBuildWithSingleBuilder() { $builder = $this->getMock('Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface'); - $breadcrumb = array('Test'); + $links = array('Test'); + $this->breadcrumb->setLinks($links); $builder->expects($this->once()) ->method('applies') @@ -59,17 +69,17 @@ public function testBuildWithSingleBuilder() { $builder->expects($this->once()) ->method('build') - ->will($this->returnValue($breadcrumb)); + ->willReturn($this->breadcrumb); $route_match = $this->getMock('Drupal\Core\Routing\RouteMatchInterface'); $this->moduleHandler->expects($this->once()) ->method('alter') - ->with('system_breadcrumb', $breadcrumb, $route_match, array('builder' => $builder)); + ->with('system_breadcrumb', $links, $route_match, array('builder' => $builder)); $this->breadcrumbManager->addBuilder($builder, 0); $result = $this->breadcrumbManager->build($route_match); - $this->assertEquals($breadcrumb, $result); + $this->assertEquals($links, $result['links']); } /** @@ -83,25 +93,26 @@ public function testBuildWithMultipleApplyingBuilders() { ->method('build'); $builder2 = $this->getMock('Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface'); - $breadcrumb2 = array('Test2'); + $links2 = array('Test2'); + $this->breadcrumb->setLinks($links2); $builder2->expects($this->once()) ->method('applies') ->will($this->returnValue(TRUE)); $builder2->expects($this->once()) ->method('build') - ->will($this->returnValue($breadcrumb2)); + ->willReturn($this->breadcrumb); $route_match = $this->getMock('Drupal\Core\Routing\RouteMatchInterface'); $this->moduleHandler->expects($this->once()) ->method('alter') - ->with('system_breadcrumb', $breadcrumb2, $route_match, array('builder' => $builder2)); + ->with('system_breadcrumb', $links2, $route_match, array('builder' => $builder2)); $this->breadcrumbManager->addBuilder($builder1, 0); $this->breadcrumbManager->addBuilder($builder2, 10); $result = $this->breadcrumbManager->build($route_match); - $this->assertEquals($breadcrumb2, $result); + $this->assertEquals($links2, $result['links']); } /** @@ -116,25 +127,26 @@ public function testBuildWithOneNotApplyingBuilders() { ->method('build'); $builder2 = $this->getMock('Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface'); - $breadcrumb2 = array('Test2'); + $links2 = array('Test2'); + $this->breadcrumb->setLinks($links2); $builder2->expects($this->once()) ->method('applies') ->will($this->returnValue(TRUE)); $builder2->expects($this->once()) ->method('build') - ->will($this->returnValue($breadcrumb2)); + ->willReturn($this->breadcrumb); $route_match = $this->getMock('Drupal\Core\Routing\RouteMatchInterface'); $this->moduleHandler->expects($this->once()) ->method('alter') - ->with('system_breadcrumb', $breadcrumb2, $route_match, array('builder' => $builder2)); + ->with('system_breadcrumb', $links2, $route_match, array('builder' => $builder2)); $this->breadcrumbManager->addBuilder($builder1, 10); $this->breadcrumbManager->addBuilder($builder2, 0); $result = $this->breadcrumbManager->build($route_match); - $this->assertEquals($breadcrumb2, $result); + $this->assertEquals($links2, $result['links']); } /**