Every field you've ever clicked "Add field" for in Drupal — text, number, image, date — is a plugin. Someone wrote a small PHP class, told Drupal about it with an annotation, and from that moment on it showed up in that big "Choose a type of field" grid you see when adding a field to a content type. In this lesson you'll read the class that does exactly that for a brand new field type: one that stores an RGB color value.
What you'll learn in this lesson
- How Drupal discovers field type plugins using the
@FieldTypeannotation - The three methods every field type plugin needs to implement, and what each one is responsible for
- The difference between a field's database storage (
schema()) and its logical data structure (propertyDefinitions()) — two different concerns that are easy to conflate as a beginner - Why a field type alone isn't enough to make a usable field — and what other two plugin types it always needs a partner from
The source file
Path: modules/field_example/src/Plugin/Field/FieldType/RgbItem.php
<?php
namespace Drupal\field_example\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
/**
* Plugin implementation of the 'field_example_rgb' field type.
*
* @FieldType(
* id = "field_example_rgb",
* label = @Translation("Example Color RGB"),
* module = "field_example",
* description = @Translation("Demonstrates a field composed of an RGB color."),
* default_widget = "field_example_text",
* default_formatter = "field_example_simple_text"
* )
*/
class RgbItem extends FieldItemBase {
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [
'columns' => [
'value' => [
'type' => 'text',
'size' => 'tiny',
'not null' => FALSE,
],
],
];
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
$value = $this->get('value')->getValue();
return $value === NULL || $value === '';
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('string')
->setLabel(t('Hex value'));
return $properties;
}
}
Forty-something lines, three methods. That's a complete, working field type.
How it works
The @FieldType annotation — how Drupal finds this class at all
/**
* @FieldType(
* id = "field_example_rgb",
* label = @Translation("Example Color RGB"),
* module = "field_example",
* description = @Translation("Demonstrates a field composed of an RGB color."),
* default_widget = "field_example_text",
* default_formatter = "field_example_simple_text"
* )
*/
This isn't a regular comment — it's a machine-readable annotation, and it's the entire reason Drupal knows this plugin exists. Drupal scans every Plugin/Field/FieldType/ namespace across all enabled modules at cache-rebuild time, reads these annotations, and builds a registry of available field types from them. No separate registration file, no hook to implement — the annotation is the registration.
id = "field_example_rgb"— the machine name. This exact string gets written into your site's configuration and database the moment someone adds this field type to a content type.label— the human-readable name a site builder sees in the "Choose a type of field" screen (you can see it in the screenshot below: "Example Color RGB").module = "field_example"— which module owns this plugin, used for dependency tracking.description— the one-line explainer shown under the label in that same screen.default_widgetanddefault_formatter— plugin IDs of the widget and formatter to use automatically if the site builder doesn't pick different ones. More on why these matter at the end of this lesson.
Extending FieldItemBase
Notice the class doesn't implement a huge list of methods from scratch. FieldItemBase is the standard abstract base class almost every field type extends, and it already implements the bulk of Drupal's FieldItemInterface contract with sensible defaults. Because of that, RgbItem only needs to override the three methods that are specific to its own data: how it's stored, whether it counts as empty, and what its data properties look like. Also notice the class name itself: the Item suffix is a Drupal convention signaling "this represents one value" — a field can hold multiple values (an unlimited-cardinality field), and this class describes just one of them.
schema() — telling Drupal how to build the database column
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [
'columns' => [
'value' => [
'type' => 'text',
'size' => 'tiny',
'not null' => FALSE,
],
],
];
}
This is pure database plumbing. When a site builder adds this field to a content type, Drupal actually runs CREATE TABLE/ALTER TABLE statements behind the scenes, and this method is what tells it what columns to create. Here, one column named value, typed as a tiny text column (good for something short like a hex code such as ff5733), nullable so the field can be left empty. If your field type needed to store more than one piece of data per value — say, separate numeric fields for red, green, and blue instead of one combined string — you'd list multiple entries under columns here.
isEmpty() — deciding what counts as "nothing entered"
public function isEmpty() {
$value = $this->get('value')->getValue();
return $value === NULL || $value === '';
}
Drupal calls this every time an entity is saved, to decide whether a given field value is worth keeping or should be silently discarded. Get this wrong — for example, forget to check for an empty string and only check for NULL — and you can end up with junk rows in the database for fields the user never actually filled in.
propertyDefinitions() — the part that's easy to confuse with schema()
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('string')
->setLabel(t('Hex value'));
return $properties;
}
This is the one beginners most often mix up with schema(), so it's worth being explicit: schema() describes the physical database layout — actual columns, actual SQL types. propertyDefinitions() describes the logical data structure using Drupal's Typed Data API — the abstraction layer that lets totally different subsystems (the Validation API, REST, JSON:API, Views) all understand "this field has a property called value, and it behaves like a string" without needing to know anything about the underlying database. The property key 'value' here must match the column name used in schema() — that's how Drupal connects the logical property back to its physical storage.
Quick check: if you wanted this field to store three separate numeric values instead of one combined string, would that change belong in
schema(),propertyDefinitions(), or both? (Answer: both — you'd need three columns inschema()and three matching entries inpropertyDefinitions(), since each describes a different aspect of the same underlying data.)
See it for yourself
Visit /admin/structure/types/manage/article/fields/add-field on your DDEV site and start adding a new field. Scroll (or search) until you find "Example Color RGB" in the list of available field types, and click it.
That's the exact label and description from the annotation you just read, rendering as a real, selectable option in Drupal's field UI — proof that a 40-line PHP class with one annotation is genuinely all it takes to add a new kind of field to Drupal.
Key takeaways
- The
@FieldTypeannotation is the sole mechanism for registering a field type plugin — Drupal discovers it by scanningPlugin/Field/FieldType/directories at cache-rebuild time. Forgot to clear the cache after adding one? That's why it's not showing up. - Extending
FieldItemBasegives you sensible defaults and leaves you to implement just three methods for a functional field type:schema(),isEmpty(), andpropertyDefinitions(). schema()maps directly to real database columns — its types and sizes affect both storage efficiency and the maximum length of values a user can enter.isEmpty()runs at save time to discard blank field items; always check the actual property value, not just whether the object exists.propertyDefinitions()bridges your field type to the Typed Data API, which is what makes it interoperable with REST, JSON:API, Views, and the validation system — describing what the data is, separately from how it's stored.- A field type on its own only handles storage. It's always paired with a widget (for editing) and a formatter (for display) — which is exactly what the next two lessons cover.
Coming up next
You've now defined what a field is — but nobody can actually type a value into it yet. Next up: the widget that puts an editable form element on the content edit form, and turns this field type from a database column into something an editor can actually use.