D8FTW: DIC Core Without Killing Kittens

D8FTW: DIC 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
services:
  mymodule.breadcrumb:
    class: Drupal\mymodule\NewsBreadcrumbBuilder
    tags:
      - { 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, andDrupal\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:
  class: Drupal\book\BookBreadcrumbBuilder
  arguments: ['@entity.manager', '@access_manager', '@current_user']
  tags:
    - { 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:

<?php
namespace Drupal\no_book_breadcrumb;

use

Drupal\Core\DependencyInjection\ServiceProviderBase;
use Drupal\Core\DependencyInjection\ContainerBuilder;

class

NoBookBreadcrumbServiceProvider extends ServiceProviderBase {
  public function alter(ContainerBuilder $container) {
    $container->removeDefinition('book.breadcrumb');
  }
}
?>

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:

<?php
  public function alter(ContainerBuilder $container) {
    $breadcrumb = $container->getDefinition('breadcrumb');
    $breadcrumb->setClass('\Drupal\no_book_breadcrumb\BreadcrumbMaster');
  }
?>

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:
<?php
class BreadcrumbMaster extends BreadcrumbManager {
  public function addBuilder(BreadcrumbBuilderInterface $builder, $priority) {
    return;
  }

  public function

build(array $attributes) {
    $breadcrumb[] = l(t('Home'), NULL);
    $breadcrumb[] = l(t('Me'), 'me');
    $breadcrumb[] = l(t('Mine'), 'me/mine');
    $breadcrumb[] =l(t('All Mine'), 'me/mine/all');
    return $breadcrumb;
  }
}
?>

First we ignore any queued up sub-builders to save a little memory. Then we override the build() method and always return our own, not particularly inventive breadcrumb. With that little bit of code, we've now set the breadcrumb to be the same on every single page in the site. OK, it's not an especially useful thing to do; presumably your own site-specific module would have site-specific logic in there.

(Side note: At the moment that class is using the legacy l() and t() functions for brevity, since the l() and t() methods are not in this class's parent. We'll explain more about how those utility methods work in a later installment, as their design is about to change to something much easier to use. Stay tuned.)

Inject all the things

This sort of deep manipulation of the system is something that was simply not possible before in Drupal. All the alter hooks in the world couldn't do this much. The concepts sound abstract, but when you get right down to it, it's actually quite simple and the code is surprisingly short. Remember: We just removed an entire service from Drupal with less than ten lines of code.

We've been using breadcrumbs as our example here, but the same principle applies to any code that is properly encapsulated into services that make use of dependency injection. That's most of the system. Certainly it's quite possible to manipulate service definitions in such a way that the system won't function (removing the entity system entirely is possible but not recommended, for instance), but a good surgical developer can now modify deep parts of Drupal core with no cost at runtime, without hacking core, and without getting the ASPCA upset about kittens.

That's true of modules as well. Well-behaved contributed modules in Drupal 8 should also be built mostly of services for exactly this reason (among others). Leveraging services for most business logic offers an unprecedented level of flexibility, both for the module developer and for users of the module. Organizing code into small, clean services also helps with stability and avoiding bugs, as loosely coupled objects are easier to debug.

To date not all of core has been refactored into clean services. Core is not small, and when dealing with a legacy codebase it can be challenging to determine exactly how the services should be structured. As a result, there may be parts of core that are more tightly coupled than they seem at first glance, or more than we want them to be. As Drupal's collective experience with service-based architecture improves we should see more and more code able to leverage the benefits discussed here.

Of course, another advantage of services is that they're easy to test. But that's a topic for next time ...

Bạn thấy bài viết này như thế nào?: 
Average: 5 (1 vote)
Ảnh của Khanh Hoang

Khanh Hoang - Kenn

Kenn is a user experience designer and front end developer who enjoys creating beautiful and usable web and mobile experiences.

Tìm kiếm bất động sản

 

Advertisement

 

jobsora

Dich vu khu trung tphcm

Dich vu diet chuot tphcm

Dich vu diet con trung

Quảng Cáo Bài Viết

 
New Samsung TV Ad Takes a Stab at iPhone Users

New Samsung TV Ad Takes a Stab at iPhone Users

Apple and Samsung have been involved in quite the court battle over the last 6 months. Apple accused Samsung of copying the trademarked design of its products, and since then, the two companies have been in and out of court.

LG Prada 3.0 start selling in UK on february

LG Prada 3.0 start selling in UK on february

We’ve already talked about the LG Prada 3.0‘s quest to the UK. The sexy device has already been announced by few phone retailers, which will offer it either unlocked or with a carrier contract.

Hướng dẫn Apply Drupal Patches với Patch Manager

Hướng dẫn Apply Drupal Patches với Patch Manager

Have you ever updated your Drupal site only to suddenly have errors?

Công ty diệt chuột T&C

 

Diet con trung