Drupal 8 API: entities

Pre-requisites

An entity is an object with an arbitrary list of properties, called fields

Entities have been in Drupal since 7, but in Drupal 8 they've become the de facto ORM, for storing and manipulating data "lumps", where each lump is identifiable and separate from the others (although it might reference them), and can have some complexity of internal structure. A particular blogpost, or news item, is an entity; each user is an entity; each file uploaded to Drupal becomes both the raw uploaded file (e.g. a PDF) and also an entity in the database. Entities allow metadata to be recorded about non-entity resources external to the database, like files, and also give a lifecycle to both the data and any external resources: the data can be retrieved later, updated and ultimately deleted.

Why did Drupal invent its own ORM, rather than using something like Doctrine? After all, Drupal already uses Doctrine's annotations, as we've seen previously, and Doctrine invented these specifically to provide metadata to (m)ap between PHP (o)bjects and a (r)elational database (hence ORM.) Well, this highlights the key difference between Drupal entities and objects: in traditional ORMs, an object declared in code has a hardwired set of properties, which map to columns in the database; in Drupal, entities of a given type can have both a shared, hardwired set of fields, but also a configurable set of fields, and even subsets of fields configured through bundles. In addition, entities can have a kind of fundamental type, meaning they can either be storing configuration data, or content data.

What fields an entity has, therefore, are not fixed like in a traditional ORM, but configurable in both code and database, based on different ways of subtyping entities: content versus config; node versus user; article versus event versus news item. These different subtypings might seem confusing, so here's a template to help illustrate where in the "subtyping" hierarchy different terminologies might lie:

A [real-world item] is a [fundamental type] [entity type] entity of bundle [bundle type], with base fields [some example fields] and configured fields [some example fields]

And here are some illustrative examples based on that template, assuming you might have configured some of the following in your website already:

  1. A blogpost is a content node entity of bundle blogpost, with base fields title, author, created datetime etc. and configured fields body and related tags.
  2. A news item is a content node entity of bundle news item, with base fields title, author, created datetime etc. and configured fields body, related news items, related files.
  3. A tag is a content taxonomy entity of bundle tags, with base fields name, description, parent term etc. and configured fields related tag.
  4. A menu link added in the UI is a content menu-link-content entity of bundle menu-link-content, with base fields title, link etc. but not usually any configured fields (although this is under discussion.)
  5. A user is a content user entity of bundle user, with base fields name, email address etc. and configured fields first name and last name.
  6. A block is a config entity, with arbitrary data.
  7. A block's content is a content block-content entity, with base fields name, email address etc. but not usually any configured fields.
  8. A field's configuration is a config entity, with arbitrary data including associated entity type and bundle.
  9. A view is a config entity, with arbitrary settings but as a minimum ID, label and other defined values.
  10. A contact settings is a config object, with arbitrary settings but including flood limiting and whether the personal contact form is enabled.

This is by no means an exhaustive list (as we'll see below, anyone can create a new entity type) but gives you some idea of what different subtypings mean.

Note from the above that config objects entities are not fieldable and have no bundles: they just store (effectively, but not quite) arbitrary structured data; we'll explain in a later tutorial how that's different from fields. In addition, users are fieldable "content" entities, but they only have one bundle, "user". A user's profile, on the other hand, can have different bundles.

Creating new entities of an existing type

Let's build a custom form, which creates a new article node (or maybe "a content node entity of type article"!) when submitted. This will illustrate the general principle of interacting with content entities of a given type (in this case, "node"):

  1. Obtain the entity manager, as a service, through dependency injection (previously discussed).
  2. Obtain (from the entity manager) the storage manager for content entities of type "node".
  3. Tell the "node" storage to perform tasks relating to nodes: creating (for later saving); retrieval by ID; discovery of many nodes by query.

Note that creating and saving are two separate tasks: the storage creates a node object, and then the object can be told to "save itself". Note also that entity queries can be instantiated via manager-service-via-storage, but can also be created by a query factory service. If you only need to e.g. find an entity ID based on certain conditions, you could inject the factory service into your code instead.

Here's a new custom form RandomNodeForm: save to a relevant place in your module (see previous discussion on naming conventions):

<?php
 
namespace Drupal\d8api\Form;
 
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
 
/**
 * Form: create a random node.
 */
class RandomNodeForm extends FormBase {
  /**
   * @var EntityManagerInterface
   *   Entity manager to create and save nodes.
   */
  protected $entityManager;
 
  /**
   * Implements __construct().
   */
  public function __construct(EntityManagerInterface $entity_manager) {
    $this->entityManager = $entity_manager;
  }
 
  /**
   * {@inheritDoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container->get('entity.manager'));
  }
 
  /**
   * {@inheritDoc}
   */
  public function getFormId() {
    return 'd8api_random_node_form';
  }
 
  /**
   * {@inheritDoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    // We'll use this storage potentially several times in the loop below.
    $node_storage = $this->entityManager->getStorage('node');
 
    // Get a list of the most recent node IDs, and generate links to them.
    $nids = $node_storage->getQuery()
      ->condition('status', 1)->condition('type', 'article')
      ->range(0, 3)->sort('created', 'DESC')->execute();
    if ($nids) {
      $markup = $this->t('Recent article nodes:');
 
      // Load a node for each ID returned from the query, and generate its link.
      foreach ($nids as $nid) {
        $node = $node_storage->load($nid);
        $markup .= ' ' . $node->toLink()->toString() . ',';
      }
 
      // Add markup with links to the top of the form.
      $form['recent_nodes'] = [
        '#type' => 'markup',
        '#markup' => '<p>' . trim($markup, ',') . '.</p>',
      ];
    }
 
    $form['create'] = [
      '#type' => 'submit',
      '#value' => $this->t('Create node'),
    ];
 
    return $form;
  }
 
  /**
   * {@inheritDoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // Some entity types refer to "bundles" differently: nodes call them "types".
    $bundle_key = $this->entityManager->getDefinition('node')->getKey('bundle');
 
    // Data for the node.
    $node_data = [
      'title' => uniqid(),
      // Body CAN be just a string, but this way we specify body format too.
      'body' => [
        'value' => uniqid(),
        'format' => 'full_html',
      ],
      $bundle_key => 'article',
    ];
 
    // Create the node from the data array, save it and redirect to it.
    $new_node = $this->entityManager->getStorage('node')->create($node_data);
    $new_node->save();
    drupal_set_message($this->t('Created new node.'));
    $form_state->setRedirectUrl($new_node->urlInfo('canonical'));
  }
 
}

This code demonstrates the following entity-related actions:

Retrieving entities
In the buildForm() above, we make an entity query, which permits us to attach conditions on what entity IDs to return: but only ever returns entityIDs. We must then use the entity manager to load each entity in turn, so we can ask the entity for a HTML link to itself.
Saving a new entity
Later, in the submitForm, we use the same entity manager to create a new node object using structured data populated with random text. The resulting object must be explicitly saved, for it to persist in the data store.
Linking to an entity
Finally, we ask the node for a link to itself. Each entity type (node, user etc.) has routes named by convention, as we'll see below. That means that asking a node for its "canonical" route here (a term we'll discuss below in the context of a custom entity type) means we don't have to ask the routing for the "entity.node.canonical" route, then replace wildcards with the node ID etc.

You can expose this new controller to the Drupal UI by adding the following to d8api.routing.yml:

d8api.create_random_node:
  path: '/tutorial/create-random-node'
  defaults:
    _title: 'Create article node with random title'
    _form: '\Drupal\d8api\Form\RandomNodeForm'
  requirements:
    _node_add_access: 'node'

This form-based routing has been discussed previously and so should be familiar. The only new key is the _node_add_access in requirements, which should be self-explanatory.

Creating a new entity type

Creating a new entity type is fairly straightforward: a new class in src/Entity/[TYPE].php, with a correctly formatted annotation comment. But we need more than that to be able to successfully create and manage entities of this new type:

  1. Along with the code and annotations defining the entity type...
  2. Routing and navigation permits access to (almost all existing) controllers which manage and list the entities.
  3. Some controllers must be extended to suit entities of the custom type.
  4. New permissions prevent unauthorized users from accessing those controllers.

Let's consider a new content entity type: contacts. For example, your website might want lightweight (i.e. non-node) entities to permit several pages to list a particular contact point: for example, when many pages need to have a link to your HR department, or to a particular person on a particular team. In future, when we discuss the field API, we'll define a difference between organizations and people using bundles, so they can have different fields on them; for now, let's treat all contact entities as having the same fields.

1. The entity type

The core of the entity type is the following class. Save it to src/Entity/Contact.php in the d8api module:

<?php
 
namespace Drupal\d8api\Entity;
 
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
 
/**
 * Defines the Contact content entity.
 *
 * @ContentEntityType(
 *   id = "contact",
 *   label = @Translation("Contact entity"),
 *   handlers = {
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
 *     "list_builder" = "Drupal\d8api\Controller\ContactListBuilder",
 *     "views_data" = "Drupal\views\EntityViewsData",
 *     "form" = {
 *       "add" = "Drupal\d8api\Form\ContactForm",
 *       "edit" = "Drupal\d8api\Form\ContactForm",
 *       "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
 *     },
 *     "access" = "Drupal\d8api\Access\ContactAccessControlHandler",
 *   },
 *   base_table = "contact",
 *   admin_permission = "administer contact entity",
 *   fieldable = TRUE,
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "name",
 *     "uuid" = "uuid",
 *   },
 *   links = {
 *     "canonical" = "/contact/{contact}",
 *     "edit-form" = "/contact/{contact}/edit",
 *     "delete-form" = "/contact/{contact}/delete",
 *     "collection" = "/contact/list"
 *   }
 * )
 */
class Contact extends ContentEntityBase implements ContentEntityInterface {
  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    $fields = parent::baseFieldDefinitions($entity_type);
 
    $fieldsConfig = [
      'id' => ['type' => 'integer', 'label' => 'ID', 'desc' => 'ID'],
      'uuid' => ['type' => 'uuid', 'label' => 'UUID', 'desc' => 'UUID'],
      'name' => ['type' => 'string', 'label' => 'Name', 'desc' => 'name'],
    ];
    foreach ($fieldsConfig as $fieldKey => $config) {
      $fields[$fieldKey] = BaseFieldDefinition::create($config['type'])
        ->setLabel(t($config['label']))
        ->setDescription(t("Contact " . $config['desc']));
    }
 
    $fields['id']->setReadOnly(TRUE);
    $fields['uuid']->setReadOnly(TRUE);
    $fields['name']->setSettings([
        'default_value' => '',
        'max_length' => 255,
        'text_processing' => 0,
      ])
      ->setDisplayOptions('view', ['label' => 'hidden', 'type' => 'string'])
      ->setDisplayOptions('form', ['type' => 'text_textfield'])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);
 
    return $fields;
  }
 
}

