Updating the Search API to D8 – Part 5: Using plugin derivatives
The greatest thing about all the refactoring in Drupal 8 is that, in general, a lot of those special Drupalisms used nowhere else were thrown out and replaced by sound design patterns, industry best practices and concepts that newcomers from other branches of programming will have an easy time of recognizing and using. While I can understand that this is an annoyance for some who have got used to the Drupalisms (and who haven't got a formal education in programming), as someone with a CS degree and a background in Java I was overjoyed at almost anything new I learned about Drupal 8, which, in my opinion, just made Drupal so much cleaner.
But, of course, this has already been discussed in a lot of other blog posts, podcasts, sessions, etc., by a lot of other people.
What I want to discuss today is one of the few instances where it seems this principle was violated and a new Drupalism, not known anywhere else (as far as I can tell, at least – if I'm mistaken I'd be grateful to be educated in the comments), introduced: plugin derivatives.
Probably some of you have already seen it there somewhere, especially if you were foolish enough to try to understand the new block system (if you succeeded, I salute you!), but I bet (or, hope) most of you had the same reaction as me: a very puzzled look and an involuntary “What the …?” In my case, this question was all the more pressing because I first stumbled upon plugin derivatives in my own module – Frédéric Hennequin had done a lot of the initial work of porting the module and since there was a place where they fit perfectly, he used them. Luckily, I came across this in Szeged where Bram Goffings was close by and could explain this to me slowly until it sank in. (Looking at the handbook documentation now, it actually looks quite good, but I remember that, back then, I had no idea what they were talking about.)
So, without (even) further ado, let me now share this arcane knowledge with you!
What, and why, are plugin derivatives?
The problem
Plugin derivatives, even though very Drupalistic (?), are actually a rather elegant solution for an interesting (and pressing) problem: dynamically defining plugins.
For example, take Search API's "datasource" plugins. These provide item types that can be indexed by the Search API, a further abstraction from the "entity" concept to be able to handle non-entities (or, indeed, even non-Drupal content). We of course want to provide an item type for each entity type, but we don't know beforehand which entity types there will be on a site – also, since entities can be accessed with a common API we can use the same code for all entity types and don't want a new class for each.
In Drupal 7, this was trivial to do:
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/**<br> * Implements hook_search_api_item_type_info().<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">search_api_search_api_item_type_info</span><span style="color: #007700">() {<br> </span><span style="color: #0000BB">$types </span><span style="color: #007700">= array();<br> foreach (</span><span style="color: #0000BB">entity_get_property_info</span><span style="color: #007700">() as </span><span style="color: #0000BB">$type </span><span style="color: #007700">=> </span><span style="color: #0000BB">$property_info</span><span style="color: #007700">) {<br> if (</span><span style="color: #0000BB">$info </span><span style="color: #007700">= </span><span style="color: #0000BB">entity_get_info</span><span style="color: #007700">(</span><span style="color: #0000BB">$type</span><span style="color: #007700">)) {<br> </span><span style="color: #0000BB">$types</span><span style="color: #007700">[</span><span style="color: #0000BB">$type</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'name' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$info</span><span style="color: #007700">[</span><span style="color: #DD0000">'label'</span><span style="color: #007700">],<br> </span><span style="color: #DD0000">'datasource controller' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'SearchApiEntityDataSourceController'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'entity_type' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$type</span><span style="color: #007700">,<br> );<br> }<br> }<br> return </span><span style="color: #0000BB">$types</span><span style="color: #007700">;<br>}<br></span><span style="color: #0000BB">?></span></span>
Since plugin definition happens in a hook, we can just loop over all entity types, set the same controller class for each, and put an additional entity_type
key into the definition so the controller knows which entity type it should use.
Now, in Drupal 8, there's a problem: as discussed in the previous part of this series, plugins now generally use annotations on the plugin class for the definition. That, in turn, would mean that a single class can only represent a single plugin, and since you can't (or at least really, really shouldn't) dynamically define classes there's also not really any way to dynamically define plugins.
One possible workaround would be to just use the alter hook which comes with nearly any plugin type and dynamically add the desired plugins there – however, that's not really ideal as a general solution for the problem, especially since it also occurs in core in several places. (The clearest example here are probably menu blocks – for each menu, you want one block plugin defined.)
The solution
So, as you might have guessed, the solution to this problem was the introduction of the concept of derivatives. Basically, every time you define a new plugin of any type (as long as the manager inherits from DefaultPluginManager
you can add a deriver
key to its definition, referencing a class. This deriver class will then automatically be called when the plugin system looks for plugins of that type and allows the deriver to multiply the plugin's definition, adding or altering any definition keys as appropriate. It is, essentially, another layer of altering that is specific to one plugin, serves a specific purpose (i.e., multiplying that plugin's definition) and occurs before the general alter hook is invoked.
Hopefully, an example will make this clearer. Let's see how we used this system in the Search API to solve the above problem with datasources.
How to use derivatives
So, how do we define several datasource plugins with a single class? Once you understand how it works (or what it's supposed to do) it's thankfully pretty easy to do. We first create our plugin like normally (or, just copy it from Drupal 7 and fix class name and namespace), but add the deriver
key and internally assume that the plugin definition has an additional entity_type
key which will tell us which entity type this specific datasource plugin should work with.
So, we put the following into src/Plugin/SearchApi/Datasource/ContentEntityDatasource.php
:
<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">search_api</span><span style="color: #007700">\</span><span style="color: #0000BB">Plugin</span><span style="color: #007700">\</span><span style="color: #0000BB">SearchApi</span><span style="color: #007700">\</span><span style="color: #0000BB">Datasource</span><span style="color: #007700">;<br><br></span><span style="color: #FF8000">/**<br> * @SearchApiDatasource(<br> * id = "entity",<br> * deriver = "Drupal\search_api\Plugin\SearchApi\Datasource\ContentEntityDatasourceDeriver"<br> * )<br> */<br></span><span style="color: #007700">class </span><span style="color: #0000BB">ContentEntityDatasource </span><span style="color: #007700">extends </span><span style="color: #0000BB">DatasourcePluginBase </span><span style="color: #007700">{<br><br> public function </span><span style="color: #0000BB">loadMultiple</span><span style="color: #007700">(array </span><span style="color: #0000BB">$ids</span><span style="color: #007700">) {<br> </span><span style="color: #FF8000">// In the real code, this of course uses dependency injection, not a global function.<br> </span><span style="color: #007700">return </span><span style="color: #0000BB">entity_load_multiple</span><span style="color: #007700">(</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">pluginDefinition</span><span style="color: #007700">[</span><span style="color: #DD0000">'entity_type'</span><span style="color: #007700">], </span><span style="color: #0000BB">$ids</span><span style="color: #007700">);<br> }<br><br> </span><span style="color: #FF8000">// Plus a lot of other methods …<br><br></span><span style="color: #007700">}<br></span><span style="color: #0000BB">?></span></span>
Note that, even though we can skip even required keys in the definition (like label
here), we still have to set an id
. This is called the "plugin base ID" and will be used as a prefix to all IDs of the derivative plugin definitions, as we'll see in a bit.
The deriver
key is of course the main thing here. The namespace and name are arbitrary (the standard is to use the same namespace as the plugin itself, but append "Deriver" to the class name), the class just needs to implement the DeriverInterface
– nothing else is needed. There is also ContainerDeriverInterface
, a sub-interface for when you want dependency injection for creating the deriver, and an abstract base class, DeriverBase
, which isn't very useful though, since the interface only has two methods. Concretely, the two methods are: getDerivativeDefinitions()
, for getting all derivative definitions, and getDerivativeDefinition()
for getting a single one – the latter usually simply a two-liner using the former.
Therefore, this is what src/Plugin/SearchApi/Datasource/ContentEntityDatasourceDeriver.php
looks like:
<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">search_api</span><span style="color: #007700">\</span><span style="color: #0000BB">Plugin</span><span style="color: #007700">\</span><span style="color: #0000BB">SearchApi</span><span style="color: #007700">\</span><span style="color: #0000BB">Datasource</span><span style="color: #007700">;<br><br>class </span><span style="color: #0000BB">ContentEntityDatasourceDeriver </span><span style="color: #007700">implements </span><span style="color: #0000BB">DeriverInterface </span><span style="color: #007700">{<br><br> public function </span><span style="color: #0000BB">getDerivativeDefinition</span><span style="color: #007700">(</span><span style="color: #0000BB">$derivative_id</span><span style="color: #007700">, </span><span style="color: #0000BB">$base_plugin_definition</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$derivatives </span><span style="color: #007700">= </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">getDerivativeDefinitions</span><span style="color: #007700">(</span><span style="color: #0000BB">$base_plugin_definition</span><span style="color: #007700">);<br> return isset(</span><span style="color: #0000BB">$derivatives</span><span style="color: #007700">[</span><span style="color: #0000BB">$derivative_id</span><span style="color: #007700">]) ? </span><span style="color: #0000BB">$derivatives</span><span style="color: #007700">[</span><span style="color: #0000BB">$derivative_id</span><span style="color: #007700">] : </span><span style="color: #0000BB">NULL</span><span style="color: #007700">;<br> }<br><br> public function </span><span style="color: #0000BB">getDerivativeDefinitions</span><span style="color: #007700">(</span><span style="color: #0000BB">$base_plugin_definition</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$base_plugin_id </span><span style="color: #007700">= </span><span style="color: #0000BB">$base_plugin_definition</span><span style="color: #007700">[</span><span style="color: #DD0000">'id'</span><span style="color: #007700">];<br> </span><span style="color: #0000BB">$plugin_derivatives </span><span style="color: #007700">= array();<br> foreach (\</span><span style="color: #0000BB">Drupal</span><span style="color: #007700">::</span><span style="color: #0000BB">entityManager</span><span style="color: #007700">()-></span><span style="color: #0000BB">getDefinitions</span><span style="color: #007700">() as </span><span style="color: #0000BB">$entity_type_id </span><span style="color: #007700">=> </span><span style="color: #0000BB">$entity_type_definition</span><span style="color: #007700">) {<br> if (</span><span style="color: #0000BB">$entity_type_definition </span><span style="color: #007700">instanceof </span><span style="color: #0000BB">ContentEntityType</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$label </span><span style="color: #007700">= </span><span style="color: #0000BB">$entity_type_definition</span><span style="color: #007700">-></span><span style="color: #0000BB">getLabel</span><span style="color: #007700">();<br> </span><span style="color: #0000BB">$plugin_derivatives</span><span style="color: #007700">[</span><span style="color: #0000BB">$entity_type_id</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'id' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$base_plugin_id </span><span style="color: #007700">. </span><span style="color: #0000BB">PluginBase</span><span style="color: #007700">::</span><span style="color: #0000BB">DERIVATIVE_SEPARATOR </span><span style="color: #007700">. </span><span style="color: #0000BB">$entity_type_id</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'label' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$label</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'description' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Provides %entity_type entities for indexing and searching.'</span><span style="color: #007700">, array(</span><span style="color: #DD0000">'%entity_type' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$label</span><span style="color: #007700">)),<br> </span><span style="color: #DD0000">'entity_type' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$entity_type_id</span><span style="color: #007700">,<br> ) + </span><span style="color: #0000BB">$base_plugin_definition</span><span style="color: #007700">;<br> }<br> }<br> return </span><span style="color: #0000BB">$plugin_derivatives</span><span style="color: #007700">;<br> }<br><br>}<br></span><span style="color: #0000BB">?></span></span>
As you see, getDerivativeDefinitions()
just returns an array with derivative plugin definitions – keyed by what's called their "derivative ID" and their id
key set to a combination of base ID and derivative ID, separated by PluginBase::DERIVATIVE_SEPARATOR
(which is simply a colon (":")). We additionally set the entity_type
key for all definitions (as we used in the plugin) and also set the other definition keys (as defined in the annotation) accordingly.
And that's it! If your plugin type implements DerivativeInspectionInterface
(which the normal PluginBase
class does), you also have handy methods for finding out a plugin's base ID and derivative ID (if any). But usually the code using the plugins doesn't need to be aware of derivatives and can simply handle them like any other plugin. Just be aware that this leads to plugin IDs now all potentially containing colons, and not only the usual "alphanumerics plus underscores" ID characters.
A side note about nomenclature
This is a bit confusing actually, especially as older documentation remains unupdated: The new individual plugins that were derived from the base defintion are referred to as "derivative plugin definitions", "plugin derivatives" or just "derivatives". Confusingly, though, the class creating the derivatives was also called a "derivative class" (and the key in the plugin definition was, consequently, derivative
).
In #1875996: Reconsider naming conventions for derivative classes, this discrepancy was discussed and eventually resolved by renaming the classes creating derivative definitions (along with their interfaces, etc.) to "derivers".
If you are reading documentation that is more than a few months old, hopefully this will prevent you from some confusion.
Image credit: DonkeyHotey