Creating your own entities with Entity API

Error message

The spam filter installed on this site is currently unavailable. Per site policy, we are unable to accept new submissions until that problem is resolved. Please try resubmitting the form in a couple of minutes.

There have been plenty of articles about Drupal 7 and entities already. Entities are great. They provide a unified way to work with different data units in Drupal. Drupal 7 is all about entities. They are everywhere: nodes, users, taxonomy terms, vocabularies...

But how, as developers, can we create our own entities? When do we really need to do that? I think these questions are really very project-specific. We can probably use nodes for nearly everything. But when it comes to performance-sensitive projects, nodes should really only be used for content, and we should separate as much as possible from nodes. Why? Nodes are revisioned, they fire a lot of hooks, and they have functionality that we likely won't need. Also if we know exactly what fields we should have in our entities, we can create our own custom entities to avoid all those joins of Field API tables.

There are plenty of examples in Drupal core of how to create entities. To create entities, you should really write a lot of code. But there is also, of course, a faster way. You can use Entity API to simplify the code structure. With Entity API we can really move a lot of code to Entity Controller using standard Entity API functions and, in this way, keep our main module clean and nice.

Entity API provides:

  • More functions to work with entities. Drupal core provides functions to work with entities: entity_load(), entity_label(), entity_extract_ids(), entity_uri(), entity_get_info(). Core doesn't have any functions to save or delete entities, but this is where Entity API helps us. It provides: entity_id(), entity_export(), entity_import(), entity_save(), entity_create(), entity_delete(), entity_view(), entity_access(), entity_get_property_info(). This allows us to move more logic for all operations to the Controller Class (we will talk about this bit later).
  • Administration UI for exportable entities. This is a really great feature that allows us to create UIs for exportable entities easily. This is needed when we have bundles as entities. The administrative interface is very similar to the content types overview page for nodes.
  • Metadata wrappers. This allows us nicely get/set values of properties and fields of entities. Also we can get/set data of related entities... like geting the value of a profile field for the author of the node. No more LANGUAGE_NONE everywhere in the code!
  • Integration with Views and Rules. Integration with Rules is done by default when we use the Entity API Controller class, so we can hook in during creating/editing/deleting entities. By default Views can create a view using properties of our Entities, but they are very generic. After we describe the properties to Entity API (this is also needed for wrappers), Views will understand much more. Then we will be able to build different kinds of relationships, exposed filters, etc.
  • Provide entity tokens (entity_token module).

Administration UI screenshot

Every entity has its own Controller. This class is responsible for all operations on this particular entity (create, edit, delete, view, etc). This is very convenient as it can be easily reused by our custom entities, thanks to inheritance. It's also worth mentioning that with Entity API, our entities are "first class" objects and not objects of stdClass.

Lets take a look at a practical implementation of an entity to sample the functionality.

Practical example

Our case is quite abstract: we create two entities, example_task and example_task_type. Task has the properties: uid (author of the task), type, created, changed, title, and description. Task type is simply information about bundles of the task, so it has the fields: id, type, label, and description. Other fields of the Task type are added by Entity API to make this entity exportable (module and status properties).

First of all, (as in case with using only core functionality) in order to implement custom entities we should describe their base tables in hook_schema(). I will avoid listing of all that code here.

Then we should implement hook_entity_info() to declare our entities:

<?php
/**
 * Implements hook_entity_info().
 */
