Skip to content

A quick introduction to Drupal Entities

Submitted by Andrej Galuf on 25.11.2015.

Drupal has always been known for its immense flexibility and exceptional adaptability. The CCK module's field system was a large step in the flexibility direction, but it had its limits. The fields were only really usable on content types (read: nodes), leading to additional modules which extended profiles, provided forms, a shopping cart, forums and more. But in Drupal 7, a new unified API, the entity system, was introduced that aimed to unify the various elements of the site - and it changed everything.

This unified API would now be responsible for accessing elements such as content types, users, taxonomies and comments. The first visible advantage of the entity system was that the fields could now easily be attached to any of them, further extending Drupal's flexibility. However, the entities proved to be a lot more than that. Suddenly, it wouldn't take a lot of code anymore to turn just about any custom table into a full-fledged entity, with all its advantages. With a couple of functions, entities would integrate with Views, Rules and similar complex modules, allowing for unmatched flexibility.

Since Drupal 7's launch, the entity system has been used for everything from field-based replacements for previously separate module systems (for example EntityForm as a replacement for Webfor to massive extensions that turned previously limited elements into world-class tools (for example Drupal Commerce. It's no wonder that Drupal 8 with its new object-oriented code is even more entity-oriented and introduces a large amount of previously contributed modules into the core.

I will be referencing entites a lot in the future, so it makes sense to look at them first. We will be taking a quick peek into the recent past (Drupal 7), dive into an entity system of Drupal 8 and learn how to best proceed if you want to use them today.

What is an entity (in Drupal)?

Entites were the Drupal's first serious step towards an object oriented future. Drupal had several elements such as nodes, taxonomies and users doing more or less the same thing a little differently. This ment that for instance Content Construction Kit module (CCK) allowed for the creation of fields on content types, but couldn't do the same for users. To simplify and unify the underlying code that would allow this, the entity API was introduced. In terms of object oriented development, think of them as an abstraction of functionality common between nodes, taxonomies and users, a base class that others extend.

However, an entity is more than that. Where previously (in Drupal 6), one could use nodes to represent 99% of the content, this presented a significant overhead and caused performance issues. If one were to not use nodes, one would need to write an implementation from scratch. Starting with Drupal 7, one could now build a new entity that would be custom tailored to the task at hand. This role only increased in Drupal 8, where entities are now used not just for content, but for configurations as well.

Creating a custom entity in Drupal 7

In order to create an entity in Drupal 7, we first needed a base table, defined by schema in - surprise! - hook_schema(). Once the base table was available, we would specify a hook_entity_info() that contained the entity name, base table, keys, settings and controllers. Depending on the complexity, custom controllers and callbacks could be specified, but Entity API module - which you needed anyway for EntityMetadataWrapper - provided reasonable defaults for simple entities. Unfortunately, even if the base table was already defined by hook_schema()), we still needed to use hook_entity_property_info() to specify properties of the entity, leading to separation of related code. The hook_entity_property_info() had a bit of a catch for beginners too - as long as one didn't define a single setter or getter callback, the code would assume defaults and they would all we available to modules such as views. However, as soon as a single callback was specified, we would only have access to the specified property. Not that big a deal, but it's a bit of a gotcha when you first face it and I for one hate magic in code.

Apart from this, not much was needed. The usual assortment of hook_menu() options would do, though you would generally extend the default Entity class to specify a custom URI.

Here's a quick look at bare minimum Drupal 7 entity code:

<?php

// This comes in MYMODULE.install

/**
 * Implements hook_schema
 */
function MYMODULE_schema() {
  $schema = array();

  $schema['hello_world'] = array(
    'description' => t('An entity containing hello world'),
    'fields' => array(
      'id' => array(
        'description' => 'Hello World ID',
        'type' => 'serial',
        'not null' => TRUE,
        'unsigned' => TRUE,
      ),
      'entity_id' => array(
        'type' => 'int',
        'description' => 'Related node id',
        'unsigned' => TRUE,
        'not null' => TRUE,
      ),
      'label' => array(
        'type' => 'varchar',
        'not null' => TRUE,
        'length' => 50,
        'default' => '',
      ),
      'value' => array(
        'type' => 'varchar',
        'not null' => FALSE,
        'length' => 255,
        'default' => NULL,
      ),
    ),
    'primary key' => array('id'),
    'foreign keys' => array(
      'node' => array(
        'table' => 'node',
        'columns' => array('entity_id' => 'nid'),
      ),
    ),
  );

  return $schema;
}

// This comes in MYMODULE.module

/**
 * Implements hook_entity_info().
 */
function MYMODULE_entity_info() {
  $info = array();

  $info['hello_word'] = array(
    'label' => t('Hello World'),
    'base table' => 'hello_world',
    'entity keys' => array(
      'id' => 'id',
      'label' => 'label',
    ),
    'module' => 'MYMODULE',
    // The following two classes are needed by EntityAPI module
    'entity class' => 'MyEntity',
    'controller class' => 'EntityAPIController',
    // And for views
    'views controller class' => 'EntityDefaultViewsController',
    // Needed for admin interface
    'access callback' => 'my_custom_access_callback',
    'uri callback' => 'entity_class_uri',
    'admin ui' => array(
      'path' => 'admin/structure/hello',
      'controller class' => 'EntityDefaultUIController',
    ),
    'plural label' => 'Hello World',
    // This is needed for fields
    'fieldable' => TRUE,
    'bundles' => array(
      'hello_world' => array(
        'label' => t('Hello World'),
        'admin' => array(
          'path' => 'admin/structure/hello',
        ),
      ),
    ),
  );

  return $info;
}


/**
 * Implements hook_entity_property_info().
 */
