chacadwa.com

Technical blog and writings by Micah Webner.

Adding full Views support to custom entities

There are a lot of examples on creating custom Drupal entities and adding support so they can be used in Views. I covered the basics in my own DrupalCamp Michigan 2013 presentation on custom entities and web services. But as is often the case, there are no examples that show all of the pieces when you have complexities like referenced entities or multiple tables. I just spent two afternoons tearing my hair out, so it's time to stop, drop, and blog before I forget how I did it and can't figure out out again next time.

Background

In my DrupalCamp session, I mentioned that we were creating custom entities from our ERP system for divisions, departments, subjects and buildings, and also used a data extract for our staff directory. I even included examples of how some of entities have a second table of Drupal-managed fields to supplement the extracted data.

And therein lies my newest problem: this second table can be accessed during entity_load() by using attachLoad() in the controller, but this will not work for Views.

Since last year's presentation, we stopped using Drupal Fields to hold contact information for the staff directory, and are extracting that from the ERP system as well. Dropping Fields means I need to add an opt_out field to the entity and use it as a filter for views. It also means setting up references to the other tables to properly display office locations. (Not to mention that divisions and departments were never properly linked.)

Starting Point

Note: This tutorial builds on my DrupalCamp presentation, so I won't repeat anything I've already explained there. I hate that tutorials like this don't contain all of the context on how these pieces fit together, so you'll want to take a look at the complete source for my hankdata and staffdir modules (which will probably be more current than this post by the time you read this.)

The starting point for Views integration is hook_entity_property_info_alter(). This provides labels and help text, and can tell Views what formatting types to use. The documentation also points out that you can specify 'type' => 'entitytype' if the field is a reference to another entity, which will add this field to the Relationships dialog in the view. It will not, however, help you format the field. In fact, it may break the display completely.

/** * Implements hook_entity_property_info_alter(). * * @see hook_entity_property_info() */ function staffdir_entity_property_info_alter(&$info) { $properties = &$info['staffdir']['properties']; // This example only shows the fields relevant to this post. // See linked project source for complete context. $properties['pos_dept'] = array( 'label' => t("Department"), 'type' => 'hankdept', // allows Views to reference hankdept entities 'schema field' => 'pos_dept', 'description' => t("The person's primary department."), ); $properties['pos_division'] = array( 'label' => t("Division"), 'type' => 'hankdiv', // allows Views to reference hankdiv entities 'schema field' => 'pos_division', 'description' => t("The person's primary division."), ); // etc, etc... }

Building the display with a custom Entity Views Controller

In order to build displays for these fields, we'll need to override EntityDefaultViewsController with a custom one. To do this, we'll update staffdir_entity_info() to set 'views controller class' => 'StaffDirViewsController', then create the new class and override the views_data() method.

/** * StaffDirViewsController extends EntityDefaultViewsController. */ class StaffDirViewsController extends EntityDefaultViewsController { /** * Edit or add extra fields to views_data(). */ public function views_data() { $data = parent::views_data(); // Set custom handlers for our staff directory fields. $data['hank_hrper']['pos_dept']['field']['handler'] = 'views_handler_field_hank_dept'; $data['hank_hrper']['pos_division']['field']['handler'] = 'views_handler_field_hank_div'; return $data; } }

In this example, we're using parent::views_data() to set up everything as usual, then only overriding the field handler value with our own custom ones.

Now we just need to add our views_handler_field_hank_dept and views_handler_field_hank_div to StaffDirViewsController, extending views_handler_field in both cases. The most important part here is the render() method.

Here's a very simple example that implements theme() to display the department name:

/** * Custom views field handler for HANK Departments. */ class views_handler_field_hank_dept extends views_handler_field { function render($values) { $value = $this->get_value($values); if (!empty($value)) { return theme('hank_dept_name', $value); } } }

For a more detailed handler that includes an options form, check out my actual views_handler_field_hank_dept.

Reading data from a second table

As I mentioned above, getting data from the second table was exceptionally difficult. I found several examples, but none of them pulled together all of the pieces in the same place, so it took hours to figure out how to do this on my own.

My entity has two tables: hank_hrper is HR person data extracted from the ERP, and hank_hrper_extra_data is the table maintained in Drupal that contains the opt_out field. Both tables have a primary key on the hrper_id field. Simple, right? So how do we let Views access this second table?

As it turns out, all we have to do is make additions to StaffDirViewsController::views_data() to define three things:

  1. Specify the hank_hrper_extra_data table as a default_relationship to the primary.
  2. Define the hank_hrper_extra_data table so our view knows it exists and how to use it.
  3. Define the field(s) we want to expose from the second table.

Here's the code to make it work:

// Define a default relationship to the staffdir extra data table. $data['hank_hrper']['table']['default_relationship'] = array( 'hank_hrper_extra_data' => array( 'table' => 'hank_hrper_extra_data', 'field' => 'hrper_id', ), ); // Now define the staffdir extra data table, so views can use it. $data['hank_hrper_extra_data'] = array( 'table' => array( 'group' => t('Staff directory'), 'entity type' => 'staffdir', 'join' => array( 'hank_hrper' => array( 'left_field' => 'hrper_id', 'field' => 'hrper_id', ), ), 'default_relationship' => array( 'hank_hrper' => array( 'table' => 'hank_hrper', 'field' => 'hrper_id', ), ), ), ); // Now we can define fields from the staffdir extra data table for use in the relationship. $data['hank_hrper_extra_data']['opt_out'] = array( 'title' => t('Opt out'), 'help' => t('Allows for users to be excluded from the directory.'), 'field' => array( 'handler' => 'views_handler_field_boolean', 'click sortable' => FALSE, // could be TRUE, but why? ), 'filter' => array( 'handler' => 'views_handler_filter_boolean_operator', 'accept null' => TRUE, 'help' => t('Filter results to a particular result set'), ), );

Note one other important thing. Not every record in my hank_hrper table has a hank_hrper_extra_data record associated with it, so as soon as I define the boolean filter, the view will only show records that do. The fix for this is to add 'accept null' => TRUE to the filter definition, telling views_handler_filter_boolean_operator to treat missing values as FALSE.

Updates

  • This post was updated on 08/12/14 to use a default_relationship to the secondary table.
Topics: