Drupal: Field Types and Entity References

This content was generated with the assistance of AI and should be verified against the official Drupal documentation before being relied on in production.

This sub-section’s Drupal bibliography lists the reference material consulted while preparing these pages.

Every piece of structured content in Drupal is stored in a field. This page is the reference for what a field is made of and for the field types that ship with Drupal core, with extra depth on the most important one for content modelling: the entity reference. It then looks at the ecosystem around it (Paragraphs, Entity Reference Revisions, Inline Entity Form and others), presented with its trade-offs, not as a default.

It was written against Drupal 11.4.x, with Drush 13 or 14 and PHP 8.3 or later, and the facts were checked against the official documentation on 2026-10-30. Drupal 12.0.0 is scheduled for the week of 2026-12-07, so everything said about Drupal 12 describes a planned release. The running example is the Meridian Museum: the content types exhibition and event, the custom content entity collection_object of the module meridian_collection and the taxonomies topic and gallery. The click path for creating a field in the UI is in Content types and fields; writing your own field type, widget and formatter is in Block, field type, widget and formatter plugins.

The anatomy of a field

A field is not one object but a small stack of configuration and plugins. Knowing which layer owns which setting explains most of the questions editors and site builders ask ("why can I not change the number of values?", "why does my setting apply to every content type?").

Field type

A plugin (#[FieldType], plugin directory Plugin/Field/FieldType) that defines the columns stored for each value (for a link: uri, title, options), the default storage and field settings, validation constraints and the default widget and formatter. The type is chosen when the field is created and cannot be changed afterwards.

Field storage (field_storage_config)

The configuration entity that says "a field named field_event_dates of type daterange exists on entity type node`". It holds the type, the cardinality (the allowed number of values: a fixed number or unlimited), the storage settings (for a reference: `target_type) and whether the field is translatable at storage level. Storage is shared by every bundle that uses the field name, which is why changing it after content exists is largely blocked.

Field instance (field_config)

The configuration entity that attaches a storage to one bundle (for example node.event). It holds the label, help text, required flag, default value and the field settings that may differ between bundles (for a reference: the selection handler and the allowed bundles). Several content types can reuse one storage, each with its own instance.

Widget

The plugin (Plugin/Field/FieldWidget) that builds the form element on the edit form. It is configured in the form display (core.entity_form_display.node.event.default). One type usually offers several widgets: an entity reference can be an autocomplete, a select list, radio buttons, check boxes or a tags-style box.

Formatter

The plugin (Plugin/Field/FieldFormatter) that turns stored values into a render array. It is configured per view mode in the view display (core.entity_view_display.node.event.teaser). The same date range can be shown as a plain string, a custom pattern or a relative time.

The following diagram shows how these layers relate for the Meridian Museum event content type.

flowchart LR T[Field type plugin<br/>daterange] --> S[Field storage<br/>node.field_event_dates<br/>cardinality 1] S --> I[Field instance<br/>node.event.field_event_dates<br/>label, required, default] I --> FD[Form display<br/>widget daterange_default] I --> VD[View display teaser<br/>formatter daterange_default] I --> V[(Entity storage<br/>node__field_event_dates)]

For a configurable field on a node the values live in a dedicated table (node__field_event_dates, plus node_revision__field_event_dates for revisions), one column per property (field_event_dates_value, field_event_dates_end_value). A base field, in contrast, is declared in code in baseFieldDefinitions() of the entity class and stored in the entity’s own table. Base fields cannot be created or deleted from the UI; their widgets and formatters can still be configured. Entity queries and Views hide this difference.

Cardinality deserves a note because it is a common pitfall: raising it from "1" to "unlimited" is allowed at any time, but lowering it, or changing a storage setting such as target_type or the list of allowed values' keys, is refused once data exists. Plan the storage up front, or migrate the data.

A UI note for people following older tutorials: since Drupal 10.2 the Field UI first asks for a group of types (for example "Reference", "Text", "Number", "Date and time") and then for the concrete type. The names of the plugin IDs have not changed, only the wizard in front of them. The Drupal User Guide chapter on content structure (6.x) follows the newer flow.

Creating fields from the command line

Drush 13 and 14 include field:create, so a field can be created without clicking, which is handy in a deployment script, a recipe’s preparation step or a local rebuild.

# An event date range, required, one value
vendor/bin/drush field:create node event \
  --field-name=field_event_dates \
  --field-label="Event dates" \
  --field-type=daterange \
  --field-widget=daterange_default \
  --is-required=1 \
  --cardinality=1

# A reference from an exhibition to collection objects, unlimited values
vendor/bin/drush field:create node exhibition \
  --field-name=field_objects \
  --field-label="Objects on show" \
  --field-type=entity_reference \
  --field-widget=entity_reference_autocomplete \
  --target-type=collection_object \
  --cardinality=-1

# Export the result so it is versioned
vendor/bin/drush config:export -y

Fields created this way are ordinary configuration. The sensible workflow is the one in Configuration management: create on a development site, export, review the YAML diff and import on the other environments. Reusing an existing storage on another bundle is possible in the UI ("Re-use an existing field") and with the --existing option.

Core field types at a glance

The table lists the field types that a site builder meets most often. The module in the second column must be enabled for the type to appear.

Field type (plugin ID) Module Stored columns Typical Meridian Museum use

Text (plain), string, string_long

Core (always)

value

Object title variants, an accession number

Text (formatted), text, text_long, text_with_summary

Text

value, format (and summary)

Exhibition description, with a text format

List, list_string, list_integer, list_float

Options

value

Object material or period from a fixed set

Number, integer, decimal, float

Core (always)

value

Ticket price, object height in millimetres

Boolean, boolean

Core (always)

value

"On display", "Wheelchair accessible"

Date, datetime

Datetime

value

Opening date of an exhibition

Date range, daterange

Datetime Range

value, end_value

Event start and end

Timestamp, timestamp, created, changed

Core (always)

value

Last-sync time of a collection object

Link, link

Link

uri, title, options

Ticket link of an event

Email, email

Core (always)

value

Booking contact of a workshop

Telephone, telephone

Telephone

value

Group-booking phone line

Image, image

Image

target_id, alt, title, width, height

Hero image (see media below)

File, file

File

target_id, display, description

Accessible PDF programme

Entity reference, entity_reference

Core (always)

target_id

Exhibition to objects, event to gallery term

Comment, comment

Comment

status, cid, last_comment_*

Visitor comments on news

Two cross-cutting rules apply to all of them. First, plain-text types store text as entered and rely on output escaping, while formatted types store the text together with the text format that decides which markup survives filtering (see Text formats and CKEditor 5). Second, the storage layer does not validate business rules: required, minimum, maximum and similar settings are enforced by field constraints in the form and the entity API, not by the database.

Text fields

Plain text (string, string_long) has a maximum length (255 characters by default for string) and a single-line or multi-line widget. Use it for data that is never shown as markup: the Meridian accession number (2026.14.3), a catalogue code, a title. Formatted text (text, text_long, text_with_summary) additionally stores a text format. The "summary" variant is what the body field of a node uses; the summary can be shown in teasers.

Pitfalls:

  • Entering HTML in a plain string field never produces markup, it produces escaped text. If editors expect markup, the field is the wrong type.

  • A formatted field with the format full_html is an XSS vector for anyone who may use that format. Grant formats by role, and give untrusted roles only a restricted format.

  • The string column length is fixed by the storage setting max_length. Raising it is allowed only while the field is empty.

  • Translatable text needs both the field and the entity to be translatable (see Multilingual).

List fields

List fields (list_string, list_integer, list_float, with the Options module) store one of a fixed set of keys and show a label. The allowed values are part of the storage because they define which keys are legal. In the UI they are entered as key|Label lines; in configuration they are a sequence under settings.allowed_values.

# config/sync/field.storage.node.field_period.yml (excerpt)
langcode: en
status: true
dependencies:
  module:
    - node
    - options
id: node.field_period
field_name: field_period
entity_type: node
type: list_string
settings:
  allowed_values:
    -
      value: neolithic
      label: Neolithic
    -
      value: roman
      label: Roman
    -
      value: industrial
      label: 'Industrial age'
  allowed_values_function: ''
module: options
locked: false
cardinality: 1
translatable: true
persist_with_no_fields: false
custom_storage: false

Use stable, machine-readable keys (roman), never labels, because the key is what is stored, exported and used in queries. Removing a key that is already in use is blocked by the form; renaming it requires an update path that rewrites the stored values. When the options must come from code (for example the open galleries), set allowed_values_function to a function or a static method that returns the array. A list that editors need to maintain belongs in a taxonomy and an entity reference, not here (see Taxonomy).

The widgets are a select list, radio buttons or check boxes (the latter two chosen automatically for single or multiple values in the form display), and the formatters show the label or the key.

Number and boolean fields

Numbers come in three storage types. integer has a size (tiny to big), optional minimum, maximum, prefix and suffix. decimal stores an exact value with a declared precision and scale and is the type for money (the ticket price of a workshop). float is a binary floating-point number: use it for measurements, never for currency, because it cannot represent most decimals exactly.

A boolean field stores 0 or 1 and has configurable on and off labels. Its widget is a single check box or a pair of radio buttons. The Meridian on_display property is a boolean. Two common mistakes follow from how it is stored. First, "unchecked" is a stored 0, not a missing value, so a Views filter "is not empty" does not mean what an editor thinks; filter on the value. Second, labels that look like questions ("Is this on display?") confuse screen-reader users in the "off" state: use a label that works in both ("On display") and describe the meaning in the help text.

Date and time fields

Core has three families, and choosing the wrong one is the most frequent date mistake.

  • Timestamp (timestamp, also the base-field types created and changed) stores a Unix integer. It is right for system moments such as "last synchronised" and is always rendered in the viewer’s or site’s time zone.

  • Date (datetime, Datetime module) stores an ISO 8601 string in UTC in one of two modes chosen in the storage settings: date only (2026-11-14) or date and time (2026-11-14T19:00:00). Date-only values carry no time zone, so an exhibition opening day stays on the same calendar day for every visitor.

  • Date range (daterange, Datetime Range module) stores value and end_value with the same two modes. It also rejects an end before the start through a constraint.

Datetime Range is a core module, but it is not enabled by the Standard profile in every version, so a recipe or installation that needs the type should list datetime_range explicitly. In the Meridian site the event dates field looks like this in configuration.

# config/sync/field.storage.node.field_event_dates.yml
langcode: en
status: true
dependencies:
  module:
    - datetime_range
    - node
id: node.field_event_dates
field_name: field_event_dates
entity_type: node
type: daterange
settings:
  datetime_type: datetime
module: datetime_range
locked: false
cardinality: 1
translatable: true
persist_with_no_fields: false
custom_storage: false
# config/sync/field.field.node.event.field_event_dates.yml
langcode: en
status: true
dependencies:
  config:
    - field.storage.node.field_event_dates
    - node.type.event
  module:
    - datetime_range
id: node.event.field_event_dates
field_name: field_event_dates
entity_type: node
bundle: event
label: 'Event dates'
description: 'Start and end of the event, in the museum time zone.'
required: true
translatable: false
default_value: {  }
default_value_callback: ''
settings: {  }
field_type: daterange

Pitfalls to know:

  • A datetime value in date-and-time mode is stored in UTC and converted for display with the site or user time zone. If the formatter prints the wrong hour, check the default time zone in the regional settings and the per-user time zone option, not the field.

  • The default value can be "current date" or a relative expression (+1 week), evaluated when the entity form is built.

  • Do not store a date as text to avoid these rules: Views date filters, sorts, contextual filters and calendars all rely on the date types.

  • Querying in code uses the storage format, so the string for a query condition is date('Y-m-d\TH:i:s') in UTC for date-time mode and Y-m-d for date-only mode.

For the code side of a range query see Database API and entity queries.

Link (link, Link module) stores a uri, an optional title and an options array. The URI can be an internal path (internal:/visit), an entity (entity:node/12), a route (route:<front>) or an external address. Storage settings: none; instance settings choose whether the title is optional, required or disabled and whether external links, internal links or both are allowed. The Meridian ticket_link of an event is a link field with an external-only restriction. Two practical points: the formatter can add rel="nofollow" and target through formatter settings, and the field validates the URI scheme, so javascript: URIs are rejected, which is the security-relevant behaviour you want to preserve in any custom widget.

Email (email) stores one address and validates its syntax; the formatter can render a mailto: link. Showing addresses as links invites harvesting by spam bots, so the museum publishes a contact form instead (see Forms for visitors: Contact and Webform).

Telephone (telephone, Telephone module) stores a string and a tel: link formatter, with no format validation (a phone number differs by country). The Telephone module is being removed from Drupal core in Drupal 12.0.0 and continues as a contributed project: the deprecation change record names Drupal 11.5.0 as the release that deprecated it, while the removal planning for 11.4 also appears in the tracker, so check the change record of the release you run. Existing fields keep working as long as the contrib module is installed; the safe sequence before upgrading is to install the contrib version while still on Drupal 11 and not to uninstall the core module first (see Removed core modules and replacements). An alternative is a plain string field plus a validation constraint, or the Telephone Validation contrib module that checks numbers per country.

Image and file fields

File (file, File module) stores a reference to a managed file entity in target_id plus a display flag (whether the file is shown) and a description. Image (image, Image module) adds alt, title, width and height. Both are entity references in disguise: the value is the id of a file entity stored in the file_managed table, with usage tracked in file_usage. That is why deleting a node does not immediately delete its files: they become temporary and are removed by cron after the configured maximum age.

Field settings that matter:

Setting Effect and pitfall

File directory

Subdirectory of the file system where uploads land. Supports tokens, for example exhibitions/[date:custom:Y]/[date:custom:m]. Avoids one huge flat directory.

Allowed file extensions

The upload whitelist (for example pdf for programmes, png jpg jpeg webp for images). Never allow php, phtml or html; Drupal appends .txt to risky extensions as a safety net but the whitelist is the control.

Maximum upload size

Per field, always capped by PHP’s upload_max_filesize and post_max_size.

Alt text required (image)

Enable it. An image without alternative text fails WCAG; the Meridian editors may tick "decorative" only for ornaments.

Image dimensions

Minimum and maximum resolution; larger uploads are resized.

Upload destination

Public or private file system. Private files are served through PHP with access checks; see File system and private files.

For editorial content, prefer a media reference over a bare image field. A media entity gives you a reusable library, a reusable alt text, per-type fields (a credit line, a licence) and the Media Library widget. A bare image field is simpler and perfectly adequate for a one-off such as a user picture. The responsive output of both goes through image styles (see Media and the Media Library and Display modes and image styles).

Comment fields

The comment field type is unusual: it does not store a value that the editor types. It stores the comment status of an entity (open, closed or hidden) and denormalised statistics (last comment id, timestamp and author, comment count), and it points at a comment type that defines the fields of the comments themselves. The Comment module therefore needs two steps in the UI: create a comment type (Structure > Comment types), then add a field of type "Comments" to the content type. Field settings control the default mode (threaded or flat), the number of comments per page, whether anonymous visitors may leave contact information, preview and the form location.

Because comment fields are normal fields, one entity can have several comment fields, and comments can be attached to any content entity (including a custom entity such as collection_object) by adding a comment field to its bundle. Moderating spam is part of operating comments: keep anonymous commenting off, require approval ("Comments require approval" via permissions), and use a CAPTCHA or an anti-spam service if the museum ever opens comments to the public. Comments are excluded from search and from the default JSON:API fields unless exposed deliberately. For most editorial sites a "contact the curator" form or a discussion on a social platform is lower maintenance than a comment system.

Entity reference fields

An entity reference stores the id of another entity (target_id) and, in the storage settings, the type of entity it may point to (target_type: node, taxonomy_term, user, media, file, collection_object and so on). It is the mechanism behind every relationship in Drupal: an exhibition showing objects, an event in a gallery, an object made of a material term, a news item written by a user. Core’s entity_reference module is always on; taxonomy term fields, media fields, file and image fields and the author of a node are all built on the same idea.

The decisions to take when creating one are:

  • What to reference: content, taxonomy term, user, media, file or a custom entity type. This is storage and cannot be changed later.

  • Cardinality: one, a limited number or unlimited. An exhibition referencing "objects on show" is unlimited.

  • Which entities are allowed: the reference method (the selection handler, see below) and, for the default handler, the allowed bundles and the sort order.

  • The widget: autocomplete, autocomplete with tags, select list, radio buttons, check boxes or a specialised widget such as the Media Library.

  • Whether editors may create the target on the fly: the auto_create option, available for single-bundle targets such as tags.

The field instance configuration of the exhibition-to-object reference looks like this.

# config/sync/field.field.node.exhibition.field_objects.yml
langcode: en
status: true
dependencies:
  config:
    - field.storage.node.field_objects
    - node.type.exhibition
id: node.exhibition.field_objects
field_name: field_objects
entity_type: node
bundle: exhibition
label: 'Objects on show'
description: 'Collection objects displayed in this exhibition.'
required: false
translatable: false
default_value: {  }
default_value_callback: ''
settings:
  handler: 'default:collection_object'
  handler_settings:
    target_bundles: null
    sort:
      field: accession_number
      direction: ASC
    auto_create: false
field_type: entity_reference

The matching storage file declares settings.target_type: collection_object. Note the dependency on both the storage and the bundle: configuration dependencies are what make a config import fail early, not at runtime, when something is missing.

Widgets for references

  • entity_reference_autocomplete suggests matching labels as the editor types; the match operator ("Contains" or "Starts with") and the number of suggestions (10 by default) are widget settings. Use it when the target set is large.

  • entity_reference_autocomplete_tags allows several values in one comma-separated box and can create entities when auto_create is on. It is the classical "tags" experience.

  • options_select, options_buttons: lists the whole target set. Right for small sets such as the six museum galleries, wrong for thousands of objects because it loads everything.

  • media_library_widget (Media Library module): opens a modal grid for media references.

  • entity_reference_autocomplete with no match shows nothing, which editors read as "the object does not exist". Check the selection handler’s filters (for example unpublished entities are hidden from users without permission) before assuming data loss.

Formatters for references

The default formatter shows the label as a link. The "Rendered entity" formatter renders the target in a chosen view mode, which is how the Meridian exhibition shows a card per object (it is the building block of component-style teasers; see Display modes and image styles). Rendered entities can cause performance and recursion trouble: a node that references itself through a chain of rendered references is cut off by a protection counter, but long chains still multiply database queries. Prefer a light view mode (teaser, card) for referenced items, and rely on the render cache metadata described in Render API and cache metadata.

Access and security

A reference never grants access. If the exhibition page is visible and references an unpublished object, the formatter checks access('view') on the target and silently omits it for visitors who may not see it. In the selection handler, the same check applies to what editors can choose: it uses the entity_reference_selection query tag and the target entity’s access rules, so an editor sees only what they may reference. When you write your own entity queries or selection plugins remember accessCheck() (see Access control and permissions in code).

Entity reference selection handlers

The "Reference method" on the field instance is a choice among selection plugins (plugin type EntityReferenceSelection, discovered in Plugin/EntityReferenceSelection). A selection plugin does two things: it returns the entities that can be selected for a given search string, and it validates that a submitted value is one of them. The latter makes selection a real server-side check, not merely a UI filter.

Handler (plugin ID) Behaviour

default:<entity_type> (for example default:node, default:taxonomy_term)

The generic handler. It offers the target bundles (target_bundles), a sort field and direction, and auto_create with the bundle for new entities. For nodes it also filters by published status and the user’s access.

views

Selection is delegated to a View: the field stores view_name, display_name and arguments and the View decides which entities qualify.

Contributed or custom handlers

Any module can define a handler, for example to limit objects to those currently on display.

The handler and its settings live under settings.handler and settings.handler_settings of the field instance, as in the configuration above. For a nodes' target the default handler restricts to selected content types: leaving target_bundles empty means "any bundle", which is often too wide, so pick them explicitly.

The selection API is small. Plugins implement SelectionInterface, usually by extending DefaultSelection: getReferenceableEntities($match, $match_operator, $limit) returns ids grouped by bundle, countReferenceableEntities() counts them, validateReferenceableEntities(array $ids) filters submitted ids and createNewEntity() and validateReferenceableNewEntities() support auto-creation. Modules can also adjust a handler without writing one with hook_entity_reference_selection_alter() and the query alter hooks entity_reference_selection.

A custom selection handler

The museum wants the "Objects on show" field to offer only objects whose on_display flag is true. A custom handler keeps that rule in one place and applies it to validation too.

<?php

declare(strict_types=1);

namespace Drupal\meridian_collection\Plugin\EntityReferenceSelection;

use Drupal\Core\Entity\Attribute\EntityReferenceSelection;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
use Drupal\Core\StringTranslation\TranslatableMarkup;

/**
 * Offers only collection objects that are currently on display.
 */
#[EntityReferenceSelection(
  id: 'meridian_on_display',
  label: new TranslatableMarkup('Meridian: objects on display'),
  group: 'meridian_on_display',
  weight: 0,
  entity_types: ['collection_object'],
)]
final class OnDisplaySelection extends DefaultSelection {

  /**
   * {@inheritdoc}
   */
  protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
    $query = parent::buildEntityQuery($match, $match_operator);
    $query->condition('on_display', 1);
    return $query;
  }

}

A field instance then uses handler: meridian_on_display. Because validateReferenceableEntities() runs the same query, an object that has since been taken off display is rejected on submit, which the default handler would not do. The built-in query already adds the entity access checks and the bundle and sort handling from parent::buildEntityQuery(), so a subclass should extend it, not rebuild it. See Plugin API and attributes for the attribute discovery model.

Views-based selection

When the rule is easier to express as a filter than as code, the views handler is the site builder’s tool: it lets a View produce the allowed entities. The setup has three parts.

  1. Create a View of the target entity type (for example Content or Collection object) with a display of the type Entity Reference. Choose the search fields that the autocomplete matches against (accession_number, the label) and, if needed, add filters such as "On display = Yes", a relationship, or a contextual filter that receives the arguments from the field.

  2. In the field instance settings, choose the reference method Views: Filter by an entity reference view, then select the view and display. The optional view arguments are a comma-separated list passed to the contextual filters.

  3. Test the widget and, importantly, test saving a value that is not in the View: validation uses the same View, so it must be rejected.

Typical uses are "only exhibitions that are not closed", "only objects of the gallery the editor chose in the previous field" (through arguments), and "only terms that have a certain field value". Trade-offs: a View is easy to change by site builders and exported with the configuration, but it hides a dependency (renaming or deleting the view breaks the field), pagination of the autocomplete follows the View’s items-per-page, and complex Views can be slow because they run for every keystroke. Sorting is done by the View, not by the field’s sort setting. Views is covered in Views.

The choice between the two approaches is summarised below.

flowchart TD Q{Which entities may an editor reference?} Q -- any of a few bundles, simple sort --> D[default handler<br/>target_bundles, sort, auto_create] Q -- depends on a field value, a relationship or an argument --> V[views handler<br/>Entity Reference display] Q -- needs custom PHP logic or an external check --> C[custom EntityReferenceSelection plugin] D --> Val[Server-side validation uses the same query] V --> Val C --> Val

Reverse references

An entity reference is stored on one side only. The exhibition knows its objects; the object does not store the exhibitions that show it. Drupal core has no "reverse reference" field type, so a back-link is built when it is needed:

  • In a View: add a relationship "Content using field_objects" (the reverse of the field) to a Collection object view, which makes the exhibitions that reference each object available as fields, filters and contextual filters. This is the standard answer and works with unlimited cardinality. It queries the field table at render time.

  • In a block or an embedded View: show the "Exhibitions featuring this object" on the object page with a View whose contextual filter is the object id from the URL (the entity ID from the route).

  • In code: run an entity query on the field with ->condition('field_objects', $object->id()) and accessCheck(TRUE).

  • Denormalised: store a second reference on the object and keep both in sync with a hook or an event subscriber. This is fast to read, but it is two sources of truth, so only do it when measurements justify it.

  • Contributed modules: modules such as Entity Reference Revisions and Backreference-style projects can render or maintain the inverse; evaluate maintenance status and Drupal 11 support before adopting.

The pitfalls are performance and cache invalidation. A reverse View on a large site is a join over the field table; give the referenced field an index in the View or restrict by bundle. Cache tags on the page that shows the reverse list must include the list tag of the referenced entities so that a new exhibition appears (Views adds the node_list tags automatically). The query itself is expressed like this.

<?php

declare(strict_types=1);

namespace Drupal\meridian_collection;

use Drupal\Core\Entity\EntityTypeManagerInterface;

/**
 * Finds the exhibitions that show a collection object.
 */
final class ExhibitionLookup {

  public function __construct(
    private readonly EntityTypeManagerInterface $entityTypeManager,
  ) {}

  /**
   * @return \Drupal\node\NodeInterface[]
   */
  public function forObject(int $object_id): array {
    $storage = $this->entityTypeManager->getStorage('node');
    $ids = $storage->getQuery()
      ->accessCheck(TRUE)
      ->condition('type', 'exhibition')
      ->condition('status', 1)
      ->condition('field_objects.target_id', $object_id)
      ->sort('title')
      ->execute();
    return $storage->loadMultiple($ids);
  }

}

The class is registered as a service in meridian_collection.services.yml with the argument @entity_type.manager; see Services and dependency injection.

Fields in code: base field definitions

Configurable fields are for site builders; a custom entity type declares its own fields as base fields. The same field type, widget and formatter plugin IDs are used, so everything on this page applies. The excerpt below is the part of the collection_object entity that defines the data fields of the museum collection (the rest of the class is in Custom entity types).

<?php

declare(strict_types=1);

namespace Drupal\meridian_collection\Entity;

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

/**
 * Excerpt of the collection object entity: only its data fields.
 *
 * The entity type attribute and the other methods are omitted here.
 */
class CollectionObject extends EditorialContentEntityBase implements CollectionObjectInterface {

  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type): array {
    $fields = parent::baseFieldDefinitions($entity_type);

    $fields['accession_number'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Accession number'))
      ->setRequired(TRUE)
      ->setSetting('max_length', 32)
      ->setDisplayOptions('form', ['type' => 'string_textfield', 'weight' => 0])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);

    $fields['period'] = BaseFieldDefinition::create('list_string')
      ->setLabel(t('Period'))
      ->setSetting('allowed_values', [
        'neolithic' => 'Neolithic',
        'roman' => 'Roman',
        'industrial' => 'Industrial age',
      ])
      ->setDisplayOptions('form', ['type' => 'options_select', 'weight' => 1])
      ->setDisplayConfigurable('form', TRUE);

    $fields['on_display'] = BaseFieldDefinition::create('boolean')
      ->setLabel(t('On display'))
      ->setDefaultValue(FALSE)
      ->setSetting('on_label', t('On display'))
      ->setSetting('off_label', t('In storage'))
      ->setDisplayOptions('form', ['type' => 'boolean_checkbox', 'weight' => 2])
      ->setDisplayConfigurable('form', TRUE);

    $fields['gallery'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Gallery'))
      ->setSetting('target_type', 'taxonomy_term')
      ->setSetting('handler', 'default:taxonomy_term')
      ->setSetting('handler_settings', ['target_bundles' => ['gallery' => 'gallery']])
      ->setDisplayOptions('form', ['type' => 'options_select', 'weight' => 3])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);

    return $fields;
  }

}

Changing a base field definition after the entity type is installed requires an update path (an entity definition update through the Update API, see Update API and post-updates), because the schema and the stored definition must stay in step. Older books such as Drupal 10 Development Cookbook (2023) show this code with plugin annotations in a @ContentEntityType doc comment; since Drupal 10.2 the same plugins are declared with PHP attributes, and annotations are on a deprecation path.

Paragraphs and other contributed field types

Core field types model attributes and relationships. Contributed projects add richer building blocks. They are useful, but each is a long-term commitment, so this section describes what they do and what they cost.

Project What it gives you Trade-offs

Paragraphs

Reusable, reorderable "paragraph types" with their own fields, placed in a field on a node; a structured alternative to one huge body field. Version 8.x-1.23 (5 August 2026) supports Drupal 10.3 and 11 and is covered by the security advisory policy.

Each paragraph is an entity, so deep nesting multiplies queries, the admin form gets heavy, and content becomes tied to the module’s data model and the Entity Reference Revisions dependency. Migrations, translation and moderation of nested items need care. For flexible page layout, evaluate Layout Builder or Canvas first.

Entity Reference Revisions

Reference field that points at a specific entity revision, needed by Paragraphs and used for composite entities.

Adds storage and complexity (revision ids), and is only worthwhile when the child must follow the parent’s revisions.

Inline Entity Form

Create and edit referenced entities inside the parent form.

Form complexity, access and validation edge cases; for media, the Media Library already covers the case.

Field Group

Groups fields in tabs or fieldsets on forms and displays.

Presentation only; it can hide required-field problems in collapsed groups.

Address, Geofield, Smart Date, Link Attributes, Computed Field, Double Field

Postal addresses, geometries, recurring date ranges, extra link attributes, derived values and two-value fields.

Each has its own release cadence, storage model and Drupal 11 and 12 status. Check the project page for the supported core versions before adopting and prefer ones that maintain the security coverage badge.

Alternatives are often better than reflexively adding Paragraphs. A structured exhibition page with a hero, a text section and a gallery strip can be built with core fields plus Layout Builder (see Layout Builder), or with Canvas, which renders Single Directory Components and code components for editors (see Canvas visual page building). The Drupal CMS 2.0 documentation does not describe an automatic migration from Paragraphs or Layout Builder to Canvas, so a site that adopts Canvas should plan to move that content itself. Whatever you choose, treat contributed field types as upgrade risk: before a major upgrade check each project for the next core version (see Preparing for Drupal 12).

A related warning from the core removals: modules that older tutorials rely on (Statistics, Book, Forum, Tour and others) were removed in earlier majors, and the Telephone module follows in Drupal 12. The correct preparation is to install the contrib replacement first and to never uninstall the core module before the upgrade.

Common errors

Symptom Cause and fix

"You cannot change this setting because the field already contains data"

Storage settings (target_type, allowed list keys, max_length reductions) are locked once data exists. Create a new field and migrate, or write an update path.

Autocomplete shows no results for an existing entity

The selection handler filters it out: unpublished status, missing access, wrong bundle list, or a Views handler whose search fields do not include the typed text.

Selecting from a list stalls the form

options_select on a large target set loads all entities. Switch to autocomplete or the Media Library.

Reference to a deleted entity renders nothing

Drupal does not cascade deletes. The reference stays as an orphan id; remove it with a cleanup or by deleting through a workflow that checks usage.

Date shows the wrong hour

Time zone: stored UTC, shown in the user or site zone.

Formatted field loses markup

The text format of the user filters it. Check the format’s allowed tags.

javascript: link rejected

The link field validates schemes, by design.

Reverse list is stale

A cache tag for the target list is missing in custom code that builds the list.

Version notes and corrections

  • Entity reference as a field type has been in core since Drupal 8. The old standalone contrib module with the same name was absorbed long ago, and references to "entity reference" modules in Drupal 7 tutorials describe contrib.

  • Field type, widget, formatter and selection plugins are declared with PHP attributes (#[FieldType], #[FieldWidget], #[FieldFormatter], #[EntityReferenceSelection]) from Drupal 10.2; annotations still work on a deprecation path. Books and tutorials from 2023 show annotations.

  • The Field UI wizard changed in 10.2 to a two-step "group, then type" flow, so screenshots in older material do not match.

  • The Standard installation profile no longer creates the Article and Page content types from Drupal 11.4, so a tutorial that adds a field to "Article" must first create the content type.

  • The Telephone module is deprecated and slated for removal in Drupal 12.0.0 (scheduled for the week of 2026-12-07); the Drupal 10 end of life is 2026-12-09, so new sites start on Drupal 11.4.

  • Statistics-driven "popular" lists in older books no longer work because the Statistics module was removed in Drupal 11.0.