The Contact class can inherit so much from ContentEntityBase that there only remains two substantial blocks of code:

  1. The @ContentEntityType() annotation in the comment.
  2. The baseFieldDefinitions() method, defining fields common to all contact entities.

The @ContentEntityType() annotation

We've discussed annotations in a previous tutorial: structured metadata, stored in PHP comments. The structure of the annotation here is:

id, label, base_table and fieldable
These properties should be self-evident. Avoid changing the latter pair once you've got the entity set up: they determine structure in the database.
handlers
The core entity system can do a lot of the heavy lifting on not just page routes (we'll look at routing below) but also e.g. if you wanted to embed an "add contact" form fragment in situ on a node's edit form. To help it do this, we define handlers for different entity-related actions here. Again, core helps with base handlers, but we have to extend a form, a controller and an access handler as you can see (and as we'll also look at, below.)
admin_permission
We'll define permissions below for the granular "CRUD" actions one might perform against an entity, but in the annotation we can also designate an "override" permission, to permit full administration of contact entities, which can be either an existing permission or one we define.
entity_keys
The keys of core entity properties as they're stored in PHP, matched to how they're stored in the database. These core properties aren't usually translatable, and are often read-only (or at any rate write-once) like ID, UUID and (as we'll see in a later tutorial) relevant bundle. We'll expose label (the odd one out) as a fieldable, editable property in the PHP code below.
links
Although we provide routing below, it's difficult for Drupal to work out e.g. the "edit" or "view all" link for a given entity, back from the routing which could be provided anywhere. As a shortcut, the essential links for "what can I do with this current entity?" should be listed here. A "canonical" link is a kind of "default" link for the entity: if in doubt, use this one.

Taken as a whole, the annotation looks complicated; it breaks down fairly easily into the above chunks, though. We'll add a bit more to it in a later post, when we discuss fields and introduce bundles.

The baseFieldDefinitions() method

The fundamental properties of the entity, defined in the entity_keys part of the annotation, need to be made into base fields so that they can both be available in the entity edit form and (where appropriate) viewable on the entity view page.

You can see in the code that we first call the parent method, which is generally good practice. Afterwards, we configure three base fields: one for each of the entity properties. Finally, we mark two of these base fields as read-only (these are the ID fields which should never change) and give the third some clearer instructions on what "type" of data it is. As we'll see later when we discuss fields in more detail, the big difference between the simple data value and an actual field is this collection of metadata about how the field should be stored, edited and displayed.

2. Permissions

Once we have the basic type class set up above, we need to define a few entity permissions, and provide a custom class to manage access through those very permissions.

In the same way as we've configured routing, services and navigation previously, we need to create a YAML file at the top level of the d8api module. Call this d8api.permissions.yml and add the following:

add contact entity:
  title: 'Add a contact'
  description: 'Add a contact entity'
administer contact entity:
  title: 'Administer any contact'
  description: 'Administer any contact entity'
delete contact entity:
  title: 'Delete a contact'
  description: 'Delete a contact entity'
edit contact entity:
  title: 'Edit a contact'
  description: 'Edit a contact entity'
view contact entity:
  title: 'View a contact'
  description: 'View a contact entity'

This provides titles and descriptions for the permissions UI, keyed by a potentially shorter text string which serves as a "machine name" (discussed previously). This machine name is what we reference e.g. in the @ContentEntityType(admin_permission = "...") annotation above.

Also referenced in the annotation is an access control handler, which you should save to src/Access/ContactAccessControlHandler.php:

<?php
 
namespace Drupal\d8api\Access;
 
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
 
/**
 * Control access to a contact.
 *
 * (Largely copied from the comment access control handler.)
 *
 * @see \Drupal\d8api\Entity\Contact.
 */
class ContactAccessControlHandler extends EntityAccessControlHandler {
 
  /**
   * {@inheritdoc}
   */
  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
    switch ($operation) {
      case 'view':
      case 'edit':
      case 'delete':
        return AccessResult::allowedIfHasPermission($account, "$operation contact entity");
    }
    return AccessResult::allowed();
  }
 
  /**
   * {@inheritdoc}
   */
  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
    return AccessResult::allowedIfHasPermission($account, 'add contact entity');
  }
 
}

