Series Parts:

Quick confession: This is actually not the “most” simple version of a custom entity. You don’t necessarily need a to provide a custom permission. You could instead use an existing permission. Additionally, all of the annotations shown in the following example don’t need to be stated explicitly (especially handlers[form]), as they will automatically use the default core values when not present. I choose to specify each annotation to help me better understand what is going on.

Now, let’s create an extremely simple custom entity that is fieldable and has bundles. To do so, we’re going to need a few things:

The Plan

  • Custom permission – Alternatively, you could use an existing Drupal permission.
  • Content Entity – In this case, named “Most Simple”.
  • Config Schema – Details how the config entity is stored within the database.
  • Config Entity – In this case, named “Most Simple Type”.
  • Config Entity Form – Custom default/add/edit form for the Config Entity.

The Code


This is pretty straight forward. Just create MODULENAME.permissions.yml file in the module and provide a new permission.

administer most_simple types:
  title: 'Administer MostSimpleEntity types'
  restrict access: true

Config Schema

Create the config/MODULENAME.schema.yml file within the module, and define the details for our Config Entity.

  type: config_entity
  label: 'Most Simple Type settings'
      type: string
      label: 'Machine-readable name'
      type: stype
      label: 'UUID'
      type: label
      label: 'Label'

Content Entity

Besides annotations, you might be surprised how little PHP code it takes to create a very simple Content Entity. That is because of the magic that happens behind the scenes. Take a look.


namespace Drupal\most_simple\Entity;

use Drupal\Core\Entity\ContentEntityBase;

 * 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",
 * )
class MostSimpleEntity extends ContentEntityBase {}

The Magic of ContentEntityBase

Aside from the annotation this file only has 3 lines of PHP. A namespace for this class, the import for the extending the ContentEntityBase class, and the custom class definition itself. But due to extending the ContentEntityBase, there is much more going on in the background when this class is instantiated.

Installation – On installation of the module containing this entity, the base_table will be created and the class’s “Base Fields” will define the columns for that table. But if you read closely, you’ll notice that we didn’t define anything in the annotation called “base fields”… so where are they coming from?

Base Field Definitions – The parent class ContentEntityBase has a static method named baseFieldDefinitions that is called at various times throughout Drupal core. Inside this method, it automatically looks for specific entity_keys and added them as base fields.

Entity Keys ContentEntityBase will automatically create field definitions for:

  • id
  • uuid
  • revision
  • langcode
  • bundle

You can also override the baseFieldDefinitions method in your own class to provide additional base fields. I’ll do this in a later example.

Config Entity

Creating a very simple Config Entity also requires very little PHP. Most of the work is done by Drupal parsing the annotation.


namespace Drupal\most_simple\Entity;

use Drupal\Core\Config\Entity\ConfigEntityBundleBase;

 * 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",
 *   }
 * )
class MostSimpleTypeEntity extends ConfigEntityBundleBase {}

The main differences to note between this Config Entity and the previous Content Entity are the new annotation properties (described in the examples at the top of this post), and the new form handlers. For a Config Entity, you’ll need to provide a custom class for the form handlers (explained below).

The Magic of ConfigEntityBundleBase

Unlike the ContentEntityBase, the ConfigEntityBundleBase does not have a constructor that provides a lot of behind the scenes magic. Most of the methods in this base class are related to operations that are performed on the entity, such as the preSave() and postSave() methods.

Config Entity Form

As previously mentioned, we need to create our own form class for Config Entities. The reason for this is because ConfigEntityBundleBase does not magically turn our entity_keys into fields.

If you haven’t created a custom form for the config entity and you go to create an entity through the UI, there will be no fields in which to provide values for our entity_keys and the form will not save.


namespace Drupal\most_simple\Form;

use Drupal\Core\Entity\BundleEntityFormBase;
use Drupal\Core\Form\FormStateInterface;

class MostSimpleTypeEntityForm extends BundleEntityFormBase {

   * {@inheritdoc}
  public function form(array $form, FormStateInterface $form_state) {
    $form = parent::form($form, $form_state);

    $entity_type = $this->entity;
    $content_entity_id = $entity_type->getEntityType()->getBundleOf();

    $form['label'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Label'),
      '#maxlength' => 255,
      '#default_value' => $entity_type->label(),
      '#description' => $this->t("Label for the %content_entity_id entity type (bundle).", ['%content_entity_id' => $content_entity_id]),
      '#required' => TRUE,

    $form['id'] = [
      '#type' => 'machine_name',
      '#default_value' => $entity_type->id(),
      '#machine_name' => [
        'exists' => '\Drupal\mostSimple\Entity\MostSimpleTypeEntity::load',
      '#disabled' => !$entity_type->isNew(),

    return $this->protectBundleIdElement($form);

   * {@inheritdoc}
  public function save(array $form, FormStateInterface $form_state) {
    $entity_type = $this->entity;
    $status = $entity_type->save();
    $message_params = [
      '%label' => $entity_type->label(),
      '%content_entity_id' => $entity_type->getEntityType()->getBundleOf(),

    switch ($status) {
      case SAVED_NEW:
        drupal_set_message($this->t('Created the %label %content_entity_id entity type.', $message_params));

        drupal_set_message($this->t('Saved the %label %content_entity_id entity type.', $message_params));


In the custom Config Entity Form we define two methods; form() and save(). The form method simply provides fields for the entity_keys defined in the annotation, and the save method simply saves the entity and redirects the user with a message.


That’s it! We now have a very simple Content Entity that has Bundles! Unfortunately, the user experience for managing these entities is terrible.

🙁 Problems:

  • No navigation. You must browse directly to the entity management URLs
  • Few messages telling us what happened. The only message we’ve created is when saving the Config Entity.
  • Few redirects dealing entities. The only redirect we’ve created is when saving the Config Entity
  • No practical information on the entity lists. If you were to navigate to the “collection” routes for these entities, all you would see is a list of operations for each entity. You can’t even tell which entity is which. We’ll fix this in the next example by adding ListBuilders to our module.

Luckily, most of these problems are pretty easy to fix…

Next – Part 3: Simple Content Entity with Bundles


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.

Leave a Reply

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