A man working on a laptop, entity bundle classes function visible on screen.
Profile picture for user alf.harald
Written by
Alf Harald Sælevik
Published on
October 18, 2021

Entity bundle classes and its possibilities

Projects vary vastly in terms of complexity. Sometimes there are just one or two entities, sometimes a standard product structure. But what happens when you have several layers of interconnected nodes or terms that rely on each other via custom entity reference fields? My take is, it may (will) tip over to becoming messy code, very fast.

Say we are doing an online class. We have 3 entities:

  1. A group of people
  2. A participant in this group
  3. A physical person (which can be in multiple groups)

And let's say we have this structure organized as nodes, with entity reference fields defining the connections.

Core & contrib entity behavior

Let's say that we are preparing some data from this structure. Based on a participant, we are going to find the description of the group to display in some template somwehere. Just traversing the fields of nodes, we would do this:

// Get the description and location of the group
$group_participant = $a_node;
$group = $group_participant->field_group_reference->entity->field_text->value;
$location = $group_participant->field_group_reference->entity->field_location->value;

This is the most standard thing to see in custom Drupal code when traversing entity reference fields. Alas, very unfriendly code in terms of readability, eh? In this case, we are using core node entities with two different bundles. If we had spent time on creating custom entities, we could of course do like so:

// Get description and location for the group
$participant = $custom_entity;
$participant->getGroup()->getDescription();
$participant->getGroup()->getLocation();

This looks a lot more intuitive, right? And, we can use such modern things as code completion in editors like PHPStorm to traverse the entity objects. But this approach does not apply to taxonomies, media entities, nodes, or any other entities that a core or contrib module provides.

Here is another example. References between nodes and taxonomies are usually one-way only. The participant can be a member of several groups. Let's say we know only of a participant, but we want to know in what groups this participant is in? Using an injected service, you would most probably do something like this, using an entityQuery in the background:

// Find the groups that this participant is in.
$participant = $a_node;
/** @var MyService $some_service */
$groups = $some_service->getGroupsByParticipant($participant);

This is type-completion capable, provided that you add that service in every class you need it. But why on earth, why would you need a service to find the groups? It should be as simple as:

$participant->getGroups($status);

Right now, we can only do clean code like this in two scenarios:

  1. By overriding the node class in its entirety, once and for all
  2. By creating a custom entity module

Using option 1, if you override the entire class completely, we would get this scenario:

// Get the Schroedinger's participant.
$participant = $node;
// Is it a participant? let's find out.
$is_participant = $participant->isParticipant();
// Or maybe like this:
$is_participant = $participant->getType() == 'participant';
// Now it is a real participant.

PHP would know what type it is, but the code completion won't know that because we cannot differentiate on bundles. So on the same object, we could both do:

$node->isGroupParticipant(); // would be false
$node->isParticipant(); // would be true

Using option 2, in many cases, there are two or three commands that we'd like to add to each bundle of a node. Creating a custom entity just to make some small alterations to the functionality of the node system is in most cases completely overkill. It is loads of custom code that can easily be avoided if we only could create custom entity classes for those bundles.

Enter the savior patch

The patch is here: https://www.drupal.org/node/2570593

This patch provides a way to add entity bundle classes, which allows us to create customized classes that applies to one single bundle. To use it, we use hook_entity_bundle_info_alter to register the bundles with its separate classes:

function fabulous_module_entity_bundle_info_alter(&$bundles) {
  $bundles['node']['participant']['class'] = ParticipantNode::class;
  $bundles['node']['group']['class'] = GroupNode::class;
};

Alas, we create two classes, one for the group:

class GroupNode extends Node {

}

And one for the participant:

class ParticipantNode extends Node {

}

Or instead, you can of course be a pro and describe your custom class in an interface:

class ParticipantNode extends Node implements ParticipantNodeInterface {

}

 

The important thing here is, the class *must* extend the registered entity class, in this case, Node. And that is basically it.

You can now start building functionality for each of the bundles separately. Here is the magic, let's say you load a node:

// If we load..
$id = $a_participant
$node = Node::load($id);
if ($node instanceOf ParticipantNode) {
  // Hooray, this is true!
}

Instead of receiving the standard Node object, you'll get your own ParticipantNode object!

Now, to make the example above work, instead of remembering field names, add this to the group node class object:

public function getParticipant() {
  return $this->field_participant->entity;
}

And you can add this to the participant node object (provided it is a 1-many relationship):

public function getGroups($status) {
  $result = $this->entityQuery('node')
    ->condition('type', 'group')
    ->condition('nid', $this->id())
    ->condtition('field_group_status', $status)
    ->execute();
  return Node::load(reset($result));
}

Then you can basically go on an eternal trail of type-completed entities, wheter it is nodes, users or taxonomies:

$participant->getGroupParticipant()->getGroup()->getMunicipality()->getRegion();

Not just nodes

Any entity bundle, core or contrib, can get an entity bundle class. Taxonomies, nodes, media, files.

$term->reIndexReferencedEntities();
$comment->whatever();
$file->doSomeOtherFunkyBusiness();

// More examples here!!

It's nice, but be careful

Using the approach that this patch provides, there are a few dos and dont's.

First of all, a single bundle type can only be overridden once. So, this approach is best used:

  • In the semi-lawless world of custom project-specific code
  • Very carefully in reusable code, but only where the entity type is defined by the module itself

So the limit on this approach is primarily for custom projects. But, that is also where code tends to get most messy.