Entities Explained


About me

  • Sascha Grossenbacher
  • https://drupal.org/u/berdir
  • Entity system maintainer
  • Contrib maintainer: TMGMT, Paragraphs, Token, Pathauto, Redirect, ...


    • Terminology, concepts
    • Basic API
    • Content entities, fields and typed data
    • Revisions, Content moderation
    • Content translation
    • Storage
    • Displaying entities
    • Bundle classes


    Entity type

    A certain type of entities. Also typically used as a variable name for the entity type definition, consisting of the ID, labels, handlers and other information that makes up an entity type.


    Node/Content (node), Term (taxonomy_term), User (user), User Role (user_role)



    A generic concept to store and access data. A specific entity, identified by entity type and ID.


    Node 1, User 17.

    \Drupal\node\Entity\Node (\Drupal\node\NodeInterface)

    Content entity (types)

    A group of entity types that use fields and typed data. Many more features like revisions, translations. Complex but predictable data structures.


    Node, User, File, Term.


    Config entity (types)

    A group of entity types that use the config system as storage. Machine names as ID, inherits features of config (export, deploy, config translations). Plain data. Often uses one or multiple plugins.


    User Role, Vocabulary, Node Type, Block, View



    An extensible system to add functionality to entity types that can be customized per entity type.


    Storage, Access Control Handler, Forms, View Builder, Views Data


    Variants/subtypes of content entities that can have different fields, form/view displays, templates.

    Bundles are often managed as config entities themselves but do not have to be, can be other plugins or hardcoded.


    Node: Node type, Term: Vocabulary, Media: Media type.

    $entity->bundle(), hook_entity_bundle_info()

    Typed data

    A generic API to describe data and data structures. Entities and related objects are either typed data or a typed data representation of it can be accessed.

    A list contains N items with the same definition, complex data (map) contains a fixed amount of items identified by a key.


    Basic API


    $s = \Drupal::entityTypeManager()->getStorage('entity_type');
    // Create an entity.
    $entity = $s->create(['type' => 'page', 'title' => 'My page']);
    $entity == Node::create();
    // Load
    $entity = $s->load(1);
    $entities = $s->loadMultiple([1,2,3]);
    $entity = Node::load(1);
    $entities = Node::loadMultiple([1,2,3]);
    // Save
    // Delete

    Entity Query

    Find entities based on their values, works for content and config entities. API documentation

      // Or: \Drupal::entityQuery('node')
      $fids = $s->getQuery()
      // Conditions.
      ->condition('type', ['page', 'article'], 'IN')
      // Content entities support relationships.
      ->condition('uid.entity.roles', 'administrator', '=')
      // D10 requires to enable/disable access checks.
      // Sorting and limit results, pager is supported too.
      ->sort('changed', 'DESC')
      ->range(0, 100)
      // Returns a list of IDs, keys are revision ids.

    Common methods

    // Get the ID of an entity.
    // Get the label.
    // Get the UUID.
    // Get all the values as an array.

    URLs, Links and Link templates

    Link templates define common URLs around entities, drupal.org/docs/drupal-apis/entity-api/link-templates. Defaults to canonical, and edit-form for config entities.

    // Generate a URL Template and work with it.
    $url = $entity->toUrl('canonical');
    // Create a Link.
    $link = $entity->toLink('Edit me', 'edit-form');
    // Check if an entity has a certain link template.


    Allows to check if a user can view/update/delete or create an entity. API documentation

    // Boolean check.
    $entity->access('view|update|delete') == TRUE;
    // Access object, to use further or check cacheability info.
    $access = $entity->access('view', $account, TRUE);
    // Fields have access too.
    // Create access.
    // Access has 3 states, use in access hooks.


    React to things happening: CRUD and viewing, access.

    // Before save, entities be changed.
    function hook_entity_presave($entity) {
    // After saving a new entity.
    function hook_entity_insert($entity) {
    // After saving an existing entity.
    function hook_entity_update($entity) {
      // Check if a value changes on update.
      if ($entity->label() != $entity->original->label()
    // Hooks also exist for specific entity types.
    function hook_ENTITY_TYPE_presave() {

    Content entities, fields, typed data


    • A content entity is a container/map of fields.
    • A field is a list of field items
    • A field item is a container/map of properties
    • Properties are typed data: primitive values (integer, string, ..), objects (language, date, ..) or references to another entity


    Name Interface Typed Data
    Entity ContentEntityInterface ::getTypedData()
    EntityAdapter (complex data)
    Field FieldItemListInterface ListInterface
    Field item FieldItemInterface ComplexDataInterface
    Property PrimitiveInterface (non-computed, stored properites) or TypedDataInterface

    Field types

    • Field types are defined as plugins with annotations
    • Represent the field item class
    • Define their children (properties)
    • Optionally provide a field item list class as well

    Accessing and setting values

    Accessing a field property

    // Complete
    // Array access
    // __get() for properties equals ->get('property)->getValue();
    // Delta 0 is the default for properties when using __get()

    Accessing and setting values

    Setting values

    // Complete
    // Magic
    $entity->get('field')->property = 'myvalue';
    // ContentEntityInterface::set(), single/main property.
    $entity->set('field', 'myvalue');
    // ContentEntityInterface::set(), multiple properties.
    $node->set('body', ['value' => 'Hello', 'format' => 'plain_text']);
    // ContentEntityInterface::set(), multiple items.
    $user->set('roles', ['content_editor', 'administrator']);
    // Append an item.

    Accessing and setting values


    // On a field item property.
    => "content_editor"
    // On a field item.
    => ["target_id" => "content_editor"]
    // On a field.
    => [
        0 => ["target_id" => "content_editor"],
        1 => ["target_id" => "administrator"],
    // Shorthand, get a single property of all items.
    array_column($user->get('roles')->getValue(), 'target_id')
    => ["content_editor", "administrator"]

    Accessing and setting values


    // Loop over all fields of an entity.
    foreach ($entity as $field_name => $field) {
      // Loop over all items of a field.
      foreach ($field as $item) {
        if ($item->entity) {

    Field definition

    • FieldStorageDefinitionInterface
      Shared between bundles, information and settings that are used for the storage (table definition): Type, machine name, storage settings, type, entity type, cardinality, ...
    • FieldDefinitionInterface
      Remaining configuration that can vary between bundles for the same field machine name: Label, bundle, settings, required, ...

    Base fields

    • BaseFieldDefinition implements FieldStorageDefinitionInterface, FieldDefinitionInterface
      Base fields are defined in code, in ::baseFieldDefinitions of the entity class and shared across all bundles.

    Configurable fields

    • FieldStorageConfig implements FieldStorageDefinitionInterface
      Storage settings for configurable fields (first step of creating a field in the UI)
    • FieldConfig implements FieldDefinitionInterface
      Field definition for configurable fields (second step of creating a field in the UI)

    Field definition

    Accessing field definitions with an entity


    Field definition

    Accessing field definitions without an entity

      $efm = \Drupal::service('entity_field.manager')
      $efm->getFieldDefinitions($entity_type_id, $bundle)

    Do's, Don'ts and gotchas

    Property names vs. generic methods

      // is shorthand for
      // And not the same as
      // ->entity on entity reference fields is the target.
      $user->get('roles')->entity == $role;
      // ->getEntity() is the host/parent entity.
      $user->get('roles')->getEntity() == $user;

    Do's, Don'ts and gotchas

    Magic methods on content entities

    // is shorthand for
    // __get()/__set() support non-fields, get() does not:
    $node->foo = 'bar';
    // throws an exception:
    $node->set('foo', 'bar');
    // Various "API"s in rely on this behavior at the moment:
    $entity->original, $entity->view, $entity->_referringItem;

    Recommendation: Avoid for fields #3281720

    Revisions, Content moderation


    • Content entities can be revisionable
    • An entity always has a default revision
    • An entity can be saved as a new revision
    • A new revision can be the new default revision or not, in which case it becomes a pending forward revision (drafts)

    Revision API

    ContentEntityInterface implement ...\RevisionableInterface.
    // Get the current revision ID.
    // Is/was the default revision or is latest revision.
    // Set a new non-default revision.

    Content moderation

    • User interface to create pending forward revisions
    • Forces new revisions on every save unless marked as sync (::setSyncing(TRUE))
    • Configurable workflows with states (published, draft, archived) and transitions (publish, archive)
    • Only a single entity can be viewed as a draft at once (Exception: paragraphs/ERR)


    • Make draft changes to multiple entities
    • View the whole site in that state
    • Publish together

    Revision graph

    Content translation


    • Content entities can be translatable
    • An entity always has a default translation
    • Translations can be added and removed
    • Each field is either translatable or not, in which case it has the same value in all translations

    Translation API

    // Get the current language.
    // Check if a translation exists.
    // Get a specific translation.
    $translation = $entity->getTranslation('de');
    // Add a translation.
    $translation = $entity->addTranslation('de', $values);
    // Remove a translation.

    Safely get a translation or fallback

    $er = \Drupal::service('entity.repository');
    $translation = $er->getTranslationFromContext($entity, 'de');

    Translations and revisions

    • Each revision contains all currently existing translations and their values
    • Revisions track which languages changed in them
    • To merge changes, only one translation may change in a given revision
    • New pending forward revisions of translations are based on the current published default language revision

    Revision graph translation drafts

    Publishing translations

    • Publishing a pending forward translation revision creates a new revision from the default translation and then copies all translatable fields values

    Revision graph translation published

    Entity Storage


    • Each entity type has a storage handler responsible for CRUD operations on entities
    • Default storage handler for content entities is SqlContentEntityStorage
    • Content entity storage is also responsible to maintain the schema for fields (storage schema handler)


    • Fields are stored in shared or dedicated tables
    • Content entities have between 1 to 4 shared tables
    • Only base fields with a cardinality of 1 are stored in shared tables
    • One or two dedicated field table(s) for other fields

    Shared tables

    • Defined in entity type annotation, currently no default
    • base_table: Used by all entity types (node, users)
    • data_table: Translatable entity types (node_field_data, users_field_data)
    • revision_table: Revisionable entity types (node_revision)
    • revision_data_table: Revisionable and translatable entity types (node_field_revision)

    Dedicated tables

    • Automatically named: ENTITYTYPE__FIELD_NAME, ENTITY_TYPE_revision__FIELD_NAME
    • Shortened to a hash of field name if otherwise too long
    • Identified by entity id, revision id, langcode, delta
    • Additionally contains bundle and deleted flag to support field deletion

    Shared table example: node

    > SELECT nid, vid, langcode FROM node;
    | nid | vid  | langcode |
    |   1 |    2 | en       |

    Shared table example: node_field_data

    > SELECT nid, vid, langcode AS lang, status,title,
      default_langcode AS def, revision_translation_affected AS affected
      FROM node_field_data;
    | nid | vid | lang | status | title      | def | affected |
    |   1 |   2 | de   |      1 | Example DE |   0 |        1 |
    |   1 |   2 | en   |      1 | Example EN |   1 |     NULL |

    Shared table example: node_revision

    > select nid, vid, langcode, revision_default from node_revision;
    | nid | vid | langcode | revision_default |
    |   1 |   1 | en       |                1 |
    |   1 |   2 | en       |                1 |
    |   1 |   3 | en       |                0 |
    |   1 |   4 | en       |                0 |

    Shared table example: node_field_revision

    > SELECT nid,vid,langcode AS lang,title,status,default_langcode AS def,revision_translation_affected As affected FROM node_field_revision;
    | nid | vid | lang | title            | status | def | affected |
    |   1 |   1 | en   | Example EN       |      1 |   1 |        1 |
    |   1 |   2 | de   | Example DE       |      1 |   0 |        1 |
    |   1 |   2 | en   | Example EN       |      1 |   1 |     NULL |
    |   1 |   3 | de   | Example DE       |      1 |   0 |     NULL |
    |   1 |   3 | en   | Example EN Draft |      0 |   1 |        1 |
    |   1 |   4 | de   | Example DE       |      1 |   0 |     NULL |
    |   1 |   4 | en   | Example EN       |      1 |   1 |     NULL |
    |   1 |   4 | fr   | Example FR Draft |      0 |   0 |        1 |

    Displaying entities


    • EntityViewBuilder is responsible for converting an entity to a render array for a given view mode
    • Two-step process due to render caching: view() only prepares a minimal render array, build does almost all the work
    • Each field is prepared using a field formatter
    • Configuration stored in view displays for a given bundle
    • Twig templates for the entity and each field

    View API

    // Render an entity.
    $view_builder = $entity_type_manager->getViewBuilder('node');
    $build = $view_builder->view($entity, 'teaser');
    // Render a single field using configuration for a given view mode.
    $build = $entity->get('body')->view('teaser');
    // Render a field using fixed configuration.
    $build = $entity->get('body')->view([
      'label' => 'hidden',
      'type' => '...',
      'settings' => [],

    View modes & displays

    • A view mode is for an entity type, indicates that a given entity may be viewed in a given way
    • View displays are for a given bundle, contain formatter settings and order for each enabled component
    • A non-existing view display falls back to "default"

    Twig: Overview

    • No generic implementation for entity templates, theme suggestions and available variables
    • Some common standards, but each entity type is responsible to implement them itself
    • Required: An entity template (often with theme suggestions for bundles and view modes), a preprocess function
    • Look for template_preprocess_ENTITY_TYPE() to learn what is available

    Twig: Node template

    • Theme suggestions: node--VIEW-MODE, node--BUNDLE, node--BUNDLE--VIEW-MODE
    • content: Render arrays for each field created based on configured formatters
    • node: The entity object, useful to check raw values
    • Avoid elements, it contains fields too, content exists because {{ elements }} would be an endless recursion

    Twig: Display fields

    {# Display a single field including wrapping field template #}
    {{ content.field_foo }}
    {# Display first delta of a field, without any wrapping HTML #}
    {{ content.field_foo.0 }}}

    Twig: Check field values

    {# Display a field if it has value #}
    {% if node.field_name.value %}

    {{ content.field_name.0 }}

    {% end %} {# Check for a field value on a referenced entity #} {% if node.field_category.entity.field_highlight.value %} {{ content.field_category.0 }} {% else %} {{ content.field_category.0 }} {% end %}

    Bundle classes


    • Allows to define a single class per entity bundle to use instead of the default entity class
    • Useful to abstract business logic away, making it everywhere, including twig templates
    • Must extend from the entity class
    • Drush 11 has a generator to create the required basic structure and hook
    • Change record

    Define a bundle class

    class BasicPage extends Node implements BasicPageInterface {
      // Implement whatever business logic specific to basic pages.
      public function hasHighlightCategory(): bool {
        if ($this->get('field_category')->entity) {
          return $this->get('field_category')->get('entity')
        return FALSE;

    Set a bundle class

    function mymodule_entity_bundle_info_alter(array &$bundles) {
      if (isset($bundles['node']['page'])) {
        $bundles['node']['page']['class'] = BasicPage::class;

    Twig & entity bundle classes

    Twig can use any method starting with get/has/is

    {# Before #}
    {% if node.field_category.entity.field_highlight.value %}
    {# After #}
    {% if node.hasHighlightCategory() %}