function example_task_entity_info() {
 
$return = array(
   
'example_task' => array(
     
'label' => t('Task'),
     
'entity class' => 'ExampleTask',
     
'controller class' => 'ExampleTaskController',
     
'base table' => 'example_task',
     
'fieldable' => TRUE,
     
'entity keys' => array(
       
'id' => 'tkid',
       
'bundle' => 'type',
      ),
     
'bundle keys' => array(
       
'bundle' => 'type',
      ),
     
'bundles' => array(),
     
'load hook' => 'example_task_load',
     
'view modes' => array(
       
'full' => array(
         
'label' => t('Default'),
         
'custom settings' => FALSE,
        ),
      ),
     
'label callback' => 'entity_class_label',
     
'uri callback' => 'entity_class_uri',
     
'module' => 'example_task',
     
'access callback' => 'example_task_access',
    ),
  );
 
$return['example_task_type'] = array(
   
'label' => t('Task Type'),
   
'entity class' => 'ExampleTaskType',
   
'controller class' => 'ExampleTaskTypeController',
   
'base table' => 'example_task_type',
   
'fieldable' => FALSE,
   
'bundle of' => 'example_task',
   
'exportable' => TRUE,
   
'entity keys' => array(
     
'id' => 'id',
     
'name' => 'type',
     
'label' => 'label',
    ),
   
'module' => 'example_task',
   
// Enable the entity API's admin UI.
   
'admin ui' => array(
     
'path' => 'admin/structure/task-types',
     
'file' => 'example_task.admin.inc',
     
'controller class' => 'ExampleTaskTypeUIController',
    ),
   
'access callback' => 'example_task_type_access',
  );

  return
$return;
}
?>

I won't describe every option in hook_entity_info() here as there is a lot of documentation and articles about it already (see the list of references in the bottom of this article).

What is specific to Entity API:

  • we define 'entity class'. It is self explanatory.
  • 'label callback' => 'entity_class_label'. This is a standard Entity API label callback that takes a look at our entity class method defaultLabel()
  • 'uri callback' => 'entity_class_uri'. This is also a standard Entity API callback for uri. It executes our entity defaultUri() method
  • 'exportable' key for example_task_type. In our example we set task types to be exportable.
  • 'admin ui' for example_task_type. This is where we define our Administration UI for task types.

The next step in our implementation is to let Drupal understand that entities of example_task_type are bundles for example_task. This should be done in following way:

<?php
/**
 * Implements hook_entity_info_alter().
 */
function example_task_entity_info_alter(&$entity_info) {
  foreach (
example_task_types() as $type => $info) {
   
$entity_info['example_task']['bundles'][$type] = array(
     
'label' => $info->label,
     
'admin' => array(
       
'path' => 'admin/structure/task-types/manage/%example_task_type',
       
'real path' => 'admin/structure/task-types/manage/' . $type,
       
'bundle argument' => 4,
      ),
    );
  }
}
?>

Next we should take care about pages where we add, view, and edit our example tasks. To do this we define various items in hook_menu():

<?php
/**
 * Implements hook_menu().
 */
function example_task_menu() {
 
$items = array();

 
$items['task/add'] = array(
   
'title' => 'Add task',
   
'page callback' => 'example_task_admin_add_page',
   
'access arguments' => array('administer example_task entities'),
   
'file' => 'example_task.admin.inc',
   
'type' => MENU_LOCAL_ACTION,
   
'tab_parent' => 'task',
   
'tab_root' => 'task',
  );

 
$task_uri = 'task/%example_task';
 
$task_uri_argument_position = 1;

 
$items[$task_uri] = array(
   
'title callback' => 'entity_label',
   
'title arguments' => array('example_task', $task_uri_argument_position),
   
'page callback' => 'example_task_view',
   
'page arguments' => array($task_uri_argument_position),
   
'access callback' => 'entity_access',
   
'access arguments' => array('view', 'example_task', $task_uri_argument_position),
   
'file' => 'example_task.pages.inc',
  );

 
$items[$task_uri . '/view'] = array(
   
'title' => 'View',
   
'type' => MENU_DEFAULT_LOCAL_TASK,
   
'weight' => -10,
  );

 
$items[$task_uri . '/delete'] = array(
   
'title' => 'Delete task',
   
'title callback' => 'example_task_label',
   
'title arguments' => array($task_uri_argument_position),
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('example_task_delete_form', $task_uri_argument_position),
   
'access callback' => 'entity_access',
   
'access arguments' => array('edit', 'example_task', $task_uri_argument_position),
   
'file' => 'example_task.admin.inc',
  );

 
$items[$task_uri . '/edit'] = array(
   
'title' => 'Edit',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('example_task_form', $task_uri_argument_position),
   
'access callback' => 'entity_access',
   
'access arguments' => array('edit', 'example_task', $task_uri_argument_position),
   
'file' => 'example_task.admin.inc',
   
'type' => MENU_LOCAL_TASK,
   
'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
  );

  foreach (
example_task_types() as $type => $info) {
   
$items['task/add/' . $type] = array(
     
'title' => 'Add task',
     
'page callback' => 'example_task_add',
     
'page arguments' => array(2),
     
'access callback' => 'entity_access',
     
'access arguments' => array('create', 'example_task', $type),
     
'file' => 'example_task.admin.inc',
    );
  }

 
$items['admin/structure/task-types/%example_task_type/delete'] = array(
   
'title' => 'Delete',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('example_task_type_form_delete_confirm', 4),
   
'access arguments' => array('administer example_task types'),
   
'weight' => 1,
   
'type' => MENU_NORMAL_ITEM,
   
'file' => 'example_task.admin.inc',
  );

  return
$items;
}
?>

