How to create a custom bundle field for a node

Drupal 8 has a powerful data structure, namely content entities and fields. Other side of power is though complexity. In this article I look at different ways of programmatically creating a field specific for one or several bundles of content type.

11 November 2020

Briefly about fields and bundles.

I assume everyone who opened this page knows that one can create different content types in Drupal and add fields to those content types through the interface. And those different fieldsets make the main difference between content types. The same way one can add fields to taxonomies, media types and many other types of entities, provided by contributed modules. Most of the readers probably know that there are also fields defined in the code for each entity type, they are called base fields. Others just noticed that on form display and view display management pages you have more fields than you attached through UI. What is a more rare case, there could be fields specific just for certain bundles (that is, content types, taxonomy vocabularies etc), but still defined in code.

As it is quite common for Drupal, there are many variants of implementation and many variants of contexts. For example, you may have created your custom entity type, and in this case you are in total control of its base fields and bundle fields through Drupal\Core\Entity\FieldableEntityInterface::baseFieldDefinitions() and Drupal\Core\Entity\FieldableEntityInterface::bundleFieldDefinitions(). While any entity type defines its base fields, and can hardly exist without them (you may have a look at Node class), implementing bundleFieldDefinitions is a more rare case. As documentation states, “This function can return definitions both for bundle fields (fields that are not defined in $base_field_definitions, and therefore might not exist on some bundles) as well as bundle-specific overrides of base fields.” In fact, only the last option is implemented by core modules, eg by Taxonomy which binds this way parent term to the same bundle, or Comment which defines this way Comment type for particular bundle.

But the next question is, if we want to add in code some field, base or bundle, for аn entity provided by core or contrib, what should we do? (Another question why and when would we need this, but I come to this later. Spoiler: almost never). There are 2 ways. First is to extend the entity you want to add your field to, that is, create a class that extends entity class and implement there your baseFieldDefinitions() or bundleFieldDefinitions() (and don’t forget to call the parent method) and then switch the entity class by implementing hook_entity_type_alter():

mymodule_entity_type_alter(array &$entity_types) {
 $entity_types[‘core_or_contrib_entity’]->setClass(MyModuleEntity::class);
} 

The second way, which I’m going to tell you about, is to use hook_entity_bundle_field_info() in conjunction with hook_entity_base_field_info(). Well, there is a third way which lies halfway to creating fields through the interface - write config yaml files by hand and put them in config/install folder of your module.

So, you see, there’s already a mess and we haven’t even started. Ok, not a mess but many options you will have to weigh and consider.

What’s the difference between all those methods?

What can each one give you that others cannot?

With base fields it’s obvious - you’re sure to have them for any bundle of an entity and you can base entity processing logic on them. More than this, adding storage definition in code vs configuration (i’ll clarify the difference between field storage definition and field configuration a bit later) allows you to add constraints to fields. Defining constraints in yaml configuration files is not possible.

For bundle fields, if you have a substantial number of bundles, and many fields that are shared between bundles to me it’s a legal wish to have this controlled from a single place in code. Or, as in my case, I wanted to add machine name widget, but there was no UI for managing the widget. Well, after all, it was much easier to create this UI, though it would not be much flexible and dependent on custom code.

Ok, let’s start.

Technical implementation

Fields are added to existing entities via hook_entity_field_storage_info() and hook_entity_bundle_field_info()[drupal.org]. I mean, just a single bundle field needs to be mentioned in implementations of these two hooks. That’s because there are two different interfaces: Drupal\Core\Field\FieldDefinitionInterface and Drupal\Core\Field\FieldStorageDefinitionInterface. The latter one is concerned about how field is stored in the database, its schema, constraints etc. Obviously, there’s no difference in storage whether the field should appear on any bundle or just on some of them. The previous one is concerned with the settings for particular bundle. You remember that there are different tabs “Field Settings” and “Field Storage Settings” on “Manage Field”, and probably noticed that configuration for each field is exported into different yaml files, field.storage.{entity}.{field}.yml and field.field.{entity}.{bundle}.{field}.yml. But why, when defining an entity, we define all its base fields in one place, baseFieldDefinitions()? Because class Drupal\Core\Field\BaseFieldDefinition implements both FieldStorageDefinitionInterface and FieldDefinitionInterface. And why don’t we have such a possibility when defining bundle fields with hook_entity_bundle_field_info()? Well, we just don’t have it, there’s a discussion around[drupal.org].

We need to implement hook_entity_field_storage_info() to tell about storage of our field and hook_entity_bundle_field_info() to tell about bundle configuration. But how? Which classes should we return? It turns out that core implementation of those is geared towards loading fields from configuration (yes, that’s how configuration makes it to the code), that’s we have classes Drupal\field\Entity\FieldStorageConfig (not to be confused with Drupal\field\FieldConfigStorage) and Drupal\field\Entity\FieldConfig, intended to load and manipulate fields from configuration. And we have BaseFieldDefinition, which also doesn’t suit us. I’m not the first to run into it.[drupal.org,drupal.org, drupal.org]. To summarize those issues on drupal.org, there is already a class Drupal\Core\Field\FieldDefinition, which allows us to create bundle field definitions in code, but Drupal\Core\Field\FieldStorageDefinition, which would serve the same purpose for hook_entity_field_storage_info() implementation, exists only as patch and not yet committed to core (though the consensus is it should). I didn’t want to use another patch and decided to write a storage definition in yaml, and then use it for creating FieldDefinition. Though essentially viable, that was not the best idea. Either you go with code, or with configuration.

The result was that I have a yaml file with field storage definition in config/install of my module. Hook_entity_bundle_field_info implementation looks like this:

if ($entity_type->id() === 'node' && in_array($bundle, $bundles) {
  $fields = [];
  $storage_definition = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('node')[$field_id];
  $fields[$field_id] = FieldDefinition::createFromFieldStorageDefinition($storage_definition) ->setDisplayOptions('form', [...]) ->setDisplayConfigurable('view', TRUE) ->setDisplayOptions('view’, [...]]) ->setLabel(t('Convivial identifier'));
  return $fields;
}

What next? Did it work? Yes, kind of. There is a field now. But many things were left aside. Main is that field should be ‘installed’. If your ever look into the guts of Drupal, that is its database, you may have noticed key_value table, which contains lots of cryptic stuff. To me it’s kind of Windows registry - all kinds of information about the system which are not determined on runtime, because it would be too costly, but instead, stored in the registry. Eg, to get the actual entity schema Drupal looks for ‘last installed definitions’ in this table. To get field map (Drupal\Core\Entity\EntityFieldManagerInterface::getFieldMap() method of entity_field.manager service), Drupal, naturally, picks map of bundle fields stored in key_value table. Obviously, just by implementing a hook we do not update this registry item. There’s an issue on that too.[drupal.org] But here Update API comes to the rescue. Basically, I traced the event of creating a field through UI and was impressed by the number of hooks invoked, parent methods called, events fired, and many of those are used by other modules, and involving hook_bundle_field_info() just does nothing. To me it’s rather striking inconsistency.

Drupal installs field from UI.
Drupal installs field from code.

So, back to the Update API [drupal.org]. At some point there was a drush ‘entity-update’ command which was meant to solve all the problems, but in fact it didn’t, so it was removed and instead module authors were charged with responsibility of updating all Drupal inner knowledge. This API is implemented with a service, entity.definition_update_manager. This service, or, better to say, its interface has a set of methods for field storage definitions: Drupal\Core\Enity::EntityDefinitionUpdateManagerInterface::install/update/delete/getFieldStorageDefinition(). Their code is pretty concise, you may check it and see that they are geared towards installing base field definitions. I’m really not sure if we should call install method in our hook_install()/hook_update() implementation if we are installing field from config, most likely, no. And the thing is that this Update API doesn’t care about field definitions in contrast to field storage definitions. So it in no way affects the above-mentioned field map. But if we scratch a bit deeper, there’s a service field_storage_definition.listener, and it has its counterpart field_definition.listener. And if we directly call Drupal\Core\Field\FieldDefinitionListenerInterface::onFieldDefinitionCreate(), that triggers a number of actions related to field definition creation, including field map update.

But still, we are not done with fixes. There was a strange bug (I’m going to post an issue to drupal.org) with layout builder and field blocks - not only with our custom, but with content moderation field provided by core module with the same name - that there appeared a block for this field in layout, and this block couldn’t be removed, because it wasn’t in configuration. Again, I traced the field installation from the UI and found that at some point entity type displays were just rebuilt: \Drupal::classResolver(EntityDisplayRebuilder::class)->rebuildEntityTypeDisplays('node', $bundle), and, of course, nothing like this happens on field installation from code. And it seems by far not the only action omitted by Update API vs UI field install.

And the last issue for the moment. Views module in views_field_default_views_data() assumes, that if there is field storage in configuration, there should be also field definition in configuration (line 386 of views.views.inc). It’s not crucial though, at least unless you have a translatable field, and just illustrates assumptions made during core development.

Conclusion

Drupal really has very flexible and powerful system of fields, but it has its own preferred ways and habits.

  • If you want to add a bundle field, configuration is your number one choice.
  • If configuration doesn’t suit you, think about implementing an entity class and base or bundle field there.

If all of the above still is not an option, be ready to use some patches and contribute to core development. But it’s quite feasible still. In all cases except #2 don’t forget about Update API and be ready to extend it.

Further reading

  1. Finalize API for creating, overriding, and altering code-defined bundle fields
  2. Applicable bundle field definitions defined in code should not have to manually implement hook_entity_field_storage_info.
  3. Add the FieldStorageDefinition class to define field storage definitions in hook_entity_field_storage_info()
  4. Add a FieldDefinition class for defining bundle fields in code.
  5. FieldDefinition class added to support defining bundle fields in code.
  6. EntityFieldManager::getFieldMap() doesn't show bundle fields
  7. Support for automatic entity updates has been removed
 

More like this