On this page
- Set up module
- content_entity_example.info.yml
- Permissions
- Routing
- content_entity_example.routing.yml
- content_entity_example.links.menu.yml
- content_entity_example.links.action.yml
- content_entity_example.links.task.yml
- Entity type classes
- src/ContactInterface.php
- src/Entity/Contact.php
- Controllers
- src/Form/ContactForm.php
- src/Form/ContactDeleteForm.php
- src/ContactListBuilder.php
- Field Settings
- src/Form/ContactSettingsForm.php
- Access control handler
- src/ContactAccessControlHandler.php
- Register your new entity
- Integration with JSON:API
Creating a content entity type in Drupal 8
This documentation needs work. See "Help improve this page" in the sidebar.
This page provides an example of how to create a content entity type, with administration management pages, for Drupal 8.
It will let you know how-to create a 'Contact' entity to add, edit and delete People (Contacts). Entity is fully fieldable and uses most of the new entity concepts available in Drupal 8.
Notes:
- This guide shows how to write the code to create a content entity type. However, this guide can be bypassed by instead using Drush, with which a new content entity type can be created from the command line using
drush generate entity:content
. - This code in this guide corresponds to the
ContentEntityExample
of the Drupal Examples module, which can be downloaded from https://www.drupal.org/project/examples.
Set up module
content_entity_example.info.yml
name: Content Entity Example
type: module
description: 'Provides ContentEntityExampleContact entity.'
package: Example modules
core: 8.x
core_version_requirement: ^8.7 || ^9
# These modules are required by the tests, must be available at bootstrap time
dependencies:
- options
- entity_reference
- examples
Permissions
content_entity_example.permissions.yml
view contact entity:
title: 'View Contact entity'
add contact entity:
title: 'Add Contact entity'
edit contact entity:
title: 'Edit Contact Entity'
delete contact entity:
title: 'Delete Contact entity'
administer contact entity:
title: 'Administer Contact entity'
restrict access: TRUE
Routing
content_entity_example.routing.yml
# This file brings everything together. Very nifty!
# Route name can be used in several places; e.g. links, redirects, and local
# actions.
entity.content_entity_example_contact.canonical:
path: '/content_entity_example_contact/{content_entity_example_contact}'
defaults:
# Calls the view controller, defined in the annotation of the contact entity
_entity_view: 'content_entity_example_contact'
_title: 'Contact Content'
requirements:
# Calls the access controller of the entity, $operation 'view'
_entity_access: 'content_entity_example_contact.view'
entity.content_entity_example_contact.collection:
path: '/content_entity_example_contact/list'
defaults:
# Calls the list controller, defined in the annotation of the contact entity.
_entity_list: 'content_entity_example_contact'
_title: 'Contact List'
requirements:
# Checks for permission directly.
_permission: 'administer contact entity'
content_entity_example.contact_add:
path: '/content_entity_example_contact/add'
defaults:
# Calls the form.add controller, defined in the contact entity.
_entity_form: content_entity_example_contact.add
_title: 'Add Contact'
requirements:
_entity_create_access: 'content_entity_example_contact'
entity.content_entity_example_contact.edit_form:
path: '/content_entity_example_contact/{content_entity_example_contact}/edit'
defaults:
# Calls the form.edit controller, defined in the contact entity.
_entity_form: content_entity_example_contact.edit
_title: 'Edit Contact'
requirements:
_entity_access: 'content_entity_example_contact.edit'
entity.content_entity_example_contact.delete_form:
path: '/contact/{content_entity_example_contact}/delete'
defaults:
# Calls the form.delete controller, defined in the contact entity.
_entity_form: content_entity_example_contact.delete
_title: 'Delete Contact'
requirements:
_entity_access: 'content_entity_example_contact.delete'
content_entity_example.contact_settings:
path: 'admin/structure/content_entity_example_contact_settings'
defaults:
_form: '\Drupal\content_entity_example\Form\ContactSettingsForm'
_title: 'Contact Settings'
requirements:
_permission: 'administer contact entity'
The route names for actions defined in the 'link' section of the entity annotation must follow the right pattern. For details please see the Content Entity Class below.
content_entity_example.links.menu.yml
In combination with the routing file, this replaces hook_menu for the module.
# Define the menu links for this module
entity.content_entity_example_contact.collection:
title: 'Content Entity Example: Contacts Listing'
route_name: entity.content_entity_example_contact.collection
description: 'List Contacts'
parent: system.admin_structure
weight: 10
content_entity_example_contact.admin.structure.settings:
title: Contact Settings
description: 'Configure Contact entity'
route_name: content_entity_example.contact_settings
parent: system.admin_structure
content_entity_example.links.action.yml
# All action links for this module
content_entity_example.contact_add:
# Which route will be called by the link
route_name: content_entity_example.contact_add
title: 'Add Contact'
# Where will the link appear, defined by route name.
appears_on:
- entity.content_entity_example_contact.collection
- entity.content_entity_example_contact.canonical
content_entity_example.links.task.yml
The "View/Edit/Delete" tabs will appear on the entity view page.
The "Settings" tab will appear on the entity settings page.
# Define the 'local' links for the module
contact.settings_tab:
route_name: content_entity_example.contact_settings
title: Settings
base_route: content_entity_example.contact_settings
contact.view:
route_name: entity.content_entity_example_contact.canonical
base_route: entity.content_entity_example_contact.canonical
title: View
contact.page_edit:
route_name: entity.content_entity_example_contact.edit_form
base_route: entity.content_entity_example_contact.canonical
title: Edit
contact.delete_confirm:
route_name: entity.content_entity_example_contact.delete_form
base_route: entity.content_entity_example_contact.canonical
title: Delete
weight: 10
Entity type classes
src/ContactInterface.php
It is good practise to provide an interface to define the public access to an entity. In addition, it invokes the 'EntityOwnerInterface' to get access to additional functionality.
<?php
namespace Drupal\content_entity_example;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\user\EntityOwnerInterface;
use Drupal\Core\Entity\EntityChangedInterface;
/**
* Provides an interface defining a Contact entity.
* @ingroup content_entity_example
*/
interface ContactInterface extends ContentEntityInterface, EntityOwnerInterface, EntityChangedInterface {
}
?>
src/Entity/Contact.php
This file defines the Contact entity class.
The database schema is automatically determined from the definition of the base fields and corresponding tables are set up in the database during installation of the module.
As mentioned in the routing section, the routes for the 'links' section must follow the right pattern. It is documented in the annotations section below.
<?php
namespace Drupal\content_entity_example\Entity;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\content_entity_example\ContactInterface;
use Drupal\user\UserInterface;
/**
* Defines the Contact entity.
*
* @ingroup content_entity_example
*
* This is the main definition of the entity type. From it, an entityType is
* derived. The most important properties in this example are listed below.
*
* id: The unique identifier of this entityType. It follows the pattern
* 'moduleName_xyz' to avoid naming conflicts.
*
* label: Human readable name of the entity type.
*
* handlers: Handler classes are used for different tasks. You can use
* standard handlers provided by D8 or build your own, most probably derived
* from the standard class. In detail:
*
* - view_builder: we use the standard controller to view an instance. It is
* called when a route lists an '_entity_view' default for the entityType
* (see routing.yml for details. The view can be manipulated by using the
* standard drupal tools in the settings.
*
* - list_builder: We derive our own list builder class from the
* entityListBuilder to control the presentation.
* If there is a view available for this entity from the views module, it
* overrides the list builder. @todo: any view? naming convention?
*
* - form: We derive our own forms to add functionality like additional fields,
* redirects etc. These forms are called when the routing list an
* '_entity_form' default for the entityType. Depending on the suffix
* (.add/.edit/.delete) in the route, the correct form is called.
*
* - access: Our own accessController where we determine access rights based on
* permissions.
*
* More properties:
*
* - base_table: Define the name of the table used to store the data. Make sure
* it is unique. The schema is automatically determined from the
* BaseFieldDefinitions below. The table is automatically created during
* installation.
*
* - fieldable: Can additional fields be added to the entity via the GUI?
* Analog to content types.
*
* - entity_keys: How to access the fields. Analog to 'nid' or 'uid'.
*
* - links: Provide links to do standard tasks. The 'edit-form' and
* 'delete-form' links are added to the list built by the
* entityListController. They will show up as action buttons in an additional
* column.
*
* There are many more properties to be used in an entity type definition. For
* a complete overview, please refer to the '\Drupal\Core\Entity\EntityType'
* class definition.
*
* The following construct is the actual definition of the entity type which
* is read and cached. Don't forget to clear cache after changes.
*
* @ContentEntityType(
* id = "content_entity_example_contact",
* label = @Translation("Contact entity"),
* handlers = {
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "list_builder" = "Drupal\content_entity_example\ContactListBuilder",
* "views_data" = "Drupal\views\EntityViewsData",
* "form" = {
* "add" = "Drupal\content_entity_example\Form\ContactForm",
* "edit" = "Drupal\content_entity_example\Form\ContactForm",
* "delete" = "Drupal\content_entity_example\Form\ContactDeleteForm",
* },
* "access" = "Drupal\content_entity_example\ContactAccessControlHandler",
* },
* base_table = "contact",
* admin_permission = "administer contact entity",
* fieldable = TRUE,
* entity_keys = {
* "id" = "id",
* "label" = "name",
* "uuid" = "uuid"
* },
* links = {
* "canonical" = "/content_entity_example_contact/{content_entity_example_contact}",
* "edit-form" = "/content_entity_example_contact/{content_entity_example_contact}/edit",
* "delete-form" = "/contact/{content_entity_example_contact}/delete",
* "collection" = "/content_entity_example_contact/list"
* },
* field_ui_base_route = "content_entity_example.contact_settings",
* )
*
* The 'links' above are defined by their path. For core to find the corresponding
* route, the route name must follow the correct pattern:
*
* entity.<entity-name>.<link-name> (replace dashes with underscores)
* Example: 'entity.content_entity_example_contact.canonical'
*
* See routing file above for the corresponding implementation
*
* The 'Contact' class defines methods and fields for the contact entity.
*
* Being derived from the ContentEntityBase class, we can override the methods
* we want. In our case we want to provide access to the standard fields about
* creation and changed time stamps.
*
* Our interface (see ContactInterface) also exposes the EntityOwnerInterface.
* This allows us to provide methods for setting and providing ownership
* information.
*
* The most important part is the definitions of the field properties for this
* entity type. These are of the same type as fields added through the GUI, but
* they can by changed in code. In the definition we can define if the user with
* the rights privileges can influence the presentation (view, edit) of each
* field.
*/
class Contact extends ContentEntityBase implements ContactInterface {
use EntityChangedTrait; // Implements methods defined by EntityChangedInterface.
/**
* {@inheritdoc}
*
* When a new entity instance is added, set the user_id entity reference to
* the current user as the creator of the instance.
*/
public static function preCreate(EntityStorageInterface $storage_controller, array &$values) {
parent::preCreate($storage_controller, $values);
$values += array(
'user_id' => \Drupal::currentUser()->id(),
);
}
/**
* {@inheritdoc}
*/
public function getCreatedTime() {
return $this->get('created')->value;
}
/**
* {@inheritdoc}
*/
public function getOwner() {
return $this->get('user_id')->entity;
}
/**
* {@inheritdoc}
*/
public function getOwnerId() {
return $this->get('user_id')->target_id;
}
/**
* {@inheritdoc}
*/
public function setOwnerId($uid) {
$this->set('user_id', $uid);
return $this;
}
/**
* {@inheritdoc}
*/
public function setOwner(UserInterface $account) {
$this->set('user_id', $account->id());
return $this;
}
/**
* {@inheritdoc}
*
* Define the field properties here.
*
* Field name, type and size determine the table structure.
*
* In addition, we can define how the field and its content can be manipulated
* in the GUI. The behaviour of the widgets used can be determined here.
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
// Standard field, used as unique if primary index.
$fields['id'] = BaseFieldDefinition::create('integer')
->setLabel(t('ID'))
->setDescription(t('The ID of the Contact entity.'))
->setReadOnly(TRUE);
// Standard field, unique outside of the scope of the current project.
$fields['uuid'] = BaseFieldDefinition::create('uuid')
->setLabel(t('UUID'))
->setDescription(t('The UUID of the Contact entity.'))
->setReadOnly(TRUE);
// Name field for the contact.
// We set display options for the view as well as the form.
// Users with correct privileges can change the view and edit configuration.
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setDescription(t('The name of the Contact entity.'))
->setSettings(array(
'default_value' => '',
'max_length' => 255,
'text_processing' => 0,
))
->setDisplayOptions('view', array(
'label' => 'above',
'type' => 'string',
'weight' => -6,
))
->setDisplayOptions('form', array(
'type' => 'string_textfield',
'weight' => -6,
))
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE);
$fields['first_name'] = BaseFieldDefinition::create('string')
->setLabel(t('First Name'))
->setDescription(t('The first name of the Contact entity.'))
->setSettings(array(
'default_value' => '',
'max_length' => 255,
'text_processing' => 0,
))
->setDisplayOptions('view', array(
'label' => 'above',
'type' => 'string',
'weight' => -5,
))
->setDisplayOptions('form', array(
'type' => 'string_textfield',
'weight' => -5,
))
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE);
// ListTextType with a drop down menu widget.
// In the view the field content is shown as a string.
// In the form the choices are presented as an options list.
$fields['primary_color'] = BaseFieldDefinition::create('list_string')
->setLabel(t('Favorite Primary Color'))
->setDescription(t('The favorite primary color of the Contact entity.'))
->setSettings([
'allowed_values' => [
'blue' => t('Blue'),
'red' => t('Red'),
'yellow' => t('Yellow'),
],
])
->setDisplayOptions('view', array(
'label' => 'above',
'type' => 'list_default',
'weight' => -4,
))
->setDisplayOptions('form', array(
'type' => 'options_select',
'weight' => -4,
))
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE);
// Owner field of the contact.
// Entity reference field, holds the reference to the user object.
// The view shows the user name field of the user.
// The form presents a auto complete field for the user name.
$fields['user_id'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('User Name'))
->setDescription(t('The Name of the associated user.'))
->setSetting('target_type', 'user')
->setSetting('handler', 'default')
->setDisplayOptions('view', array(
'label' => 'above',
'type' => 'entity_reference_label',
'weight' => -3,
))
->setDisplayOptions('form', array(
'type' => 'entity_reference_autocomplete',
'settings' => array(
'match_operator' => 'CONTAINS',
'size' => 60,
'autocomplete_type' => 'tags',
'placeholder' => '',
),
'weight' => -3,
))
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE);
$fields['langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Language code'))
->setDescription(t('The language code of Contact entity.'));
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Created'))
->setDescription(t('The time that the entity was created.'));
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time that the entity was last edited.'));
return $fields;
}
}
?>
Controllers
src/Form/ContactForm.php
Define the form for adding and editing Contact entity content.
It is called by the '_entity_form' definition in the routing.
<?php
namespace Drupal\content_entity_example\Form;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Form controller for the content_entity_example entity edit forms.
*
* @ingroup content_entity_example
*/
class ContactForm extends ContentEntityForm {
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
/* @var $entity \Drupal\content_entity_example\Entity\Contact */
$form = parent::buildForm($form, $form_state);
$entity = $this->entity;
$form['langcode'] = array(
'#title' => $this->t('Language'),
'#type' => 'language_select',
'#default_value' => $entity->getUntranslated()->language()->getId(),
'#languages' => LanguageInterface::STATE_ALL,
);
return $form;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$status = parent::save($form, $form_state);
$entity = $this->entity;
if ($status == SAVED_UPDATED) {
$this->messenger()
->addMessage($this->t('The contact %feed has been updated.', ['%feed' => $entity->toLink()->toString()]));
} else {
$this->messenger()
->addMessage($this->t('The contact %feed has been added.', ['%feed' => $entity->toLink()->toString()]));
}
$form_state->setRedirectUrl($this->entity->toUrl('collection'));
return $status;
}
}
?>
src/Form/ContactDeleteForm.php
Confirmation form when deleting Contact entity content.
<?php
namespace Drupal\content_entity_example\Form;
use Drupal\Core\Entity\ContentEntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Provides a form for deleting a content_entity_example entity.
*
* @ingroup content_entity_example
*/
class ContactDeleteForm extends ContentEntityConfirmFormBase {
/**
* Returns the question to ask the user.
*
* @return string
* The form question. The page title will be set to this value.
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete %name?', array('%name' => $this->entity->label()));
}
/**
* Returns the route to go to if the user cancels the action.
*
* @return \Drupal\Core\Url
* A URL object.
*/
public function getCancelUrl() {
return new Url('entity.content_entity_example_contact.collection');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* {@inheritdoc}
*
* Delete the entity and log the event. logger() replaces the watchdog.
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$entity = $this->getEntity();
$entity->delete();
$this->logger('content_entity_example')->notice('deleted %title.',
array(
'%title' => $this->entity->label(),
));
// Redirect to term list after delete.
$form_state->setRedirect('entity.content_entity_example_contact.collection');
}
}
?>
src/ContactListBuilder.php
Define header and row content for the Contact listing. The 'Operations' links are added automatically from the 'links' definition in the entityType annotation when the parent functions are called.
<?php
namespace Drupal\content_entity_example;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;
use Drupal\Core\Url;
/**
* Provides a list controller for content_entity_example_contact entity.
*
* @ingroup content_entity_example
*/
class ContactListBuilder extends EntityListBuilder {
/**
* {@inheritdoc}
*
* We override ::render() so that we can add our own content above the table.
* parent::render() is where EntityListBuilder creates the table using our
* buildHeader() and buildRow() implementations.
*/
public function render() {
$build['description'] = [
'#markup' => $this->t('Content Entity Example implements a Contacts model. These contacts are fieldable entities. You can manage the fields on the <a href="@adminlink">Contacts admin page</a>.', array(
'@adminlink' => Url::fromRoute('content_entity_example.contact_settings', [], ['absolute' => 'true'])->toString(),
)),
];
$build += parent::render();
return $build;
}
/**
* {@inheritdoc}
*
* Building the header and content lines for the contact list.
*
* Calling the parent::buildHeader() adds a column for the possible actions
* and inserts the 'edit' and 'delete' links as defined for the entity type.
*/
public function buildHeader() {
$header['id'] = $this->t('ContactID');
$header['name'] = $this->t('Name');
$header['first_name'] = $this->t('First Name');
$header['gender'] = $this->t('Gender');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
/* @var $entity \Drupal\content_entity_example\Entity\Contact */
$row['id'] = $entity->id();
$row['name'] = $entity->link();
$row['first_name'] = $entity->first_name->value;
$row['gender'] = $entity->gender->value;
return $row + parent::buildRow($entity);
}
}
?>
Field Settings
src/Form/ContactSettingsForm.php
Create a settings form for Contacts. Fields can be managed from here.
<?php
namespace Drupal\content_entity_example\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Class ContentEntityExampleSettingsForm.
* @package Drupal\content_entity_example\Form
* @ingroup content_entity_example
*/
class ContactSettingsForm extends FormBase {
/**
* Returns a unique string identifying the form.
*
* @return string
* The unique string identifying the form.
*/
public function getFormId() {
return 'content_entity_example_settings';
}
/**
* Form submission handler.
*
* @param array $form
* An associative array containing the structure of the form.
* @param FormStateInterface $form_state
* An associative array containing the current state of the form.
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Empty implementation of the abstract submit class.
}
/**
* Define the form used for ContentEntityExample settings.
* @return array
* Form definition array.
*
* @param array $form
* An associative array containing the structure of the form.
* @param FormStateInterface $form_state
* An associative array containing the current state of the form.
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['contact_settings']['#markup'] = 'Settings form for ContentEntityExample. Manage field settings here.';
return $form;
}
}
?>
Access control handler
src/ContactAccessControlHandler.php
Create the access control handler Class.
<?php
namespace Drupal\content_entity_example;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Access controller for the contact entity.
*
* @see \Drupal\content_entity_example\Entity\Contact.
*/
class ContactAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*
* Link the activities to the permissions. checkAccess is called with the
* $operation as defined in the routing.yml file.
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
switch ($operation) {
case 'view':
return AccessResult::allowedIfHasPermission($account, 'view contact entity');
case 'edit':
return AccessResult::allowedIfHasPermission($account, 'edit contact entity');
case 'delete':
return AccessResult::allowedIfHasPermission($account, 'delete contact entity');
}
return AccessResult::allowed();
}
/**
* {@inheritdoc}
*
* Separate from the checkAccess because the entity does not yet exist, it
* will be created during the 'add' process.
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
return AccessResult::allowedIfHasPermission($account, 'add contact entity');
}
}
?>
Register your new entity
In case you enabled the module halfway through this guide before creating the Contact class, Drupal won't create the database table for your entity. This will result in an error page.
In order to register a new entity (create the database table according to your details in your entity class) you can simply execute drush updatedb --entity-updates
or drupal update:entities
. When executing this command, Drupal will inform you that it found a new entity and that there's a pending update. After applying the pending update, you will be able to start using the new entity on your Drupal website!
In #2976035: Entity type CRUD operations must use the last installed entity type and field storage definitions the ability to run drush entup
was removed.
See the module Devel Entity Updates.
Integration with JSON:API
By default, having the module jsonapi enabled will expose all the content entity types through their corresponding jsonapi URLs. In case your custom entity type is not prepared for this, or you want to simply disable it and you don't want it to be accesible, you can add the internal attribute in the root level of the @ContentEntityType annotation.
internal = TRUE,
Help improve this page
You can:
- Log in, click Edit, and edit this page
- Log in, click Discuss, update the Page status value, and suggest an improvement
- Log in and create a Documentation issue with your suggestion