Skip to main content

Creating fields programmatically and not through field configuration

Published on

Drupal is great for its content (data) modeling capabilities with its entity and field system. Not only is this system robust, but it is also completely manageable from a user interface! When fields are created through the user interface, they are managed through configuration. Drupal automates all schema changes required in the database based on this configuration when it is first created or when it might be removed. I have always had one issue with managing fields through configuration – your business logic relies on trusting that no one modifies this configuration. I prefer to define my fields programmatically, ensuring they exist and that fields tied to business logic are always present. Of course, this means I must also manage their installation and deletion for schema changes. But I prefer that level of control.

Before diving into how you would define fields programmatically, I will briefly explain the difference between base fields and bundle fields and a special class provided by the Entity API module.

Base fields and bundle fields

There are two ways a field may exist on an entity type: base fields and bundle fields.

  • Base fields are at the entity type level and available to all bundles of that entity type. If the field supports a single value, Drupal will add that field's columns to the shared data table for the entity type. Otherwise, the field will receive a dedicated table to store the multiple values associated with an entity.
  • Bundle fields are only available to specific bundles for an entity type. Bundle fields always require a dedicated table to store their values since they are not available for every entity of an entity type.

That's how bundles got their names since they are a bundle of fields for an entity type!

Out of the box with Drupal core, there is the \Drupal\Core\Field\BaseFieldDefinition class to represent base fields. This class defines base fields and implements \Drupal\Core\Field\FieldDefinitionInterface along with \Drupal\Core\Field\FieldStorageDefinitionInterface. That means the base field definition can provide information about how the field type should be stored in the database with its schema and interact with the entity it is attached to.

There isn't a similar class in Drupal core that allows defining bundle fields. The Field module in Drupal core provides the configuration field_config and field_storage_config entity types that match FieldDefinitionInterface and FieldStorageDefinitionInterface, respectively. However, the Entity API module provides the \Drupal\entity\BundleFieldDefinition. It is a simple class that extends BaseFieldDefinition and has a straightforward override: return false when asked if it is a base field.

class BundleFieldDefinition extends BaseFieldDefinition {

  public function isBaseField() {
    return FALSE;
  }

}

This class was created six years ago by bojanz while we worked on building Drupal Commerce 2.x to provide a way to define bundles as plugins, which we leveraged for payment gateways, payment methods, and shipping methods.

Defining a base field for an entity type

Defining a base field for an entity type is relatively easy, and there are a lot of examples in the wild. Base fields are added to an entity type using the hook_entity_base_field_info hook. The fields returned from this hook are marked as provided by the module implementing this hook.

Here is an example from the Path module, which adds a path field for custom URL paths to taxonomy terms, nodes, and media entity types:

function path_entity_base_field_info(EntityTypeInterface $entity_type) {
  if (in_array($entity_type->id(), ['taxonomy_term', 'node', 'media'], TRUE)) {
    $fields['path'] = BaseFieldDefinition::create('path')
      ->setLabel(t('URL alias'))
      ->setTranslatable(TRUE)
      ->setDisplayOptions('form', [
        'type' => 'path',
        'weight' => 30,
      ])
      ->setDisplayConfigurable('form', TRUE)
      ->setComputed(TRUE);

    return $fields;
  }
}

 

In the hook, you check what is the incoming entity type and determine if you wish to attach your field. You then return an array of fields keyed by the field name and a BaseFieldDefinition object. The return type is array<string, BaseFieldDefinition>. When a module is installed, Drupal will check to see if it provides base fields. The module installer iterates through each entity type and the field definitions for the entity type. If any field is identified as being provided by the installed module, Drupal will install that field's storage and perform the required schema changes.

Suppose you add a new base field after your module has been installed, which is usually when managing fields programmatically in a project. In that case, you must implement an update hook and install the field yourself.

Here is an example from Drupal Commerce where we added the is_default boolean field to the Store entity type.

function commerce_store_update_8204() {
  $storage_definition = BaseFieldDefinition::create('boolean')
    ->setDefaultValue(FALSE);

  $update_manager = \Drupal::entityDefinitionUpdateManager();
  $update_manager->installFieldStorageDefinition('is_default', 'commerce_store', 'commerce_store', $storage_definition);
}

To install a new base field definition, you must use the entity.definition_update_manager service. This service installs, updates, and deletes entity types or fields. The installFieldStorageDefinition method accepts the following arguments:

  • The field's name
  • The ID of the entity type
  • The provider (module name)
  • The field storage definition object

Since Drupal's entity type information is cached during this process, you must manually recreate the field definition in your update hook. After the update hook is run, the field will be installed to the entity type, and all schema changes will be applied. In the case of Drupal Commerce, this creates a new is_default column on the commerce_store table.

Defining a bundle field for an entity type

