Skip to content

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.

  • 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 the implements part of the class declaration of the Event class

  • Add , EntityChangedTrait to the use part inside of the Event class

  • Add the following to the baseFieldDefinitions() method in src/Entity/Event.php above the return 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.

  • 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 in src/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)
  • 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 the baseFieldDefinitions() method of the Event class before the semicolon:

    ->addConstraint('AttendeeCount')
    ->addConstraint('UniqueAttendees')
  • 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 the baseFieldDefinitions() method of the Event class before the semicolon:

    ->setClass(RemainingFieldItemList::class)
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\Entity\ContentEntityDeleteForm;
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\entity\EntityAccessControlHandler;
use Drupal\entity\EntityPermissionProvider;
use Drupal\entity\Menu\EntityCollectionLocalActionProvider;
use Drupal\entity\Routing\DefaultHtmlRouteProvider;
use Drupal\event\Controller\EventListBuilder;
use Drupal\event\Form\EventForm;
use Drupal\user\EntityOwnerInterface;
use Drupal\user\EntityOwnerTrait;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\views\EntityViewsData;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\user\UserInterface;
use Drupal\event\Field\RemainingFieldItemList;

/**
 * Defines the event entity class.
 */
#[ContentEntityType(
id: 'event',
label: new TranslatableMarkup('Event'),
label_collection: new TranslatableMarkup('Events'),
label_singular: new TranslatableMarkup('event'),
label_plural: new TranslatableMarkup('events'),
entity_keys: [
    'id' => 'id',
    'uuid' => 'uuid',
    'label' => 'title',
    'owner' => 'author',
    'published' => 'published',
],
handlers: [
    'access' => EntityAccessControlHandler::class,
    'permission_provider' => EntityPermissionProvider::class,
    'route_provider' => [
        'default' => DefaultHtmlRouteProvider::class,
    ],
    'form' => [
        'add' => EventForm::class,
        'edit' => EventForm::class,
        'delete' => ContentEntityDeleteForm::class,
    ],
    'list_builder' => EventListBuilder::class,
    'local_action_provider' => [
        'collection' => EntityCollectionLocalActionProvider::class,
    ],
    'views_data' => EntityViewsData::class,
],
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',
    'collection' => '/admin/content/events',
],
admin_permission: 'administer event',
base_table: 'event',
)]
class Event extends ContentEntityBase implements EntityOwnerInterface, EntityPublishedInterface, EntityChangedInterface {

    use EntityOwnerTrait, EntityPublishedTrait, EntityChangedTrait;

    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 'author' and 'published' from the trait.
        $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)
            ->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])
            ->addConstraint('AttendeeCount')
            ->addConstraint('UniqueAttendees');

        $fields['remaining'] = BaseFieldDefinition::create('integer')
            ->setLabel(t('Remaining number of attendees'))
            ->setComputed(TRUE)
            ->setDisplayOptions('view', [
                'label' => 'inline',
                'weight' => 30,
            ])
            ->setClass(RemainingFieldItemList::class);

        $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(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,
        ]);
    }

    /**
     * @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;
    }

}
  • Run drush entity:updates

  • Note that the {event__attendees} table was created and maximum and changed fields have been created in the {event} table.

Note that there is no remaining column. There is no path 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.