Drupal 8's plugin system is one of the most flexible and widely used subsystems in Drupal. If you are a developer, chances that you will write plugins in Drupal 8 are high.
A couple of months ago I made a case in favor of unit tests in a series of articles. Today I have good news for you, your plugins are good candidates for testing! Before you get carried away by overexcitement, it's likely that your plugins depend on other parts of the system, and that complicates things a little bit. In these cases, it is a good idea to inject the services that include the dependencies you need. Dependency injection is an alternative to the static \Drupal::service
. If you don't inject your services you will have a hard time writing unit tests for your plugin's code.
There are, at least, two widely spread patterns in Drupal 8 in order to inject services into your objects. The first uses a service to inject services. The second uses a factory method that receives Drupal's container. Both of these involve the dependency injection container, although you can still inject services using other manual techniques.
Injection via services
When you declare a new service you can also specify the new service’s dependencies by using the arguments
property in the YAML file. You can even extend other services by using the parent
property, having the effect of inheriting all the dependencies from that one. There is thorough documentation about writing services on drupal.org. The following example defines a service that receives the entity manager:
services:
plugin.manager.network.processor:
class: Drupal\my_modyle\Plugin\MyPluginManager
arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@entity.manager']
You cannot use this pattern directly to inject services to a plugin, since your plugin cannot be a service. This is because services are one-instance classes, a global object of sorts. Plugins are –by their definition– multiple configurable objects of a given class. However, the plugin manager is a great candidate to be a service. If you declare your plugin manager as a service, and inject other services to it, then you are only one step away from injecting those services into the actual plugin instances. To do so you only need to do something similar to:
class MyPluginManager extends DefaultPluginManager {
protected $entityManager;
…
public function createInstance($plugin_id, array $configuration = array()) {
$instance = parent::createInstance($plugin_id, $configuration);
$instance->setEntityManager($this->entityManager);
return $instance;
}
}
As you can see (aside from the lack of docblocks for brevity), every time that your plugin manager creates an instance of a plugin it will set the entity manager for that particular instance. in this scenario you only need to write setEntityManager
in your plugin. This strategy is a mix of service injection and setter injection.
Factory injection
The other big injection pattern in Drupal 8 involves adding all your dependencies as arguments in the constructor for your class (also known as the constructor injection pattern). This pattern is very straightforward, you just pass in all the dependencies upon object creation. The problem arises for objects that Drupal instantiates for you, meaning that you don't get to do new MyClass($service1, $service2, ...)
. How do you pass the services to the constructor then? There is a convention in the Drupal community to use a factory method called create
. To implement it you need to write create
static method that will receive the dependency injection container as the first parameter. The create method should use the container to extract the services from it, and then call the constructor. This looks like:
class MyOtherClass {
…
public function __construct($service1, $service2, ...) {
// Store the services in class properties.
}
public static function create(ContainerInterface $container) {
// new static() means "Call the constructor of the current class".
// Check PHP’s late static binding.
return new static(
$container.get('service1'),
$container.get('service2'),
…
);
}
}
There is the remaining question of "who calls the create method with the container as the first parameter?". If Drupal is instantiating that object for you, then the system should know about the _create method pattern_ for that particular object.
The default plugin manager (\Drupal\Core\Plugin\DefaultPluginManager
) will use the container factory (\Drupal\Core\Plugin\Factory\ContainerFactory
) to create your plugin objects. It is very likely that your new plugin manager extends from the default plugin manager –via code scaffolding or manual creation–. That means that by default if you add a create
method to your plugin, it will be ignored. In order to have your new plugin manager use the create
pattern, the only thing your plugins need is to implement the ContainerFactoryPluginInterface
. In that case your plugin will look like:
class MyPluginBase extends PluginBase implements PluginInspectionInterface, ContainerFactoryPluginInterface {
protected $entityManager;
…
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityManager = $entity_manager;
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity.manager')
);
}
}
The plugin manager will detect that the object that it's about to create implements that interface and will call the create
method instead of the basic constructor.
The plugin system is so extensible that you may run into corners where those techniques are not enough. Just know that there are other ways to achieve this. Some of those, for instance, involve using a different factory in the plugin manager, but that is out of the scope of this article.
Sometimes the create factory method pattern can be considered a bit more robust, since it does not involve injecting the services to an object that doesn't need them –the plugin manager– only to pass them down to the plugin. Regardless of the approach that you use, you are now in a good position to start writing unit tests for your plugin class.