Disclaimer: This is not a tutorial about the services module, but rather the object-oriented PHP concept of Services, the Service Container and Dependency Injection.
As a dyed-in-the-wool Drupal programmer looking to get into coding Drupal 8, there were a few modern subjects I had to familiarize myself with. Chief among them was the concept in Symfony and Drupal 8 called Services, which help keep your code decoupled and, in my opinion, easier to read.
A Service is simply an object, usually with one instance of each service's class for each service on a site. For example, Drupal 8 sites have a service for sending email, for logging errors, for making HTTP requests, and dozens of other common tasks.
While Services are objects, not all objects are suitable services. For example, a node is not a service; it is content. A View is also not a service. There is, however, a token service, which is a great example, since you only really need one token service for your entire site. The new Configuration Management systems in Drupal 8 use services extensively, and you will learn a bit about Config in this blog post. First, I'll show you how a common function, making links, uses services.
A Quick Example of Services
Services are used in a number of ways in Drupal. Many of the core systems are implemented as services, and many familiar core functions are now procedural wrappers for services.
For example, the l() function is now a wrapper for the LinkGenerator::generate() function. You call the function as such \Drupal::l(), because everything is namespaced now, but that's another blog post.
The source code of l() is:
This says to get the service called link_generator and call its generate() method. This way, you don't have to know where the link_generator class is defined, or even what class name it uses, but you can always find it if you need to.
An Example of Services
One way of thinking about Services, if this is a new concept for you, is the set-top-box on your TV. Whether it's made by Apple, Amazon, Google or Roku, these set-top boxes all have Services in common. I don't need to know the IP address and the API schema for Netflix in order to watch a film. If I'd rather watch HBO Go, I can ask my set-top-box to load that Service instead. The set-top-box is a Service Container that gives you a means of accessing any of these Services, obscuring the technical details. Drupal also uses the idea of a Service Container to abstract the loading and instantiation of service objects. The above code called the static::getContainer() method, returning the service container, which was then used to load link_gernerator.
A Practical Example of Services and the Service Container in Drupal 8
For the following example, let's assume we are creating a NetTV module for a Drupal 8 site that will give us some of the features we expect out of an internet TV service. In Drupal 8, you can put your custom modules in the /modules directory in the document root of your Drupal site. All your files for the following example will live in the /modules/custom/nettv directory.
Each Drupal 8 site comes with close to 300 Services defined by core. One example we might use in our set-top-box example is the Config service or Simple Configuration API. The config service is referenced by the /core/core.services.yml file, like so:
The ConfigFactory service is a class that can load Config information out of your Drupal site (Config has replaced variable_get() and variable_set() in Drupal 8). The code above maps an alias that can be invoked in a modulename.services.yml file with the string @config.factory. The @ symbol in this case tells Drupal's Service Container to find the ConfigFactory class and how it should be instantiated with the arguments line. The @config.storage is another service that knows where to store variables on your site - usually in YAML files. @config.typed helps to store different data types in config objects, and the @event_dispatcher helps to "lazy load" classes so loading code and instantiating objects only happens when the objects are actually needed. This reduces the overhead of your application and keeps the site fast and lean.
Part of the NetTv module will be a nettv.services.yml file that lives in your nettv module directory:
There are lots of things to point out about this file:
- There is a reference to a class called WatchCartoons, which is defined as a Service provided by this module.
- Just as with the definition of config.factory in core, we are naming a Service called nettv.watch_shows.
- The name of the Service is anything you want. It does not depend on the class
name, which keeps Services flexible.
- The WatchCartoons class you write does not need to be specific to this module or
even Drupal to be used because eventually the Service Container will call it by
using this YAML definition. i.e. You can bring in outside libraries from github
or other projects.
- By using this Service model and namespacing, you could have another module
on the site that defines a WatchCartoons class and never anticipate a conflict
between the two.
- When you add the ConfigFactory to your class' constructor, it will be added
using Dependency Injection. You can rest easy knowing that Drupal's Service
Container will take care of loading the correct code and getting the right object
to the constructor at the appropriate time.
Before you go any farther, I'm going to save you several steps by having you clone this github repository, which contains a copy of the code for this tutorial. Go and get a copy of this module and open it in your favorite editor, then come back and keep reading.
Constructing an Object that Depends on a Service
In WatchCartoons.php, look at the constructor for the class:
Notice that WatchCartoons is just a basic PHP class. It does not extend or implement anything specific to Drupal. In this case, we are using ConfigFactory but any code that uses this object does not know that. The implementation is kept inside our methods.
As soon as you create a new WatchCartoons object, it anticipates instance of ConfigFactory passed in so its methods can use it to retrieve and store config data. The argument to the constructor is type-hinted, so it must be a ConfigFactory object. We made sure this would work in our nettv.services.yml above.
When you write code that expects an instance of the ConfigFactory, you're using Dependency Injection.You don't instantiate the $config_factory; your system provides it with the Service Container. You'll sometimes see a Service Container referred to as a Dependency Injection Container.
Using the Config Factory as a Service
Finally, look at the next method on the WatchCartoons class:
The config itself is loaded from a YAML file inside your project config/install/nettv.basic_information.yml - until Drupal saves it. You can set the default values for your NetTV module like so:
The code in the getBasicInformation() method will load the values from Config and use them to print out the message with the configurable variables you define. Right now there is no way to change this config once it is loaded. That's another tutorial.
Bonus: there is also an instance of \Drupal::l() from before, just so you can see it in practice.
Using Your Class in Drupal
This wouldn't be a complete tutorial unless you could view the information about your NetTV service somewhere on your Drupal site, so we'll make a block. In your project you'll need a file at src/Plugin/Block/NetTVBlock.php with this code:
This code provides a Block you can enable through Drupal's admin interface. I assume you know how to place a block on a Drupal site, even in Drupal 8. Once you do, you will see the output from the getBasicInformation() method in the body of the block, like this:
Notice that while there are lots of OOP-isms in this project, we never really had to use the new keyword. We are not in the weeds instantiating objects, just snapping together Services, Config and finishing everything off in the Drupal UI. Once you learn how to work with Services and Dependency Injection, you'll be thinking at a higher level and you can focus on one specific task. This is one of the promises of Drupal to solve 80% of the problems for you and let you focus on the 20% of your project that is unique.
Using Service Container for a Block
In our Block definition, we used the create() method, which is part of the ContainerFactoryPluginInterface. This allows us to take advantage of the power of the Service Container when loading our block, and it keeps implementation-specific details out of the build() method of the block. If we wanted to switch out the WatchCartoons class with another one, we would simply need to make sure any new class also had a getBasicInformation() method and change the nettv.services.yml in our module. If the new class had a different means of getting this information, we still wouldn't have to touch the build() method, as long as we passed in a string. At the end of the day, this is just an example. Your mileage will vary.
The new concept of using a Service Container to instantiate objects should be a bit clearer to you now. Remember that it exists to give you a standard way to work with objects (Services) you need to include in your project. Also, remember that using it the right way will save you time and headaches while making it easier for non-Drupal programmers to read and understand your code. While this can feel like more work to a Drupal veteran, remember that procedural Drupal 7 code looks pretty dense to other coders. This method employs less "magic naming"; instead, it says what it does and how the system should go about loading and running everything.