This guide documents the process of creating a custom entity type in Drupal 8 using the example of an Event entity type.
You can reach this guide at https://git.io/d8entity.
The starting point is a stock Drupal 8.6 core Standard installation with the
contributed Entity API available at modules/entity
and an
empty module directory at modules/event
.
Having Drush 9 available is required to follow along. When Drush
commands are to be run, run them from within the Drupal installation. When PHP
code is to be executed, this can be done by running drush core:cli
or by
creating a test script and then running drush php:script <name-of-script>
.
Table of contents
- Using entities for data storage
- Viewing entities on a page
- Manipulating entities through forms
- Listing entities
- Adding administrative links
- Adding additional fields
- Tracking changes to entities
- Storing dynamic data in configuration
- Providing a user interface for configuration entities
- Categorizing different entities of the same entity type
- Configuring bundles in the user interface
- Translating content
- Translating configuration
1. Using entities for data storage
1.1. Create a module
-
Within the
/modules/event
directory create anevent.info.yml
file with the following:name: Event type: module core: 8.x
-
Run
drush pm:enable event
or visit/admin/modules
and install the Event module
1.2. Create a minimal entity class
Classes allow categorizing objects as being of a certain type. Event entities, that will be created below, will be instances of the entity class. In terms of code organization, classes can be used to group related functionality.
-
Create a
src
directoryIn Drupal 8 the
src
directory contains all object-oriented code (classes, interfaces, traits). Procedural code (functions) is placed in the.module
file (or other files) outside of thesrc
directory. -
Create a
src/Entity
directoryAs modules often contain many classes, they can be placed into arbitrary subdirectories for organizational purposes. Certain directory names have a special meaning in Drupal and are required for certain things. In particular, Drupal looks in
Entity
for entity types. -
Create a
src/Entity/Event.php
file with the following:<?php namespace Drupal\event\Entity; use Drupal\Core\Entity\ContentEntityBase; /** * @ContentEntityType( * id = "event", * label = @Translation("Event"), * base_table = "event", * entity_keys = { * "id" = "id", * "uuid" = "uuid", * }, * ) */ class Event extends ContentEntityBase { }
More information on the above
-
Namespace:
namespace Drupal\event\Entity;
Namespaces allow code from different frameworks (Drupal, Symfony, …) to be used simultaneously without risking naming conflicts. Namespaces can have multiple parts. All classes in Drupal core and modules have
Drupal
as the top-level namespace. The second part of module classes must be the module name. Further sub-namespaces correspond to the directory structure within thesrc
directory of the module. -
Import:
use Drupal\Core\Entity\ContentEntityBase;
In the same way we declare a namespace for the
Event
class theContentEntityBase
class used below also belongs to a namespace. Thus, in order to use it below, we need to import the class using the full namespace. -
Annotation:
Annotations are a way to provide metadata about code. Because the annotation is placed right next to the code itself, this makes classes truly self-contained as both functionality and metadata are in the same file.
Even though the annotation is part of a comment block, it is required for the entity type to function.
-
ID:
* id = "event",
This is the ID of the entity type that is needed whenever interacting with a specific entity type in code.
-
Label:
* label = @Translation("Event"),
This is the label of this entity type when presented to a user.
To make the values we provide in the annotation translatable we need to wrap them in
@Translation
which is themself an annotations. -
Storage information:
* base_table = "event", * entity_keys = { * "id" = "id", * "uuid" = "uuid", * },
We need to specify the name of the database table we want the event data to be stored. (This is called base table, as there can be multiple tables that store entity information, as will be seen below.)
Entities are required to have an ID which they can be loaded by. We need to specify what the ID field will be called for our entity. This will also determine the name of the database column that will hold the entity IDs. Similarly entity types can (and are encouraged to) provide a UUID field. Again, we can specify the name of the UUID field.
Note that top-level keys of the annotation are not quoted, but keys in mappings (such as the
entity_keys
declaration) are quoted and trailing commas are allowed in mappings.
See Drupal API: Annotations for more information.
-
-
Class declaration:
class Event extends ContentEntityBase { }
The file name must correspond to class name (including capitalization).
-
Inheritance:
extends
Base classes can be used to implement functionality that is generic and useful for many classes. Classes can inherit all functionality from such a base class by using the
extends
keyword. they them. Then they only need to provide functionality specific to them, which avoids code duplication. -
Content entities:
ContentEntityBase
Content entities are entities that are created by site users. They are typically stored in the database, often with a auto-incrementing integer ID.
See Drupal API: Object-oriented programming conventions for more information.
-
-
1.3. Install the entity type
Drupal can create the database schema for our entity type automatically but this needs to be done explicitly. The preferred way of doing this is with Drush.
-
Run
drush entity:updates
Note that the
{event}
table has been created in the database withid
anduuid
columns. -
Create and save an event
Run the following PHP code:
use Drupal\event\Entity\Event; $event = Event::create(); $event->save();
Note that there is a new row in the
{event}
table with an ID and a UUID.More information on the above
The
Event
class inherits thecreate()
andsave()
methods fromContentEntityBase
so they can be called without being present in theEvent
class itself.create()
is a static method so it is called by using the class name and the::
syntax.save()
is not a static method so it is used with an instance of the class and the->
syntax. -
Load an event fetch its ID and UUID
Run the following PHP code:
use Drupal\event\Entity\Event; $event = Event::load(1); print 'ID: ' . $event->id() . PHP_EOL; print 'UUID: ' . $event->uuid() . PHP_EOL;
Note that the returned values match the values in the database.
-
Delete the event
Run the following PHP code:
use Drupal\event\Entity\Event; $event = Event::load(1); $event->delete();
Note that the row in the
{event}
table is gone.
1.4. Add field definitions
Fields are the pieces of data that make up an entity. The ID and UUID that were saved as part of the event above are examples of field values. To be able to store actual event data in our entities, we need to declare additional fields.
Just like with the ID and UUID fields above, Drupal can automatically provide Author and Published fields for us, which we will take advantage of so that we can track who created events and distinguish published and unpublished events.
-
Add the following to
event.info.yml
:dependencies: - drupal:datetime - drupal:user
-
Add the following use statements to
src/Entity/Event.php
:use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityPublishedTrait; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\user\EntityOwnerInterface; use Drupal\user\EntityOwnerTrait;
-
Add the following to the
entity_keys
part of the annotation of theEvent
class:* "label" = "title", * "owner" = "author", * "published" = "published",
Declaring a
label
key makes the (inherited)label()
method on theEvent
class work and also allows autocompletion of events by their title. -
Add the following to the end of the class declaration of the
Event
class:implements EntityOwnerInterface, EntityPublishedInterface
-
Add the following inside of the class declaration of the
Event
class:use EntityOwnerTrait, EntityPublishedTrait; public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // Get the field definitions for 'id' and 'uuid' from the parent. $fields = parent::baseFieldDefinitions($entity_type); $fields['title'] = BaseFieldDefinition::create('string') ->setLabel(t('Title')) ->setRequired(TRUE); $fields['date'] = BaseFieldDefinition::create('datetime') ->setLabel(t('Date')) ->setRequired(TRUE); $fields['description'] = BaseFieldDefinition::create('text_long') ->setLabel(t('Description')); // Get the field definitions for 'author' and 'published' from the trait. $fields += static::ownerBaseFieldDefinitions($entity_type); $fields += static::publishedBaseFieldDefinitions($entity_type); return $fields; }
More information on the above
-
Type hint:
EntityTypeInterface $entity_type
Interfaces are contracts that specify the methods a class must have in order to fulfill it.
The interface name in front of the
$entity_type
parameter is a type hint. It dictates what type of object must be passed. Type hinting an interface allows any class that implements the interface to be passed. -
Entity ownership:
EntityOwnerInterface
When creating an entity with owner support from an entity reference widget, the owner of the host entity is taken over.
-
Inheritance:
$fields = parent::baseFieldDefinitions($entity_type);
The class that is extended (
ContentEntityBase
in this case) is called the parent class. ThebaseFieldDefinitions()
method inContentEntityBase
provides field definitions for theid
anduuid
fields. Inheritance allows us to re-use those field definitions while still adding additional ones. -
Field definition:
BaseFieldDefinition::create('string');
Field definitions are objects that hold metadata about a field. They are created by passing the field type ID into the static
create
method. There is no list of IDs of available field types, but Drupal API: List of classes annotated with FieldType lists all field type classes in core. The ID of a given field type can be found in its class documentation or by inspecting the@FieldType
annotation. -
Chaining:
->setLabel(t('Title')) ->setRequired(TRUE)
Many setter methods return the object they were called on to allow chaining multiple setter methods after another. The setting up of the
title
field definition above is functionally equivalent to the following code block which avoids chaining:$fields['title'] = BaseFieldDefinition::create('string'); $fields['title']->setLabel(t('Title')); $fields['title']->setRequired(TRUE);
The entire
Event.php
file at this point<?php namespace Drupal\event\Entity; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityPublishedTrait; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\user\EntityOwnerInterface; use Drupal\user\EntityOwnerTrait; /** * @ContentEntityType( * id = "event", * label = @Translation("Event"), * base_table = "event", * entity_keys = { * "id" = "id", * "uuid" = "uuid", * "label" = "title", * "owner" = "author", * "published" = "published", * }, * ) */ class Event extends ContentEntityBase implements EntityOwnerInterface, EntityPublishedInterface { use EntityOwnerTrait, EntityPublishedTrait; public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // Get the field definitions for 'id' and 'uuid' from the parent. $fields = parent::baseFieldDefinitions($entity_type); $fields['title'] = BaseFieldDefinition::create('string') ->setLabel(t('Title')) ->setRequired(TRUE); $fields['date'] = BaseFieldDefinition::create('datetime') ->setLabel(t('Date')) ->setRequired(TRUE); $fields['description'] = BaseFieldDefinition::create('text_long') ->setLabel(t('Description')); // Get the field definitions for 'author' and 'published' from the traits. $fields += static::ownerBaseFieldDefinitions($entity_type); $fields += static::publishedBaseFieldDefinitions($entity_type); return $fields; } }
-
1.5. Install the fields
Drupal notices changes to the entity type that affect the database schema and can update it automatically.
-
Run
drush entity:updates
Note that
title
,date
,description__value
,description__format
andpublished
columns have been created in the{event}
table.Although most field types consist of a single
value
property, text fields, for example, have an additionalformat
property. Therefore two database columns are required for text fields. -
Create and save an event
Run the following PHP code:
use Drupal\event\Entity\Event; $event = Event::create([ 'title' => 'Drupal User Group', 'date' => (new \DateTime())->format(DATETIME_DATETIME_STORAGE_FORMAT), 'description' => [ 'value' => '<p>The monthly meeting of Drupalists is happening today!</p>', 'format' => 'restricted_html', ], ]); $event->save();
Note that there is a new row in the
{event}
table with the proper field values. -
Load an event and fetch its field values.
Run the following PHP code:
use Drupal\event\Entity\Event; $event = Event::load(1); print 'Title: ' . $event->get('title')->value . "\n\n"; print 'Date value: ' . $event->get('date')->value . "\n"; print 'Date object: ' . var_export($event->get('date')->date, TRUE) . "\n\n"; print 'Description value: ' . $event->get('description')->value . "\n"; print 'Description format: ' . $event->get('description')->format . "\n"; print 'Processed description: ' . var_export($event->get('description')->processed, TRUE) . "\n\n"; print 'Author: ' . $event->get('author')->entity->getDisplayName() . "\n\n"; print 'Published: ' . $event->get('published')->value . "\n";
Note that the returned values match the values in the database.
In addition to the stored properties field types can also declare computed properties, such as the
date
property of a datetime field or theprocessed
property of text fields. -
Update an event's field values and save them.
Run the following PHP code:
use Drupal\event\Entity\Event; $event = Event::load(2); $event ->set('title', 'DrupalCon') ->set('date', (new \DateTime('yesterday'))->format(DATETIME_DATETIME_STORAGE_FORMAT)) ->set('description', [ 'value' => '<p>DrupalCon is a great place to meet international Drupal superstars.</p>', 'format' => 'basic_html', ]) ->set('author', 1) ->set('published', FALSE) ->save();
Note that the values in the database have been updated accordingly.
1.6. Add field methods
Instead of relying on the generic get()
and set()
methods it is recommended
to add field-specific methods that wrap them. This makes interacting with
events in code more convenient. Futhermore, it is recommended to add an
interface
-
Add the following use statements to
src/Entity/Event.php
:use Drupal\Core\Datetime\DrupalDateTime;
-
Add the following methods to the
Event
class:/** * @return string */ public function getTitle() { return $this->get('title')->value; } /** * @param string $title * * @return $this */ public function setTitle($title) { return $this->set('title', $title); } /** * @return \Drupal\Core\Datetime\DrupalDateTime */ public function getDate() { return $this->get('date')->date; } /** * @param \Drupal\Core\Datetime\DrupalDateTime $date * * @return $this */ public function setDate(DrupalDateTime $date) { return $this->set('date', $date->format(DATETIME_DATETIME_STORAGE_FORMAT)); } /** * @return \Drupal\filter\Render\FilteredMarkup */ public function getDescription() { return $this->get('description')->processed; } /** * @param string $description * @param string $format * * @return $this */ public function setDescription($description, $format) { return $this->set('description', [ 'value' => $description, 'format' => $format, ]); }
Field methods not only provide autocompletion, but also allow designing richer APIs than the bare field types provide. The
setDate()
method, for example, hides the internal storage format of datetime values from anyone working with events. Similarly thesetDescription()
method requires setting the description and the text format simultaneously for security. Thepublish()
andunpublish()
methods make the code more readable than with a genericsetPublished()
method.Note that entity types in core provide an entity-type-specific interface (such as
EventInterface
in this case) to which they add such field methods. This is omitted here for brevity.The entire
Event.php
file at this point<?php namespace Drupal\event\Entity; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityPublishedTrait; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\user\EntityOwnerInterface; use Drupal\user\EntityOwnerTrait; /** * @ContentEntityType( * id = "event", * label = @Translation("Event"), * base_table = "event", * entity_keys = { * "id" = "id", * "uuid" = "uuid", * "label" = "title", * "owner" = "author", * "published" = "published", * }, * ) */ class Event extends ContentEntityBase implements EntityOwnerInterface, EntityPublishedInterface { use EntityOwnerTrait, EntityPublishedTrait; public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // Get the field definitions for 'id' and 'uuid' from the parent. $fields = parent::baseFieldDefinitions($entity_type); $fields['title'] = BaseFieldDefinition::create('string') ->setLabel(t('Title')) ->setRequired(TRUE); $fields['date'] = BaseFieldDefinition::create('datetime') ->setLabel(t('Date')) ->setRequired(TRUE); $fields['description'] = BaseFieldDefinition::create('text_long') ->setLabel(t('Description')); // Get the field definitions for 'author' and 'published' from the traits. $fields += static::ownerBaseFieldDefinitions($entity_type); $fields += static::publishedBaseFieldDefinitions($entity_type); return $fields; } /** * @return string */ public function getTitle() { return $this->get('title')->value; } /** * @param string $title * * @return $this */ public function setTitle($title) { return $this->set('title', $title); } /** * @return \Drupal\Core\Datetime\DrupalDateTime */ public function getDate() { return $this->get('date')->date; } /** * @param \Drupal\Core\Datetime\DrupalDateTime $date * * @return $this */ public function setDate(DrupalDateTime $date) { return $this->set('date', $date->format(DATETIME_DATETIME_STORAGE_FORMAT)); } /** * @return \Drupal\filter\Render\FilteredMarkup */ public function getDescription() { return $this->get('description')->processed; } /** * @param string $description * @param string $format * * @return $this */ public function setDescription($description, $format) { return $this->set('description', [ 'value' => $description, 'format' => $format, ]); } }
-
Try out the new getter methods
Run the following PHP code:
use Drupal\event\Entity\Event; $event = Event::load(1); $event->getTitle(); $event->getDate(); $event->getDescription(); $event->isPublished();
Note that the returned values match the values in the database.
-
Try out the new setter methods
Run the following PHP code:
use Drupal\Core\Datetime\DrupalDateTime; use Drupal\event\Entity\Event; $event ->setTitle('Drupal Developer Days') ->setDate(new DrupalDateTime('tomorrow')) ->setDescription( '<p>The Drupal Developer Days are a great place to nerd out about all things Drupal!</p>', 'basic_html' ) ->setOwnerId(0) ->setPublished(FALSE) ->save();
Note that the values in the database have been updated accordingly.
2. Viewing entities on a page
Viewing an entity on a page requires a route on which the entity's field values are output on a given path. This can be automated by amending the entity annotation.
2.1. Install the contributed Entity API module.
The contributed Entity API provides various enhancements to the core Entity API. One such enhancement is the ability to more easily provide permissions entity types which we will now use.
-
Run
drush pm:enable entity
or visit/admin/modules
and install the Entity API module -
Add the following to the
dependencies
section ofevent.info.yml
:- entity:entity
2.2. Add a route
-
Add the following to the annotation in
src/Entity/Event.php
:* handlers = { * "access" = "Drupal\entity\EntityAccessControlHandler", * "permission_provider" = "Drupal\entity\EntityPermissionProvider", * "route_provider" = { * "default" = "Drupal\entity\Routing\DefaultHtmlRouteProvider", * }, * }, * links = { * "canonical" = "/event/{event}", * }, * admin_permission = "administer event",
The entire
Event.php
file at this point<?php namespace Drupal\event\Entity; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityPublishedTrait; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\user\EntityOwnerInterface; use Drupal\user\EntityOwnerTrait; /** * @ContentEntityType( * id = "event", * label = @Translation("Event"), * base_table = "event", * entity_keys = { * "id" = "id", * "uuid" = "uuid", * "label" = "title", * "owner" = "author", * "published" = "published", * }, * handlers = { * "access" = "Drupal\entity\EntityAccessControlHandler", * "permission_provider" = "Drupal\entity\EntityPermissionProvider", * "route_provider" = { * "default" = "Drupal\entity\Routing\DefaultHtmlRouteProvider", * }, * }, * links = { * "canonical" = "/event/{event}", * }, * admin_permission = "administer event" * ) */ class Event extends ContentEntityBase implements EntityOwnerInterface, EntityPublishedInterface { use EntityOwnerTrait, EntityPublishedTrait; public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // Get the field definitions for 'id' and 'uuid' from the parent. $fields = parent::baseFieldDefinitions($entity_type); $fields['title'] = BaseFieldDefinition::create('string') ->setLabel(t('Title')) ->setRequired(TRUE); $fields['date'] = BaseFieldDefinition::create('datetime') ->setLabel(t('Date')) ->setRequired(TRUE); $fields['description'] = BaseFieldDefinition::create('text_long') ->setLabel(t('Description')); // Get the field definitions for 'owner' and 'published' from the traits. $fields += static::ownerBaseFieldDefinitions($entity_type); $fields += static::publishedBaseFieldDefinitions($entity_type); return $fields; } /** * @return string */ public function getTitle() { return $this->get('title')->value; } /** * @param string $title * * @return $this */ public function setTitle($title) { return $this->set('title', $title); } /** * @return \Drupal\Core\Datetime\DrupalDateTime */ public function getDate() { return $this->get('date')->date; } /** * @param \Drupal\Core\Datetime\DrupalDateTime $date * * @return $this */ public function setDate(DrupalDateTime $date) { return $this->set('date', $date->format(DATETIME_DATETIME_STORAGE_FORMAT)); } /** * @return \Drupal\filter\Render\FilteredMarkup */ public function getDescription() { return $this->get('description')->processed; } /** * @param string $description * @param string $format * * @return $this */ public function setDescription($description, $format) { return $this->set('description', [ 'value' => $description, 'format' => $format, ]); } }
More information on the above
-
Entity handlers:
* handlers = { ... * },
Entity handlers are objects that take over certain tasks related to entities. Each entity type can declare which handler it wants to use for which task. In many cases - as can be seen above - Drupal core provides generic handlers that can be used as is. In other cases or when more advanced functionality is required, custom handlers can be used instead.
-
Route providers:
* "route_provider" = { * "default" = "Drupal\entity\Routing\DefaultHtmlRouteProvider", * },
Instead of declaring routes belonging to entities in a
*.routing.yml
file like other routes, they can be provided by a handler, as well. This has the benefit of being able to re-use the same route provider for multiple entity types, as is proven by the usage of the generic route provider provided by core. -
Links:
* links = { * "canonical" = "/event/{event}", * },
Entity links denote at which paths on the website we can see an entity (or multiple entities) of the given type. They are used by the default route provider to set the path of the generated route. The usage of
canonical
(instead ofview
, for example) stems from the specification of link relations in the web by the IANA.See Wikipedia: Link relation and IANA: Link relations for more information.
-
-
Rebuild caches
Run
drush cache:rebuild
-
Verify the route has been generated
Visit
/event/2
Note that an empty page is shown. However, no field values are shown.
2.3. Configure fields for display
Which fields to display when rendering the entity, as well as how to display them, can be configured as part of the field definitions. Fields are not displayed unless explicitly configured to.
-
Add the following to the
$fields['date']
section of thebaseFieldDefinitions()
method of theEvent
class before the semicolon:->setDisplayOptions('view', [ 'label' => 'inline', 'settings' => [ 'format_type' => 'html_date', ], 'weight' => 0, ])
More information on the above
-
Display mode:
->setDisplayOptions('view'
Display options can be set for two different display modes:
view
andform
. Form display options will be set below. -
Label display:
'label' => 'inline',
The field label can be configured to be displayed above the field value (the default), inline in front of the field value or hidden altogether. The respective values of the
label
setting areabove
,inline
andhidden
. -
Formatter settings:
'settings' => [ 'format_type' => 'html_date', ],
Each field is displayed using a formatter. The field type declares a default formatter which is used unless a different formatter is chosen by specifying a
type
key in the display options. Some formatters have settings which can be configured through thesettings
key in the display options. There is no list of IDs of available field types, but Drupal API: List of classes annotated with FieldFormatter lists all field formatter classes (for all field types) in core. The ID of a given field formatter can be found in its class documentation or by inspecting the@FieldFormatter
annotation which also lists the field types that the formatter can be used for. Given a formatter class the available settings can be found by inspecting the keys returned by the class'defaultSettings()
method. -
Weight:
'weight' => 0,
Weights allow the order of fields in the rendered output to be different than their declaration order in the
baseFieldDefinitions()
method. Fields with heigher weights "sink" to the bottom and are displayed after fields with lower weights.
Altogether, setting the view display options is comparable to using the Manage display table provided by Field UI module, which also allows configuring the label display, formatter, formatter settings and weight for each field.
-
-
Add the following to the
$fields['description']
section of thebaseFieldDefinitions()
method of theEvent
class before the semicolon:->setDisplayOptions('view', [ 'label' => 'hidden', 'weight' => 10, ])
The entire
Event.php
file at this point<?php namespace Drupal\event\Entity; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityPublishedTrait; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\user\EntityOwnerInterface; use Drupal\user\EntityOwnerTrait; /** * @ContentEntityType( * id = "event", * label = @Translation("Event"), * base_table = "event", * entity_keys = { * "id" = "id", * "uuid" = "uuid", * "label" = "title", * "owner" = "author", * "published" = "published", * }, * handlers = { * "access" = "Drupal\entity\EntityAccessControlHandler", * "permission_provider" = "Drupal\entity\EntityPermissionProvider", * "route_provider" = { * "default" = "Drupal\entity\Routing\DefaultHtmlRouteProvider", * }, * }, * links = { * "canonical" = "/event/{event}", * }, * admin_permission = "administer event" * ) */ class Event extends ContentEntityBase implements EntityOwnerInterface, EntityPublishedInterface { use EntityOwnerTrait, EntityPublishedTrait; public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // Get the field definitions for 'id' and 'uuid' from the parent. $fields = parent::baseFieldDefinitions($entity_type); $fields['title'] = BaseFieldDefinition::create('string') ->setLabel(t('Title')) ->setRequired(TRUE); $fields['date'] = BaseFieldDefinition::create('datetime') ->setLabel(t('Date')) ->setRequired(TRUE) ->setDisplayOptions('view', [ 'label' => 'inline', 'settings' => [ 'format_type' => 'html_date', ], 'weight' => 0, ]); $fields['description'] = BaseFieldDefinition::create('text_long') ->setLabel(t('Description')) ->setDisplayOptions('view', [ 'label' => 'hidden', 'weight' => 10, ]); // Get the field definitions for 'owner' and 'published' from the traits. $fields += static::ownerBaseFieldDefinitions($entity_type); $fields += static::publishedBaseFieldDefinitions($entity_type); return $fields; } /** * @return string */ public function getTitle() { return $this->get('title')->value; } /** * @param string $title * * @return $this */ public function setTitle($title) { return $this->set('title', $title); } /** * @return \Drupal\Core\Datetime\DrupalDateTime */ public function getDate() { return $this->get('date')->date; } /** * @param \Drupal\Core\Datetime\DrupalDateTime $date * * @return $this */ public function setDate(DrupalDateTime $date) { return $this->set('date', $date->format(DATETIME_DATETIME_STORAGE_FORMAT)); } /** * @return \Drupal\filter\Render\FilteredMarkup */ public function getDescription() { return $this->get('description')->processed; } /** * @param string $description * @param string $format * * @return $this */ public function setDescription($description, $format) { return $this->set('description', [ 'value' => $description, 'format' => $format, ]); } }
-
Rebuild caches
Run
drush cache:rebuild
-
Verify that the fields are shown
As the event title is automatically used as a page title we do not explicitly enable the title field for display.
Note that the output of the entity can be further customized by adding a theme function. This is omitted for brevity.
3. Manipulating entities through forms
3.1. Add the routes
-
Add the following to the
handlers
section of the annotation insrc/Entity/Event.php
:* "form" = { * "add" = "Drupal\Core\Entity\ContentEntityForm", * "edit" = "Drupal\Core\Entity\ContentEntityForm", * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm", * },
-
Add the following to the
links
section of the annotation insrc/Entity/Event.php
:* "add-form" = "/admin/content/events/add", * "edit-form" = "/admin/content/events/manage/{event}", * "delete-form" = "/admin/content/events/manage/{event}/delete",
The entire
Event.php
file at this point<?php namespace Drupal\event\Entity; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityPublishedTrait; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\user\EntityOwnerInterface; use Drupal\user\EntityOwnerTrait; /** * @ContentEntityType( * id = "event", * label = @Translation("Event"), * base_table = "event", * entity_keys = { * "id" = "id", * "uuid" = "uuid", * "label" = "title", * "owner" = "author", * "published" = "published", * }, * handlers = { * "access" = "Drupal\entity\EntityAccessControlHandler", * "form" = { * "add" = "Drupal\Core\Entity\ContentEntityForm", * "edit" = "Drupal\Core\Entity\ContentEntityForm", * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm", * }, * "permission_provider" = "Drupal\entity\EntityPermissionProvider", * "route_provider" = { * "default" = "Drupal\entity\Routing\DefaultHtmlRouteProvider", * }, * }, * links = { * "canonical" = "/event/{event}", * "add-form" = "/admin/content/events/add", * "edit-form" = "/admin/content/events/manage/{event}", * "delete-form" = "/admin/content/events/manage/{event}/delete", * }, * admin_permission = "administer event" * ) */ class Event extends ContentEntityBase implements EntityOwnerInterface, EntityPublishedInterface { use EntityOwnerTrait, EntityPublishedTrait; public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // Get the field definitions for 'id' and 'uuid' from the parent. $fields = parent::baseFieldDefinitions($entity_type); $fields['title'] = BaseFieldDefinition::create('string') ->setLabel(t('Title')) ->setRequired(TRUE); $fields['date'] = BaseFieldDefinition::create('datetime') ->setLabel(t('Date')) ->setRequired(TRUE) ->setDisplayOptions('view', [ 'label' => 'inline', 'settings' => [ 'format_type' => 'html_date', ], 'weight' => 0, ]); $fields['description'] = BaseFieldDefinition::create('text_long') ->setLabel(t('Description')) ->setDisplayOptions('view', [ 'label' => 'hidden', 'weight' => 10, ]); // Get the field definitions for 'owner' and 'published' from the traits. $fields += static::ownerBaseFieldDefinitions($entity_type); $fields += static::publishedBaseFieldDefinitions($entity_type); return $fields; } /** * @return string */ public function getTitle() { return $this->get('title')->value; } /** * @param string $title * * @return $this */ public function setTitle($title) { return $this->set('title', $title); } /** * @return \Drupal\Core\Datetime\DrupalDateTime */ public function getDate() { return $this->get('date')->date; } /** * @param \Drupal\Core\Datetime\DrupalDateTime $date * * @return $this */ public function setDate(DrupalDateTime $date) { return $this->set('date', $date->format(DATETIME_DATETIME_STORAGE_FORMAT)); } /** * @return \Drupal\filter\Render\FilteredMarkup */ public function getDescription() { return $this->get('description')->processed; } /** * @param string $description * @param string $format * * @return $this */ public function setDescription($description, $format) { return $this->set('description', [ 'value' => $description, 'format' => $format, ]); } }
-
Rebuild caches
Run
drush cache:rebuild
-
Visit
/admin/content/events/add
Note that a route exists and a Save button is shown, but no actual form fields are shown.
-
Visit
/admin/content/events/manage/1
Note that a route exists and Save and Delete buttons are shown, but no actual form fields are shown.
3.2. Configure fields for display
-
Add the following to the
$fields['title']
section of thebaseFieldDefinitions()
method of theEvent
class before the semicolon:->setDisplayOptions('form', ['weight' => 0])
-
Add the following to the
$fields['date']
section of thebaseFieldDefinitions()
method of theEvent
class before the semicolon:->setDisplayOptions('form', ['weight' => 10])
-
Add the following to the
$fields['description']
section of thebaseFieldDefinitions()
method of theEvent
class before the semicolon:->setDisplayOptions('form', ['weight' => 20])
-
Add the following before the
return
statement of thebaseFieldDefinitions()
method of theEvent
class:$fields['published']->setDisplayOptions('form', [ 'settings' => [ 'display_label' => TRUE, ], 'weight' => 30, ]);
The entire
Event.php
file at this point<?php namespace Drupal\event\Entity; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityPublishedTrait; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\user\EntityOwnerInterface; use Drupal\user\EntityOwnerTrait; /** * @ContentEntityType( * id = "event", * label = @Translation("Event"), * base_table = "event", * entity_keys = { * "id" = "id", * "uuid" = "uuid", * "label" = "title", * "owner" = "author", * "published" = "published", * }, * handlers = { * "access" = "Drupal\entity\EntityAccessControlHandler", * "form" = { * "add" = "Drupal\Core\Entity\ContentEntityForm", * "edit" = "Drupal\Core\Entity\ContentEntityForm", * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm", * }, * "permission_provider" = "Drupal\entity\EntityPermissionProvider", * "route_provider" = { * "default" = "Drupal\entity\Routing\DefaultHtmlRouteProvider", * }, * }, * links = { * "canonical" = "/event/{event}", * "add-form" = "/admin/content/events/add", * "edit-form" = "/admin/content/events/manage/{event}", * "delete-form" = "/admin/content/events/manage/{event}/delete", * }, * admin_permission = "administer event" * ) */ class Event extends ContentEntityBase implements EntityOwnerInterface, EntityPublishedInterface { use EntityOwnerTrait, EntityPublishedTrait; public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // Get the field definitions for 'id' and 'uuid' from the parent. $fields = parent::baseFieldDefinitions($entity_type); $fields['title'] = BaseFieldDefinition::create('string') ->setLabel(t('Title')) ->setRequired(TRUE) ->setDisplayOptions('form', ['weight' => 0]); $fields['date'] = BaseFieldDefinition::create('datetime') ->setLabel(t('Date')) ->setRequired(TRUE) ->setDisplayOptions('view', [ 'label' => 'inline', 'settings' => [ 'format_type' => 'html_date', ], 'weight' => 0, ]) ->setDisplayOptions('form', ['weight' => 10]); $fields['description'] = BaseFieldDefinition::create('text_long') ->setLabel(t('Description')) ->setDisplayOptions('view', [ 'label' => 'hidden', 'weight' => 10, ]) ->setDisplayOptions('form', ['weight' => 20]); // Get the field definitions for 'owner' and 'published' from the traits. $fields += static::ownerBaseFieldDefinitions($entity_type); $fields += static::publishedBaseFieldDefinitions($entity_type); $fields['published']->setDisplayOptions('form', [ 'settings' => [ 'display_label' => TRUE, ], 'weight' => 30, ]); return $fields; } /** * @return string */ public function getTitle() { return $this->get('title')->value; } /** * @param string $title * * @return $this */ public function setTitle($title) { return $this->set('title', $title); } /** * @return \Drupal\Core\Datetime\DrupalDateTime */ public function getDate() { return $this->get('date')->date; } /** * @param \Drupal\Core\Datetime\DrupalDateTime $date * * @return $this */ public function setDate(DrupalDateTime $date) { return $this->set('date', $date->format(DATETIME_DATETIME_STORAGE_FORMAT)); } /** * @return \Drupal\filter\Render\FilteredMarkup */ public function getDescription() { return $this->get('description')->processed; } /** * @param string $description * @param string $format * * @return $this */ public function setDescription($description, $format) { return $this->set('description', [ 'value' => $description, 'format' => $format, ]); } }
-
Rebuild caches
Run
drush cache:rebuild
-
Add an event in the user interface
Visit
/admin/content/events/add
Note that the form fields are displayed.
Enter a title, date and description and press Save.
Verify that the event was saved by checking that a new row was created in the
{event}
table.Note that no message is displayed and no redirect is performed.
3.3. Add a specialized form
-
Add a
src/Form
directory -
Add a
src/Form/EventForm.php
file with the following:<?php namespace Drupal\event\Form; use Drupal\Core\Entity\ContentEntityForm; use Drupal\Core\Form\FormStateInterface; class EventForm extends ContentEntityForm { public function save(array $form, FormStateInterface $form_state) { parent::save($form, $form_state); $entity = $this->getEntity(); $entity_type = $entity->getEntityType(); $arguments = [ '@entity_type' => $entity_type->getSingularLabel(), '%entity' => $entity->label(), 'link' => $entity->toLink($this->t('View'), 'canonical')->toString(), ]; $this->logger($entity->getEntityTypeId())->notice('The @entity_type %entity has been saved.', $arguments); $this->messenger()->addStatus($this->t('The @entity_type %entity has been saved.', $arguments)); $form_state->setRedirectUrl($entity->toUrl('canonical')); } }
-
Replace the value of the
add
andedit
annotation keys in the form handlers section of the annotation insrc/Entity/Event.php
with"Drupal\event\Form\EventForm"
. -
Rebuild caches
Run
drush cache:rebuild
-
Edit an event in the user interface
Visit
/admin/content/events/manage/2
Note that a route exists and form fields are displayed including proper default values.
Modify the title, date and description and published status and press Save.
Note that a message is displayed and a redirect is performed.
Verify that the values in the respective row in the
{event}
table have been updated. Also note that the default values of the form fields are correct on the reloaded page. -
Delete an event in the user interface
Visit
/admin/content/events/manage/2/delete
Note that a route exists and a confirmation form is shown.
Press Delete.
Note that a message is shown and you are redirected to the front page.
Verify that the respective row in the
{event}
table has been deleted.
4. Listing entities
4.1. Add a route
-
Add the following to the annotation of the
Event
class:* label_collection = @Translation("Events"), * label_singular = @Translation("event"), * label_plural = @Translation("events"),
-
Add the following to the
handlers
section of the annotation of theEvent
class:* "list_builder" = "Drupal\Core\Entity\EntityListBuilder", * "local_action_provider" = { * "collection" = "Drupal\entity\Menu\EntityCollectionLocalActionProvider", * },
-
Add the following to the
links
section of the annotation insrc/Entity/Event.php
:* "collection" = "/admin/content/events",
The entire
Event.php
file at this point<?php namespace Drupal\event\Entity; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityPublishedTrait; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\user\EntityOwnerInterface; use Drupal\user\EntityOwnerTrait; /** * @ContentEntityType( * id = "event", * label = @Translation("Event"), * label_collection = @Translation("Events"), * label_singular = @Translation("event"), * label_plural = @Translation("events"), * base_table = "event", * entity_keys = { * "id" = "id", * "uuid" = "uuid", * "label" = "title", * "owner" = "author", * "published" = "published", * }, * handlers = { * "access" = "Drupal\entity\EntityAccessControlHandler", * "form" = { * "add" = "Drupal\Core\Entity\ContentEntityForm", * "edit" = "Drupal\Core\Entity\ContentEntityForm", * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm", * }, * "list_builder" = "Drupal\Core\Entity\EntityListBuilder", * "local_action_provider" = { * "collection" = "Drupal\entity\Menu\EntityCollectionLocalActionProvider", * }, * "permission_provider" = "Drupal\entity\EntityPermissionProvider", * "route_provider" = { * "default" = "Drupal\entity\Routing\DefaultHtmlRouteProvider", * }, * }, * links = { * "canonical" = "/event/{event}", * "collection" = "/admin/content/events", * "add-form" = "/admin/content/events/add", * "edit-form" = "/admin/content/events/manage/{event}", * "delete-form" = "/admin/content/events/manage/{event}/delete", * }, * admin_permission = "administer event" * ) */ class Event extends ContentEntityBase implements EntityOwnerInterface, EntityPublishedInterface { use EntityOwnerTrait, EntityPublishedTrait; public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // Get the field definitions for 'id' and 'uuid' from the parent. $fields = parent::baseFieldDefinitions($entity_type); $fields['title'] = BaseFieldDefinition::create('string') ->setLabel(t('Title')) ->setRequired(TRUE) ->setDisplayOptions('form', ['weight' => 0]); $fields['date'] = BaseFieldDefinition::create('datetime') ->setLabel(t('Date')) ->setRequired(TRUE) ->setDisplayOptions('view', [ 'label' => 'inline', 'settings' => [ 'format_type' => 'html_date', ], 'weight' => 0, ]) ->setDisplayOptions('form', ['weight' => 10]); $fields['description'] = BaseFieldDefinition::create('text_long') ->setLabel(t('Description')) ->setDisplayOptions('view', [ 'label' => 'hidden', 'weight' => 10, ]) ->setDisplayOptions('form', ['weight' => 20]); // Get the field definitions for 'owner' and 'published' from the traits. $fields += static::ownerBaseFieldDefinitions($entity_type); $fields += static::publishedBaseFieldDefinitions($entity_type); $fields['published']->setDisplayOptions('form', [ 'settings' => [ 'display_label' => TRUE, ], 'weight' => 30, ]); return $fields; } /** * @return string */ public function getTitle() { return $this->get('title')->value; } /** * @param string $title * * @return $this */ public function setTitle($title) { return $this->set('title', $title); } /** * @return \Drupal\Core\Datetime\DrupalDateTime */ public function getDate() { return $this->get('date')->date; } /** * @param \Drupal\Core\Datetime\DrupalDateTime $date * * @return $this */ public function setDate(DrupalDateTime $date) { return $this->set('date', $date->format(DATETIME_DATETIME_STORAGE_FORMAT)); } /** * @return \Drupal\filter\Render\FilteredMarkup */ public function getDescription() { return $this->get('description')->processed; } /** * @param string $description * @param string $format * * @return $this */ public function setDescription($description, $format) { return $this->set('description', [ 'value' => $description, 'format' => $format, ]); } }
-
Rebuild caches
Run
drush cache:rebuild
-
Visit
/admin/content/events
Note that a route is provided and a list of entities is provided with Edit and Delete operation links for each entity.
By not showing at least the title of each event the list is not actually usable so we need to provide a specialized list builder.
4.2. Add a specialized list builder
-
Create a
src/Controller
directory -
Add a
src/Controller/EventListBuilder.php
file with the following:<?php namespace Drupal\event\Controller; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityListBuilder; class EventListBuilder extends EntityListBuilder { public function buildHeader() { $header = []; $header['title'] = $this->t('Title'); $header['date'] = $this->t('Date'); $header['published'] = $this->t('Published'); return $header + parent::buildHeader(); } public function buildRow(EntityInterface $event) { /** @var \Drupal\event\Entity\Event $event */ $row = []; $row['title'] = $event->toLink(); $row['date'] = $event->getDate()->format('m/d/y h:i:s a'); $row['published'] = $event->isPublished() ? $this->t('Yes') : $this->t('No'); return $row + parent::buildRow($event); } }
More information on the above
-
Separate methods:
public function buildHeader() {
List builders build the table header and the table rows in separate methods.
-
Translation:
$this->t('Title')
The base
EntityListBuilder
class, like many other base classes in Drupal, provides at()
function that can be used to translate strings. -
Array merging:
$header + parent::buildHeader()
Arrays with string keys can be merged in PHP by "adding" them. Because the base class provides the operations column we put our own part of the header first and add the part from the parent last.
-
Inline type hint:
/** @var \Drupal\event\Entity\EventInterface $event */
Because
EntityListBuilderInterface
, the interface for list builders, dictates that we type hint the$event
variable withEntityInterface
instead of our more specificEventInterface
, IDEs are not aware that the$event
variable has the methodsgetTitle()
andgetDate()
in this case. To inform IDEs that these methods are in fact available an inline type hint can be added to the$event
variable. -
Entity links:
$event->toLink()
Entities have a
toLink()
method to generate links with a specified link text to a specified link relation of the entity. By default a link with the entity label as link text to thecanonical
link relation is generated which is precisely what we want here. -
Date formatting:
$row['date'] = $event->getDate()->format('m/d/y h:i:s a');
Because the
getDate()
method returns a date object we can attain the formatted date by using itsformat()
method. If the same date format is to be used in multiple places on the site, hardcoding it here can lead to duplication or worse, inconsistent user interfaces. To prevent this, Drupal associates PHP date formats with machine-readable names to form a Date format configuration entity. (More on configuration entities in general below.) That way the name, such asshort
,medium
orlong
can be used without having to remember the associated PHP date format. This also allows changing the PHP date format later without having to update each place it is used. To utilize Drupal's date format system thedate.formatter
service can be used. Unfortunately, Drupal's date formatter cannot handle date objects but works with timestamps instead. It is not used above because it would be more verbose and introduce new concepts, such as services and dependency injection, even though it would be the preferred implementation. For reference, the respective parts ofEventListBuilder.php
would then be:use Drupal\Core\Datetime\DateFormatterInterface; ... use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; class EventListBuilder extends EntityListBuilder { /** * @var \Drupal\Core\Datetime\DateFormatterInterface */ protected $dateFormatter; public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, DateFormatterInterface $date_formatter) { parent::__construct($entity_type, $storage); $this->dateFormatter = $date_formatter; } public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { return new static( $entity_type, $container->get('entity.manager')->getStorage($entity_type->id()), $container->get('date.formatter') ); } ... public function buildRow(EntityInterface $event) { /** @var \Drupal\event\Entity\EventInterface $event */ ... $row['date'] = $this->dateFormatter->format($event->getDate()->getTimestamp(), 'medium'); ... } }
-
-
Replace the value of the
list_builder
annotation key in thehandlers
section of the annotation insrc/Entity/Event.php
with"Drupal\event\Controller\EventListBuilder"
. -
Rebuild caches
Run
drush cache:rebuild
-
Visit
/admin/content/events
Note that the entity list now shows the event title and date.
-
Delete an event again
Visit
/admin/content/events/manage/2/delete
and press Delete.Note that this time you are redirected to the administrative event listing. The redirect to the front page that happened above is only a fallback in case no
collection
route exists.
4.3. Add an administrative view
While a specialized entity list builder has the benefit of being re-usable one can also take advantage of Drupal's Views module to create an administrative listing of events.
-
Add the following to the
handlers
section of the annotation insrc/Entity/Event.php
:* "views_data" = "Drupal\views\EntityViewsData",
Note that the views data that is provided by the default views data handler is partially incomplete so - in particular when dealing with date or entity reference fields - using Views for entity listings should be evaluated carefully.
-
Rebuild caches
Run
drush cache:rebuild
-
Add an Events view to replace the list builder
-
Select Show: Event
-
Check Create a page
-
Use
admin/content/events
as the PathThis will make Views replace the previously existing collection route.
Note that the path is entered without a leading slash in Views.
-
Select Display format: Table
-
Press Save and edit
-
Click on Settings in the Format section and check Sortable for the Title, Date and Published fields, as well as Enable Drupal style "sticky" table headers (Javascript) and Show the empty text in the table
-
Add an empty text field to the No results behavior area
-
Add Date and Published fields and select _Output format: ✔ / ✖ for the Published field
-
Add Link to edit Event and Link to delete Event fields and check the Exclude from display checkbox
-
Add a Dropbutton field for the operations and enable the edit and delete links
Views provides a number of features that increase the usability of administrative listings when compared to the stock entity list builder. These include:
-
Exposed filters
-
Using formatters for fields
This allows using Drupal's date formatting system for the date field (as discussed above), for example, and using check (✔) and cross (✖) marks for the published field.
-
A click-sortable table header
-
An Ajax-enabled pager
-
A "sticky" table header
-
The ability to add a menu link or local task for the view from the user interface. (Note that adding a local task does not work in this case as it conflicts with the overriding of the list-builder route.)
-
-
Verify that events can be accessed via JSON API, as well, by visiting, for example, following paths:
- /jsonapi/event/event
- /jsonapi/event/event?include=author
- /jsonapi/event/event?filter[date][operator]=<&filter[date][value]=2019-01-01T00:00:00
5. Adding administrative links
To provide a usable and integrated administration experience the different pages need to be connected and enriched with Drupal's standard administrative links.
5.1. Add a menu link for the event listing
For the event listing to show up in the toolbar menu under Content, we need to provide a menu link for it.
-
Add an
event.links.menu.yml
file with the following:entity.event.collection: title: 'Events' route_name: entity.event.collection parent: system.admin_content
-
Rebuild caches
Run
drush cache:rebuild
-
Verify that there is an Events link in the toolbar menu.
Note that there is no Event local task on
/admin/content
.
5.2. Add a local task for the event listing
-
Add an
event.links.task.yml
file with the following:entity.event.collection: title: 'Events' route_name: entity.event.collection base_route: system.admin_content
-
Rebuild caches
Run
drush cache:rebuild
-
Verify that the Events local task appears on
/admin/content
5.3. Add local tasks for the edit and delete forms
-
Add the following to
event.links.task.yml
:entity.event.canonical: title: 'View' route_name: entity.event.canonical base_route: entity.event.canonical entity.event.edit_form: title: 'Edit' route_name: entity.event.edit_form base_route: entity.event.canonical entity.event.delete_form: title: 'Delete' route_name: entity.event.delete_form base_route: entity.event.canonical
-
Rebuild caches
Run
drush cache:rebuild
-
Visit
/events/4
Verify that View, Edit and Delete local tasks are shown.
6. Adding additional fields
Now that our basic implementation of Events is functional and usable from the user interface, we can add some more fields. This is both to recap the above chapters and to show some additional features that are often used for content entities.
6.1. Add the field definitions
-
Add the following use statements to
src/Entity/Event.php
:use Drupal\Core\Entity\EntityChangedInterface; use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Field\FieldStorageDefinitionInterface;
-
Add
EntityChangedInterface,
to theimplements
part of the class declaration of theEvent
class -
Add
EntityChangedTrait,
to theuse
part inside of theEvent
class -
Add the following to the
baseFieldDefinitions()
method insrc/Entity/Event.php
above thereturn
statement:$fields['maximum'] = BaseFieldDefinition::create('integer') ->setLabel(t('Maximum number of attendees')) ->setSetting('min', 1) ->setDisplayOptions('form', ['weight' => 23]); $fields['attendees'] = BaseFieldDefinition::create('entity_reference') ->setLabel(t('Attendees')) ->setSetting('target_type', 'user') ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) ->setDisplayOptions('view', ['weight' => 20]) ->setDisplayOptions('form', ['weight' => 27]); $fields['remaining'] = BaseFieldDefinition::create('integer') ->setLabel(t('Remaining number of attendees')) ->setComputed(TRUE) ->setDisplayOptions('view', [ 'label' => 'inline', 'weight' => 30, ]); $fields['path'] = BaseFieldDefinition::create('path') ->setLabel(t('Path')) ->setComputed(TRUE) ->setDisplayOptions('form', ['weight' => 5]); $fields['changed'] = BaseFieldDefinition::create('changed') ->setLabel(t('Changed'));
More information on the above
-
Multiple-value fields:
->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
Fields can be allowed to have multiple values by changing the cardinality of the field definition. If an unlimited amount of field values should be possible, the constant
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
should be used as the cardinality value. -
Changed time tracking:
EntityChangedInterface
When an entity that supports changed time tracking is being saved, Drupal checks whether the entity has been updated by someone else in the meantime so that the changes do not get overwritten.
Note that the and
changed
field is not exposed on the form. -
6.2. Add additional field methods
-
Add the following to the use statements at the top of
src/Entity/Event.php
:use Drupal\user\UserInterface;
-
Add the following to the
Event
class insrc/Entity/Event.php
:/** * @return int */ public function getMaximum() { return $this->get('maximum')->value; } /** * @return \Drupal\user\UserInterface[] */ public function getAttendees() { return $this->get('attendees')->referencedEntities(); } /** * @param \Drupal\user\UserInterface $attendee * * @return $this */ public function addAttendee(UserInterface $attendee) { $field_items = $this->get('attendees'); $exists = FALSE; foreach ($field_items as $field_item) { if ($field_item->target_id === $attendee->id()) { $exists = TRUE; } } if (!$exists) { $field_items->appendItem($attendee); } return $this; } /** * @param \Drupal\user\UserInterface $attendee * * @return $this */ public function removeAttendee(UserInterface $attendee) { $field_items = $this->get('attendees'); foreach ($field_items as $delta => $field_item) { if ($field_item->target_id === $attendee->id()) { $field_items->set($delta, NULL); } } $field_items->filterEmptyItems(); return $this; } /** * @return int */ public function getRemaining() { return $this->get('remaining')->value; }
More information on the above
-
Field item lists:
$this->get('attendees')->referencedEntities();
-
Object traversal:
foreach ($field_items as $field_item)
-
6.3. Add validation constraints
-
Add a
src/Plugin
directory -
Add a
src/Plugin/Validation
directory -
Add a
src/Plugin/Validation/Constraint
directory -
Add a
src/Plugin/Validation/Constraint/AttendeeCountConstraint.php
with the following:<?php namespace Drupal\event\Plugin\Validation\Constraint; use Symfony\Component\Validator\Constraint; /** * @Constraint( * id = "AttendeeCount", * label = @Translation("Attendee count"), * ) */ class AttendeeCountConstraint extends Constraint { /** * @var string */ public $message = 'The event %title only allows %maximum attendees.'; }
-
Add a
src/Plugin/Validation/Constraint/AttendeeCountConstraintValidator.php
with the following:<?php namespace Drupal\event\Plugin\Validation\Constraint; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; class AttendeeCountConstraintValidator extends ConstraintValidator { public function validate($value, Constraint $constraint) { /* @var \Drupal\Core\Field\FieldItemListInterface $value */ /* @var \Drupal\event\Plugin\Validation\Constraint\AttendeeCountConstraint $constraint */ /* @var \Drupal\Core\Entity\Plugin\DataType\EntityAdapter $adapter */ $adapter = $value->getParent(); /* @var \Drupal\event\Entity\Event $event */ $event = $adapter->getEntity(); $maximum = $event->getMaximum(); if (count($value) > $maximum) { $this->context->buildViolation($constraint->message) ->setParameter('%title', $event->getTitle()) ->setParameter('%maximum', $maximum) ->addViolation(); } } }
-
Add a
src/Plugin/Validation/Constraint/UniqueAttendeesConstraint.php
with the following:<?php namespace Drupal\event\Plugin\Validation\Constraint; use Symfony\Component\Validator\Constraint; /** * @Constraint( * id = "UniqueAttendees", * label = @Translation("Unique attendees"), * ) */ class UniqueAttendeesConstraint extends Constraint { /** * @var string */ public $message = 'The user %name is already attending this event.'; }
-
Add a
src/Plugin/Validation/Constraint/UniqueAttendeesConstraintValidator.php
with the following:<?php namespace Drupal\event\Plugin\Validation\Constraint; use Drupal\user\Entity\User; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; class UniqueAttendeesConstraintValidator extends ConstraintValidator { public function validate($value, Constraint $constraint) { /* @var \Drupal\Core\Field\FieldItemListInterface $value */ /* @var \Drupal\event\Plugin\Validation\Constraint\UniqueAttendeesConstraint $constraint */ $user_ids = []; foreach ($value as $delta => $item) { $user_id = $item->target_id; if (in_array($user_id, $user_ids, TRUE)) { $this->context->buildViolation($constraint->message) ->setParameter('%name', User::load($user_id)->getDisplayName()) ->atPath((string) $delta) ->addViolation(); return; } $user_ids[] = $user_id; } } }
-
Add the following to the
$fields['attendees']
section of thebaseFieldDefinitions()
method of theEvent
class before the semicolon:->addConstraint('AttendeeCount') ->addConstraint('UniqueAttendees')
6.4. Implement the computed field
-
Add a
src/Field
directory -
Add a
src/Field/RemainingFieldItemList.php
with the following:<?php namespace Drupal\event\Field; use Drupal\Core\Field\FieldItemList; use Drupal\Core\TypedData\ComputedItemListTrait; class RemainingFieldItemList extends FieldItemList { use ComputedItemListTrait; protected function computeValue() { /* @var \Drupal\event\Entity\Event $event */ $event = $this->getEntity(); $remaining = $event->getMaximum() - count($event->getAttendees()); $this->list[0] = $this->createItem(0, $remaining); } }
-
Add the following use statements to
src/Entity/Event.php
:use Drupal\event\Field\RemainingFieldItemList;
-
Add the following to the
$fields['remaining']
section of thebaseFieldDefinitions()
method of theEvent
class before the semicolon:->setClass(RemainingFieldItemList::class)
The entire
Event.php
file at this point<?php namespace Drupal\event\Entity; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityChangedInterface; use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityPublishedTrait; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\event\Field\RemainingFieldItemList; use Drupal\user\EntityOwnerInterface; use Drupal\user\EntityOwnerTrait; use Drupal\user\UserInterface; /** * @ContentEntityType( * id = "event", * label = @Translation("Event"), * label_collection = @Translation("Events"), * label_singular = @Translation("event"), * label_plural = @Translation("events"), * base_table = "event", * entity_keys = { * "id" = "id", * "uuid" = "uuid", * "label" = "title", * "owner" = "author", * "published" = "published", * }, * handlers = { * "access" = "Drupal\entity\EntityAccessControlHandler", * "form" = { * "add" = "Drupal\Core\Entity\ContentEntityForm", * "edit" = "Drupal\Core\Entity\ContentEntityForm", * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm", * }, * "list_builder" = "Drupal\Core\Entity\EntityListBuilder", * "local_action_provider" = { * "collection" = "Drupal\entity\Menu\EntityCollectionLocalActionProvider", * }, * "permission_provider" = "Drupal\entity\EntityPermissionProvider", * "route_provider" = { * "default" = "Drupal\entity\Routing\DefaultHtmlRouteProvider", * }, * "views_data" = "Drupal\views\EntityViewsData", * }, * links = { * "canonical" = "/event/{event}", * "collection" = "/admin/content/events", * "add-form" = "/admin/content/events/add", * "edit-form" = "/admin/content/events/manage/{event}", * "delete-form" = "/admin/content/events/manage/{event}/delete", * }, * admin_permission = "administer event" * ) */ class Event extends ContentEntityBase implements EntityChangedInterface, EntityOwnerInterface, EntityPublishedInterface { use EntityChangedTrait, EntityOwnerTrait, EntityPublishedTrait; public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { // Get the field definitions for 'id' and 'uuid' from the parent. $fields = parent::baseFieldDefinitions($entity_type); $fields['title'] = BaseFieldDefinition::create('string') ->setLabel(t('Title')) ->setRequired(TRUE) ->setDisplayOptions('form', ['weight' => 0]); $fields['date'] = BaseFieldDefinition::create('datetime') ->setLabel(t('Date')) ->setRequired(TRUE) ->setDisplayOptions('view', [ 'label' => 'inline', 'settings' => [ 'format_type' => 'html_date', ], 'weight' => 0, ]) ->setDisplayOptions('form', ['weight' => 10]); $fields['description'] = BaseFieldDefinition::create('text_long') ->setLabel(t('Description')) ->setDisplayOptions('view', [ 'label' => 'hidden', 'weight' => 10, ]) ->setDisplayOptions('form', ['weight' => 20]); // Get the field definitions for 'owner' and 'published' from the traits. $fields += static::ownerBaseFieldDefinitions($entity_type); $fields += static::publishedBaseFieldDefinitions($entity_type); $fields['published']->setDisplayOptions('form', [ 'settings' => [ 'display_label' => TRUE, ], 'weight' => 30, ]); $fields['maximum'] = BaseFieldDefinition::create('integer') ->setLabel(t('Maximum number of attendees')) ->setSetting('min', 1) ->setRequired(TRUE) ->setDefaultValue(10) ->setDisplayOptions('form', ['weight' => 23]); $fields['attendees'] = BaseFieldDefinition::create('entity_reference') ->setLabel(t('Attendees')) ->setSetting('target_type', 'user') ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) ->addConstraint('AttendeeCount') ->addConstraint('UniqueAttendees') ->setDisplayOptions('view', ['weight' => 20]) ->setDisplayOptions('form', ['weight' => 27]); $fields['remaining'] = BaseFieldDefinition::create('integer') ->setLabel(t('Remaining number of attendees')) ->setComputed(TRUE) ->setClass(RemainingFieldItemList::class) ->setDisplayOptions('view', [ 'label' => 'inline', 'weight' => 30, ]); $fields['path'] = BaseFieldDefinition::create('path') ->setLabel(t('Path')) ->setComputed(TRUE) ->setDisplayOptions('form', ['weight' => 5]); $fields['changed'] = BaseFieldDefinition::create('changed') ->setLabel(t('Changed')); return $fields; } /** * @return string */ public function getTitle() { return $this->get('title')->value; } /** * @param string $title * * @return $this */ public function setTitle($title) { return $this->set('title', $title); } /** * @return \Drupal\Core\Datetime\DrupalDateTime */ public function getDate() { return $this->get('date')->date; } /** * @param \Drupal\Core\Datetime\DrupalDateTime $date * * @return $this */ public function setDate(DrupalDateTime $date) { return $this->set('date', $date->format(DATETIME_DATETIME_STORAGE_FORMAT)); } /** * @return \Drupal\filter\Render\FilteredMarkup */ public function getDescription() { return $this->get('description')->processed; } /** * @param string $description * @param string $format * * @return $this */ public function setDescription($description, $format) { return $this->set('description', [ 'value' => $description, 'format' => $format, ]); } /** * @return int */ public function getMaximum() { return $this->get('maximum')->value; } /** * @return \Drupal\user\UserInterface[] */ public function getAttendees() { return $this->get('attendees')->referencedEntities(); } /** * @param \Drupal\user\UserInterface $attendee * * @return $this */ public function addAttendee(UserInterface $attendee) { $field_items = $this->get('attendees'); $exists = FALSE; foreach ($field_items as $field_item) { if ($field_item->target_id === $attendee->id()) { $exists = TRUE; } } if (!$exists) { $field_items->appendItem($attendee); } return $this; } /** * @param \Drupal\user\UserInterface $attendee * * @return $this */ public function removeAttendee(UserInterface $attendee) { $field_items = $this->get('attendees'); foreach ($field_items as $delta => $field_item) { if ($field_item->target_id == $attendee->id()) { $field_items->set($delta, NULL); } } $field_items->filterEmptyItems(); return $this; } /** * @return int */ public function getRemaining() { return $this->get('remaining')->value; } }
6.5. Install the fields
-
Run
drush entity:updates
-
Note that the
{event__attendees}
table was created andmaximum
andchanged
fields have been created in the{event}
table.Note that there is no
remaining
column. There is nopath
column because path aliases are stored separately in the{url_alias}
table.
-
-
Try out the new methods
Run the following PHP code:
use Drupal\event\Entity\Event; use Drupal\user\Entity\User; $event = Event::load(3); $user = User::load(1); print 'Maximum number of attendees: ' . $event->getMaximum() . "\n"; print 'Remaining number of attendees: ' . $event->getRemaining() . "\n"; $event->addAttendee($user)->save(); $event = Event::load($event->id()); print 'Remaining number of attendees: ' . $event->getRemaining() . "\n"; $event->removeAttendee($user)->save(); $event = Event::load($event->id()); print 'Remaining number of attendees: ' . $event->getRemaining() . "\n";
Note that an empty array is returned initially, then an array with one user, then an empty array again.
-
Try out the new fields in the user interface
Visit
/admin/content/events/manage/4
and add a path and an attendee.Verify that the path alias gets created correctly and that the attendees are displayed correctly.
-
Verify that the remaining number of attendees is displayed and updated correctly.
-
Verify that the constraints prevent the creation of invalid events via the user interface.
The event entities are feature complete for our purposes as of now.
7. Tracking changes to entities
Drupal provides a mechanism to track changes of entities by storing a new revision of an entity each time it is saved.
7.1. Make events revisionable
-
Add the following use statements to
src/Entity/Event.php
:use Drupal\Core\Entity\RevisionLogEntityTrait;
-
Add the following to the annotation of the
Event
class:* revision_table = "event_revision", * revision_metadata_keys = { * "revision_user" = "revision_author", * "revision_created" = "revision_created", * "revision_log_message" = "revision_log_message", * }, * show_revision_ui = true,
-
Add
, RevisionLogEntityTrait
to theuse
part inside of theEvent
class -
Add the following before the
return
statement of thebaseFieldDefinitions()
method of theEvent
class:$fields += static::revisionLogBaseFieldDefinitions($entity_type);
-
Run
drush entity:updates
-
Verify that the
event_revision
table was created -
Verify that the Revision information vertical tab appears on the event edit form
7.2. Add a user interface for managing revisions
-
Add the following to the
route_provider
entry of thehandlers
section of the annotation of theEvent
class:* "revision" = "Drupal\entity\Routing\RevisionRouteProvider",
-
Add the following to the
links
section of the annotation of theEvent
class:* "version-history" = "/event/{event}/revisions", * "revision" = "/event/{event}/revisions/{event_revision}", * "revision-revert-form" = "/event/{event}/revisions/{event_revision}/revert",
8. Storing dynamic data in configuration
Apart from content entities there is a second type of entities in Drupal, the configuration entities. These have a machine-readable string ID and can be deployed between different environments along with the rest of the site configuration.
8.1. Create an entity class
While there are some distinctions, creating a configuration entity type is very similar to creating a content entity type.
-
Create a
src/Entity/EventType.php
with the following:<?php namespace Drupal\event\Entity; use Drupal\Core\Config\Entity\ConfigEntityBase; /** * @ConfigEntityType( * id = "event_type", * label = @Translation("Event type"), * label_collection = @Translation("Event types"), * label_singular = @Translation("event type"), * label_plural = @Translation("event types"), * config_prefix = "type", * config_export = { * "id", * "label", * }, * entity_keys = { * "id" = "id", * "label" = "label", * }, * ) */ class EventType extends ConfigEntityBase { protected $id; protected $label; }
More information on the above
-
Configuration prefix:
config_prefix = "type"
To clearly identify the source of all configuration, the names of the respective configuration files of configuration entities are automatically prefixed with the module name (
event
in this case) and a period (.
) as a separator. To distinguish different configuration entity types from the same module, each configuration entity type specifies a configuration prefix which is the second part of the configuration file name prefix followed by an additional period. The full name of a configuration entity's configuration file is, thus,"$module_name.$config_prefix.$entity_id"
. -
Export properties:
config_export = { "id", "label", }
protected $id; protected $label;
Configuration entities do not have a notion of (base) field definitions like content entities. Instead simple PHP properties can be declared in the entity class to hold the values of the entity. The names of those properties need to be specified as export properties in the entity annotation.
-
8.2. Provide a configuration schema
To ensure that the structure of each configuration object is correct, a schema is provided. When importing configuration from another environment, each configuration object is validated against this schema.
-
Add a
config/schema/event.schema.yml
with the following:event.type.*: type: config_object mapping: id: type: string label: 'ID' label: type: label label: 'Label'
8.3. Install the entity type
-
Run
drush entity:updates
Note that there is no schema change
-
Create and save an event type
Run the following PHP code:
use Drupal\event\Entity\EventType; EventType::create([ 'id' => 'webinar', 'label' => 'Webinar', ])->save();
Note that there is a new row in the
{config}
table with the nameevent.type.webinar
-
Load the event type by its ID
Run the following PHP code:
use Drupal\event\Entity\EventType; $event_type = EventType::load('webinar'); $event_type->label();
Note that the proper label is returned.
-
Update the label of the event type
Run the following PHP code:
use Drupal\event\Entity\EventType; $event_type = EventType::load('webinar'); $event_type ->set('label', 'Online webinar') ->save();
-
Delete the event type
Run the following PHP code:
use Drupal\event\Entity\EventType; $event_type = EventType::load('webinar') $event_type->delete();
Note that the row in the
{config}
table is gone.
9. Providing a user interface for configuration entities
9.1. Add a list of event types
-
Add a
src/Controller/EventTypeListBuilder.php
file with the following:<?php namespace Drupal\event\Controller; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityListBuilder; class EventTypeListBuilder extends EntityListBuilder { public function buildHeader() { $header = []; $header['label'] = $this->t('Label'); return $header + parent::buildHeader(); } public function buildRow(EntityInterface $event) { $row = []; $row['label'] = $event->label(); return $row + parent::buildRow($event); } }
-
Add the following to the annotation in
src/Entity/EventType.php
:* handlers = { * "access" = "Drupal\entity\EntityAccessControlHandler", * "list_builder" = "Drupal\event\Controller\EventTypeListBuilder", * "local_action_provider" = { * "collection" = "Drupal\entity\Menu\EntityCollectionLocalActionProvider", * }, * "permission_provider" = "Drupal\entity\EntityPermissionProvider", * "route_provider" = { * "default" = "Drupal\entity\Routing\DefaultHtmlRouteProvider", * }, * }, * links = { * "collection" = "/admin/structure/event-types", * }, * admin_permission = "administer event_type",
-
Add the following to
event.links.menu.yml
:entity.event_type.collection: title: 'Event types' route_name: entity.event_type.collection parent: system.admin_structure
-
Rebuild caches
Run
drush cache:rebuild
-
Verify that there is a Event types menu link in the toolbar menu
-
Visit
/admin/structure/event-types
Note that a listing of event types is shown.
9.2. Add forms for event types
In contrast to content entities, configuration entities do not have the ability to use widgets for their forms, so we need to provide the respective form elements ourselves.
-
Add a
src/Form/EventTypeForm.php
file with the following:<?php namespace Drupal\event\Form; use Drupal\Core\Entity\EntityForm; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Form\FormStateInterface; class EventTypeForm extends EntityForm { public function form(array $form, FormStateInterface $form_state) { $form = parent::form($form, $form_state); $event_type = $this->getEntity(); $form['label'] = [ '#type' => 'textfield', '#title' => $this->t('Label'), '#default_value' => $event_type->label(), '#required' => TRUE, ]; $form['id'] = [ '#type' => 'machine_name', '#title' => $this->t('ID'), '#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH, '#default_value' => $event_type->id(), '#machine_name' => [ 'exists' => [$event_type->getEntityType()->getClass(), 'load'], ], '#disabled' => !$event_type->isNew(), ]; return $form; } public function save(array $form, FormStateInterface $form_state) { parent::save($form, $form_state); $entity = $this->getEntity(); $entity_type = $entity->getEntityType(); $arguments = [ '@entity_type' => $entity_type->getLowercaseLabel(), '%entity' => $entity->label(), 'link' => $entity->toLink($this->t('Edit'), 'edit-form')->toString(), ]; $this->logger($entity->getEntityTypeId())->notice('The @entity_type %entity has been saved.', $arguments); drupal_set_message($this->t('The @entity_type %entity has been saved.', $arguments)); $form_state->setRedirectUrl($entity->toUrl('collection')); } }
-
Add the following to the
handlers
section of the annotation insrc/Entity/EventType.php
:* "form" = { * "add" = "Drupal\event\Form\EventTypeForm", * "edit" = "Drupal\event\Form\EventTypeForm", * "delete" = "Drupal\Core\Entity\EntityDeleteForm", * },
-
Add the following to the
links
section of the annotation insrc/Entity/EventType.php
:* "add-form" = "/admin/structure/event-types/add", * "edit-form" = "/admin/structure/event-types/manage/{event_type}", * "delete-form" = "/admin/structure/event-types/manage/{event_type}/delete",
-
Add the following to
event.links.task.yml
:entity.event_type.edit_form: title: 'Edit' route_name: entity.event_type.edit_form base_route: entity.event_type.edit_form entity.event_type.delete_form: title: 'Delete' route_name: entity.event_type.delete_form base_route: entity.event_type.edit_form
-
Rebuild caches
Run
drush cache:rebuild
-
Verify that a local action appears to add an event type
Add an event type.
Edit an event type.
Verify that Edit and Delete local tasks are shown.
Delete an event type.
10. Categorizing different entities of the same entity type
Drupal provides a mechanism to distinguish content entities of the same type and attach different behavior to the entities based on this distinction. In the case of event entities, for example, it allows events to have different behavior based on the type of event they are. The nomenclature is that entity types can have bundles where each entity of that entity type belongs to a certain bundle.
Generally a configuration entity type is used to provide the bundles for a content entity type. In this case each Event type entity will be a bundle for the Event entity type.
10.1. Add the bundle field
-
Delete the existing event(s)
Visit
/admin/content/event/manage/4/delete
and press Delete.Adding a bundle field cannot be done when there are existing entities.
-
Add the following to the
entity_keys
section of the annotation insrc/Entity/Event.php
:* "bundle" = "type",
-
Add the following to the annotation in
src/Entity/Event.php
:* bundle_entity_type = "event_type",
-
Replace the
add-form
link in the annotation insrc/Entity/Event.php
with:"/admin/content/events/add/{event_type}"
-
Add the following to the
links
section of the annotation insrc/Entity/Event.php
with:* "add-page" = "/admin/content/events/add",
-
Add the following to the annotation in
src/Entity/EventType.php
:* bundle_of = "event",
Like for the id
and uuid
fields, the field definition for the type
field
is automatically generated by ContentEntityBase::baseFieldDefinitions()
.
10.2. Install the bundle field
-
Run
drush entity:updates
- Note that the
type
column has been added to the{event}
table.
- Note that the
-
Visit
/admin/structure/event-types/add
and add a Conference event type -
Visit
/admin/content/events/add
Note that the event types are displayed as options.
-
Create an event
11. Configuring bundles in the user interface
11.1. Enable Field UI for events
-
Add the following to the annotation in
src/Entity/Event.php
:* field_ui_base_route = "entity.event_type.edit_form",
-
Rebuild caches
Run
drush cache:rebuild
-
Visit
/admin/structure/event-types
Note that there is a Manage fields, Manage form display and Manage display operation for each event type.
11.2. Add dynamic fields to events
The ability to have comments is managed as a field in Drupal, so we can use Field UI to add a Comments field to an event type.
-
Add a comment type on
/admin/structure/comment/types/add
Select Event as the target entity type
-
Add a Comments field to an event type
11.3. Configure view modes
-
Visit the Manage display page
Note that only the Comments field appears
-
Add the following to all field definitions in the
baseFieldDefinitions()
method of theEvent
class before the semicolon:->setDisplayConfigurable('view', TRUE)
-
Rebuild caches
Run
drush cache:rebuild
-
Verify that all fields now appear
-
Upload user pictures for the existing users
-
Use Rendered entity for the Attendees field on the Manage display page
-
Add a Teaser view mode on
/admin/structure/display-modes/view/add/event
-
Make the Teaser view mode configurable on the Manage display page
-
Rebuild caches
Run
drush cache:rebuild
-
Configure the Teaser view mode
-
Add an Event teasers view
-
Add a Page views display with the path
events
Note that the path is entered without a leading slash in Views.
-
Use the Unformatted list style for the display
Display Events in the Teaser view mode
-
-
Verify that the event teasers are displayed correctly
11.4. Configure the form
-
Visit the Manage form display page
Note that only the Comments field appears
-
Add the following to all field definitions in the
baseFieldDefinitions()
method of theEvent
class before the semicolon:->setDisplayConfigurable('form', TRUE)
-
Note that all fields now appear
-
Configure the form display
-
Use the Select list widget for the Date field
-
Use the Check boxes/radio buttons widget for the Attendees field
-
12. Translating content
Content entities can be made translatable in the storage by amending the entity type annotation. However, this by itself does not make the content entity translatable in the user interface. It only allows site builders to make it translatable in the user interface with the Content Translation module.
12.1. Install the Content Translation module
-
Install the Content Translation module on
/admin/modules
-
Add a second language on
/admin/config/regional/language
-
Visit
/admin/config/regional/content-language
Note that events cannot be selected for translation.
12.2. Make events translatable
-
Delete all existing events
-
Delete the Events and Event teaser views
-
Add the following to the annotation in
src/Entity/Event.php
:* translatable = TRUE, * data_table = "event_field_data",
-
Add the following to the
entity_keys
part of the annotation insrc/Entity/Event.php
:* "langcode" = "langcode",
Like for the
id
,uuid
andtype
fields, the field definition for thelangcode
field is automatically generated byContentEntityBase::baseFieldDefinitions()
. -
Run
drush entity:updates
Note that the
{event_field_data}
table has been created and thetype
column has been added to the{event}
table. -
Verify that Events can be marked as translatable
Note that only the Comments field is translatable.
-
Add the following to field definitions for the
title
,description
,published
,path
andchanged
fields in thebaseFieldDefinitions()
method of theEvent
class before the semicolon:->setTranslatable(TRUE)
-
Run
drush entity:updates
-
Mark all fields of all event types as translatable
-
Add an event entity
Note there is a Translate local task
Notice the following exception is thrown when visiting the Translate local task:
The "event" entity type did not specify a "default" form class.
-
Add the following to the form handlers part of the annotation in
src/Entity/Event.php
:* "default" = "Drupal\event\Form\EventForm"
-
Rebuild caches
Run
drush cache:rebuild
-
Translate entity
Note that non-translatable fields are still shown. Editing these will change the values in the source translation
Verify that translation works
- Re-add the Events view
13. Translating configuration
13.1. Install the Configuration Translation module
-
Install the Content Translation module on
/admin/modules
-
Verify that a Translate operation appears for event types
-
Verify that translation works