A Peek at Traits in Drupal 8

Image
It's a Drupal 8 Blog Post from Drupalize.Me

Update: April 21, 2015. This article was originally titled "Dependency Injection with Traits in Drupal 8." Turns out, saying that this example was Dependency Injection was quite incorrect, so I've updated the title to be, "A Peek at Traits in Drupal 8." The rest of the article's content remains the same, an in-depth look at how Drupal makes use of PHP Traits, and how the Traits in question use a pattern that allows their underlying services to remain decoupled enough that they are still easy to mock for testing. Similar goals to the Dependency Injection pattern, but a distinctly not DI.

I want to thank Drupal community member Larry Garfield for his helpful explanation in the comments of this blog post, which prompted me to explore a little more and learn something new. It's great that we have a community to help us along the way. Learning this stuff can be hard, but it's also a lot of fun. —Amber Matz

Have you ever gone to the grocery store and bought something that you already had in your pantry? Sometimes I forget to peek into the dark corners of my cupboard before heading out to the store. Then as I stare at the pasta sauces lining the store shelves, I wonder to myself, do I already have spaghetti sauce? Should I buy another jar? I don’t know because I didn’t look in the pantry cupboard beforehand, so I buy another jar. When I get home, I discover that I now have five jars of the same spaghetti sauce, with perhaps slight variations in flavor.

Part of learning Drupal’s API is learning about “what’s in the pantry.” In Drupal 8, that pantry is configured quite a bit differently than before. Instead of getting the whole warehouse of Drupal functions on every page load, functions—well, now methods—are contained in objects which are defined by classes. Most, if not all, of these classes, which exist in their own PHP files, can be extended and many of them are specifically designed to be extended. These extensible classes are the pantries. They contain properties and methods that we can just use when we need them, in the classes that extend them. When we extend these classes, we need to make sure we peek inside to see what’s available before we go elsewhere for something that might already be in the cupboard.

Image
Should have checked the pantry first...


Illustration by Justin Harrell

I ran into this problem when I was working on porting a custom Drupal 7 form module, Form Fun, to Drupal 8, with the assistance of the Drupal Module Upgrader project on Drupal.org. One of the really helpful things that Drupal Module Upgrader does for you is create a static HTML file with links to the change records that pertain to the changes needed, or made, in your module.

The thing about change records though, is that they’re meant to be super generic. It doesn’t know which class you might be extending and thus, what methods might be available to you in the parent class. Since it’s possible in PHP to call a method any number of ways, it’s easy to do something that works, but that might not be the best practice. This is what I discovered when creating links to display on my main Form Fun page, the content of which is generated through a controller.

The change record "l() and url() are removed in favor of a routing based URL generation API" indicated that two new factory methods were introduced and gave examples. I simply used the code example provided to create the following code in my DefaultController class:


$url = Url::fromRoute('form_fun.cake_page');
$text = 'Death, or cake? (The basics)';
$links[] = \Drupal::l(t($text), $url);

Just to quickly explain what’s going on here:

$url = Url::fromRoute(‘form_fun.cake_page’)

This calls a static factory method (fromRoute) in \Drupal\Core\Url which I can use to get a url with a route name. I pass in the name of my route, which I have defined in my form_fun.routing.yml file. It returns an object that defines a url, given the route provided. Here’s a screenshot of a dpm($url);:

Image
Inspecting the \Drupal\Core\Url object
$text = 'Death, or cake? (The basics)';

This simply assigns a string of text to a variable.

$links[] = \Drupal::l(t($text), $url);

This adds to an array called $links, a generated link given a string of translatable text and a url object based on a given route name (‘form_fun.cake_page’).

Now this technically works. But it’s not best practice. Why? Because now my class is tightly coupled, or, to state it differently, quite dependent, on the global method \Drupal::l and the global function t(), which is still kicking around Drupal 8. Now it’s not the end of the world because it is a Drupal global function after all, but that’s the problem — it requires a whole lot more of Drupal than we actually need here. It’s like saying, hey, they’ve got spaghetti sauce at the store, why should I check the pantry to see if I already have it? Plus, by making my code dependent on this global \Drupal class, my class becomes a lot more difficult to unit test.

But it’s going to be ok! Because it turns out that my code lives in a class that extends ControllerBase.


<php?
/**
 * @file
 * Contains \Drupal\form_fun\Controller\DefaultController.
 */

namespace Drupal\form_fun\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Url;

/**
 * Default controller for the form_fun module.
 */
class DefaultController extends ControllerBase {

  public function form_fun_page() {
    // List of links to the other forms.

    // Death or Cake? (The basics)
    $links[] = $this->l($this->t('Death, or cake? (The basics)'),
      new Url('form_fun.cake_page')
    );
...more code goes here...
}

Let’s take a look inside the ControllerBase pantry cupboard and see what’s already available for me to use!

In my PhpStorm IDE, I’ll ctrl-click (right-click) on ControllerBase and in the Go To… menu, select Declaration (keyboard shortcut: Cmd-B).

Image
phpStorm - Go To... Declaration

That gets me here:


abstract class ControllerBase implements ContainerInjectionInterface {

use StringTranslationTrait;
use LinkGeneratorTrait;
use UrlGeneratorTrait;

...more code goes here...
}

The first thing I notice are these three lines:


use StringTranslationTrait;
use LinkGeneratorTrait;
use UrlGeneratorTrait;

Notice the common term at the end of each of these, Trait. What’s a Trait? It’s a way to reuse code that your child classes can then inherit without duplication. Familiar with Sass? Used @extend before? It’s kind of like that. You declare a bunch of code inside a Trait definition and when you use it, it’s just like you copied and pasted that code into your class, except you didn’t, and now you don’t have a maintenance nightmare of copying in code that might change in the future. You’re reusing it, without duplication. You’re keeping it DRY. (You Don’t Repeat Yourself.)

Since my DefaultController class extends ControllerBase, that means that I inherit these Traits and the methods inside of them. This is exciting because I can truly take advantage of the benefits of inheritance that object-oriented PHP is capable of providing. I don’t have to rely on the global \Drupal methods. I don’t have to run off to the grocery store to get things I already have nearby in my pantry.

When I go to the declaration for the StringTranslationTrait (same process, in PhpStorm, put your cursor in the word StringTranslationTrait and cmd-B), the first method I see is t(). Hey, that means I don’t need Drupal’s global t() function! Peeking inside the LinkGeneratorTrait, I’ve got an l() method. That means I don’t need \Drupal::l. And inside the UrlGenerationTrait, a url() method that takes a route name as an argument and generates a url. That means I don’t need \Url::fromRoute to generate a Url. (I’ll get to why I’ll still need to use \Drupal\Core\Url in a moment.)

This means that I can, and should, refactor my code to use $this->t and $this->l, since I’m inheriting these from the LinkGeneratorTrait and StringTranslationTrait being used in the ControllerBase. So now my code is as follows:


$links[] = $this->l($this->t('Death, or cake? (The basics)'), new Url('form_fun.cake_page'));

My code needs the l() and t() functionality. It’s dependent on them. In fact many implementers of controllers will find themselves in need of these methods, which is why they’re provided in the ControllerBase class through those Traits. But instead of hardcoding these dependencies into my class by calling a static method of \Drupal::l and using the global t() function, I am taking advantage of the fact that these dependencies have been, in a cleaner and more reusable way, virtually copied and pasted into the ControllerBase through the use of Traits.

Instead of tightly coupling my code to the global \Drupal class and a global t() function, my code is abstracted out (using $this) to take advantage of the closer-to-home methods that I’ve inherited from the parent class. I’m using what’s in my pantry at home, instead of paying for the trip to the grocery store two miles away.

Now what about this Url class that I’m passing in as an argument to $this->l()? Why can’t I use $this->url instead? Well, on a super-practical level, $this->url just doesn’t return what is required by $this->l. It returns a string of the path of the url, not an instance of \Drupal\Core\Url, which is what is required.

\Drupal\Core\Url replaces an array of information about a url. This Url object can be created given a route name, for example. Since it isn’t doing anything to this data, it’s just providing information about it, it’s not something that we’d want to swap out. There’s no functionality to swap because it’s not doing any actions, it’s simply defining this piece of data.

So, since the outside methods upon which my class is dependent have been inherited from our parent class through the use of Traits, I am able to make my class a bit more loosely coupled to the global Drupal methods or functions than it was before.

I learned many things from this refactoring exercise:

  • Sample code in change records will be necessarily generic. They will show you how to call a method statically (you’ll know this from the use of double-colons (::)). They will be technically correct but not necessarily utilizing best practices, since there’s no way to assume what methods and properties you might be inheriting.
  • Search Drupal documentation (api.drupal.org and the handbook) for recommended classes to extend before either making up your own classes (reinventing the wheel) or statically calling methods in the global \Drupal class. You may even get pointed in the right direction in the change records themselves. This is often the case.
  • If you’re working from examples or generated code in which a class, like my DefaultController, generated by Drupal Module Upgrader, extends another class, ControllerBase, don’t forget to look inside the parent class to see what properties and methods you have available to you.
  • When use statements exist in a class, take a peek at those things as well. In my case, these were a relatively new concept to me, Traits—reusable code that was injected into the ControllerBase class and which my class inherited.
  • Ask questions from folks who are a step or two ahead of you in learning Drupal 8. Much of my learning happened from asking questions and getting insightful answers from awesome coworkers! Have someone else look at your code. Even if it “works”, there might still be a better way.
  • I originally thought that using Traits in this way was a form of Dependency Injection, but it's not!

The key takeaway when it comes to calling methods and properties inside your class is don’t forget to check the pantry for what you already have close at hand before going the \Drupal store.

Resources:

Comments

We are a group of volunteers and starting a brand new scheme in our community.
Your website offered us with helpful info to work on.
You have performed a formidable process and our entire neighborhood shall be grateful to you.

Oh that's awesome. It saves doing it the other way which is implementing the containerinjectioninterface and a static create method. This is how you dependency inject with FormBase and some plugins.

Please be very careful here! Traits are not, in any way, a form of Dependency Injection.

If you look at StringTranslationTrait, you'll see this code:

if (!$this->stringTranslation) {
$this->stringTranslation = \Drupal::service('string_translation');
}

That is, under the hood it's still calling out to \Drupal. The rest of the class includes the ability to inject an alternative implementation via a method for use in testing, but that's still second-best. You still have the same issues that Amber talks about, just further separated from you.

Those selected traits have been included in a few select base classes (ControllerBase and FormBase in particular) precisely because they're so commonly needed in those cases (controllers and forms). They're not something you should commonly use. In fact, I would go as far as saying you should *never ever* write your own pseudo-DI traits like this in your own modules, ever. Use proper DI via the container, or use the create() method when in a Controller, Form, or Plugin.

That means to the anonymous commenter above who said "yay, now I don't need to use create()", sorry, you do. :-) That is the correct, testable way to handle dependencies for Controllers, Forms, and Services. (For actual service objects in the container, just use the contanier.)

There's one exception there: The translatable strings exracting tool looks for specific method names, so if your service needs to have translatable strings in it (most do not, actually), then you should include the StringTranslationTrait, *and* inject the translation service via the container. That way you get a nicely unit testable, mockable service with clean DI and all of the utility methods that make your life easier and that the extrator looks for.

Once again: Please please please do not mistake using a trait for Dependency Injection. It's not. It's not even in the same ballpark. It is, actually, very close to what Amber described it at: Compiler-assisted copy-and-paste. Copy-and-paste is not Dependency Injection. :-)

See also: http://www.garfieldtech.com/blog/beyond-abstract

Larry, I want to thank you for your comment and explanation. Boy, this stuff can be hard to grasp sometimes, but it is fun to tackle. Thank you for your help here. I have updated the article's title and removed references to DI in the body and added a little note to the top as well. Thanks again for taking the time to comment.

About us

Drupalize.Me is the best resource for learning Drupal online. We have an extensive library covering multiple versions of Drupal and we are the most accurate and up-to-date Drupal resource. Learn more