Build Your Own Custom Entities in Drupal – Implementation

Daniel Sipos
This entry is part 2 of 2 in the series Build Your Own Custom Entities in Drupal

Build Your Own Custom Entities in Drupal

Welcome back to the second part of this tutorial in which we explore the world of custom entities in Drupal. If you haven’t already, I strongly recommend you read the first installment, but let’s do a short recap nonetheless.

In the previous article we’ve defined the schema for our entity type and registered it with Drupal. We’ve also overridden the EntityAPIController to build up the display for our entities.

In this part of the tutorial we will continue and talk about a few other cool things we can do with entities in Drupal. First, we’ll quickly set up the pages where we can display the individual project entities. Next, we will build a straightforward but very powerful admin interface to manage them. Then, we will make our entity type fieldable so we can add fields through the UI. And finally, we’ll expose it to Views so we can create proper listings of project entities.

If you want, you can follow along with the source code from the first branch of the Git repository, or take a peek into the second branch which contains all the code we will cover today.

Individual entity pages

The first thing we’ll do is create the pages for displaying individual project entities. We’ll start by adding a new item to our hook_menu() implementation:

  $items['project/%'] = array(
    'title' => 'Project',
    'page callback' => 'demo_view_project',
    'page arguments' => array(1),
    'access arguments' => array('access content'),
  );

We are registering a path (project/id) and a callback function (demo_view_project()) to which we pass the wildcard URL argument (the ID of the project). As for access, anybody with the access content permission can see the page.

Next, let’s write the said callback function (keep in mind this is a simple example just for demonstration purposes):

/**
 * Callback function for displaying the individual project page
 */
function demo_view_project($id) {

  $projects = entity_load('project', array($id));
  $project = $projects[$id];

  drupal_set_title($project->name);
  $output = entity_view('project', array($project));

  return $output;

}

This is again very simple: we load the entity with the ID passed from the URL, we set the title of the page, run the entity object through entity_view() and return it as page output. We’ve covered these Entity API concepts last time when we listed our projects. You can now clear the cache and navigate to project/1 and you should see the project with the ID of 1. If you see the project name twice, don’t worry, this will become clear in the next section when we let Drupal know which one is the default URI callback for the project entities.

Admin user interface

Now that we can display the individual entities, let’s leverage the power of the Entity API module to set up a quick admin user interface to manage them. There are a few simple steps we need to take for this.

First, let’s edit our hook_entity_info() implementation for our entity type and add the following (I’ll explain everything after):

...

'access callback' => 'demo_access_callback',
'uri callback' => 'entity_class_uri',
'admin ui' => array(
  'path' => 'admin/projects',
  'controller class' => 'EntityDefaultUIController',
),

...

And replace this line:

'entity class' => 'Entity',

With this:

'entity class' => 'ProjectEntity', 

With these modification, we do 4 things:

  1. We specify an access callback function for the entity type. We’ll need this for the admin UI and we’ll declare the callback function in a minute.
  2. We set the uri callback to the default one provided by the entity class (I will come back to this at point 4).
  3. We set the admin ui information: path to the UI page and the controller class that will handle it. EntityDefaultUIController is the default UI class that comes with Entity API and it is declared in the entity.ui.inc file.
  4. We change the name of the entity class for this entity type to one that does not exist yet. We will create it now by extending the previous one so that we can override its defaultUri() method:

    /**
     * Project entity class extending the Entity class
     */
    class ProjectEntity extends Entity {
    
      /**
       * Change the default URI from default/id to project/id
       */
      protected function defaultUri() {
        return array('path' => 'project/' . $this->identifier());
      }
    
    }
    

As you can see, we are basically changing that path to the individual project entity returned by this class method. When the time comes, I will point out why this was handy but this will be the default one I mentioned at point 2. Now let’s quickly also declare our access callback function mentioned at point 1:

/**
 * Access callback for project entities.
 */
function demo_access_callback($op, $project = NULL, $account = NULL) {
  if ($op == 'view' || $op == 'update' || $op == 'create' || $op == 'delete') {
    return TRUE;
  }
  else {
    return FALSE;
  }
}

As you can see, this is not much of an access callback function as it returns true for everything. Here you will normally perform proper access checks but for our demonstration purposes it works just fine.

Now there is one last thing we need to do to make use of our admin interface: declare a simple add/edit form for the project entity and its submit handler:

/**
 * Form definition for adding / editing a project.
 */
function project_form($form, &$form_state, $project = NULL) {

  $form['name'] = array(
    '#title' => t('Project name'),
    '#type' => 'textfield',
    '#default_value' => isset($project->name) ? $project->name : '',
    '#required' => TRUE,
  );

  $form['description'] = array(
    '#title' => t('Project description'),
    '#type' => 'textarea',
    '#default_value' => isset($project->description) ? $project->description : '',
    '#required' => TRUE,
  );

  $form['deadline'] = array(
    '#title' => t('Project deadline'),
    '#type' => 'textfield',
    '#default_value' => isset($project->deadline) ? $project->deadline : '',
    '#required' => TRUE,
  );  

  $form['submit'] = array(
    '#type' => 'submit', 
    '#value' => isset($project->id) ? t('Update project') : t('Save project'),
    '#weight' => 50,
  );

  return $form;
}

/**
 * Submit handler for the project add/edit form.
 */
function project_form_submit($form, &$form_state) {
  $project = entity_ui_form_submit_build_entity($form, $form_state);
  $project->save();
  drupal_set_message(t('The project: @name has been saved.', array('@name' => $project->name)));
  $form_state['redirect'] = 'admin/projects';
}

For the deadline field, I went with a simple text field just to make this demonstration quicker. Normally, you’d want to use the contrib Date module for a nice widget and then save that value as a timestamp. But this will do for our example.

As for the rest, there is nothing major going on. A few things you should keep in mind though. In order for the Entity API to pick up automatically on the fact that this is the form for the project entities, the declaring function name needs to be exactly that. If the entity type was called chocolate, the function name would have been chocolate_form(). You’ll also notice that we pass the $project entity as a third parameter. This will be handled by the Entity API for the edit form. And finally, in the submit handler, you notice the use of the entity_ui_form_submit_build_entity() function. This is a great helper that will take all the values from the $form_state array (the user input) and populate the entity properties with them.

And that’s it. We can clear the cache and navigate to http://example.com/admin/projects where we should see a table of our project entities, with links to view, edit and delete them + an additional one for adding new ones.

Earlier I mentioned that I will let you know why we had to override the Entity class and change the default URI for the entity type. The link Drupal placed behind the project names are generated based on this information. So as long as the path exists, you should now have a full blown CRUD system + pages for displaying individual entities. Not bad.

Fields

The next thing we’ll see is how to make our entities fieldable. Let’s again edit the hook_entity_info() implementation and add some more information there:

...

'fieldable' => TRUE,
'bundles' => array(
  'project' => array(
    'label' => t('Project'),
    'admin' => array(
      'path' => 'admin/projects',
    ),
  ),
),

...

In the first line we tell Drupal that this entity type should use fields and in the following lines we define a bundle they can be attached to. Bundles are to what we attach fields in Drupal and any entity type can have multiple bundles. For example, in a fresh Drupal install, the node entity has the article and page bundles (content types).

We define a single one, called project and specify some basic information. Now if you clear the cache and go to admin/projects you should see two new tabs at the top of the page resembling what you see when you add fields to regular content types. Now we can add fields to our entity.

You’ll also notice that after you add some fields through the UI and try to edit or add a project, you don’t see the fields in the form. That’s because we need to include them. Edit the project_form() function we wrote earlier and right before the submit element, paste the following:

field_attach_form('project', $project, $form, $form_state);

Using the Field API field_attach_form(), we attach to the form all the necessary elements declared through the UI. Now go to the form again and you should see the new fields. And even on the individual project entity display page the field values get rendered as you configure them through the UI.

Exposing the entity type to Views

The final thing we’re going to look at is how to expose our project entities to Views. The most basic thing we can do in this respect is to edit the hook_entity_info() implementation and specify another controller class that would handle this functionality:

...

'views controller class' => 'EntityDefaultViewsController',

...

Now you can just clear the cache and create a new View. Make sure you choose Project over Content and you’ll have all the entity properties available. One thing to note is that Views cannot interpret the kind of values we are storing in those properties. The deadline field, for instance, is actually a date and not a simple integer. So if you add that field, it will treat it as a simple numeric value.

To fix this, we need to implement hook_entity_property_info() and specify exactly what kind of data the project properties are storing:

/**
 * Implements hook_entity_property_info().
 */
function demo_entity_property_info() {

  $info = array();

  $info['project']['properties']['id'] = array(
    'label' => t('Project ID'),
    'description' => t('The ID of the project.'),
    'type' => 'integer',
    'schema field' => 'id',
  );

  $info['project']['properties']['name'] = array(
    'label' => t('Project name'),
    'description' => t('Name of the project.'),
    'type' => 'text',
    'schema field' => 'name',
  );
  $info['project']['properties']['description'] = array(
    'label' => t('Project description'),
    'description' => t('Description of the project.'),
    'type' => 'text',
    'schema field' => 'description',
  );
  $info['project']['properties']['deadline'] = array(
    'label' => t('Deadline'),
    'description' => t('Project deadline.'),
    'type' => 'date',
    'schema field' => 'deadline',
  );

  return $info;
}

As you can see, it’s a big nested array situation in which for the project entity we map its properties to their schema column and specify what type of data is found there. Now if you clear the cache and add these fields in your View, Views will know what values are stored in the properties. For the deadline field, it will use its date handler this time and transform the timestamp into a date string for us.

An important thing to keep in mind when implementing this hook: you cannot do it only for one property. As soon as you implemented and described one of your properties, you need to add the rest as well. Otherwise Views won’t show the rest at all anymore.

Conclusion

We’ve reached the end of our exploration of Drupal entities. And although we covered quite a lot, there is loads more you can do to perfect and customize your entity types. We’ve seen in this part how to integrate with Views, how to add fields to our entities and even how to create an admin interface for them. But there’s more you can do: view modes, revisions, etc.

We’ve written some example code and created some awesome functionality. And yes, from a strictly “boy this is a long read” perspective, it was quite intense and complex. But if you take a step back and think about what we achieved with what I guarantee you is almost no code at all, you’ll understand why the Drupal entity system is so great.

Build Your Own Custom Entities in Drupal

<< Build Your Own Custom Entities in Drupal – Setup

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Paul

    is there a way to show the table fields in the manage fields or display fields tabs, e.g. so you could re-order them for display? Is there a way to have these fields recognised by DS (display suite) module?

  • fred

    Hi when i go to http://example.com/admin/projects with on example.com my site then i got an error “No acces”

  • fred

    Another question. When I have a project and project tasks, can i make then one entity with 2 bundels and can i join the information in a view with a common item? Or must I make two entities?

  • Daniel Harper

    Hi,

    I’ve implemented a custom entity and was just wondering how I can include my hard coded fields in the mage display options so that the wright can be changed with ones added by the field api. Currently my entity has a coded title and body field which I would like to include in the manage display tab.

    Also the admin UI on mine doesn’t quite work it shows the tabs but also renders a grey table with an empty row and an add entity link which has the wrong URL

    Great Post it filled in a lot of missing information for me.

    Cheers Dan

  • duffyj007

    Great Tutorial!