function MYMODULE_entity_property_info() {
  $info = array();

  $info['hello_world']['properties']['id'] = array(
    'type' => 'integer',
    'label' => t('Hello World ID'),
    'description' => t('The ID of the Hello World Entity'),
    'schema field' => 'id',
  );
  $info['hello_world']['properties']['entity_id'] = array(
    'type' => 'node',
    'label' => t('Node ID'),
    'description' => t('ID of the related node'),
    'schema field' => 'entity_id',
    'setter callback' => 'entity_property_verbatim_set',
    'getter callback' => 'entity_property_verbatim_get',
  );
  $info['hello_world']['properties']['label'] = array(
    'type' => 'text',
    'label' => t('Label'),
    'description' => t('Hello World Label'),
    'schema field' => 'label',
    'setter callback' => 'entity_property_verbatim_set',
    'getter callback' => 'entity_property_verbatim_get',
  );
  $info['hello_world']['properties']['value'] = array(
    'type' => 'text',
    'label' => t('Value'),
    'description' => t('Hello World Value'),
    'schema field' => 'value',
    'setter callback' => 'entity_property_verbatim_set',
    'getter callback' => 'entity_property_verbatim_get',
  );

  return $info;
}

As you can see, it's all really straightforward and not that much code, yet this is all it takes to have Rules, Views and Drupal's general entity tools fall in line and recognize your lowly table as one of their mighty entities. As soon as you have this, you can add fields to your entity, you can have views create queries and recognize relationships (in our case a node) and you can use EntityFieldQuery and EntityMetadataWrapper to set or get your properties or field values.

Entity system in Drupal 8

It's no wonder then that the importance of Entity system only grew in Drupal 8. Where in Drupal 7, the entities were little more than an afterthought and built upon after the CMS already launched, Drupal 8 is built all around them. Entities were further decoupled and split into two general subtypes - the content and the configuration entities. Don't worry, thanks to the awesome Symfony 2+ base and object oriented programming, you can easily add more. But much like the CMS itself, the entities can seem a little daunting at first.

Simple things first - hook_menu() is gone, replaced by a routing system split into a plethora of .yml files, where you can find menu links, tasks, actions. Entity is defined in a class that's usually located within the src/Entity subfolder of the module. hook_entity_info() is also gone, replaced by Doctrine's Annotations, which take over the definition of any meta information. hook_entity_property_info and hook_schema are replaced by a method baseFieldDefinitions, which automatically defines and sets the schema for your entity's base table, as well as any properties and validations you might set on entity's property.

Let's look at a quick example of how such an entity file might look like:

<?php

/**
 * @file
 * Contains \Drupal\hello\Entity\HelloEntity.
 */

namespace Drupal\hello\Entity;

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\hello\HelloEntityInterface;

/**
 * Defines the Hello entity.
 *
 * @ingroup hello
 *
 * @ContentEntityType(
 *   id = "hello_world",
 *   label = @Translation("Hello World"),
 *   base_table = "hello_world",
 *   handlers = {
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
 *   },
 *   admin_permission = "administer HelloEntity entity",
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "label",
 *     "uuid" = "uuid"
 *   },
 *   links = {
 *     "canonical" = "/admin/hello/{hello_world}",
 *     "edit-form" = "/admin/hello/{hello_world}/edit",
 *     "delete-form" = "/admin/hello/{hello_world}/delete"
 *   },
 *   field_ui_base_route = "hello.settings"
 * )
 */
class HelloEntity extends ContentEntityBase implements HelloEntityInterface {
  
  public function getLabel() {
    return $this->get('label')->value;
  }

  public function getValue() {
    return $this->get('value')->value;
  }

  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    $fields['id'] = BaseFieldDefinition::create('integer')
      ->setLabel(t('ID'))
      ->setDescription(t('The ID of the Hello entity.'))
      ->setReadOnly(TRUE);

    $fields['uuid'] = BaseFieldDefinition::create('uuid')
      ->setLabel(t('UUID'))
      ->setDescription(t('The UUID of the Hello entity.'))
      ->setReadOnly(TRUE);

    $fields['entity_id'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Node ID'))
      ->setDescription(t('The node related to this hello'))
      ->setSettings(array(
        'target_type' => 'node',
        'handler' => 'default',
      ))
      ->setDisplayOptions('form', array(
        'type' => 'entity_reference_autocomplete',
        'settings' => array(
          'match_operator' => 'CONTAINS',
          'size' => 60,
          'placeholder' => '',
        ),
        'weight' => 0,
      ))
      ->setRequired(TRUE);

    $fields['label'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Label'))
      ->setDescription(t('The Label of the hello entity'))
      ->setSetting('max_length', 50)
      ->setDisplayOptions('form', array(
        'type' => 'textfield',
        'settings' => array(
          'size' => 60,
        ),
        'weight' => 5,
      ))
      ->setRequired(TRUE);

    $fields['value'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Value'))
      ->setDescription(t('Value of the hello entity'))
      ->setSetting('max_length', 255)
      ->setDisplayOptions('form', array(
        'type' => 'textfield',
        'settings' => array(
          'size' => 60,
        ),
        'weight' => 5,
      ))
      ->setRequired(TRUE);


    return $fields;
  }

}

As you can see, this one file contains everything to declare and define the entity. When you activate the module, the Entity will be added to the list, any necessary tables are created (if we need them at all) and you may now query it with EntityQuery or load it from the database. It doesn't get any simpler than that.

Creating Drupal 8 entities in practice

Obviously, this isn't quite enough for any serious work. After all, we only have an object here that holds certain information, which may or may not be persisted somewhere. For CRUD, we will need much more, from various forms to pages, to actions. That's a lot of boilerplate code to create from scratch. Fortunately, both Drush and Drupal Console can help us with that. Just remember that knowing what they do is key to efficient development in Drupal.