Developing A Custom Drupal Entity

Chauncey Thorn, Senior Developer
#Drupal | Posted

The introduction of entities in Drupal 7 gave developers nodes, users, taxonomy term and comments. Many developers wonder when it's appropriate to create a custom entity? The answer is, it depends on the project. However, a good example is when a developer needs to expose custom tables and needs to maintain core functionality.  So, the real question is, how as developers, can we create our own custom entities if required? Drupal core provides some functions to work with entities. However, the entity API provides a number of very useful functions:

  • entity_create()
  • entity_save()
  • entity_delete()
  • entity_load()
  • entity_id()
  • entity_export()
  • entity_import()
  • entity_view()
  • entity_access()
  • entity_get_property_info()

Plus every custom entity you create will have it's own Controller that's responsible for all operations on this particular entity (create, edit, delete, view, etc).

I've attempted to create a practical implementation of an entity that allows you to see the resulting functionality (CRUD, search, Bulk delete and tests). The entity was developed around a dataset provided here. I've included a drush script that imports the legislators.csv included in the github repo. Also, I've provided an example CasperJS behavioral test and a phpunit test.

I've tried to format this blog post as a mini how-to guide with code available as an example. So let's begins.

The code for this entity is here: https://github.com/chaunceyt/lawmakers

Let's begin by implementing our hook_entity_info(). This will declare our entity and inform Drupal about the custom entity. I've bolded the directives that are highlighted in this post:

  1. /**
  2.  * Implements hook_entity_info().
  3.  */
  4. function lawmakers_entity_info() {
  5. $lawmakers_entity_info['lawmakers'] = array(
  6. 'label' => t('Lawmakers'),
  7. 'label callback' => 'lawmakers_label_callback',
  8. 'entity class' => 'Lawmakers',
  9. 'controller class' => 'LawmakersController',
  10. 'base table' => 'lawmakers',
  11. 'uri callback' => 'lawmakers_uri',
  12. 'fieldable' => TRUE,
  13. 'entity keys' => array(
  14. 'id' => 'lawmakers_id'
  15. ),
  16. 'uri callback' => 'entity_class_uri',
  17. 'load hook' => 'lawmakers_load',
  18. 'static cache' => TRUE,
  19. 'admin ui' => array(
  20. 'path' => 'admin/content/lawmakers',
  21. 'controller class' => 'LawmakersUIController',
  22. 'file' => 'includes/lawmakers.admin.inc',
  23. ),
  24. 'module' => 'lawmakers',
  25. 'access callback' => 'lawmakers_access_callback',
  26. 'bundles' => array(
  27. 'lawmakers' => array(
  28. 'label' => 'Lawmakers',
  29. 'admin' => array(
  30. 'path' => 'admin/structure/lawmakers/manage',
  31. 'access arguments' => array('administer lawmakers'),
  32. ),
  33. ),
  34. ),
  35. 'views controller class' => 'EntityDefaultViewsController',
  36. );
  37.  
  38. return $lawmakers_entity_info;
  39. }

Directive 'label callback':

'lawmakers_label_callback'. This is a standard Entity API label callback that takes a look at our entity class method defaultLabel().

  1. function lawmakers_label_callback($lawmakers, $type) {
  2. return empty($lawmakers->username) ? 'Untitled Lawmaker' : $lawmakers->username;
  3. }

Directive 'entity class' :

'Lawmakers'. Here we will "extends" the Entity class setting our own defaultUri path.

  1. class Lawmakers extends Entity {
  2. protected function defaultUri() {
  3. return array('path' => 'lawmakers/' . $this->identifier());
  4. }
  5. }

Directive 'controller class':

'LawmakersController'. Here we will override our save() method to deal with the created/changed timestamp.

  1. class LawmakersController extends EntityAPIController {
  2. /**
  3.   * Override the save method.
  4.   */
  5.  
  6. public function save($entity, DatabaseTransaction $transaction = NULL) {
  7. if (isset($entity->is_new)) {
  8. $entity->created = REQUEST_TIME;
  9. }
  10. $entity->changed = REQUEST_TIME;
  11. return parent::save($entity, $transaction);
  12. }
  13. }

Directive 'base table':

'lawmakers'. Here we will define our custom table. See lawmakers.install hook_schema().

Directive 'load hook' => 'lawmakers_load'.

Here we will create our own "node_load()" hook. Note the lawmakers_load is just a wrapper for our lawmakers_load_multiple()

  1. function lawmakers_load($lawmakers_id, $reset = FALSE) {
  2. $lawmakers = lawmakers_load_multiple(array($lawmakers_id), array(), $reset);
  3. return reset($lawmakers);
  4. }

Directive 'admin ui': 'controller class':

'LawmakersUIController' Here we will extends the EntityDefaultUIController which provides a controller for building an entity overview form.

Directive 'access callback' => 'lawmakers_access_callback'

Here we will create a custom function returning TRUE if the user has access rights to this menu item, and FALSE if not.

  1. function lawmakers_access_callback() {
  2. if (user_is_anonymous() && !user_access('administer lawmakers entities')) {
  3. return FALSE;
  4. }
  5. else {
  6. return TRUE;
  7. }
  8. }

The screenshot below displays the pagination, basic search input options, and bulk operations functionality provided.

screen shot of the admin content list

You can check out the github repo for the complete entity example. Check back later for part two of my blog series. I will explain how to write CasperJS behavioral test for your entities.  In the meantime, enjoy writing custom entities!

 

Chauncey Thorn

Senior Developer