Of note in the hook_menu() implementation is that we use entity_access for most of the access callbacks. The purpose of this function is to check for an access callback defined by the entity ('access callback' property) and execute it. So our access callback for tasks looks like this:

<?php
/**
 * Access callback for Task.
 */
function example_task_access($op, $task, $account = NULL, $entity_type = NULL) {
  global
$user;

  if (!isset(
$account)) {
   
$account = $user;
  }
  switch (
$op) {
    case
'create':
      return
user_access('administer example_task entities', $account)
          ||
user_access('create example_task entities', $account);
    case
'view':
      return
user_access('administer example_task entities', $account)
          ||
user_access('view example_task entities', $account);
    case
'edit':
      return
user_access('administer example_task entities')
          ||
user_access('edit any example_task entities')
          || (
user_access('edit own example_task entities') && ($task->uid == $account->uid));
  }
}
?>

And of course our hook_permissions() for defining our permissions

<?php
/**
 * Implements hook_permission().
 */
function example_task_permission() {
 
$permissions = array(
   
'administer example_task types' => array(
     
'title' => t('Administer task types'),
     
'description' => t('Allows users to configure task types and their fields.'),
     
'restrict access' => TRUE,
    ),
   
'create example_task entities' => array(
     
'title' => t('Create tasks'),
     
'description' => t('Allows users to create tasks.'),
     
'restrict access' => TRUE,
    ),
   
'view example_task entities' => array(
     
'title' => t('View tasks'),
     
'description' => t('Allows users to view tasks.'),
     
'restrict access' => TRUE,
    ),
   
'edit any example_task entities' => array(
     
'title' => t('Edit any tasks'),
     
'description' => t('Allows users to edit any tasks.'),
     
'restrict access' => TRUE,
    ),
   
'edit own example_task entities' => array(
     
'title' => t('Edit own tasks'),
     
'description' => t('Allows users to edit own tasks.'),
     
'restrict access' => TRUE,
    ),
  );

  return
$permissions;
}
?>

Now let's take a look at the most interesting part – controllers. The Task entity class looks like this:

<?php
/**
 * Task class.
 */
class ExampleTask extends Entity {
  protected function
defaultLabel() {
    return
$this->title;
  }

  protected function
defaultUri() {
    return array(
'path' => 'task/' . $this->identifier());
  }
}
?>

We only define our label for the entity and what path it should have.

Now Controller class:

<?php
class ExampleTaskController extends EntityAPIController {

  public function
create(array $values = array()) {
    global
$user;
   
$values += array(
     
'title' => '',
     
'description' => '',
     
'created' => REQUEST_TIME,
     
'changed' => REQUEST_TIME,
     
'uid' => $user->uid,
    );
    return
parent::create($values);
  }