This is a very simple class which negotiates access requests for certain entity actions, and responds accordingly. For view, edit and delete we straightforwardly return the permission. However, because our create permission is actually called add contact entity, we need to stub out the checkCreateAccess() method to map that separately.

3. Routing and adding some navigation

To establish some basic routes we edit d8api.routing.yml as discussed before:

entity.contact.canonical:
  path: '/contact/{contact}'
  defaults:
    _entity_view: 'contact'
    _title: 'Contact'
  requirements:
    _entity_access: 'contact.view'
 
entity.contact.collection:
  path: '/contact/list'
  defaults:
    _entity_list: 'contact'
    _title: 'Contact list'
  requirements:
    _permission: 'view contact entry'
 
entity.contact.add_form:
  path: '/contact/add'
  defaults:
    _entity_form: 'contact.add'
    _title: 'Create contact'
  requirements:
    _entity_create_access: 'contact'
 
entity.contact.edit_form:
  path: '/contact/{contact}/edit'
  defaults:
    _entity_form: 'contact.edit'
    _title: 'Edit contact'
  requirements:
    _entity_access: 'contact.edit'
 
entity.contact.delete_form:
  path: '/contact/{contact}/delete'
  defaults:
    _entity_form: 'contact.delete'
    _title: 'Delete contact'
  requirements:
    _entity_access: 'contact.delete'

Here we have added routes for the four CRUD actions, plus a "collection" or listing route. There are new parameters, not discussed in routing before, like _entity_form and _entity_access: these are hopefully self-explanatory, and lets the routing system reference the controllers and forms defined already in the annotation rather than hardwiring the same classes twice.

Sometimes entities have complex routing, that might not be suited to hardwiring in a YAML file. In those situations, you can use a route provider class, which will need to be registered in the handlers annotation section as a route_provider. However, we don't cover that complexity here. (Note also that route providers and YAML can co-exist, so take care if you do decide to use both.)

To implement some simple navigation links in menus, we also add to the following, existing YAML files:

d8api.links.action.yml:

# Entity action links.
d8api.contact_add:
  route_name: entity.contact.add_form
  title: 'Add contact'
  appears_on:
    - system.admin_content
    - entity.contact.collection
    - entity.contact.canonical
d8api.contact_list:
  route_name: entity.contact.collection
  title: 'View contacts'
  appears_on:
    - system.admin_content
    - entity.contact.canonical

