Defining and using Content Entity Field definitions

Last updated on
13 July 2017

Content entities have to define all their fields explicitly by providing definitions for the entity class. The field definitions are based on the Typed data API (see how entities implement it).

Field definitions

Entity types define their base fields in a static method on the entity class. Base fields are non-configurable fields that always exist on a given entity type, like the node title or created and changed dates. The entity manager complements those with configurable and non-configurable fields provided by other modules by invoking hook_entity_field_info() and hook_entity_field_info_alter(). This is also how fields configured via Field UI are added in as well.(These hooks no longer exists according to the API).

Field definitions are simple objects implementing the FieldDefinitionInterface whereas base fields are usually created with the FieldDefinition class and configurable fields directly implement the interface with the respective configuration entities (aka Field and FieldInstance).
Field definitions are also the place to define validation constraints for field items or field item properties. All field type plugin implementations can be used. (This interface and class no longer exist).

Fields are currently always a list of field items, which means that the FieldItem class that is defined as the type will be wrapped in a FieldItemList class that represents a list of those field items (#1869574: Support single valued Entity fields might change this).

All fields (including base fields) can also have widgets and formatters to display and edit them (#1988612: Apply formatters and widgets to rendered entity base fields, starting with node.title, TBD).

Base fields

The following is a shortened, exemplary list of field definitions of the node entity type.

use Drupal\Core\Field\BaseFieldDefinition;

class Node implements NodeInterface {

  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions($entity_type) {
    // The node id is an integer, using the IntegerItem field item class.
    $fields['nid'] = BaseFieldDefinition::create('integer')
      ->setLabel(t('Node ID'))
      ->setDescription(t('The node ID.'))
      ->setReadOnly(TRUE);

    // The UUID field uses the uuid_field type which ensures that a new UUID will automatically be generated when an entity is created.
    $fields['uuid'] = BaseFieldDefinition::create('uuid')
      ->setLabel(t('UUID'))
      ->setDescription(t('The node UUID.'))
      ->setReadOnly(TRUE);

    // The language code is defined as a language_field, which, again, ensures that a valid default language
    // code is set for new entities.
    $fields['langcode'] = BaseFieldDefinition::create('language')
      ->setLabel(t('Language code'))
      ->setDescription(t('The node language code.'));

    // The title is StringItem, the default value is an empty string and defines a property constraint for the
    // value to be at most 255 characters long.
    $fields['title'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Title'))
      ->setDescription(t('The title of this node, always treated as non-markup plain text.'))
      ->setRequired(TRUE)
      ->setTranslatable(TRUE)
      ->setSettings(array(
        'default_value' => '',
        'max_length' => 255,
      ));

    // The uid is an entity reference to the user entity type, which allows to access the user id with $node->uid->target_id
    // and the user entity with $node->uid->entity. NodeInterface also defines getAuthor() and getAuthorId(). (@todo: check owner vs. revisionAuthor)
    $fields['uid'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('User ID'))
      ->setDescription(t('The user ID of the node author.'))
      ->setSettings(array(
        'target_type' => 'user',
        'default_value' => 0,
      ));

    // The changed field type automatically updates the timestamp every time the
    // entity is saved.
    $fields['changed'] = BaseFieldDefinition::create('changed')
      ->setLabel(t('Changed'))
      ->setDescription(t('The time that the node was last edited.'))
    return $fields;
  }
}

An entity can also provide fields that only exist for a specific bundle or alter them by the bundle. For example, the node title can have a different label for each bundle. To provide alterations by the bundle, the base field definition must be cloned before changes are made, as it would otherwise change the base field definition and affect all bundles.

use Drupal\node\Entity\NodeType;

  /**
   * {@inheritdoc}
   */
  public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
    $node_type = NodeType::load($bundle);
    $fields = array();
    if (isset($node_type->title_label)) {
      $fields['title'] = clone $base_field_definitions['title'];
      $fields['title']->setLabel($node_type->title_label);
    }
    return $fields;
  }

Field types

Drupal core provides a list of field types that can be used for base fields. Additionally, modules can provide additional field types that can be used too.

  • string: A simple string.
  • boolean: A boolean stored as an integer.
  • integer: An integer, with settings for min and max value validation (also provided for decimal and float)
  • decimal: A decimal with configurable precision and scale.
  • float: A float number
  • language: Contains a language code and the language as a computed property
  • timestamp: A Unix timestamp stored as an integer
  • created: A timestamp that uses the current time as a default value.
  • changed: A timestamp that is automatically updated to the current time if the entity is saved.
  • datetime: A date stored as an ISO 8601 string.
  • uri: Contains a URI. The link module also provides a link field type that can include a link title and can point to an internal or external URI/route.
  • uuid: A UUID field that generates a new UUID as the default value.
  • email: An email, with corresponding validation and widgets and formatters.
  • entity_reference: An entity reference with a target_id and a computed entity field property. entity_reference.module provides widgets and formatters when enabled.
  • map: Can contain any number of arbitrary properties, stored as a serialized string

Configurable fields

Additional fields can be registered in hook_entity_base_field_info() and hook_entity_bundle_field_info(). The following examples add base and by bundle fields.

use Drupal\Core\Field\BaseFieldDefinition;

/**
 * Implements hook_entity_base_field_info().
 */
function path_entity_base_field_info(EntityTypeInterface $entity_type) {
  if ($entity_type->id() === 'taxonomy_term' || $entity_type->id() === 'node') {
    $fields['path'] = BaseFieldDefinition::create('path')
      ->setLabel(t('The path alias'))
      ->setComputed(TRUE);

    return $fields;
  }
}

/**
 * Implements hook_entity_bundle_field_info().
 */
function field_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
  if ($entity_type->isFieldable()) {
    // Configurable fields, which are always attached to a specific bundle, are
    // added 'by bundle'.
    return Field::fieldInfo()->getBundleInstances($entity_type->id(), $bundle);
  }
}

Corresponding alter hooks exist for each of the above.

Field storage

If your field doesn't have any special requirements Entity Field API can take care of the database storage and update the database schemas accordingly. This is the default for fields that are not marked as being a computed field (setComputed(TRUE)), or has specifically indicated to provide its own field storage (setCustomStorage(TRUE)).

Say you want to add a new base field to all Node entities that contains a simple boolean value to indicate whether the content is "highlighted".

use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;

/**
 * Implements hook_entity_base_field_info().
 */
function MYMODULE_entity_base_field_info(EntityTypeInterface $entity_type) {
  $fields = array();

  // Add a 'Highlight' base field to all node types.
  if ($entity_type->id() === 'node') {
    $fields['highlight'] = BaseFieldDefinition::create('boolean')
      ->setLabel(t('Highlight'))
      ->setDescription(t('Whether or not the node is highlighted.'))
      ->setRevisionable(TRUE)
      ->setTranslatable(TRUE)
      ->setDisplayOptions('form', array(
        'type' => 'boolean_checkbox',
        'settings' => array(
          'display_label' => TRUE,
        ),
      ))
      ->setDisplayConfigurable('form', TRUE);
  }

  return $fields;
}

Now all it takes is to visit update.php to run the database updates, and an additional 'highlight' column will be added to the tables holding the node data. @see: this change record
I've tried so many times and visit update.php won't add column to the database, but run

  \Drupal::entityTypeManager()->clearCachedDefinitions();
  \Drupal::service('entity.definition_update_manager')->applyUpdates();

can have the column created in database. Note: this will also run any updates that may be pending for other field definitions.

Alternatively, run:

drush updatedb --entity-updates

to install the newly added field.

To run this database update automatically when your custom module is enabled or disabled, you can trigger it by executing the event listeners that are responsible for performing these updates in your hook_install() and hook_uninstall() implementations:

/**
 * Implements hook_install().
 */
function MYMODULE_install() {
  // Create field storage for the 'Highlight' base field.
  $definition = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('node')['highlight'];
  \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionCreate($definition);
}

/**
 * Implements hook_uninstall().
 */
function MYMODULE_uninstall() {
  $definition = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledFieldStorageDefinitions('node')['highlight'];
  \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionDelete($definition);
}

If your module is already installed and you need to write a hook_update_N to update the field definitions you can do something like:

/**
 * Add in highlight field to all nodes.
 */
function MYMODULE_update_8000(&$sandbox) {
  $entity_type = \Drupal::service('entity_type.manager')->getDefinition('node');
  \Drupal::service('entity.definition_update_manager')->updateEntityType($entity_type);
}

See https://www.drupal.org/node/2554097 for more further details.

Working with field definitions

Note: As entities are complex data, they have to follow the ComplexDataInterface. From a Typed Data perspective, all contained typed data elements in a complex data object are properties. This limitation/naming enforcement might still be removed.

// Checks whether an entity has a certain field.
$entity->hasField('field_tags');

// Returns an array with named keys for all fields and their
// definitions. For example the ‘image’ field.
$field_definitions = $entity->getFieldDefinitions();

// Returns an array with name keys for all field item properties and their
// definitions of the image field. For example the ‘file_id’ and ‘alt’ properties.
$property_definitions = $entity->image->getFieldDefinition()->getPropertyDefinitions();

// Returns only definition for the ‘alt’ property.
$alt_definition = $entity->image->getFieldDefinition()->getPropertyDefinition('alt');

// Entity field definitions can also be requested from the entity manager,
// the following returns all fields that are available for all bundles.
Drupal::entityManager()->getFieldDefinitions($entity_type);

// The following returns fields that are available for the given bundle.
Drupal::entityManager()->getFieldDefinitions($entity_type, $bundle);

Widgets and Formatters for base fields

Base fields can specify the widgets and formatters they should use just like configurable fields. The widget and formatter and necessary settings are specified on the FieldDefinition class like this:

use Drupal\Core\Field\BaseFieldDefinition;

// ...

    $fields['title'] = use Drupal\Core\Field\BaseFieldDefinition;
FieldDefinition::create('string')
      ->setLabel(t('Title'))
      ->setDescription(t('The title of this node, always treated as non-markup plain text.'))
      ->setRequired(TRUE)
      ->setTranslatable(TRUE)
      ->setSettings(array(
        'default_value' => '',
        'max_length' => 255,
      ))
      ->setDisplayOptions('view', array(
        'label' => 'hidden',
        'type' => 'string',
        'weight' => -5,
      ))
      ->setDisplayOptions('form', array(
        'type' => 'string',
        'weight' => -5,
      ))
      ->setDisplayConfigurable('form', TRUE);

This uses the "string" formatter and widget and configures the weight for the node title. setDisplayConfigurable() can be used to make the field visible in the manage form display/manage display UI's, so that the order and label display can be changed. Core currently does not allow a change to the widget or their settings in the UI.