Drupal 9.3 brought a very cool new developer feature to the Drupal entity API: bundle classes. So let me give you a brief explanation of how this works and what are some of the changes you can take advantage of.
Background
As you know, content entities come with a class which can have specific methods that manipulate the data in the fields of the entity. However, these methods can only interact with base fields as that is the maximum context the class can or should be aware of. For example, if the entity type gets 3 bundles with configurable fields on them, it’s not a good idea to codify those fields in the class as the fields may not be available on all bundles.
We can create custom entity types without bundles which have a specific interface with methods that we can type hint and rely on. Sure. But for multi-bundle entities we have no solution.
From Drupal 9.3 this changes with the introduction of individual classes for entity bundles. Which…is…awesome.
Essentially, this works exactly as it sounds. We can now create a bundle with configuration fields and a class that maps to it. This class can then implement a specific interface and can interact with fields defined on that bundle. And the resulting entity objects are being seamlessly passed through the system, type hinted and relied on in a much more OOP way than before. In other words, instead of:
if ($entity->bundle() === 'page') {
$value = $entity->get('field_subtitle')->value;
}
…we can now do this:
if ($entity instanceof PageInterface) {
$value = $entity->getSubtitle();
}
Another great use case for this is simplifying templating work for entity bundle templates (or those that need to print entity values). It becomes easier to access values via the magic get
methods, without the need for a preprocessor to get the value out of a field first:
{{ node.getSubtitle() }}
How do we create bundle classes?
There are two main things we need to do in order to define a bundle specific entity class.
First, we need to create the class that extends from the main entity class. For example, \Drupal\my_module\Entity\Article
extending from \Drupal\node\Entity\Node
. If it doesn’t extend from the entity class, Drupal will throw a BundleClassInheritanceException
. The bundle class can implement its own interfaces, have base classes used across multiple bundles and use various traits as needed.
Second, this class needs to be defined on the entity bundle it applies to. For entities that belong to us, we need to implement hook_entity_bundle_info()
like so:
use \Drupal\my_module\Entity\Brand;
function hook_entity_bundle_info() {
$bundles['my_entity']['my_bundle']['class'] = Brand::class;
return $bundles;
}
For entities coming from other modules, like Node for instance, we use the alter hook:
use \Drupal\my_module\Entity\Article;
function my_module_entity_bundle_info_alter(array &$bundles) {
$bundles['node']['article']['class'] = Article::class;
}
One thing to note is that the same exact class should not be used for multiple bundles, lest you get an AmbiguousBundleClassException
. If you want to reuse logic across bundles, consider a base class or trait which individual bundle classes can share.
And that’s pretty much the whole thing to be aware of. There are minor API changes under the hood, particularly driven by the fact that the entity storage can load multiple entities at once which may or may not use the same bundle classes and need individual instantiating. But other than that, things should keep working as they did with minimal potential impact for custom code that deals with the entity storage and loading.
You can read the full change record here: https://www.drupal.org/node/3191609.
Hope this helps.
Daniel Sipos
Danny founded WEBOMELETTE in 2012 as a passion project, mostly writing about Drupal problems he faced day to day, as well as about new technologies and things that he thought other developers would find useful. Now he now manages a team of developers and designers, delivering quality products that make businesses successful.
Comments
Update to hook
hook_entity_bundle_info() is now hook_entity_bundle_info_alter(array &$bundles): void
Add new comment