Create custom content types with bundle classes

Last updated on
12 May 2023

This page has not yet been reviewed by Drupal maintainer(s) and added to the menu.

This documentation needs review. See "Help improve this page" in the sidebar.

As of Drupal 9.3, entity bundles (a.k.a. content types) can be created using classes that extend a content entity type class (https://www.drupal.org/node/3191609#comment-14603206).  Since content types most commonly add fields to a content entity, this is a much more intuitive way of extending entities than the old way, where content types are config entities defined in the UI, and content type and field definitions have to be exported to .yml files in order to be moved across environments.

Create a content entity

This is exactly the same as what you would do in Drupal <= 9.2.  The easiest way to do this is to use drush generate content-entity

<?php

namespace Drupal\wme_gep_content\Entity;

use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\RevisionableContentEntityBase;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\user\EntityOwnerTrait;
use Drupal\my_module\MyContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;

/**
 * Defines the my_content_entity content entity class.
 *
 * @ContentEntityType(
 *   id = "my_content_entity",
 *   label = @Translation("My entity type"),
 *   label_collection = @Translation("My entity types"),
 *   label_singular = @Translation("My entity type"),
 *   label_plural = @Translation("My entity types"),
 *   label_count = @PluralTranslation(
 *     singular = "@count My entity types",
 *     plural = "@count My entity types",
 *   ),
 *   bundle_label = @Translation("My entity type type"),
 *   handlers = {
 *     "list_builder" = "Drupal\my_module\MyEntityTypeListBuilder",
 *     "views_data" = "Drupal\views\EntityViewsData",
 *     "access" = "Drupal\my_module\MyEntityTypeAccessControlHandler",
 *     "form" = {
 *       "add" = "Drupal\my_module\Form\MyEntityTypeForm",
 *       "edit" = "Drupal\my_module\Form\MyEntityTypeForm",
 *       "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
 *     },
 *     "route_provider" = {
 *       "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
 *     }
 *   },
 *   base_table = "my_content_entity",
 *   revision_table = "my_content_entity_revision",
 *   show_revision_ui = TRUE,
 *   admin_permission = "administer my entity type types",
 *   entity_keys = {
 *     "id" = "id",
 *     "revision" = "revision_id",
 *     "bundle" = "bundle",
 *     "label" = "label",
 *     "uuid" = "uuid",
 *     "owner" = "uid",
 *   },
 *   revision_metadata_keys = {
 *     "revision_user" = "revision_uid",
 *     "revision_created" = "revision_timestamp",
 *     "revision_log_message" = "revision_log",
 *   },
 *   links = {
 *     "collection" = "/admin/content/my_content_entity",
 *     
 *     // Drush generate puts 'my_content_entity' as the parameter here.  
 *     // It needs to be 'bundle' in order to get a blank Add form for your
 *     // content type. 
 *     "add-form" = "/my_entity_type/add/{bundle}",
 *     "canonical" = "/my_entity_type/{my_content_entity}",
 *     "edit-form" = "/my_entity_type/{my_content_entity}/edit",
 *     "delete-form" = "/my_content_entity/{my_content_entity}/delete",
 *   },
 * )
 */
class MyContentEntity extends RevisionableContentEntityBase implements MyContentEntityInterface {

  use EntityChangedTrait;
  use EntityOwnerTrait;

    /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {

    $fields = parent::baseFieldDefinitions($entity_type);

    $fields['label'] = BaseFieldDefinition::create('string')
      ->setRevisionable(TRUE)
      ->setLabel(t('Label'))
      ->setRequired(TRUE)
      ->setSetting('max_length', 255)
      ->setDisplayOptions('form', [
        'type' => 'string_textfield',
        'weight' => -5,
      ])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayOptions('view', [
        'label' => 'hidden',
        'type' => 'string',
        'weight' => -5,
      ])
      ->setDisplayConfigurable('view', TRUE);

      ...
    return $fields;
  }

}

An interesting line in that annotation is

 "add-form" = "/my_entity_type/add/{bundle}",

Drush generate puts

"add-form" = "/my_entity_type/add/{my_content_entity}",

in there.  That works if you only have one bundle type.  But if you have more than one, you need to tell the add form what bundle to work with.

When you've created your content entity, declare any fields that will be shared by all bundles by implementing the baseFieldDefinitions() method. 

Since a base class shouldn't have to know anything about its children, call bundleFieldDefinitions() on the bundle of the current instance.

public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
  // Fields to be shared by all bundles go here.
  $definitions = [];

  // Then add fields from the bundle in the current instance.
  $bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('my_content_entity');
  foreach ($bundles as $key => $values) {
    if ($bundle == $key) {
      // Get a string we can call bundleFieldDefinitions() on that Drupal will
      // be able to find, like
      // "\Drupal\my_module\Entity\Bundle\MyBundleClass"
      $qualified_class = '\\' . $values['class'];
      $definitions = $qualified_class::bundleFieldDefinitions($entity_type, $bundle, []);
    }
  }
  return $definitions;
}

Create a bundle class

Drush can do this for you, too, with drush generate bundle-class.  A bundle class extends the content entity class and implements the content entity interface.  Add whatever fields you want for your bundle by implementing the bundleFieldDefinitions() method that will be called by your base class.

Unfortunately, as of this writing, this only works for one bundle class per content entity.  If you want to extend your content entity to multiple bundles, you have to create them manually.

You can also add other methods that might be useful for the bundle. 

<?php
 
namespace Drupal\dc_character\Entity\Bundle;  
 
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\my_module\MyContentEntityInterface;
use Drupal\my_module\BundleFieldDefinition;

//  This is an abstract class that gets created by drush generate bundle-class when you say "yes"
//  to using a base class for your bundle class.  It extends the base class.
use Drupal\my_module\Entity\Bundle\MyBundleClassBundle;   
 
/**
 * A bundle class for MyBundleClass
 */
class MyBundleClass extends MyBundleClassBundle implements MyContentEntityInterface {
 
  /**
   * {@inheritdoc}
   */
  public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
    $definitions = [];
    if ($bundle == 'my_bundle') {
      $definitions['field 1'] = BundleFieldDefinition::create('string')
        // These three attributes must be set for each field.
        // They are what associate the field with the bundle.
        ->setName('field_1')
        ->setTargetEntityTypeId($entity_type->id())
        ->setTargetBundle($bundle)
 
        // and on to regular field definition stuff.
        ->setLabel(t('Field 1)'))
        ->setRequired(FALSE)
        ->setSetting('max_length', 255)
        ->setDisplayOptions('form', [
          'type' => 'string_textfield',
          'weight' => -5,
        ])
        ->setDisplayConfigurable('form', TRUE)
        ->setDisplayOptions('view', [
          'label' => 'inline',
          'type' => 'string',
          'weight' => -5,
        ])
        ->setDisplayConfigurable('view', TRUE);
 
      $definitions['field_2'] = BundleFieldDefinition::create('string')
        ->setName('field_2')
        ->setTargetEntityTypeId($entity_type->id())
        ->setTargetBundle($bundle)
 
        ->setLabel(t('Field 2'))
        ->setRequired(FALSE)
        ->setSetting('max_length', 255)
        ->setDisplayOptions('form', [
          'type' => 'string_textfield',
          'weight' => -5,
        ])
        ->setDisplayConfigurable('form', TRUE)
        ->setDisplayOptions('view', [
          'label' => 'inline',
          'type' => 'string',
          'weight' => -5,
        ])
        ->setDisplayConfigurable('view', TRUE);

        //etc.
    }
 
    return $definitions;
  }
}  

Register your bundle(s) and define field storage for your bundle class(es)

Now that you've got a content entity and a bundle, you need to tell Drupal about it.  This happens in hook_entity_bundle_info() in your my_module.module file.  If you're adding a bundle for an existing content entity like Node, use hook_entity_bundle_info_alter().

/**
 * Implements hook_entity_bundle_info().
 */

function my_module_entity_bundle_info() {
  $bundles['my_content_entity']['my_bundle'] = [
    'label' => t('My Content Type'),
    'class' => MyBundleClass::class
    // You can also add other attributes you find useful
  ];

  ...
  return $bundles;
}

/**
 * Implements hook_entity_field_storage_info()
 *
 * Defines storage for all bundle fields.
 */
function my_module_entity_field_storage_info(EntityTypeInterface $entity_type) {
  if ($entity_type->id() == 'my_content_entity') {
    $definitions = [];
    $bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('my_content_entity');
    foreach ($bundles as $bundle_id=>$bundle_info) {
      if (isset($bundle_info['class'])) {
        $class = '\\'.$bundle_info['class'];
        if (class_exists($class)) {
          $bundle_defs = $class::bundleFieldDefinitions($entity_type, $bundle_id, []);
          array_merge($definitions, $bundle_defs);
        }
      }
    }
    return $definitions;
  }
}

Install fields in your database

Now Drupal knows what your bundle fields look like and how they'll be stored (thanks to your BundleFieldDefinitions), but you still need to install the necessary tables in the DB.  Do this in your my_module.install file.  If you only have one bundle class in your module, you can do this in hook_install().  If your module adds other bundle classes down the road, use hook_update_N() so you don't have to uninstall/reinstall your module.

function my_module_hook_install() {
  \Drupal::messenger()->addStatus(__FUNCTION__);
 
  $efm = \Drupal::service('entity_field.manager');
  $base_fields = $efm->getFieldStorageDefinitions('my_content_entity', 'my_bundle');
  // This gets all fields defined for the bundle, including base fields.
  $type_fields = $efm->getFieldDefinitions('my_content_entity', 'my_bundle');
  // This gets us the list of fields defined by the bundle.
  $bundle_fields = array_diff(array_keys($type_fields), array_keys($base_fields));
  foreach ($bundle_fields as $id => $field_name) {
    \Drupal::entityDefinitionUpdateManager()->installFieldStorageDefinition($field_name, 'my_content_entity', 'my_module', $type_fields[$field_name]);
  }
}

Help improve this page

Page status: Needs review

You can: