Blog

How to create a custom entity in Drupal 8

Entity API in Drupal 8 is now baked into core and is now so well organised that there is almost no excuse for creating database tables, which are not entities at the same time. If you are serious about Drupal development, check this article below. Entities in Drupal really rock!

If you create an entity you get Views integration for free, you can allow the entity to be fieldable and this will out or the box allows you to add various fields to it. Also, you can search for the entity with Entity Drupal::EntityQuery and many more.

I recently had to create a simple entity of an online dictionary and think this is a great opportunity to share with you what I learned.

Dictionary term entity

The entity will store translations of words from English to Polish. It's a really simple entity with just 2 data fields:

  • pl - field to store the Polish word
  • en - field to store the English word

I will actually add some more fields which are worth adding to almost any entity:

  • id - unique identifier
  • uuid - Drupal 8 has native support now to create universally unique identifiers
  • user_id - id of the creator of the entity (a reference to the Drupal user)
  • created - a timestamp of when the entity was created
  • changed - a timestamp of when the entity was last update

Let's create a 'dictionary' module

In /sites/modules/custom I created a 'dictionary' folder, with the following initial files:

dictionary.info.yml

name: dictionary
type: module
description: Dictionary
core: 8.x
package: Application
<?php
/**
* @file
* Contains \Drupal\content_entity_example\Entity\ContentEntityExample.
*/

namespace Drupal\dictionary\Entity;

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\user\UserInterface;
use Drupal\Core\Entity\EntityChangedTrait;

/**
* Defines the ContentEntityExample entity.
*
* @ingroup dictionary
*
*
* @ContentEntityType(
* id = "dictionary_term",
* label = @Translation("Dictionary Term entity"),
* handlers = {
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "list_builder" = "Drupal\dictionary\Entity\Controller\TermListBuilder",
* "form" = {
* "add" = "Drupal\dictionary\Form\TermForm",
* "edit" = "Drupal\dictionary\Form\TermForm",
* "delete" = "Drupal\dictionary\Form\TermDeleteForm",
* },
* "access" = "Drupal\dictionary\TermAccessControlHandler",
* },
* list_cache_contexts = { "user" },
* base_table = "dictionary_term",
* admin_permission = "administer dictionary_term entity",
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid",
* "user_id" = "user_id",
* "created" = "created",
* "changed" = "changed",
* "pl" = "pl",
* "en" = "en",
* },
* links = {
* "canonical" = "/dictionary_term/{dictionary_term}",
* "edit-form" = "/dictionary_term/{dictionary_term}/edit",
* "delete-form" = "/dictionary_term/{dictionary_term}/delete",
* "collection" = "/dictionary_term/list"
* },
* field_ui_base_route = "entity.dictionary.term_settings",
* )
*/
class Term extends ContentEntityBase {

use EntityChangedTrait;

/**
* {@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);
// Default author to current user.
$values += array(
'user_id' => \Drupal::currentUser()->id(),
);
}

/**
* {@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 Term 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['pl'] = BaseFieldDefinition::create('string')
->setLabel(t('Polish'))
->setDescription(t('Polish version.'))
->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['en'] = BaseFieldDefinition::create('string')
->setLabel(t('English'))
->setDescription(t('English version.'))
->setSettings(array(
'default_value' => '',
'max_length' => 255,
'text_processing' => 0,
))
->setDisplayOptions('view', array(
'label' => 'above',
'type' => 'string',
'weight' => -4,
))
->setDisplayOptions('form', array(
'type' => 'string_textfield',
'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' => 'author',
'weight' => -3,
))
->setDisplayOptions('form', array(
'type' => 'entity_reference_autocomplete',
'settings' => array(
'match_operator' => 'CONTAINS',
'size' => 60,
'placeholder' => '',
),
'weight' => -3,
))
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE);

$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;
}

}

You can see that entities Term class extends ContentEntityBase which is the base class used to create entities in Drupal. An important thing to note is the annotations in the comment block above the class. Many important definitions are set here. In particular note:

  • id - unique identifier of the entity in the system
  • handlers - links to all controllers
  • base_table - the name of the table that will be used for this entity. You do not have to separately create schema, it is read from the field definitions

That is it. Your Entity is ready. All further work is devoted to getting all the View and Edit and list screens and forms to work.
Let's set up routing and permissions for these first.

dictionary.routing.yml

# This file brings everything together. Very nifty!

# Route name can be used in sevaral place (links, redirects, local actions etc.)
entity.dictionary_term.canonical:
path: '/dictionary_term/{dictionary_term}'
defaults:
# Calls the view controller, defined in the annotation of the dictionary_term entity
_entity_view: 'dictionary_term'
_title: 'dictionary_term Content'
requirements:
# Calls the access controller of the entity, $operation 'view'
_entity_access: 'dictionary_term.view'

entity.dictionary_term.collection:
path: '/dictionary_term/list'
defaults:
# Calls the list controller, defined in the annotation of the dictionary_term entity.
_entity_list: 'dictionary_term'
_title: 'dictionary_term List'
requirements:
# Checks for permission directly.
_permission: 'view dictionary_term entity'

entity.dictionary.term_add:
path: '/dictionary_term/add'
defaults:
# Calls the form.add controller, defined in the dictionary_term entity.
_entity_form: dictionary_term.add
_title: 'Add dictionary_term'
requirements:
_entity_create_access: 'dictionary_term'

entity.dictionary_term.edit_form:
path: '/dictionary_term/{dictionary_term}/edit'
defaults:
# Calls the form.edit controller, defined in the dictionary_term entity.
_entity_form: dictionary_term.edit
_title: 'Edit dictionary_term'
requirements:
_entity_access: 'dictionary_term.edit'

entity.dictionary_term.delete_form:
path: '/dictionary_term/{dictionary_term}/delete'
defaults:
# Calls the form.delete controller, defined in the dictionary_term entity.
_entity_form: dictionary_term.delete
_title: 'Delete dictionary_term'
requirements:
_entity_access: 'dictionary_term.delete'

entity.dictionary.term_settings:
path: 'admin/structure/dictionary_term_settings'
defaults:
_form: '\Drupal\dictionary\Form\TermSettingsForm'
_title: 'dictionary_term Settings'
requirements:
_permission: 'administer dictionary_term entity'

 

dictionary.persmissions.yml

'delete dictionary_term entity':
title: Delete term entity content.
'add dictionary_term entity':
title: Add term entity content
'view dictionary_term entity':
title: View term entity content
'edit dictionary_term entity':
title: Edit term entity content
'administer dictionary_term entity':
title: Administer term settings

 

Usually, entities also have helpful local links and tasks useful, like the local /edit task.

dictionary.links.tasks.yml

Add local tasks

# Define the 'local' links for the module

entity.dictionary_term.settings_tab:
route_name: dictionary.term_settings
title: Settings
base_route: dictionary.term_settings

entity.dictionary_term.view:
route_name: entity.dictionary_term.canonical
base_route: entity.dictionary_term.canonical
title: View

entity.dictionary_term.page_edit:
route_name: entity.dictionary_term.edit_form
base_route: entity.dictionary_term.canonical
title: Edit

entity.dictionary_term.delete_confirm:
route_name: entity.dictionary_term.delete_form
base_route: entity.dictionary_term.canonical
title: Delete
weight: 10

 

dictionary.links.action.yml

Add an Add term to the list terms

dictionary._term_add:
# Which route will be called by the link
route_name: entity.dictionary.term_add
title: 'Add term'

# Where will the link appear, defined by route name.
appears_on:
- entity.dictionary_term.collection
- entity.dictionary_term.canonical

 

Now when menus and tasks are created, let's create the page which lists our entities and the add/edit and delete forms

/src/Entity/Con­troller/TermList­Builder.php

From all the controllers we will implement the controller for the list of entities. After all we would like to see the list in a meaningful fashion with both the English and Polish word side by side in a table

<?php

/**
* @file
* Contains \Drupal\dictionaryEntity\Controller\TermListBuilder.
*/

namespace Drupal\dictionary\Entity\Controller;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityListBuilder;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Provides a list controller for dictionary_term entity.
*
* @ingroup dictionary
*/
class TermListBuilder extends EntityListBuilder {

/**
* The url generator.
*
* @var \Drupal\Core\Routing\UrlGeneratorInterface
*/
protected $urlGenerator;

/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity.manager')->getStorage($entity_type->id()),
$container->get('url_generator')
);
}

/**
* Constructs a new DictionaryTermListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type term.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The url generator.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, UrlGeneratorInterface $url_generator) {
parent::__construct($entity_type, $storage);
$this->urlGenerator = $url_generator;
}

/**
* {@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'] = array(
'#markup' => $this->t('Content Entity Example implements a DictionaryTerms model. These are fieldable entities. You can manage the fields on the <a href="@adminlink">Term admin page</a>.', array(
'@adminlink' => $this->urlGenerator->generateFromRoute('entity.dictionary.term_settings'),
)),
);
$build['table'] = parent::render();
return $build;
}

/**
* {@inheritdoc}
*
* Building the header and content lines for the dictionary_term 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('TermID');
$header['pl'] = $this->t('Polish');
$header['en'] = $this->t('English');
return $header + parent::buildHeader();
}

/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
/* @var $entity \Drupal\dictionary\Entity\Term */
$row['id'] = $entity->id();
$row['pl'] = $entity->pl->value;
$row['en'] = $entity->en->value;
return $row + parent::buildRow($entity);
}

}

You can see the buildHeader and buildRow functions which set the columns for our table.

add/edit form - src/Form/TermForm.php

<?php
/**
* @file
* Contains Drupal\dictionary\Form\TermForm.
*/

namespace Drupal\dictionary\Form;

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

/**
* Form controller for the content_entity_example entity edit forms.
*
* @ingroup content_entity_example
*/
class TermForm extends ContentEntityForm {

/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
/* @var $entity \Drupal\dictionary\Entity\Term */
$form = parent::buildForm($form, $form_state);
return $form;
}

/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
// Redirect to term list after save.
$form_state->setRedirect('entity.dictionary_term.collection');
$entity = $this->getEntity();
$entity->save();
}

}

 

delete form - src/Form/TermForm.php

<?php

/**
* @file
* Contains \Drupal\dictionary\Form\TermDeleteForm.
*/

namespace Drupal\dictionary\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 dictionary
*/
class TermDeleteForm extends ContentEntityConfirmFormBase {

/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete entity %name?', array('%name' => $this->entity->label()));
}

/**
* {@inheritdoc}
*
* If the delete command is canceled, return to the contact list.
*/
public function getCancelUrl() {
return new Url('entity.dictionary_term.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('dictionary')->notice('deleted %title.',
array(
'%title' => $this->entity->label(),
));
// Redirect to term list after delete.
$form_state->setRedirect('entity.dictionary_term.collection');
}

}

 

src/Form/TermSettingsForm.php

Last but not least is the Settings form which allows us to manage the entity settings. Our form here will remain empty. I don't need any settings here really but I could manage additional form settings here.

<?php
/**
 * @file
 * Contains \Drupal\dictionary\Form\TermSettingsForm.
 */

namespace Drupal\dictionary\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Class ContentEntityExampleSettingsForm.
 *
 * @package Drupal\dictionary\Form
 *
 * @ingroup dictionary
 */
class TermSettingsForm extends FormBase {
  /**
   * Returns a unique string identifying the form.
   *
   * @return string
   *   The unique string identifying the form.
   */
  public function getFormId() {
    return 'dictionary_term_settings';
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // Empty implementation of the abstract submit class.
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['dictionary_term_settings']['#markup'] = 'Settings form for Dictionary Term. Manage field settings here.';
    return $form;
  }

}

 

And this is is for now. Your entity is created. You can use it.
 
Complete code from this example is here
 
 
3. Best practices for software development teams