  public function
buildContent($entity, $view_mode = 'full', $langcode = NULL, $content = array()) {
   
$wrapper = entity_metadata_wrapper('example_task', $entity);
   
$content['author'] = array('#markup' => t('Created by: !author', array('!author' => $wrapper->uid->name->value(array('sanitize' => TRUE)))));

   
// Make Description and Status themed like default fields.
   
$content['description'] = array(
     
'#theme' => 'field',
     
'#weight' => 0,
     
'#title' =>t('Description'),
     
'#access' => TRUE,
     
'#label_display' => 'above',
     
'#view_mode' => 'full',
     
'#language' => LANGUAGE_NONE,
     
'#field_name' => 'field_fake_description',
     
'#field_type' => 'text',
     
'#entity_type' => 'example_task',
     
'#bundle' => $entity->type,
     
'#items' => array(array('value' => $entity->description)),
     
'#formatter' => 'text_default',
     
0 => array('#markup' => check_plain($entity->description))
    );

    return
parent::buildContent($entity, $view_mode, $langcode, $content);
  }
}
?>

Please pay attention to the fact that we inherit our Controller class from EntityAPIController class that is provided by Entity API.

The create() method is responsible for the default values of entity properties when creating a new object. It is imperative that our entity has values when saving the object. In order to create an 'empty' entity we use the entity_create function.

buildContent() is needed for us to have our Description field displayed on the view page for the entity. We show this field using a theming function from the Field API, so the behaviour will be the same as other fields added with fields. Also you can see usage of a wrapper here, but lets come back to it a bit later.

Controller and entity class for example task type look different:

<?php
/**
 * Task Type class.
 */
class ExampleTaskType extends Entity {
  public
$type;
  public
$label;
  public
$weight = 0;

  public function
__construct($values = array()) {
   
parent::__construct($values, 'example_task_type');
  }

  function
isLocked() {
    return isset(
$this->status) && empty($this->is_new) && (($this->status & ENTITY_IN_CODE) || ($this->status & ENTITY_FIXED));
  }
}

 class
ExampleTaskTypeController extends EntityAPIControllerExportable {
   public function
create(array $values = array()) {
   
$values += array(
     
'label' => '',
     
'description' => '',
    );
    return
parent::create($values);
  }

 
/**
   * Save Task Type.
   */
 
public function save($entity, DatabaseTransaction $transaction = NULL) {
   
parent::save($entity, $transaction);
   
// Rebuild menu registry. We do not call menu_rebuild directly, but set
    // variable that indicates rebuild in the end.
    // @see _<a href="http://drupal.org/node/1399618
">http://drupal.org/node/1399618
</a>    variable_set('menu_rebuild_needed', TRUE);
  }
}
?>

Please pay attention to the fact that we inherit our Controller class from EntityAPIControllerExportable class that is provided by Entity API. This is needed to make our entity exportable.

For entity class we define the method isLocked(). This is needed to understand whether this entity is locked so it is not possible to edit/delete it.

In the Controller class we also define the create() method for an empty entity and in the save() method we should call field_attach_create_bundle() to let Fields API know that a new bundle is created. Of course, we also rebuild the menu to refresh our newly created bundle admin paths.

Now we need to define our forms for saving/deleting entities. You can find them in attached module. There is nothing non standard compared to not using Entity API there so let's skip this part (there is plenty of code there as well).

Instead lets take a closer look at usage of other standard Entity API functions in our module.

What we are missing is an API to work with our entities. Here it is:

<?php
/**
 * Load a task.
 */
function example_task_load($tkid, $reset = FALSE) {
 
$tasks = example_task_load_multiple(array($tkid), array(), $reset);
  return
reset($tasks);
}

/**
 * Load multiple tasks based on certain conditions.
 */
function example_task_load_multiple($tkids = array(), $conditions = array(), $reset = FALSE) {
  return
entity_load('example_task', $tkids, $conditions, $reset);
}

/**
 * Save task.
 */
function example_task_save($task) {
 
entity_save('example_task', $task);
}

/**
 * Delete single task.
 */
function example_task_delete($task) {
 
entity_delete('example_task', entity_id('example_task' ,$task));
}

/**
 * Delete multiple tasks.
 */
function example_task_delete_multiple($task_ids) {
 
entity_delete_multiple('example_task', $task_ids);
}
?>