This adds action "shortcuts" on the admin/content page, and between the canonical and collection routes for contact entities.

d8api.links.task.yml:

# Entity tasks.
d8api.contact.view:
  route_name: entity.contact.canonical
  base_route: entity.contact.canonical
  title: 'View'
d8api.contact.edit_form:
  route_name: entity.contact.edit_form
  base_route: entity.contact.canonical
  title: 'Edit'
d8api.contact.delete_form:
  route_name: entity.contact.delete_form
  base_route: entity.contact.canonical
  title: 'Delete'

This adds a tab group of view/edit/delete for a given contact entity, permitting quick navigation between all three routes.

4. Controllers

Finally, we extend a couple of core controllers, to match the ones registered in the annotation above: for listing a collection of contacts; and a form for editing a contact. Core Drupal does provide us with basic classes for both of these, but they're not sufficient. The following two simple classes provide the extension we need:

src/Controller/ContactListBuilder.php:

<?php
 
namespace Drupal\d8api\Controller;
 
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;
use Drupal\Core\Url;
 
/**
 * Controller: list of contact entity.
 */
class ContactListBuilder extends EntityListBuilder {
 
  /**
   * {@inheritdoc}
   */
  public function buildHeader() {
    $header['id'] = $this->t('ID');
    $header['name'] = $this->t('Name');
    return $header + parent::buildHeader();
  }
 
  /**
   * {@inheritdoc}
   */
  public function buildRow(EntityInterface $entity) {
    $row['id'] = $entity->id();
    $row['name'] = $entity->link();
    return $row + parent::buildRow($entity);
  }
 
}

These two methods just add columns to the collection table, so we can identify each contact entity by its name (label). Otherwise, the table contains no columns (because every entity's properties are potentially different.)

src/Form/ContactForm.php:

<?php
 
namespace Drupal\d8api\Form;
 
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Form\FormStateInterface;
 
/**
 * Form controller: add/edit form for contacts.
 */
class ContactForm extends ContentEntityForm {
 
  /**
   * {@inheritdoc}
   */
  public function save(array $form, FormStateInterface $form_state) {
    // Save the entity, so it has an ID.
    parent::save($form, $form_state);
    // Redirect to the entity's own page.
    $form_state->setRedirect(
      'entity.contact.canonical', ['contact' => $this->entity->id()]
    );
  }
 
}

This one method redirects to the entity's canonical (i.e. view) route, when the entity form is saved. Otherwise, the form does not redirect.

What you should see

As previously discussed, you should have Drush installed, and always clear caches when registering new configuration like services, routes etc.

Creating a random node

Navigate to /tutorial/create-random-node:

Form to create a new node with a random title

You should see the form to create a new node with a random title. If you hit the "Create node" button, then you'll see something like this:

Random node, just created, on its view or canonical route

Because the title and body text are randomly generated, you'll obviously see a different title from this screenshot. But if you now return to the form to create a new node:

Returning to the form to create a new node, to see the most recent node listed

You should see that the title of that most recently created node is also present now, above the "Create node" button.

Creating an entity of the new custom type

You should invoke the following Drush command, to update the database with the custom entity:

drush updatedb --entity-updates

Because we've implemented lots of navigation above, you can now visit Drupal core's content administration page:

Admin/content page with new buttons for add and list contact

You should see two new "blue button" actions under the breadcrumbs, to add a new contact or view existing ones. Let's just quickly look at that view first:

Empty list of contacts

You can see that the view copes quite happily with zero items in it: the boilerplate text here is provided by Drupal core based on the label in the class annotation ("Contact entity").

We can navigate from this list to add a new contact; when you do so, you should see a form with a single text field:

Create a new contact

Fill this in and submit it, and you're taken to the new contact:

Contact just created, showing label/name as title of page

The contact's name is defined in the entity_keys as the entity's label, and is therefore used as the title of the entity's own page (in this case, "J-P").

Now, if you click on the action to view the list of all contacts:

List of contacts, now with newly created contact on it

The new contact is listed, with the name linked to its view page. The dropdown of actions permits editing and even deleting of the contact, which you should definitely test! As you can see, I've done so, because the numeric ID of the contact is 8 (meaning I've created and deleted seven other contacts during testing.)

If you can see all of the above, the congratulations! you have manipulated node content entities, and created entities of your own entirely custom entity type.

Further reading etc.

Add new comment