A quick introduction to Drupal Entities

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 Webform) to massive extensions that turned previously limited elements into world-class tools (for example Drupal Commerce). It's no wonder that Drupal 8 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?

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 CCK allowed the creation of fields for 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. 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, one first needed a base table, defined by schema in - surprise! - hook_schema. Once the base table was available, one 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, one 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, one 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.

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 get 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 located within the src/Entity subfolder of the module. hook_entity_info is now a simple annotation of the class, as per Symfony 2 standards, and hook_entity_property_info is a method baseFieldDefinitions on the Entity's class. Wait a moment, that's all our hooks from Drupal 7 in that one class, except the hook_schema?!? Oh, it gets better. There is no hook_schema either, because baseFieldDefinitions automatically defines and sets the schema for your entity's base table. The only catch is that you need to either define the fields before you activate the module or write an update hook for it if you add or change the field afterwards.

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

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

  /**
   * [email protected]}
   */
  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. As soon as the class is (automatically) loaded by Drupal, the Entity is added to the list, a base table gets established in the database and you may now query it with EntityQuery or load it from the database. It doesn't get any simpler than that.

Drupal 8 entities in practice

Obviously, this isn't enough for any serious work. We need to create add / edit / delete forms, action links, view pages and more. That's a lot of files and a lot of work from scratch, although the power you get by this setup is immense - you can literally create just about anything. But for a simple entity the only purpose of which is to contain and possibly pass on some simple data, it seems like a direct database query would be faster. Right? Not necessarily.

Remember, this file structure has a purpose. For instance, back in Drupal 7, every path and their mother was an array in hook_menu. Finding a local task was a royal pain in the ass - in Drupal 8, every task, action, path has a specific place to be at, easy to find, easy to debug, clean to read. So let me give you a hint here: learn the Drupal 8 structure, so you'll know what you're doing, but after you do so, take a peek at Drupal Console. You don't need it, but you definitely want it. With it, you'll be able to create a module with an entity in a matter of seconds. After that, the imagination is yours.

Tags
Comments

Add new comment

Restricted HTML

  • Allowed HTML tags: <em> <strong> <code> <span>
  • Lines and paragraphs break automatically.