A lot of the time, a custom content entity type only needs a single bundle: all the entities of this type have the same structure. But there are times where they need to vary, typically to have different fields, while still being the same type. This is where entity bundles come in.
Bundles are to a custom entity type what node types are to the node entity type. They could be called sub-types instead (and there is a long-running core issue to rename them to this; I'd say go help but I'm not sure what can be done to move it forward), but the name 'bundle' has stuck because initially the concept was just about different 'bundles of fields', and then the name ended up being applied across the whole entity system.
History lesson over; how do you define bundles on your entity type? Well there are several ways, and of course they each have their use cases.
And of course, Module Builder can help generate your code, whichever of these methods you use.
Quick and simple: hardcode
The simplest way to do anything is to hardcode it. If you have a fixed number of bundles that aren't going to change over time, or at least so rarely that requiring a code deployment to change them is acceptable, then you can simply define the bundles in code. The way to do this is with a hook (though there's a core issue -- which I filed -- to allow these to be defined in a method on the entity class).
Here's how you'd define your hardcoded bundles:
/**
* Implements hook_entity_bundle_info().
*/
function mymodule_entity_bundle_info() {
$bundles['my_entity_type'] = [
'bundle_alpha_' => [
'label' => t('Alpha'),
'description' => t('Represents an alpha entity.')
],
'bundle_beta_' => [
'label' => t('Beta'),
'description' => t('Represents a beta entity.')
],
];
return $bundles;
}
The machine names of the bundles (which are used as the bundle field values, and in field names, admin paths, and so on) are the keys of the array. The labels are used in UI messages and page titles. The descriptions are used on the 'Add entity' page's list of bundles.
Classic: bundle entity type
The way Drupal core does bundles is with a bundle entity type. In addition to the content entity type you want to have bundles, there is also a config entity type, called the 'bundle entity type'. Each single entity of the bundle entity type defines a bundle of the content entity type. So for example, a single node type entity called 'page' defines the 'page' node type; a single taxonomy vocabulary entity called 'tags' defines the 'tags' taxonomy term type.
This is great if you want extensibility, and you want the bundles to be configurable by site admins in the UI rather than developers. The downside is that it's a lot of extra code, as there's a whole second entity type to define.
Very little glue is required between the two entity types, though. They basically each need to reference the other in their entity type annotations:
The content entity type needs:
* bundle_entity_type = "my_entity_type_bundle",
and the bundle entity needs:
* bundle_of = "my_entity_type",
and the bundle entity class should inherit from \Drupal\Core\Config\Entity\ConfigEntityBundleBase
.
Per-bundle functionality: plugins as bundles
This third method needs more than just Drupal core: it's a technique provided by Entity module.
Here, you define a plugin type (an annotation plugin type, rather than YAML), and each plugin of that type corresponds to a bundle. This means you need a whole class for each bundle, which seems like a lot of code compared to the hook technique, but there are cases where that's what you want.
First, because Entity module's framework for this allows each plugin class to define different fields for each bundle. These so-called bundle fields are installed in the same way as entity base fields, but are only on one bundle. This gives you the diversification of per-bundle fields that you get with config fields, but with the definition of the fields in your code where it's easier to maintain.
Second, because in your plugin class you can code different behaviours for different bundles of your entity type. Suppose you want the entity label to be slightly different. No problem, in your entity class simply hand over to the bundle:
class PluginAlpha {
public function label() {
$bundle_plugin = \Drupal::service('plugin.manager.my_plugin_type')
return $bundle_plugin->label($this);
}
}
Add a label() method to the plugin classes, and you can specialise the behaviour for each bundle. If you want to have behaviour that's grouped across more than one plugin, one way to do it is to add properties to your plugin type's annotation, and then implement the functionality in the plugin base class with a conditional on the value in the plugin's definition.
/**
* @MyPluginType(
* id = "plugin_alpha",
* label = @Translation("Alpha"),
* label_handling = "combolulate"
class MyPluginBase {
public function label() {
switch ($this->getPluginDefinition()['floopiness']) {
case 'combolulate':
// Return the combolulated label.
}
}
}
For a plugin type to be used as entity bundles, the plugins need to implement \Drupal\entity\BundlePlugin\BundlePluginInterface, and your entity type needs to declare the plugin:
* bundle_plugin_type = "my_plugin_type",
Here, the string 'my_plugin_type' is the part of the plugin manager's service name that comes after the 'plugin.manager' prefix. (The plugin system unfortunately doesn't have a standard concept of a plugin type's name, but this is informally what is used in various places such as Entity module and Commerce.)
The bundle plugin technique is well-suited to entities that are used as 'machinery' in some way, rather than content. For example, in Commerce License, each bundle of the license entity is a plugin which provides the behaviour that's specific to that type of license.
One thing to note on this technique: if you're writing kernel tests, you'll need to manually install the bundle plugins for them to exist in the test. See the tests in Entity module for an example.
These three techniques for bundles each have their advantages, so it's not unusual to find all three in use in a single project codebase. It's a question of which is best for a particular entity type.