Blue background with text reading D8FTW!

D8FTW: Breadcrumbs That Work

How breadcrumbs in Drupal 8 not only rock, but exemplify the benefits of Drupal's new architecture.

Drupal 8 includes many changes large and small that will improve the lives of site builders, site owners, and developers. In a series we're calling, "D8FTW," we look at some of these improvements in more detail, including and especially the non-obvious ones.

Breadcrumbs have long been the bane of every Drupal developer's existence. In simple cases, they work fine out of the box. Once you get even a little complex, though, they get quite unwieldy.

That's primarily because Drupal 7 and earlier don't have a breadcrumb system. They just have an effectively-global value that modules can set from "anywhere," and some default logic that tries to make a best-guess based on the menu system if not otherwise specified. That best guess, however, is frequently not enough and letting multiple modules or themes specify a breadcrumb "anywhere" is a recipe for strange race conditions. Contrib birthed a number of assorted tools to try to make breadcrumbs better but none of them really took over, because the core system just wasn't up to the task.

Enter Drupal 8. In Drupal 8, breadcrumbs have been rewritten from the ground up to use the new system's architecture and style. In fact, breadcrumbs are now an exemplar of a number of "new ways" in Drupal 8. The result is the first version of Drupal where we can proudly say "Hooray, breadcrumbs rock!"

More power to the admin

There are two key changes to how breadcrumbs work in Drupal 8. The first is how they're placed. In Drupal 7 and earlier, there was a magic $breadcrumb variable in the page template. As a stray variable, it didn't really obey any rules about placement, visibility, caching, or anything else. That made sense when there were 100 modules and a slightly fancy blog was the typical Drupal use case. In a modern enterprise-ready CMS, though, having lots of special-case exceptions like that hurts the overall system.

In Drupal 8, breadcrumbs are an ordinary block. That’s it. Site administrators can place that block in any region they'd like, control visibility of it, even put it on the page multiple times right from the UI. (The new Blocks API makes that task easy; more on that another time.) And any new functionality added to blocks, either by core or contrib, will apply equally well to the breadcrumb block as to anything else. Breadcrumbs are no longer a unique and special snowflake.

More predictability to the developer

The second change is more directly focused at developers. Gone are the twin menu_set_breadcrumb() and menu_get_breadcrumb functions that acted as a wrapper around a global variable. Instead, breadcrumbs are powered by a chained negotiated service.

A chained negotiated whosawhatsis? Let's define a few new terms, each of which introduces a crucial change in Drupal 8. A service is simply an object that does something useful for client code and does so in an entirely stateless fashion. That is, calling it once or calling it a dozen times with the same input will always yield the same result. Services are hugely important in Drupal 8. Whenever possible, logic in a modern system like Drupal 8 should be encapsulated into services rather than simply inlined into application code somewhere else. If a service requires another service, then that dependency should be passed to it in its constructor and saved rather than manually created on the fly. Generally, only a single instance of a service will exist throughout the request but it's not hard-coded to that.

A negotiated service is a service where the code that is responsible for doing whatever needs to be done could vary. You call one service and ask it to do something, and that service will, in turn, figure out some other service to pass the request along to rather than handling it itself. That's an extremely powerful technique because the whole "figuring out" process is completely hidden from you, the developer. To someone writing a module, whether there's one object or 50 responsible for determining breadcrumbs is entirely irrelevant. They all look the same from the caller’s point of view.

The simplest and most common "figuring out" mechanism is a pattern called Chain of Responsibility. In short, the system has a series of objects that could handle something, and some master service just asks each one, in turn, "Hey, you got this?" until one says yes, then stops. It's up to each object to decide in what circumstances it cares.

Breadcrumbs in Drupal 8 implement exactly this pattern. The breadcrumb block depends on the breadcrumb_manager service, which by default is an object of the BreadcrumbManager class. That object is simply a wrapper around many objects that implement BreadcrumbBuilderInterface, which it implements itself as well. When the breadcrumb block calls $breadcrumb_manager->build() that object will simply forward the request on to one of the other breadcrumb builders it knows about; including those you, as a module developer, provide.

Core ships with five such builders out of the box. One is a default that will build a breadcrumb off of the path and always runs last. Then there are four specialty builders for forum nodes, taxonomy term entity pages, stand-alone comment pages, and book pages. Core does not currently ship with one that uses the menu tree — as was the case in Drupal 7 — because the menu system is still in flux and calculating that was quite difficult. That could certainly be re-added in contrib or later in core, however.

Let's try it!

Let's add our own new builder that will make all "News" nodes appear as breadcrumb children of a View we've created at /news. Although all we need to do is implement the BreadcrumbBuilderInterface, it's often easier to start from the BreadcrumbBuilderBase utility class. (Side note: This may turn into one or more traits before 8.0 is released.) We'll add a class to our module like so:

<!--?php
// mymodule/lib/Drupal/mymodule/NewsBreadcrumbBuilder.php

namespace Drupal\mymodule;

use Drupal\Core\Breadcrumb\BreadcrumbBuilderBase;

class NewsBreadcrumbBuilder extends BreadcrumbBuilderBase {
  /**
   * {@inheritdoc}
   */
  public function applies(array $attributes) {
    if ($attributes['_route'] == 'node_page') {
      return $attributes['node']--->bundle() == 'news'; } } /** * {@inheritdoc} */ public function build(array $attributes) { $breadcrumb[] = $this-&gt;l($this-&gt;t('Home'), NULL); $breadcrumb[] = $this-&gt;l($this-&gt;t('News'), 'news'); return $breadcrumb; } } ?&gt;

 

Two methods, that's it! In the applies() method, we are passed an array of values about the current request. In our case, we know that this builder only cares about showing the node page, and only when the node being shown is of type "news". So we return TRUE if that's the case, indicating that our build() method should be called, or FALSE to say "ignore me!" The second method, then, just builds the breadcrumb array however we feel like. In this case we're just going to hard code a few links but we could use whatever logic we want, safe in the knowledge that our code, and only our code, will be in control of the breadcrumb on this request. A few important things to note:

 

  • The $this-&gt;l() and $this-&gt;t() methods are provided by the base class, and function essentially the same as their old procedural counterparts but are injectable; we'll discuss what that means in more detail in a later installment.
  • The breadcrumb does not include the name of the page we're currently viewing. The theme system is responsible for adding that (or not).

Now we need to tell the system about our class. To do that, we define a new service (remember those?) referencing our new class. We'll do that in our *.services.yml file, which exists for exactly this purpose:

# mymodule.services.yml services: mymodule.breadcrumb: class: Drupal\mymodule\NewsBreadcrumbBuilder tags: - { name: breadcrumb_builder, priority: 100 }

 

Similar to an "info hook" in previous Drupal versions, we're defining a service named mymodule.breadcrumb. It will be an instance of our breadcrumb class. If necessary we could pass arguments to our our class's constructor as well. Importantly, though, we also tag the service. Tagged services are a feature of the Symfony DependencyInjection component specifically and tell the system to automatically connect our builder to the breadcrumb manager. The priority specifies in what order various builders should be called, highest first. In case two applies() methods might both return true, whichever builder has the higher priority will be used and the other ignored.

 

That's a wrap

And that's it. One simple class, a few lines of YAML, and we've slotted our new breadcrumb rule into the system. What have we gained over the old system?

  • There's no more mystery about who called drupal_set_breadcrumb() when, and who overrode who. It's all clear and predictable (and documented, if you look at the priorities). If you want to know where a given breadcrumb is coming from, just look for any services tagged breadcrumb_builder or classes that implement BreadcrumbBuilderInterface. A BreadcrumbBuilder object is the only place it could come from.
  • All of the breadcrumb logic is in classes that are easily unit testable with PHPUnit.
  • Breadcrumbs are no longer a special snowflake for front-end developers who can now treat them the exact same as any other block. That includes disabling them entirely, or selectively.
  • The builders are very extensible. Modules can very easily register builders that rely on user customization or other services to build very robust breadcrumb management tools without bumping into any existing code or assumptions.

The chain-of-responsibility pattern is used in a number of places in Drupal 8, including the process for determining the active theme as well as user authentication, among others. All work in essentially the same way. It's a good approach any time different systems may want to be responsible for a task in different situations, but only one will be responsible at a time. We'll likely see more examples in both core and contrib.

Finally, because everything is a service, it is possible for a site-specific module to completely disable other modules' breadcrumb logic without hacking them. In fact, you could take over the entire breadcrumb system completely on your site and become the One True Breadcrumb(tm) without so much as touching a core file. But we'll discuss how to do that in our next installment.

Any other non-obvious wins in Drupal 8 that deserve a mention? Let us know and we'll add it to the list.