Drupal provides content types for most content-related requirements. But sometimes a content type is not the right solution.
You may need to store application-specific data that has its own fields, permissions, admin pages and business logic.
For example, suppose you are building a product management feature. You could create a Product content type, but products may not really be editorial content in your application.
In this situation, creating a custom content entity can be a cleaner approach.
In this tutorial, we’ll create a simple fieldable Product entity in Drupal 10/11 and understand what each part is doing.
When Should You Create a Custom Entity?
Before writing any code, first decide whether you actually need one.
Drupal nodes are already fieldable and provide revisions, permissions, publishing workflows and many other features.
If your requirement fits naturally into a content type, use a content type.
A custom entity becomes useful when the data represents a separate business object in your application.
Some examples could be:
- Products
- Orders
- Subscriptions
- Applications
- Internal records
- Booking information
The main advantage is control.
You can define your own base fields, permissions, handlers, routes and behaviour while still using Drupal’s Entity and Field APIs.
For this example, we’ll create a simple Product entity.
1. Create the Custom Module
Let’s call our module:
product_entity
Create the following directory:
modules/custom/product_entity
Our initial module structure will look something like this:
product_entity/
├── config/
│ └── schema/
├── src/
│ └── Entity/
└── product_entity.info.yml
Now create product_entity.info.yml:
name: 'Product Entity'
type: module
description: 'Provides a custom fieldable Product content entity.'
package: Custom
core_version_requirement: ^10 || ^11
dependencies:
- drupal:field
- drupal:field_ui
The field_ui dependency is useful because we want administrators to be able to manage additional fields through Drupal’s UI.
2. Create the Product Entity
Now create:
src/Entity/Product.php
Add the following entity definition:
<?php
namespace Drupal\product_entity\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Defines the Product content entity.
*
* @ContentEntityType(
* id = "product",
* label = @Translation("Product"),
* label_collection = @Translation("Products"),
* label_singular = @Translation("product"),
* label_plural = @Translation("products"),
* label_count = @PluralTranslation(
* singular = "@count product",
* plural = "@count products",
* ),
* handlers = {
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "list_builder" = "Drupal\Core\Entity\EntityListBuilder",
* "form" = {
* "add" = "Drupal\Core\Entity\ContentEntityForm",
* "edit" = "Drupal\Core\Entity\ContentEntityForm",
* "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm"
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider"
* }
* },
* base_table = "product",
* admin_permission = "administer product entities",
* entity_keys = {
* "id" = "id",
* "label" = "name",
* "uuid" = "uuid"
* },
* links = {
* "collection" = "/admin/content/products",
* "add-form" = "/admin/content/products/add",
* "canonical" = "/admin/content/products/{product}",
* "edit-form" = "/admin/content/products/{product}/edit",
* "delete-form" = "/admin/content/products/{product}/delete"
* },
* field_ui_base_route = "entity.product.settings"
* )
*/
class Product extends ContentEntityBase {
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Product name'))
->setDescription(t('The name of the product.'))
->setRequired(TRUE)
->setSettings([
'max_length' => 255,
])
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => 0,
])
->setDisplayOptions('view', [
'label' => 'above',
'type' => 'string',
'weight' => 0,
])
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE);
return $fields;
}
}
There is quite a lot happening here, so let’s understand the important parts.
Understanding the Entity Definition
Our Product class extends:
ContentEntityBase
This tells Drupal that Product is a content entity.
The entity ID is:
id = "product"
Drupal will use this machine name internally when loading and working with Product entities.
We have also specified:
base_table = "product"
Drupal will create the base database table required for the entity when the module is installed.
The entity keys tell Drupal which fields have special meaning:
entity_keys = {
"id" = "id",
"label" = "name",
"uuid" = "uuid"
}
The id and uuid fields are provided through the parent entity implementation, while name is the field we define ourselves.
3. Define the Product Name Field
Inside baseFieldDefinitions() we created:
$fields['name'] = BaseFieldDefinition::create('string')
This is a base field.
Base fields are defined in code and are part of the entity itself.
We then make the field required:
->setRequired(TRUE)
and limit its maximum length:
->setSettings([
'max_length' => 255,
])
The display configuration tells Drupal how the field should initially appear on entity forms and when viewing the entity.
This gives every Product entity a product name.
Later, administrators can add configurable fields such as price, category or description through Field UI.
Base Fields vs Configurable Fields
This distinction is important when working with Drupal entities.
Fields defined using BaseFieldDefinition live in code.
For example:
Product Name
could be a required part of every Product in the application.
Fields added through:
Manage fields
are configurable fields.
For example, an administrator could later add:
Price
Product Image
Description
Category
SKU
without changing the entity class.
This combination is one of the useful parts of Drupal’s fieldable entity system.
You can keep essential data structure in code while allowing site builders to extend the entity through the UI.
4. Add the Settings Route
We used this in our entity definition:
field_ui_base_route = "entity.product.settings"
That route needs to exist.
Create:
product_entity.routing.yml
Add:
entity.product.settings:
path: '/admin/structure/product'
defaults:
_form: '\Drupal\field_ui\Form\FieldStorageConfigEditForm'
_title: 'Product'
requirements:
_permission: 'administer product entities'
However, in a real project I normally prefer providing a dedicated settings form rather than using an unrelated form class simply to satisfy the route.
A cleaner solution is to create a small settings form.
Create:
src/Form/ProductSettingsForm.php
Then add:
<?php
namespace Drupal\product_entity\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a settings form for Product entities.
*/
class ProductSettingsForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'product_settings_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['description'] = [
'#markup' => $this->t('Configure Product entity fields and display settings using the available tabs.'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// No settings are currently stored by this form.
}
}
Now update product_entity.routing.yml:
entity.product.settings:
path: '/admin/structure/product'
defaults:
_form: '\Drupal\product_entity\Form\ProductSettingsForm'
_title: 'Product'
requirements:
_permission: 'administer product entities'
Now field_ui_base_route has a proper route that Drupal can use as the base for Field UI pages.
5. Add Permission
We referenced this permission:
administer product entities
So we also need to define it.
Create:
product_entity.permissions.yml
Add:
administer product entities:
title: 'Administer Product entities'
description: 'Create, edit, delete and configure Product entities.'
restrict access: true
For a tutorial this keeps things simple.
In a production project, you may want separate permissions for viewing, creating, editing, deleting and administering products instead of using one permission for everything.
6. Add Product to the Administration Menu
It is useful to provide an admin menu link so users don’t have to remember the URL.
Create:
product_entity.links.menu.yml
Add:
entity.product.collection:
title: 'Products'
description: 'Manage Product entities.'
route_name: entity.product.collection
parent: system.admin_content
This gives administrators a convenient way to reach the Product listing.
We can also add a link for the Product structure page:
entity.product.settings:
title: 'Product'
description: 'Configure Product fields and display.'
route_name: entity.product.settings
parent: system.admin_structure
Now Product configuration can be accessed from Drupal’s Structure section.
7. Add Field UI Tabs
Because our entity is fieldable, we want Drupal’s normal field management pages.
Create:
product_entity.links.task.yml
Add:
entity.product.settings:
title: 'Settings'
route_name: entity.product.settings
base_route: entity.product.settings
entity.product.field_ui_fields:
title: 'Manage fields'
route_name: entity.product.field_ui_fields
base_route: entity.product.settings
entity.entity_form_display.product.default:
title: 'Manage form display'
route_name: entity.entity_form_display.product.default
base_route: entity.product.settings
entity.entity_view_display.product.default:
title: 'Manage display'
route_name: entity.entity_view_display.product.default
base_route: entity.product.settings
Drupal’s Field UI module provides the underlying field-management functionality.
After everything is configured correctly, administrators can extend the Product entity without modifying PHP code every time a new configurable field is required.
8. Enable the Module
Now enable the module:
drush en product_entity -y
Then rebuild the cache:
drush cr
Because this is a new content entity, Drupal will create the required entity storage when the module is installed.
You can then check the administration pages.
Go to:
/admin/content/products
for the Product collection.
And:
/admin/structure/product
for its field and display configuration.
9. Add Fields Through Drupal Admin
Once Field UI is working, you can add additional fields without modifying Product.php.
For example, you might add:
Price
Field type: Number (decimal)
Description
Field type: Text (formatted, long)
Product Image
Field type: Image
Category
Field type: Entity reference
Now our simple Product entity can gradually become a proper product data model.
10. Create a Product Programmatically
One benefit of using Drupal’s Entity API is that custom entities can be handled in much the same way as other Drupal entities.
For example:
$product = \Drupal::entityTypeManager()
->getStorage('product')
->create([
'name' => 'Sample Product',
]);
$product->save();
This will create and save a new Product.
For quick testing, this is fine.
Inside services and production application code, however, avoid relying on the global \Drupal service container shortcut where dependency injection is practical.
For example, inject entity_type.manager into your service and use the storage from there.
That makes the code easier to test and maintain.
11. Load an Existing Product
You can load the entity using its ID:
$product = \Drupal::entityTypeManager()
->getStorage('product')
->load($product_id);
Then access its values:
$name = $product->get('name')->value;
Again, dependency injection is preferred when this code becomes part of an actual service or larger implementation.
A Few Things to Consider Before Using This in Production
Our example is intentionally simple so that the basic Entity API concepts remain easy to understand.
A real project may require additional functionality such as:
- Separate access permissions
- Custom access control
- Entity owner/user support
- Created and changed timestamps
- Revision support
- Publishing status
- Custom list builder
- Custom forms
- Validation constraints
- Entity references
- REST or JSON:API exposure
- Automated tests
Don’t add all of these features just because they are available.
Add them when the business requirement actually needs them.
For example, if Product records need an approval process, revision support may become important.
If Products are purely internal records and only administrators can manage them, a complicated publishing workflow may be unnecessary.
Should You Use a Custom Entity or a Content Type?
This is probably the most important decision in the entire tutorial.
Creating a custom entity is not automatically better than creating a content type.
If you simply need another type of editorial content with fields, revisions, moderation and normal Drupal content management, a content type may be enough.
Use a custom entity when the data represents its own application concept and you need more control over its storage, behaviour, permissions or lifecycle.
For example:
Blog Article → Content type makes sense.
News Article → Content type makes sense.
Order → Custom entity may make more sense.
Subscription → Custom entity may make more sense.
Application Record → Custom entity may make more sense.
There are always exceptions, but thinking about the data this way usually helps you make a better decision.
Common Mistake: Creating Custom Entities Too Early
Developers sometimes create custom entities because they look cleaner or more advanced.
But custom code also becomes something your team needs to maintain.
Before creating one, ask:
Can Drupal already handle this requirement properly with a content type or another existing entity type?
If yes, using Drupal’s existing functionality may save development and maintenance time.
If the requirement genuinely needs its own data model and behaviour, a custom content entity is a good option.
Final Thoughts
Drupal’s Entity API is one of the reasons Drupal can handle applications that go far beyond normal page-based websites.
Once you understand content entities, base fields and configurable fields, you can build data models that behave like native parts of Drupal instead of creating separate custom database tables and manually implementing everything around them.
The basic flow is:
Create module → Define content entity → Add base fields → Configure handlers and routes → Enable Field UI → Add permissions → Install and test
Start with a simple entity like the Product example above.
Once that makes sense, the next useful step is learning how to add revision support, custom access control and custom entity forms.
Those are the features that usually turn a basic custom entity into something you can comfortably use in a real Drupal project.