Writing an Entity Reference Selection Plugin

February 4, 2020 | 10 Minute Read

Tags: Drupal , Quality Assurance


We are building an internal tool to manage the knowledge sharing sessions we have (almost) every week. One of the functions of this tool is to schedule one or more individual topics in a particular time slot. Our show and tell sessions can have 2-3 topics in a single session. For this reason, we represented this using two different content types: event and topic. A topic represents an individual talk or topic presented by someone and an event represents the time slot. They are linked to each other with an entity reference field on the event content type.

The entity reference field in Drupal is a standard way of referring to other entities from any entity. After creating an entity reference field (let’s call it ER field from now on for short), Drupal allows us to customize what types of entities can be referred to from the entity. For referring to other nodes, by default, there are two different methods.

Figure 1: Entity reference field settings config for a node

Figure 1: Entity reference field settings config for a node


The “Default” reference method is very simple and just allows you to limit the content type and change the order of the results returned. There is a more powerful “Views” method which lets you select a view. The ER field will be able to refer to nodes returned by that view.

Prerequisites

To keep the article a reasonable length, I have written this with an intermediate to advanced Drupal developer in mind. Specifically, you should:

  • Have enough site building experience to build complex interconnected content types
  • Be able to write custom modules of intermediate complexity
  • Understand entity queries, dependency injection, plugin architecture in Drupal
  • Understand Drupal views and SQL (not mandatory, but I use those terminologies in optional sections)

What we needed

We wanted to make it easier for the organizers (people who plan and organize the sessions) to select topics in an event. Since there would be a lot of topics, it gets harder to find the correct one. We wanted to limit the allowable list of nodes in two ways:

  1. Only show topics which have not been added in any other event, but maybe added to the event we are editing.
  2. Only show topics which are of the same type as the event.

Part of the first requirement is possible to implement using a view. We just built a view which returned all topics not associated with an event and set it on the ER field. The second requirement is harder. But let’s talk more about that first.

Types of topics

If you have read the knowledge sharing blog post, you might know that we have multiple types of sessions—for example, webinar, show & tell, and learning club sessions. Now, an event which is marked for a webinar should be able to point to topics which are webinars. These types are stored as a taxonomy and both event and topic content types have a field which sets the type of the topic. More specifically:

  • On topic content type, we have a field called field_topic_type.
  • On event content type, we have a field called field_event_topic_type.

For a topic to be set on an event, they should be of the same type.

Why the views method doesn’t help

While views helped us with our first requirement, it couldn’t help us with our second problem. Right now, the view has no way to know anything about the entity which has the ER field. So, we couldn’t filter the nodes based on the topic type.

We decided to drop the second requirement initially but then we hit a roadblock for the first requirement as well. If you remember, our condition is that we should only be able to refer to topics which have not been put in any event. This works fine when we create the event. But if we wanted to edit an event which already had one or more topics in the ER field, we got an error saying the topic cannot be added to the ER field. This was because the view would no longer return that topic (because it is already stored in the event we created).

If that wasn’t clear, I don’t blame you. It is difficult to imagine this problem. Let’s imagine a scenario. Let’s say there are three topics, T1, T2, and T3, and an event E1. All the three topics have not been set in any event yet. Let’s consider the scenario further.

  • Our view would initially return all three topics–T1, T2, and T3 because none of these are set for an event yet.
  • When we edit our event E1, any of those three topics may be added. Let’s select T1. Save the event.
  • Now, edit the event E1 again. T1 was already selected and is associated with the node. Let’s not change anything and just save the node.
  • But now, the view will only return T2 and T3 (because T1 is already associated with E1). Because view doesn’t return T1 anymore, Drupal thinks that T1 should not be associated with E1 and throws the error.
  • As mentioned before, there is no way to let the view know that it is okay to return topics which are already linked to E1, because the view cannot know which node is being edited.

We did want to solve this problem, which meant we had to write a plugin. With this, we could solve both of our requirements in one go.

Before the code

As much fun as it is writing a new plugin, we at Axelerant try to reuse code as much as possible. I wanted to see how I could have made this work with views itself. The plugin is implemented in a file called ViewsSelection.php, deep inside the views module directory.

We needed a way to make the currently edited entity available to the view as an argument (or some other means). Since the view is initialized by the selection plugin, that is the place we should be passing it in. The view is executed in a method called getDisplayExecutionResults and as expected, it doesn’t pass in any entity ID. All it passes in is views arguments which were configured in the ER field settings page. This is not useful to us in any way. I considered modifying the behaviour but it seemed like a challenge to make it work along with the existing arguments being passed. If there were no arguments being passed at all, it would have been easier to write this functionality and contribute it to the Drupal community.

Writing the plugin

With the views selection plugin ruled out, we began creating our plugin. I won’t go too deep into our research process to write this, but I will share our learnings.

  • Central to our functionality is making the entity ID available in the selection plugin. This ID is passed in using the plugin’s $configuration but only for the dropdown list widget. For the autocomplete widget, the entity is not passed in at all. Looking at the source code where the widget creates the form element confirms it.
  • This means that the plugin we write will effectively work only with the dropdown widget. In case it gets configured with the autocomplete widget, the behavior falls back to allowing only topics not in any event to be referenced.

The entire code for the plugin is available here. We’ll look at relevant parts of the code in this post.

The plugin essentials

Let’s get the simple stuff out of the way first. Like most plugins in Drupal, this is defined by an annotation. In this case, we’ll use the annotation @EntityReferenceSelection. It needs a unique ID, label, group, weight, and entity types this plugin can handle. In our case, we only want this for the node entity type.

Figure 2: Plugin Annotation

Figure 2: Plugin Annotation


The class itself is in the namespace Drupal\module_name\Plugin\EntityReferenceSelection and in a corresponding directory (src/Plugin/EntityReferenceSelection inside your module). We are going to extend SelectionPluginBase which brings us a lot of basic functionality necessary for all selection plugins.

Figure 3: Class Definition

Figure 3: Class Definition


We are also implementing the ContainerFactoryPluginInterface which is the standard Drupal mechanism for any plugin to be able to use the Dependency Injection Container.

In our create method, we are also going to load the entity type manager service (which lets us get entity storage for node) and database service (so that we can directly run LEFT JOINs on the database).

We need to implement three methods which are required by the base class.

  • getReferenceableEntities is called to get the list of entities to show in the widget. This should return a multi-level keyed array with the first key being the bundle ID and the second level key being the entity ID. The value would be the label shown. Look at the code sample to understand this better. This function accepts a match parameter to filter entities (used with autocomplete widget, for example).
  • countReferenceableEntities is called to just get a count of the results based on the matches.
  • validateReferenceableEntities is called during the entity validation to check if the entities referred are valid or not. This function gets a list of entity IDs referred currently and it should just return those IDs that are valid.
     
Figure 4: Required methods for the SelectionPluginBase abstract class

Figure 4: Required methods for the SelectionPluginBase abstract class


Of course, you can write and use helper methods as required. Look at the full source code to look at all the various helper methods.

The logic explained

By now, you know how to write an entity reference selection plugin. But if you’re interested in my exact use case and how we wrote the methods, read on.

As explained in the previous section, we had to implement the getReferenceableEntities method to return our options. Let’s look at the method in more detail.

Figure 5: getReferenceableEntities Part 1

Figure 5: getReferenceableEntities Part 1


First, we are just going to call our helper method buildEntityQueryForTopics to build the entity query we are going to use. The entity query filters on the match parameters if required (we don’t). Internally, it calls another method to actually get the list of topics. More on that later.

Figure 6: getReferenceableEntities Part 2

Figure 6: getReferenceableEntities Part 2


Now that we have our list of topics, we loop over that and construct a nice informative label for each of them. This helps build a list which also shows quick information about the topic itself. You may just want to return the node title and not worry about anything else.

Figure 7: Topics list in the ER field widget when editing an event

Figure 7: Topics list in the ER field widget when editing an event


With this, let’s look at some of our helper methods which do the actual work of getting the results.

The buildEntityQueryForTopics method is very straightforward and derived almost entirely from the default plugin in Drupal core. Like the default plugin, it applies the match filter which is required for the autocomplete widget. We don’t need this since we are only going to use this with a dropdown list, but it doesn’t hurt to match the implementation. If the autocomplete widget ever supports passing the entity, then our plugin will work with the autocomplete widget as well.

From the default plugin’s implementation, the only variation is that this further limits the options to the topics that are not scheduled in any event and also the topic type set on the event node. Since this query needs LEFT JOINs which are not supported by entity queries, we are going to talk directly to the database in another helper method getReferenceableTopics.

Figure 8: getReferenceableTopics method

Figure 8: getReferenceableTopics method


Here, we construct the complex SQL query which filters the topics based on the two requirements we listed in the beginning of this blog post. We directly execute the query and return a list of node IDs back. This list is used in the entity query to limit our results.

In closing

All this code gets us the field which we see in figure 7 above. Once again, the entire source code is available as a Gist for you to easily copy-paste. Writing plugins may be confusing at first but breaking down the functionality like above helps in understanding the process better. I hope this post was useful in helping you understand how you could write a similar plugin. Do share your feedback and experience with us in the comments.