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 directoryPlugin/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_datesof typedaterangeexists on entity typenode`". 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.
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
Source: Drush: field:create
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), |
Core (always) |
|
Object title variants, an accession number |
Text (formatted), |
Text |
|
Exhibition description, with a text format |
List, |
Options |
|
Object material or period from a fixed set |
Number, |
Core (always) |
|
Ticket price, object height in millimetres |
Boolean, |
Core (always) |
|
"On display", "Wheelchair accessible" |
Date, |
Datetime |
|
Opening date of an exhibition |
Date range, |
Datetime Range |
|
Event start and end |
Timestamp, |
Core (always) |
|
Last-sync time of a collection object |
Link, |
Link |
|
Ticket link of an event |
Email, |
Core (always) |
|
Booking contact of a workshop |
Telephone, |
Telephone |
|
Group-booking phone line |
Image, |
Image |
|
Hero image (see media below) |
File, |
File |
|
Accessible PDF programme |
Entity reference, |
Core (always) |
|
Exhibition to objects, event to gallery term |
Comment, |
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
stringfield never produces markup, it produces escaped text. If editors expect markup, the field is the wrong type. -
A formatted field with the format
full_htmlis an XSS vector for anyone who may use that format. Grant formats by role, and give untrusted roles only a restricted format. -
The
stringcolumn length is fixed by the storage settingmax_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 typescreatedandchanged) 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) storesvalueandend_valuewith 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
datetimevalue 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 andY-m-dfor date-only mode.
For the code side of a range query see Database API and entity queries.
Link, email and telephone fields
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 |
Allowed file extensions |
The upload whitelist (for example |
Maximum upload size |
Per field, always capped by PHP’s |
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_createoption, 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_autocompletesuggests 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_tagsallows several values in one comma-separated box and can create entities whenauto_createis 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_autocompletewith 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 |
|---|---|
|
The generic handler. It offers the target bundles ( |
|
Selection is delegated to a View: the field stores |
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;
}
}
Source: Drupal APIs: Plugin API
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.
-
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 theargumentsfrom the field. -
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.
-
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.
Source: API: ViewsSelection
The choice between the two approaches is summarised below.
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())andaccessCheck(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. |
Source: Paragraphs project page
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 ( |
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 |
|
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. |
|
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.