As you can see, these are simply wrappers around Entity API functions. This is a great benefit of using Entity API as we make our API very simple and transparent.

The same applies for our Task type entity:

<?php
/**
 * Load task Type.
 */
function example_task_type_load($task_type) {
  return
example_task_types($task_type);
}

/**
 * List of task Types.
 */
function example_task_types($type_name = NULL) {
 
$types = entity_load_multiple_by_name('example_task_type', isset($type_name) ? array($type_name) : FALSE);
  return isset(
$type_name) ? reset($types) : $types;
}

/**
 * Save task type entity.
 */
function example_task_type_save($task_type) {
 
entity_save('example_task_type', $task_type);
}

/**
 * Delete single case type.
 */
function example_task_type_delete($task_type) {
 
entity_delete('example_task_type', entity_id('example_task_type' ,$task_type));
}

/**
 * Delete multiple case types.
 */
function example_task_type_delete_multiple($task_type_ids) {
 
entity_delete_multiple('example_task_type', $task_type_ids);
}
?>

I hope now you start to have a feeling how convenient it is to use standard Entity API functions to build your own entities.

Now let's take a look at metadata wrappers which simplify getting and setting values of entity properties/fields. E.g.

<?php
// Create wrapper around the node.
$wrapper = entity_metadata_wrapper('node', $node);

// We can do it also using only $nid.
$wrapper = entity_metadata_wrapper('node', $nid);

// Get the value of field_name of the nodes author's profile.
$wrapper->author->profile->field_name->value();
$wrapper->author->profile->field_name->set('New name');

// Value of the node's summary in german language.
$wrapper->language('de')->body->summary->value();

// Check whether we can edit node's author email address.
$wrapper->author->mail->access('edit') ? TRUE : FALSE;

// Get roles of node's author.
$wrapper->author->roles->optionsList();

// Set description of the node's first file in field field_files.
$wrapper->field_files[0]->description = 'The first file';
$wrapper->save();

// Get node object.
$node = $wrapper->value();
?>

The entity definition informs meta data wrappers about the raw data structure(base table definition), but this doesn't describe the context of a field's use. For example, the 'uid' field from our task entity is a simple integer and Drupal doesn't know that it is a reference to a user object. In order to create this link we use following code:

<?php
/**
 * Implements hook_entity_property_info_alter().
 */
function example_task_entity_property_info_alter(&$info) {
 
$properties = &$info['example_task']['properties'];
 
$properties['created'] = array(
   
'label' => t("Date created"),
   
'type' => 'date',
   
'description' => t("The date the node was posted."),
   
'setter callback' => 'entity_property_verbatim_set',
   
'setter permission' => 'administer nodes',
   
'schema field' => 'created',
  );
 
$properties['changed'] = array(
   
'label' => t("Date changed"),
   
'type' => 'date',
   
'schema field' => 'changed',
   
'description' => t("The date the node was most recently updated."),
  );
 
$properties['uid'] = array(
   
'label' => t("Author"),
   
'type' => 'user',
   
'description' => t("The author of the task."),
   
'setter callback' => 'entity_property_verbatim_set',
   
'setter permission' => 'administer example_task entities',
   
'required' => TRUE,
   
'schema field' => 'uid',
  );
}
?>

We also define 'date' type for our 'created' and 'changed' fields. Now we can use the following way to retrieve the name of our entity author:

<?php
$wrapper
= entity_metadata_wrapper('example_task', $entity);
$wrapper->uid->name->value(array('sanitize' => TRUE));
?>

We can also build views relationship to our entity author. See the screenshot below.


Revisions

At the moment entity revisions are not supported by Entity API. There are steps being made to include this functionality in issue http://drupal.org/node/996696, so your help to move this issue forward is very welcome.

Other examples and documentation

I hope it has become clearer how to simplify creating entities using Entity API. There are a lot of modules that leverage the EntityAPI already like Profile2 and DrupalCommerce.

I would like to thank Wolfgang Ziegler (fago) a lot for this great module and all his work.

References to great resources about Entities and Entity API module: