Skip to content

1. Using entities for data storage

  • Within the /modules/event directory create an event.info.yml file with the following:

    name: Event
    type: module
    description: Provides a custom entity type for events.
    core_version_requirement: ^11

    See the documentation on Drupal.org for more information about module info files.

  • Run drush pm:enable event or visit /admin/modules and install the Event module

  • Create a src/Entity/Event.php file with the following:

    <?php
    
    namespace Drupal\event\Entity;
    
    use Drupal\Core\Entity\Attribute\ContentEntityType;
    use Drupal\Core\Entity\ContentEntityBase;
    use Drupal\Core\StringTranslation\TranslatableMarkup;
    
    #[ContentEntityType(
        id: 'event',
        label: new TranslatableMarkup('Event'),
        entity_keys: [
            'id' => 'id',
            'uuid' => 'uuid',
        ],
        base_table: 'event',
    )]
    class Event extends ContentEntityBase {
    
    }
    More information on the above
    • File and Directory Structure:

      The src directory contains all object-oriented PHP code of a module. Each file contains a single unit of code: a class, trait, interface or an enum. In this tutorial only classes will be created.

      See the PHP documentation for more information on object-oriented PHP.

      Files can either be directly in the src directory or in any (direct or nested) subdirectory. Certain directory names have a special meaning in Drupal, however. In particular, Drupal looks in Entity for entity type classes.

      See Drupal API: Object-oriented programming conventions for more information.

    • 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 the src directory of the module.

    • Import:

      use Drupal\Core\Entity\Attribute\ContentEntityType;

      In the same way we declare a namespace for the Event class the ContentEntityType 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.

    • Attributes:

      #[ContentEntityType(
      )]

      Attributes are a way to provide metadata about code. Because the attributes are placed right next to the code itself, this allows making classes truly self-contained as both functionality and metadata are in the same file. Note that we have not registered or declared this class in any way to Drupal (and will not do so below, either).

      The concrete values that are part of the attribute are defined by the ContentEntityType attribute class. The ones used here will be explained below as they are used. See the class API documentation for more information.

      See Drupal API: Attribute based plugins for more information.

    • Entity Type ID:

      id: 'event',

      This is the ID of the entity type that is needed whenever interacting with a specific entity type in code. Other entity type IDs used in Drupal are media, node, taxonomy_term or user, for example.

    • Label:

      label: new TranslatableMarkup('Event'),

      This is the label of this entity type when presented to a user. This is only used in administrative interfaces.

      To make the values we provide in the annotation translatable we need to wrap them in new TranslatableMarkup(). This would allow us to translate this label as part of Drupal’s Interface Translation system, but we will not do this as part of this tutorial.

    • Storage information:

      entity_keys: [
        'id' => 'id',
        'uuid' => 'uuid',
      ],
      base_table: 'event',

      We need to specify the name of the database table we want the event data to be stored. This is called the base table, as there can be multiple tables that store entity information, as we will see 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. Other entity types use the convention of prefixing the ID field with the first letter of the entity type ID. For example the media entity type uses mid as the name of its ID field and the node entity type uses nid. Following that convention we could name our field eid and replace the respective line with

      'id' => 'eid',

      This is only pointed out to clarify the difference between the key and the value in the 'id' => 'id' line. We do not actually follow that convention here as it makes the field naming inconsistent.

      Similarly to the ID entity types can (and are encouraged to) provide a UUID field. We specify the name of the UUID field to be uuid.

      Note that the parameter names of the attribute (such as entity_keys) are not quoted, but keys in arrays (such as id and uuid) are strings and need to be quoted as such. Also note that trailing commas are allowed both for the parameter list and for arrays.

      See the PHP documentation for more information on the parameter syntax.

    • Class declaration:

      class Event extends ContentEntityBase {
      
      }

      The file name (without the file extension) must correspond to class name (including capitalization).

      Classes allow categorizing objects as being of a certain type. Event entities, that will be created below, will be objects that are instances of the entity class Event. In terms of code organization, classes can be used to group related functionality, which will also be done below.

    • 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. 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. The examples noted above (Media, Nodes, Taxonomy terms and Users) are all examples of content entities.

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 with id and uuid 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 the create() and save() methods from ContentEntityBase so they can be called without being present in the Event 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.

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 attributes of the Event class:

      'label' => 'title',
      'owner' => 'author',
      'published' => 'published',

    Declaring a label key makes the (inherited) label() method on the Event 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. The baseFieldDefinitions() method in ContentEntityBase provides field definitions for the id and uuid 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;

/**
 * Defines the event entity class.
 */
#[ContentEntityType(
id: 'event',
label: new TranslatableMarkup('Event'),
entity_keys: [
    'id' => 'id',
    'uuid' => 'uuid',
    'label' => 'title',
    'owner' => 'author',
    'published' => 'published',
],
base_table: '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 'author' and 'published' from the traits.
        $fields += static::ownerBaseFieldDefinitions($entity_type);
        $fields += static::publishedBaseFieldDefinitions($entity_type);

        return $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 and published columns have been created in the {event} table.

    Although most field types consist of a single value property, text fields, for example, have an additional format property. Therefore two database columns are required for text fields.

  • Create and save an event

    Run the following PHP code:

    use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
    use Drupal\event\Entity\Event;
    
    $event = Event::create([
      'title' => 'Drupal User Group',
      'date' => (new \DateTime())->format(DateTimeItemInterface::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(2);
    
    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 the processed property of text fields.

  • Update an event’s field values and save them.

    Run the following PHP code:

    use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
    use Drupal\event\Entity\Event;
    
    $event = Event::load(2);
    
    $event
      ->set('title', 'DrupalCon')
      ->set('date', (new \DateTime('yesterday'))->format(DateTimeItemInterface::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.

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 the setDescription() method requires setting the description and the text format simultaneously for security. The publish() and unpublish() methods make the code more readable than with a generic setPublished() 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\Entity\Attribute\ContentEntityType;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
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;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;

/**
* Defines the event entity class.
*/
#[ContentEntityType(
id: 'event',
label: new TranslatableMarkup('Event'),
entity_keys: [
    'id' => 'id',
    'uuid' => 'uuid',
    'label' => 'title',
    'owner' => 'author',
    'published' => 'published',
],
base_table: '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 'author' and 'published' from the trait.
        $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(DateTimeItemInterface::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(2);
    
    print 'Title: ' . $event->getTitle() . "\n\n";
    
    print 'Date value: ' . $event->getDate() . "\n";
    print 'Date (object): ' . var_export($event->getDate(), TRUE) . "\n\n";
    
    print 'Description: ' . $event->getDescription() . "\n\n";
    print 'Description (object): ' . var_export($event->getDescription(), TRUE) . "\n\n";
    
    print 'Published: ' . $event->isPublished() . "\n";
    print 'Published (boolean value): ' . var_export($event->isPublished(), TRUE) . "\n";

    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 = Event::load(2);
    
    $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(TRUE)
        ->save();

    Note that the values in the database have been updated accordingly.