D8FTW: Breadcrumbs That Work
The upcoming 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:
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">// mymodule/lib/Drupal/mymodule/NewsBreadcrumbBuilder.php<p></p></span><span style="color: #007700">namespace </span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">mymodule</span><span style="color: #007700">;<p>use </p></span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">Core</span><span style="color: #007700">\</span><span style="color: #0000BB">Breadcrumb</span><span style="color: #007700">\</span><span style="color: #0000BB">BreadcrumbBuilderBase</span><span style="color: #007700">;<p>class </p></span><span style="color: #0000BB">NewsBreadcrumbBuilder </span><span style="color: #007700">extends </span><span style="color: #0000BB">BreadcrumbBuilderBase </span><span style="color: #007700">{<br> </span><span style="color: #FF8000">/**<br> * {@inheritdoc}<br> */<br> </span><span style="color: #007700">public function </span><span style="color: #0000BB">applies</span><span style="color: #007700">(array </span><span style="color: #0000BB">$attributes</span><span style="color: #007700">) {<br> if (</span><span style="color: #0000BB">$attributes</span><span style="color: #007700">[</span><span style="color: #DD0000">'_route'</span><span style="color: #007700">] == </span><span style="color: #DD0000">'node_page'</span><span style="color: #007700">) {<br> return </span><span style="color: #0000BB">$attributes</span><span style="color: #007700">[</span><span style="color: #DD0000">'node'</span><span style="color: #007700">]-></span><span style="color: #0000BB">bundle</span><span style="color: #007700">() == </span><span style="color: #DD0000">'news'</span><span style="color: #007700">;<br> }<br> }<p> </p></span><span style="color: #FF8000">/**<br> * {@inheritdoc}<br> */<br> </span><span style="color: #007700">public function </span><span style="color: #0000BB">build</span><span style="color: #007700">(array </span><span style="color: #0000BB">$attributes</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$breadcrumb</span><span style="color: #007700">[] = </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">l</span><span style="color: #007700">(</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Home'</span><span style="color: #007700">), </span><span style="color: #0000BB">NULL</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$breadcrumb</span><span style="color: #007700">[] = </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">l</span><span style="color: #007700">(</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'News'</span><span style="color: #007700">), </span><span style="color: #DD0000">'news'</span><span style="color: #007700">);<br> return </span><span style="color: #0000BB">$breadcrumb</span><span style="color: #007700">;<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
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->l()
and$this->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: