Series Parts:

Disclaimer: This article assumes you have a working knowledge of the following things:

  • Drupal’s Administration UI
  • PHP OOP syntax. Understanding namespaces will be helpful, but not 100% necessary to get the main points.
  • YAML syntax.

All of the examples used in this post are available in my custom_entities Github repo

This post covers:

  • Content Entities & Bundle Config Entities
  • Annotations
  • Entity Base Fields
  • Entity Forms
  • Routing & Menu items
  • List Builders
  • Generating Permissions
  • Access Control

About Entity API

Entities in Drupal 7+ are generic data models that are extended for specific functionality. For example, in Drupal 8 all of the following things are based on the generic Entity: Nodes, Comments, Users, and Blocks. Being entities means that each item shares the same foundation that offer common functionality, such as CRUD (create/read/update/delete) operations. A more Drupal-like example of shared functionality would be “fieldable”. Each of the items listed above can have custom fields added to them because they are all extensions of the foundational Drupal Entity.

The Entity API is how developers create, extend, and otherwise interact with entities in Drupal.

This post is an introduction to creating custom entities and bundles of those entities in Drupal 8. Let’s get started! The first thing you need to know is that Entities come in 2 main flavors. The Content Entity and the Configuration Entity.

Content Entity

Content Entities are stored in their own database table and are normally displayed on the site in some manner. All of the examples listed above are Content Entities, such as Nodes, Blocks, etc. That is not to say all Content Entities must be displayed, just that it is common to do so.

The most familiar example of a Content Entity is the Node, which are stored as rows within the node table in the database

Configuration Entity

Configuration Entities are based on the Config API, but used to store multiple sets of configuration data. Config Entity stores its data in the config database table, prefixed by the module name. A module can define additional prefixes for a configuration entity in its definition (annotation). This will be made more clear in the examples.

Simple description of Configuration data in general: Information about your site that is not content and changes infrequently, such as the name of your site.


Bundles are a special variant of the Configuration Entity. They are Configuration Entities that enhance a content entity by providing mechanisms for having different Content Entity “Types”. A bundle configuration entity stores the different configurations for the various Content Entities, such as settings and custom fields.

The most notable example of Bundles in Drupal are Node Types (aka, Content Types). In the core Node module “Content Types” are setup as a configuration entity that stores the various fields and settings for an individual content type. While Nodes themselves are Content Entities that store the values for a single node.

Confused yet?

The mixture of terminology between “Bundle” vs “Type” are due to legacy Drupal versions. In Drupal 6 and lower, variants of a node were once called a “Node Type” and later renamed to “Content Type” for UX purposes. Drupal 7 introduced the concept of “Entities” and “Entity Types”, as well as the terminology “Bundle” to mean a sub-type of an Entity Type.

A simple breakdown of how this looks hierarchically:

  • Entity Type: Content Entity
    • Content Entity Type: Node
      • Bundle: Article
      • Bundle: Basic Page
    • Content Entity Type: User
      • Has no bundles

For an overview of different types of data within Drupal, see this documentation page.


The Drupal 8 Entity API leverages the concept of Annotations to a great degree. Annotations look like common PHP docblock comments, but are parsed by the system to provide basic information/settings to the PHP class or method the annotations represent. Annotations in Drupal 8 are a product of using Symfony as the underlining framework. Drupal has extended the Symfony annotations system with its own custom annotations.

Annotation Syntax

Function (or, “Object”) to execute with the contained parameters.
parameter = "value"
Parameter (or, “property”) that is passed into a function. Note that the parameter name itself is not surrounded by quotation marks.
array_parameter = { "some_key" = "some_value" }
Parameter that represents an array. Note the use of curly braces to define the array, and how keys in the array are surrounded by quotation marks.
Annotations are contained within the syntax of a docblock, and appear immediately before a class or method.
Commas must separate parameters as well as items in an array.

Syntax usage example:

 * @Function(
 *   parameter = "value",
 *   array_parameter = {
 *     "some_key" = "some_value",
 *     "another_key" = "another_value"
 *   }
 * )

When viewed this way, it should become apparent where commas are necessary. You may also include commas after the final parameter or item in an array. I do this because it’s easier for me to remain consistent.

Entity Annotation Examples

Content Example:

 * Defines the Most Simple entity.
 * @ContentEntityType(
 *   id = "most_simple",
 *   label = @Translation("Most Simple"),
 *   base_table = "most_simple",
 *   entity_keys = {
 *     "id" = "id",
 *     "bundle" = "bundle",
 *   },
 *   fieldable = TRUE,
 *   admin_permission = "administer most_simple types",
 *   handlers = {
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
 *     "list_builder" = "Drupal\Core\Entity\EntityListBuilder",
 *     "access" = "Drupal\Core\Entity\EntityAccessControlHandler",
 *     "views_data" = "Drupal\views\EntityViewsData",
 *     "form" = {
 *       "default" = "Drupal\Core\Entity\ContentEntityForm",
 *       "add" = "Drupal\Core\Entity\ContentEntityForm",
 *       "edit" = "Drupal\Core\Entity\ContentEntityForm",
 *       "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
 *     },
 *     "route_provider" = {
 *       "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
 *     },
 *   },
 *   links = {
 *     "canonical" = "/most_simple/{most_simple}",
 *     "add-page" = "/most_simple/add",
 *     "add-form" = "/most_simple/add/{most_simple_type}",
 *     "edit-form" = "/most_simple/{most_simple}/edit",
 *     "delete-form" = "/most_simple/{most_simple}/delete",
 *     "collection" = "/admin/content/most_simples",
 *   },
 *   bundle_entity_type = "most_simple_type",
 *   field_ui_base_route = "entity.most_simple_type.edit_form",
 * )

Okay, that’s a lot of moving parts. Let’s break it down a little bit.

New Annotations

Object that is instantiated with the contained parameters.
Machine-safe name for the Entity.
Human readable name for the Entity, passed through the @Translation() function so it can be translated by the UI.
Database table where this Entity is stored. This shodld be a custom table name, used only by your new Entity.
Array of aliases for base fields on the Entity. In this example, none of the field names are aliased. More about this later.
Boolean value for whether or not this Entity can have Drupal fields attached/assigned to it through the Fields UI.
Permission used to allow adminstrative access to this entity’s settings / configuration.
Array map of common Entity operations to PHP classes that handle the functionality.
Array map of common Entity form names to PHP classes that handle the form operations.
Array map of of route types to PHP classes. I’ve only personally dealt with the “html” route provider.
Array map of common Entity route names to the URIs where the functionality can be found.
Id of the Config Entity that serves as bundles for this Content Entity.
The name of the Entity route where the Fields UI shodld be attached.


It is not required to explicitly specify all of these annotation properties. Many of the ones shown here (especially “handlers”) are automatically generated when not provided. In my examples, I specify the default values of these properties in order to better understand what is expected and what happening behind the scenes.

Bundle Config Example:

 * Defines the Most Simple Type entity. A configuration entity used to manage
 * bundles for the Most Simple entity.
 * @ConfigEntityType(
 *   id = "most_simple_type",
 *   label = @Translation("Most Simple Type"),
 *   bundle_of = "most_simple",
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "label",
 *     "uuid" = "uuid",
 *   },
 *   config_prefix = "most_simple_type",
 *   config_export = {
 *     "id",
 *     "label",
 *   },
 *   handlers = {
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
 *     "list_builder" = "Drupal\Core\Entity\EntityListBuilder",
 *     "form" = {
 *       "default" = "Drupal\most_simple\Form\MostSimpleTypeEntityForm",
 *       "add" = "Drupal\most_simple\Form\MostSimpleTypeEntityForm",
 *       "edit" = "Drupal\most_simple\Form\MostSimpleTypeEntityForm",
 *       "delete" = "Drupal\Core\Entity\EntityDeleteForm",
 *     },
 *     "route_provider" = {
 *       "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
 *     },
 *   },
 *   admin_permission = "administer most_simple types",
 *   links = {
 *     "canonical" = "/admin/structure/most_simple_type/{most_simple_type}",
 *     "add-form" = "/admin/structure/most_simple_type/add",
 *     "edit-form" = "/admin/structure/most_simple_type/{most_simple_type}/edit",
 *     "delete-form" = "/admin/structure/most_simple_type/{most_simple_type}/delete",
 *     "collection" = "/admin/structure/most_simple_type",
 *   }
 * )

New Annotations

Object that is instantiated with the provided properties.
Id for the Content Entity that this Config Entity enhances as a bundle.
Additional prefix for the values saved to the config database table.
Base fields that will be exported along with the entity.
Though not completely new, it’s worth pointing out that a custom Config Entity needs to provide its own Form handler for “default”, “add”, and “edit”. More on this later.


In many cases, it’s fair to consider annotations as a replacement for an older Drupal 7- hook. Looking at the examples, you can imagine how in previous versions of Drupal you might be required to leverage a hook within your custom module that returns an array with these settings.

Enough with the no-context examples, let’s make it work!

Next – Part 2: Most Simple Entity with Bundles


Resources used to put all this together

See anything in this post that is incorrect or you think could be better explained? Let me know below!

About the Author

Jonathan Daggerhart

Long time Drupal and WordPress developer. I like to write modules and plugins, and I dabble in frontend and design.

2 thoughts on “Drupal 8 Content Entity with Bundles – Part 1: Overview

Leave a Reply

Your email address will not be published. Required fields are marked *