Defining a bundle field is a bit trickier and involves two hooks: one to define the field definition and one to define the field storage definition. There is an issue from 2014 for finalizing this approach. With our heavy usage of bundle fields in Drupal Commerce, we built our solutions and pushed them into the Entity API module as part of the bundle plugin system. It never made its way into Drupal core. But that does not mean it's impossible to do or that difficult. There hasn't been a concerted effort to make this easier – YET!

An example of leveraging bundle fields can be found in the Acquia DAM module, a module I helped lead development as part of my first duties when joining Acquia. The Acquia DAM module provides a way to reference assets in your DAM from media entities in your Drupal site. To do so, we need a way to store the remote asset ID for a DAM asset on the media entity. This field is a vital part of our business logic, but it should only be available to media types that are used for the DAM integration. That's where bundle fields come in!

To provide bundle fields programmatically, two hooks need to be defined: hook_entity_field_storage_info and hook_entity_bundle_field_info. The hook_entity_field_storage_info hook is used to return FieldStorageDefinitionInterface instances, while hook_entity_bundle_field_info is used to return FieldDefinitionInterface instances. Luckily the BundleFieldDefinition can be used to represent both interfaces. 

For hook_entity_field_storage_info, you return all bundle field storages for an entity type, regardless of what bundle they may use. Since BundleFieldDefinition::isBaseField returns false, Drupal will always provide a dedicated storage table for these field storages. All field definitions need a backing field storage. Here is an adapted implementation example from the Acquia DAM module (we have multiple bundle fields and helper classes for returning their definitions. See the original here.)

function acquia_dam_entity_field_storage_info(EntityTypeInterface $entity_type): array {
  $definitions = [];
  if ($entity_type->id() === 'media') {
    $definitions['acquia_dam_asset_id'] = BundleFieldDefinition::create('acquia_dam_asset')
      ->setProvider('acquia_dam')
      ->setName('acquia_dam_asset_id')
      ->setLabel(new TranslatableMarkup('Asset reference'))
      ->setReadOnly(TRUE)
      ->setTargetEntityTypeId('media');
  }
  return $definitions;
}

This tells Drupal that the media entity type has a field named acquia_dam_asset_id with a field storage for an acquia_dam_asset field type (the schema is inferred from the field type's definition.)

We need to tell Drupal about the field definition, which will use this field storage definition. This is done with the hook_entity_bundle_field_info hook, which knows the entity type and a specific bundle. This is a bit more complicated since the Acquia DAM module does some inspection on the media type itself. I'll again provide an adapted implementation from the Acquia DAM module (original here.)

function acquia_dam_entity_bundle_field_info(
  EntityTypeInterface $entity_type,
  $bundle,
  array $base_field_definitions
) {
  $definitions = [];
  if ($entity_type->id() === 'media') {
    $media_type_storage = Drupal::entityTypeManager()->getStorage('media_type');
    $media_type = $media_type_storage->load($bundle);
    $source = $media_type->getSource();
    if ($source instanceof Asset) {
      $definitions['acquia_dam_asset_id'] = BundleFieldDefinition::create('acquia_dam_asset')
        ->setProvider('acquia_dam')
        ->setName('acquia_dam_asset_id')
        ->setLabel(new TranslatableMarkup('Asset reference'))
        ->setReadOnly(TRUE)
        ->setTargetEntityTypeId('media')
        ->setTargetBundle($bundle)
        ->setDisplayConfigurable('view', TRUE)
        ->setDisplayOptions('view', [
          'type' => 'acquia_dam_embed_code',
          'weight' => -5,
        ])
        ->setDisplayConfigurable('form', TRUE);
    }
  }
  return $definitions;
}

This code loads the bundle and checks if it uses the DAM module's source plugin. If the media type does, we know it is our media type and should have the asset ID reference field definition. This makes Drupal aware of the acquia_dam_asset_id field and allows media types to store data for the field's value!

Joachim posted a blog post about defining bundle fields and has a simple approach to these two hooks. He implements hook_entity_bundle_field_info and then uses that result in hook_entity_field_storage_info.

Just like base fields, adding new bundle fields requires using the entity.definition_update_manager service and calling the installFieldStorageDefinition method. That is one reason we used helper classes to provide field storage and field definitions, to avoid any mistakes between the storage hook, bundle field info hook, and update hooks.

function acquia_dam_update_9005() {
  $field_storage_definition = ImageAltTextField::getStorageDefinition('media');
  $update_manager = \Drupal::entityDefinitionUpdateManager();
  $update_manager->installFieldStorageDefinition(
    ImageAltTextField::IMAGE_ALT_TEXT_FIELD_NAME,
    'media',
    'acquia_dam',
    $field_storage_definition
  );
}

After the initial release of the Acquia DAM module, we added a bundle field for mapping alt text for images. We needed to install this bundle field via the update hook above.

Thanks for reading!

I hope you found this interesting and a different way to manage fields on your Drupal site to codify the fields representing your application's business logic.

I'm available for one-on-one consulting calls – click here to book a meeting with me 🗓️

#