D8FTW: Hacking Core Without Killing Kittens
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.
Many years ago — back when Drupal 4.7 was cutting edge, I was a Drupal neophyte, and giant reptiles such as Sue the T-Rex (a client of Palantir's) still had meat on their bones — someone inquired on the Drupal development mailing list about how to modify or replace a function in Drupal core that didn't quite do what he wanted. His inquiry was met with polite apology for the fact that our guest was asking something utterly impossible: In most cases it was impossible to change what files Drupal would load other than adding modules; removing code that shipped with Drupal was impossible without modifying Drupal directly, a process affectionately known as "hacking core" and less affectionately for resulting in harm to adorably cute kittens (if only in effigy).
The problem wasn't PHP itself per se; the problem was the nature of procedural code, upon which Drupal of the day was based. Fast forward to Drupal 8, however, and that problem has been neatly resolved in most cases. The key is services, those objects we introduced in our last episode. Drupal 8, for the first time, effectively enables developers to "hack core without hacking core", and provides the tools necessary to allow modules to enable the same superpower.
The basics
Let's define a few first-principles that help make this magic trick possible. As discussed previously, a service is an object that contains some useful logic, is stateless, and in practice will usually only instantiate once (although it's not the service's job to enforce that). A service may well make use of another service, that is, it depends on it. If a service calls another service directly, that is a "hard dependency."
Generally speaking, hard dependencies are a bad idea as it means we cannot make use of a service without also making use of all of its dependencies, even if we just want to test it. Instead, we can make use of an interface, a PHP language construct that defines what methods an object has, and what they do, but not how they do it. We then pass an object that implements that interface to a service's constructor to be saved. By design we can pass any object that implements that interface and everything still works. That process of "pass objects into the constructor to be used later" is known as "dependency injection" (where "injection" is simply the needlessly medical way of saying "pass stuff in").
Of course, passing all of those objects to other objects can get quite tedious, which is why many people recoil at the effort necessary. Most projects of notable size, therefore, use something called a "dependency injection container," which is again an overly-pedantic way of saying "one object to rule them all, one object to find them, one object to inject them all and in the bootstrap instantiate them." (Sorry, I just got back from Drupal South in New Zealand; the Tolkien runs deep.)
Put less poetically, a Dependency Injection Container (DIC) is simply an easier place to wire up what objects get passed to what objects. The container will then take care of creating the object on-demand, including creating its dependent objects (if they haven't been created already) and passing them in (injecting them).
Now we get to the key point: That "wiring up" concept is not, technically, code. It's configuration. And configuration can be changed without breaking the code itself, as long as the code's assumptions (the interfaces of its dependencies) don't change. Therein lies the power of dependency injection: It makes the way an application is built configuration, not code.
Get the syringe
Let's look back at the breadcrumb example from episode 1. We registered our breadcrumb builder like so, in mymodule.services.yml
:
# mymodule.services.yml<br>services:<br> mymodule.breadcrumb:<br> class: Drupal\mymodule\NewsBreadcrumbBuilder<br> tags:<br> - { name: breadcrumb_builder, priority: 100 }
Those few lines of YAML are configuration instructions for the DIC. The particular syntax is a Symfony thing (there are other DIC implementations with their own syntax and quirks), but the concept is universal: The service named "mymodule.breadcrumb" is an instance of Drupal\mymodule\NewsBreadcrumbBuilder
, built on demand. The Symfony DependencyInjection component allows for that configuration to exist in YAML, or in code. It's also possible to change it from code. Each system that uses the Symfony DependencyInjection component implements it a little differently, but in Drupal it comes down to two simple interfaces.
A key feature of Drupal's DIC implementation is the "provider class". Every module may have one specially-named class in its root namespace named $CamelizedModuleServiceProvider
. That is, if our module is called "my_module" then the class will be named Drupal\my_module\MyModuleServiceProvider
. That class may implement one or both of two interfaces: Drupal\Core\DependencyInjection\ServiceProviderInterface
, which has a register() method, and Drupal\Core\DependencyInjection\ServiceModifierInterface
, which has an alter() method.
If that class implements ServiceProviderInterface
, then the register() method is passed the container definition and the module can register additional services using the container's API. See the Symfony documentation for the full details on what is available. In practice that's mostly only needed for registering compiler passes as the YAML file is much easier to work with. (More on compiler passes another time.) More useful is the ServiceModifierInterface
, whose alter() method will also be passed the container definition.
If you've worked with Drupal before, you probably know how this works. The container definition is built in one pass, and then passed to any "alter objects," just as alter hooks have worked in the past. In the alter() method, we can add services based on other services or, more realistically, change or even remove existing services.
Let's say we want to completely remove the "book" breadcrumb logic from core. We simply don't want that code to run at all, period. First we look up the name of that service in the book.services.yml file, where we find this:
book.breadcrumb:<br> class: Drupal\book\BookBreadcrumbBuilder<br> arguments: ['@entity.manager', '@access_manager', '@current_user']<br> tags:<br> - { name: breadcrumb_builder, priority: 701 }
Now we can add our own module with a service modifier and remove that book.breadcrumb service. All we need is the class below, placed in our module:
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">namespace </span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">no_book_breadcrumb</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">DependencyInjection</span><span style="color: #007700">\</span><span style="color: #0000BB">ServiceProviderBase</span><span style="color: #007700">;<br>use </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">DependencyInjection</span><span style="color: #007700">\</span><span style="color: #0000BB">ContainerBuilder</span><span style="color: #007700">;<p>class </p></span><span style="color: #0000BB">NoBookBreadcrumbServiceProvider </span><span style="color: #007700">extends </span><span style="color: #0000BB">ServiceProviderBase </span><span style="color: #007700">{<br> public function </span><span style="color: #0000BB">alter</span><span style="color: #007700">(</span><span style="color: #0000BB">ContainerBuilder $container</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$container</span><span style="color: #007700">-></span><span style="color: #0000BB">removeDefinition</span><span style="color: #007700">(</span><span style="color: #DD0000">'book.breadcrumb'</span><span style="color: #007700">);<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
Wait, that's it? Really? Really! Give it a try. This class completely removes the book breadcrumb builder from the system; it's now just taking up space on disk but has no runtime impact on the system at all. Seriously, how cool is that?
We can do much more than that, of course. For instance, rather than just removing one breadcrumb builder, let's take over the entire breadcrumb system and declare that, for our site, we have absolute control over breadcrumbs and no other module has any say. (We're professionals; don't try this at home. Or do. It's kinda fun.) We can take over the entire breadcrumb manager, like so:
<span style="color: #000000"><span style="color: #0000BB"><?php<br> </span><span style="color: #007700">public function </span><span style="color: #0000BB">alter</span><span style="color: #007700">(</span><span style="color: #0000BB">ContainerBuilder $container</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$breadcrumb </span><span style="color: #007700">= </span><span style="color: #0000BB">$container</span><span style="color: #007700">-></span><span style="color: #0000BB">getDefinition</span><span style="color: #007700">(</span><span style="color: #DD0000">'breadcrumb'</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$breadcrumb</span><span style="color: #007700">-></span><span style="color: #0000BB">setClass</span><span style="color: #007700">(</span><span style="color: #DD0000">'\Drupal\no_book_breadcrumb\BreadcrumbMaster'</span><span style="color: #007700">);<br> }<br></span><span style="color: #0000BB">?></span></span>
Now, the system will ignore the core BreadcrumbManager
entirely and use our class instead. Ideally every service has a separate interface, but BreadcrumManager
doesn't as of this writing. (Someone file a patch!) Instead we'll just subclass it: