Entity API
Entities in Drupal are objects used for persistent storage of content and configuration information. Almost every component is an entity or relies on entities to function. Examples of entity types in Drupal include Nodes, Users, Blocks, Menus, Taxonomy terms, Node types, Paragraphs, and more.
There are two entity variants:
Configuration Entities:
- Used by the Configuration System.
- Supports translations and can provide custom defaults for installations.
- Exported as
.yml
files and stored in the config sync folder. - Can be imported into other environments.
- Primarily has simple fields, making it difficult to integrate complex field widgets due to the use of a simple Form API element.
Content Entities:
- Consist of configurable and base fields, can have revisions, and support translations.
- Can have bundles, form modes, and display modes configured.
- Field widgets and formatters can be set up, with support for Layout Builder.
- Content moderation workflows can be applied.
- Supports the Views module (though Views does not work with Config entities by default—there's a contrib module for that).
- Stored in the database and not synced during regular deployments.
- Site builders configure content entity types, while editors or content managers create the content.
- Entities can also have subtypes, known as bundles.
When to Create a Content Entity Type vs. a Configuration Entity?
Configuration Entities:
- Stable and usually defined by developers.
- They can be changed across environments using tools like Config Split, but by default, these entities are global among all environments.
- They are stored in configuration files and should not be changed by customers, users, or editors.
Content Entities:
- Stored in the database and are fieldable, allowing site builders to add fields, manage form and view displays.
- They differ between environments unless the database is synced.
Why create custom entity types instead of using nodes for everything?
For simple websites, Drupal's default entities are usually sufficient. However, for complex systems, a well-architected solution may involve developing custom configuration and content entity types.
The Node entity comes with many content-related fields and a lot of built-in functionality. For a clean architecture, it’s better to create a new content entity type with only the required fields. This allows developers to easily customize the entity, add Views handlers, and tweak the list builder and save()
method without relying on hooks.
The same applies to config entity types. Sometimes, simple configuration forms that save values to a single config file are not enough, especially when you need to create different configurations of the same structure/type.
For example, if you need a vocabulary like CompanyType, it's not ideal to store it alongside content like Countries or Tags. System configurations should be static, and developers should refer to entities like CompanyType by machine name (available in custom entity types), not by term ID.
So, in this example, CompanyType should be a configuration entity type, which is static and should be referred to by its machine name. This is in contrast to content entities (taxonomy terms) like Countries or Tags, which are more dynamic and typically referred to by term ID.
Creating a Custom Entity Type
To create a custom entity type, you need a custom module and a set of interconnected files with consistent syntax and machine names. To simplify the process, tools for automatic file generation are available, such as:
- Drupal Console (deprecated)
- Drush 11-13 (generate command)
Generating a Custom Config Entity Type
Command:
drush generate entity:configuration
Basic File Structure for a Config Entity
For example, to create a config entity type Promotion:
example_module
config
schema
example_module.schema.yml
src
Entity
Promotion.php
Form
PromotionForm.php
PromotionInterface.php
PromotionListBuilder.php
example_module.links.action.yml
example_module.permissions.yml
example_module.routing.yml
Example of Promotion.php
File
<?php declare(strict_types = 1);
namespace Drupal\example_module\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\example_module\PromotionInterface;
/**
* Defines the promotion entity type.
*
* @ConfigEntityType(
* id = "promotion",
* label = @Translation("Promotion"),
* label_collection = @Translation("Promotions"),
* handlers = {
* "list_builder" = "Drupal\example_module\PromotionListBuilder",
* "form" = {
* "add" = "Drupal\example_module\Form\PromotionForm",
* "edit" = "Drupal\example_module\Form\PromotionForm",
* "delete" = "Drupal\Core\Entity\EntityDeleteForm",
* },
* },
* config_prefix = "promotion",
* admin_permission = "administer promotions",
* links = {
* "collection" = "/admin/structure/promotions",
* "add-form" = "/admin/structure/promotion/add",
* "edit-form" = "/admin/structure/promotion/{promotion}",
* "delete-form" = "/admin/structure/promotion/{promotion}/delete",
* },
* entity_keys = {
* "id" = "id",
* "label" = "label",
* "uuid" = "uuid",
* },
* config_export = {
* "id",
* "label",
* "display_label",
* "percentage_off",
* "time_start",
* "time_end",
* },
* )
*/
final class Promotion extends ConfigEntityBase implements PromotionInterface {
/**
* The machine name.
*/
protected string $id;
/**
* The admin label of the promotion.
*/
protected string $label;
/**
* The display label of the promotion.
*/
protected string $display_label;
/**
* The percentage off the order total.
*/
protected float $percentage_off;
/**
* The timestamp when the promotion starts.
*/
protected int $time_start;
/**
* The timestamp when the promotion ends.
*/
protected int $time_end;
}
Annotations define the entity type, and Drupal parses and caches them, creating the necessary database structures during installation or database updates.
Generating a Custom Content Entity Type
Command:
drush generate entity:content
Basic File Structure for a Content Entity
For example, to create a content entity type Order:
example_module
src
Entity
Order.php
OrderRouteProvider.php
Form
OrderForm.php
OrderDeleteForm.php
OrderInterface.php
OrderViewsData.php
OrderListBuilder.php
example_module.links.action.yml
example_module.permissions.yml
example_module.routing.yml
Example of Content Entity annotation
<?php
/**
* Defines the config page entity class.
*
* @ContentEntityType(
* id = "config_pages",
* label = @Translation("Config page"),
* bundle_label = @Translation("Config page type"),
* handlers = {
* "storage" = "Drupal\config_pages\ConfigPagesStorage",
* "access" = "Drupal\config_pages\ConfigPagesAccessControlHandler",
* "list_builder" = "Drupal\config_pages\ConfigPagesListBuilder",
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "views_data" = "Drupal\config_pages\ConfigPagesViewsData",
* "form" = {
* "add" = "Drupal\config_pages\ConfigPagesForm",
* "edit" = "Drupal\config_pages\ConfigPagesForm",
* "default" = "Drupal\config_pages\ConfigPagesForm"
* },
* "translation" = "Drupal\config_pages\ConfigPagesTranslationHandler"
* },
* admin_permission = "administer config_pages types",
* base_table = "config_pages",
* links = {
* "canonical" = "/config_pages/{config_pages}",
* "edit-form" = "/config_pages/{config_pages}",
* "collection" = "/admin/structure/config_pages/config-pages-content",
* },
* entity_keys = {
* "id" = "id",
* "bundle" = "type",
* "label" = "label",
* "context" = "context",
* "uuid" = "uuid"
* },
* bundle_entity_type = "config_pages_type",
* field_ui_base_route = "entity.config_pages_type.edit_form",
* render_cache = TRUE,
* )
*/
class ConfigPages extends ContentEntityBase implements ConfigPagesInterface {
You can also add RouteProvider, form handlers, operations, etc.
Base field definitions
This is an important part of content entity implementation. Base fields are the ones that will be columns in the main entity DB table. You can specify type of the field, title, default value, and even field widget and formatter (but you don't have to). Implement method like this:
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type): array {
$fields = parent::baseFieldDefinitions($entity_type);
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setDescription(t('The name of the test entity.'))
->setTranslatable(TRUE)
->setSetting('max_length', 32)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
])
->setDisplayConfigurable('form', TRUE) // to make field widget for edit in the UI
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => -5,
]);
$fields['shared_by'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Shared by'))
->setDescription(t('The ID of the sharer.'))
->setSetting('target_type', 'user');
// ->isReadOnly(TRUE) will make field readonly.
$fields['shared_to'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Shared to'))
->setDescription(t('The ID of the receiver.'))
->setSetting('target_type', 'user');
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Shared on'))
->setDescription(t('The time that the favorites list was shared.'));
return $fields;
}
More info on possible configurations in annotation in Introduction to Entity API in Drupal 8 on drupal.org - updated Mar 2024
Bundles
Sometimes, a content entity type needs bundles. For example, a Node is a content entity, while its bundles are NodeType config entities.
A content entity type can have multiple bundles, which are essentially subtypes of the entity. For example, the Node entity type has bundles like Article and Page.
So:
- Content Entity Type: A general category of content, such as Node, User, or Taxonomy Term.
- Bundle: A specific subtype of a content entity type, such as Article or Page for the Node entity type.
Example of a Bundle Entity
<?php
namespace Drupal\taxonomy\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
use Drupal\taxonomy\VocabularyInterface;
/**
* Defines the taxonomy vocabulary entity.
*
* @ConfigEntityType(
* id = "taxonomy_vocabulary",
* label = @Translation("Taxonomy vocabulary"),
* bundle_of = "taxonomy_term",
* entity_keys = {
* "id" = "vid",
* "label" = "name",
* },
* links = {
* "edit-form" = "/admin/structure/taxonomy/manage/{taxonomy_vocabulary}",
* }
* )
*/
class Vocabulary extends ConfigEntityBundleBase implements VocabularyInterface {
/**
* The taxonomy vocabulary ID.
*/
protected string $vid;
/**
* Name of the vocabulary.
*/
protected string $name;
}
Entity Type Manager
To work with entities programmatically, you can use the EntityTypeManager
service:
// It returns array of objects.
\Drupal::entityTypeManager()
// Specify entity type machine name, can be both content or config.
->getStorage('taxonomy_term')->load(777);
// Accepts array of IDs as param. If not provided - all entities of this type will be loaded.
\Drupal::entityTypeManager()->getStorage('node')->loadMultiple();
// Load by properties. No support for operators.
\Drupal::entityTypeManager()->getStorage('user')->loadByProperties(['status' => 1]);
// Query. Will return array if IDs which you can later load.
$node_ids = \Drupal::entityTypeManager()->getStorage('node')->getQuery()
->condition('created', 1727112974, '>=')
->accessCheck(FALSE)
->execute();
More Examples:
- Create an entity:php
$node = \Drupal::entityTypeManager()->getStorage('node')->create(['type' => 'article', 'title' => 'Another node']);
- Delete an entity:php
$entity = \Drupal::entityTypeManager()->getStorage('node')->load(1); $entity->delete();
Hooks
hook_entity_update()
hook_entity_insert()
hook_entity_delete()
hook_entity_access()
hook_entity_presave()
hook_entity_type_alter()
More info:
Entity Type Update API
If you make changes to your entity after installation, you may need to update its schema. This involves using hook_update_N()
. You can see an example of this update process in the hook_update_N documentation.
Example: adding a new base field to an entity type The field definition has to be repeated from the entity class, as the definition in the entity class can't be relied on during the update process:
function example_update_8701() {
$field_storage_definition = BaseFieldDefinition::create('boolean')
->setLabel(t('Revision translation affected'))
->setDescription(t('Indicates if the last edit of a translation belongs to current revision.'))
->setReadOnly(TRUE)
->setRevisionable(TRUE)
->setTranslatable(TRUE);
\Drupal::entityDefinitionUpdateManager()
->installFieldStorageDefinition('revision_translation_affected', 'block_content', 'block_content', $field_storage_definition);
}
More info at Updating Entities and Fields in Drupal 8 on drupal.org - updated Mar 2024
Questions
You have an existing custom Content Entity Type on the Live site. You need to add new BASE field to this entity type programatically. How will you do it?
We should use hook_update() for such changes.
function custom_module_update_1001() {
// Use any base field definition settings here. Don't forget that definition for new field should be included not only in update hook but also in the Entity Type class, in baseFieldDefinitions() method.
// Adding a new base field into the entity type is adding a new column into the base table for this entity type.
$field_storage_definition = BaseFieldDefinition::create('integer')
->setLabel(t('New field'))
->setDescription(t('Field description'))
->setSettings([
'min' => 1,
'unsigned' => TRUE,
'size' => 'small',
])
->setReadOnly(TRUE);
$entity_type = 'commerce_product'; // for example
$module = 'custom_module';
\Drupal::entityDefinitionUpdateManager()
->installFieldStorageDefinition('new_field', $entity_type, $module, $field_storage_definition);
}
You have custom Content Entity Type. You have added new operation into it's declaration. How to apply your changes?
We should use hook_update() as well.
/**
* Add new operation to entity.
*/
function custom_module_update_1002() {
// First of all you need to go to your custom entity class. In this case it is ProductReview.php and the you
// need to edit the annotation for the entity by adding a new operation there. (you need a form class for new
// operation as well obviously)
// But simply editing the annotation won't help if module was previously installed. So now you need this update hook
// to update the entity definition.
// So what the code below basically does: it reads the entity type annotation from the file again and updates its
// schema in the database to reflect the current entity type definition.
$manager = \Drupal::entityDefinitionUpdateManager();
$entity_type = $manager->getEntityType('product_review');
$manager->updateEntityType($entity_type);
}
You are implementing new content entity and you need a simple text list field in base field definitions. How do you implement it with a widget and a formatter?
$fields['role'] = BaseFieldDefinition::create('list_string')
->setLabel(t('Role'))
->setDescription(t('The role of the Contact entity.'))
->setSettings([
'allowed_values' => [
'administrator' => 'administrator',
'user' => 'user',
],
])
->setDefaultValue('user')
->setDisplayOptions('view', [
'label' => 'above',
'type' => 'string',
'weight' => -2,
])
->setDisplayOptions('form', [
'type' => 'options_select',
'weight' => -2,
])
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE);