Adding fields to custom entities programmaticallyfor Drupal 8 , 9 , 10 , and 11

Last updated :  

A field on a fieldable UI content type is something a site builder adds by clicking through Structure → Content types → Manage fields. A field on a code-defined content entity is something you declare in PHP, inside a single static method: baseFieldDefinitions(). Same underlying Field API, same storage mechanics — but instead of an admin filling in a form, you're writing the equivalent directly, which means it ships with your module and every site that installs it gets the exact same fields, guaranteed.

What you'll learn in this lesson

  • Where baseFieldDefinitions() lives and when Drupal calls it
  • How to define a plain field, an entity-reference field, and a list field with fixed options
  • The difference between a field's storage settings and its display settings

baseFieldDefinitions() — one method, one field per entry

public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {

  $fields['name'] = BaseFieldDefinition::create('string')
    ->setLabel(t('Name'))
    ->setDescription(t('The name of the Contact entity.'))
    ->setSettings([
      'max_length' => 255,
      'text_processing' => 0,
    ])
    ->setDisplayOptions('view', [
      'label' => 'above',
      'type' => 'string',
      'weight' => -6,
    ])
    ->setDisplayOptions('form', [
      'type' => 'string_textfield',
      'weight' => -6,
    ])
    ->setDisplayConfigurable('form', TRUE)
    ->setDisplayConfigurable('view', TRUE);

  return $fields;
}

Drupal calls this method once, at install time, to determine your entity's storage schema — the database columns are generated automatically from whatever fields you return here, and again whenever the entity type's field definitions are considered (so a genuine change usually needs an update hook, not just edited code, on a site that's already installed). Each entry is keyed by machine name and built with BaseFieldDefinition::create($field_type), where $field_type is the exact same string you'd see in the Field UI's "Add field" dropdown — string, integer, entity_reference, boolean, and so on.

Storage settings vs. display settings — a distinction worth getting right

setSettings() configures the field's storage — max_length and text_processing above affect what's actually written to the database column and can't be changed later without a real schema migration. setDisplayOptions('view', ...) and setDisplayOptions('form', ...) only control how the field is presented — the formatter used to render it, or the widget used to edit it — and can be changed freely at any time with zero data impact.

An entity reference field

$fields['user_id'] = BaseFieldDefinition::create('entity_reference')
  ->setLabel(t('User Name'))
  ->setSetting('target_type', 'user')
  ->setSetting('handler', 'default')
  ->setDisplayOptions('form', [
    'type' => 'entity_reference_autocomplete',
    'settings' => [
      'match_operator' => 'CONTAINS',
      'match_limit' => 10,
    ],
  ]);

An entity_reference field stores another entity's ID, not a copy of its data — here, a link to a user entity. target_type says which entity type it can point to; handler controls the selection logic (the default handler offers every entity of that type, but you can restrict by bundle or write your own). The entity_reference_autocomplete widget is what gives editors that "type to search" behavior in the form.

A field with a fixed set of options

$fields['role'] = BaseFieldDefinition::create('list_string')
  ->setLabel(t('Role'))
  ->setSettings([
    'allowed_values' => [
      'administrator' => 'administrator',
      'user' => 'user',
    ],
  ])
  ->setDefaultValue('user')
  ->setDisplayOptions('form', [
    'type' => 'options_select',
  ]);

list_string stores one of a fixed set of string keys — exactly what you'd reach for to model a dropdown of predefined choices. allowed_values is a storage setting (the valid values are part of the schema contract), while options_select is purely a display choice — you could swap it for radio buttons without touching a single stored value.

setDisplayConfigurable(TRUE) hands control to the site builder. Call it on both 'form' and 'view' and a site administrator can rearrange or hide this field via Manage Form Display / Manage Display in the UI, exactly like a field added through the Field UI. Leave it FALSE (the default) and your display options above are the only ones that will ever apply — useful for fields that should never be reconfigurable, like an internal ID.

Quick check: you need to change a field's max_length from 255 to 500 on a site that already has this entity type installed with live data. Is editing the number in baseFieldDefinitions() and clearing cache enough? No — storage settings for an installed entity type require an hook_update_N() that calls the entity/field definition update mechanism (or, in this specific case, an actual schema change); code changes to baseFieldDefinitions() alone only affect brand-new installs.

Key takeaways

  • baseFieldDefinitions() is a static method returning an array of BaseFieldDefinition objects, keyed by machine name — this is where your entity's schema comes from.
  • Storage settings (setSettings(), setDefaultValue()) affect the database schema; display settings (setDisplayOptions()) only affect presentation and are always safe to change.
  • Use entity_reference fields to link to other entities by ID, and list_string/list_integer for a fixed set of allowed choices.
  • setDisplayConfigurable(TRUE) lets a site builder rearrange the field via the normal Manage Display/Manage Form Display UI; leave it off for fields that must never be reconfigured.
  • Changing storage settings on an entity type that's already installed on a live site needs an update hook, not just a code edit.

Coming up next

You now know how to shape the fields your entity stores. Next, in Querying Entities, we put those fields to work — building conditions against them with \Drupal::entityQuery() to find exactly the records you need.