Building Custom Block Entities With Beans

Update: Please see the code at https://gist.github.com/1460818 for a working example with the current bean code base.

In my previous post I talked about how we decided to leverage EntityFieldQUery as an alternative to Views for aggregating lists of content. In this post we'll be looking at how Treehouse created the Bean module to create and place blocks containing these lists of content.

Neil Hastings
#Drupal | Posted

Update: Please see the code at https://gist.github.com/1460818 for a working example with the current bean code base.

In my previous post I talked about how we decided to leverage EntityFieldQUery as an alternative to Views for aggregating lists of content. In this post we'll be looking at how Treehouse created the Bean module to create and place blocks containing these lists of content.

What Are Beans?

The purpose of the Bean module is to create blocks as entities. We can build out block types, using fields as with any Drupal 7 entity, and have instances of blocks. The concept is similar to how node types are commonly used in Drupal 7. An individual instance of the block type can be placed on any number of pages.

Depending on the construction of the block type, different instances of the block type could have different display settings as well as content. As with nodes, we're not limited to simply adding content to a Bean block. We can use fields to configure the display parameters of the block. This can allow us to give an editor the ability to easily and quickly construct custom queries for blocks.

Let's look at a real example from Energy.gov where we used Beans to aggregate and display content.

Building A Content Listing

This example is from the Energy.gov News Landing Page. The requirement for this block was to collect recent article nodes, optionally filtered by some taxonomy terms, and allow for 3 different type of displays. Users should be able to construct a block, choosing which article types they want to display, which topics they want to restrict their search to, and how many nodes to display for each display type.

To accomplish this task, we created a article listing block type. Let's walk through some of the code used to construct the block type.

Building The Plugin

The Bean module uses the Ctools plugin architecture so defining one should look familiar to some Drupal developers. First, in your module file, tell the Bean module that you are defining new block types.

 

/**
* Implements hook_bean_types_api_info().
*/

function mymodule_bean_types_api_info() {
return array( 'api' => 1);
}

/**
* Implements hook_bean_types().
*/

function mymodule_bean_types() {
$plugins = array();
$plugin_path = drupal_get_path('module', 'mymodule') .
'/plugins/bean'; 

$plugins['listing'] = array(
'label' => t('Listing'),
&bbsp; 'handler' => array(
'class' => 'listing_bean',
'parent' => 'bean',
),
'path' => $plugin_path,
'file' => 'listing.inc',
);

return $plugins;
}

We defined a "Listing" Block type. The block type will use the class "listing_bean" and extends the plugin "bean." The code for the listing block type can be found in /plugins/bean/listing.inc. Nice and simple. Since we are adding a new file, be sure to add the following line to your mymodule.info file:

"files[] = plugins/bean/listing.inc"

This tells the Drupal code registry to look in this file for code to autoload.

For more detail about the full API for block types, see the bean_type_plugin_interface in includes/bean.core.inc in the Bean module. The bean_plugin abstract class takes care of most of the work, but you can always use your own base class if you wish. If you extend the default bean plugin then there are three methods you need to implement:

  • Values: These are the properties that your plugin will be using along with their defaults.
  • Form: This is the form that is used when creating/editing the block.
  • View: This is the code used to render the block.

First we create the shell for the class.

 

/**
* @file
* Listing Plugin
*/
 

class listing_bean extends bean_plugin {
}

Here are some more detailed requirements for this block type.

  • Ability to define the number of records in each of the three display types.
  • Filter by article type, topic and intended audience.
  • Integration with OG
  • More link with custom URL and text.

The content we are displaying is of the "article" content type. This content type has an "article_type" vocabulary that defines if it's a blog article, news article, etc. Topics and audience are a vocabularies used to categorize articles. This gives us three different option filters for vocabularies.

Setting Up Properties and Values

Let's define what properties our block type will have.:

 

class listing_bean extends bean_plugin {
public function values() {
return array(
'filters' => array(
'term' => FALSE,
'topic' => FALSE,
'audience' => FALSE,
),
'items_per_page' => array(
'large_image' => 0,
'small_image' => 0,
'listing' => 0,
),
'more_link' => array(
'text' => '',
'path' => '',
),
);
}
}

Building The Form

Each of the properties are set via the block creation form. Let's define the form.

 

class listing_bean extends bean_plugin {
public function form($bean) {
$form = array(); 

$options = array();
if ($vocabulary =
taxonomy_vocabulary_machine_name_load('article_type')) {
if ($terms = taxonomy_get_tree($vocabulary->vid)) {
foreach ($terms as $term) {
$options[$term->tid] =
str_repeat('-', $term->depth) . $term->name;x
}
}
}

$topic_options = array();
if ($vocabulary =
taxonomy_vocabulary_machine_name_load('topics')) {
if ($terms = taxonomy_get_tree($vocabulary->vid)) {
foreach ($terms as $term) {
$topic_options[$term->tid] =
str_repeat('-', $term->depth) . $term->name;
}
}
}

$audience_options = array();
if ($vocabulary =
taxonomy_vocabulary_machine_name_load('audience')) {
if ($terms = taxonomy_get_tree($vocabulary->vid)) {
foreach ($terms as $term) {
$audience_options[$term->tid] =
str_repeat('-', $term->depth) . $term->name;
}
}
}

$form['filters'] = array(
'#type' => 'fieldset',
'#tree' => 1,
'#title' => t('Filters'),
);
$form['filters']['term'] = array(
'#type' => 'select',
'#title' => t('Article Type'),
'#options' => $options,
'#default_value' => $bean->filters['term'],
'#multiple' => TRUE,
'#size' => 5,
);
$form['filters']['topic'] = array(
'#type' => 'select',
'#title' => t('Topic'),
'#options' => $topic_options,
'#default_value' => $bean->filters['topic'],
'#multiple' => TRUE,
'#size' => 10,
);
$form['filters']['audience'] = array(
'#type' => 'select',
'#title' => t('Audience'),
'#options' => $audience_options,
'#default_value' => $bean->filters['audience']
'#multiple' => TRUE,
'#size' => 10,
);

$form['items_per_page'] = array(
'#type' => 'fieldset',
'#tree' => 1,
'#title' => t('Items per page'),
);
$form['items_per_page']['large_image'] = array(
'#type' => 'textfield',
'#title' => t('Number of items with a large image'),
'#description' => t('These items will be displayed
first in the list and will include a large image,
title, a short teaser and a read more link.'
),
'#default_value' => $bean->items_per_page['large_image'],
'#size' => 5,
);
$form['items_per_page']['small_image'] = array(
'#type' => 'textfield',
'#title' => t('Number of items with a small image'),
'#description' => t('These items will be displayed
second in the list and will include a small picture,
title and short teaser.'
),
'#default_value' => $bean->items_per_page['small_image'],
'#size' => 5,
);
$form['items_per_page']['listing'] = array(
'#type' => 'textfield',
'#title' => t('Number of items in the listing'),
'#description' => t('These items will be displayed
last in the list and will include only a title.'
),
'#default_value' => $bean->items_per_page['listing'],
'#size' => 5,
);

$form['more_link'] = array(
'#type' => 'fieldset',
'#tree' => 1,
'#title' => t('More link'),
);
$form['more_link']['text'] = array(
'#type' => 'textfield',
'#title' => t('Link text'),
'#default_value' => $bean->more_link['text'],
);
$form['more_link']['path'] = array(
'#type' => 'textfield',
'#title' => t('Link path'),
'#default_value' => $bean->more_link['path'],
);

return $form;
}
}

The form method is based on the Bean object at its current state. New beans are initiated with the defaults defined in the values method. This is very useful because you know that value will always be there; there is no need to check for its existence. You can access the properties defined in the values method as properties of the object. Since we defined arrays as our properties, they are arrays on our object. This makes organizing data simple.

It also integrates very nicely with fieldsets. When the form is saved, it matched the name of the property in the values method against the $form_state['values], then stores the settings. When you define #tree=1 in the fieldset, it matches up with the array properties we setup in the values method,

Here is another useful example of a form method that integrates with the media module to provide the media selection UI. If you use the media module, you do not have to worry about setting the file entity status in the validation method.

 

public function form($bean) {
$form = array(); 

$form['image_fid'] = array(
'#title' => t('Image'),
'#type' => 'media',
'#description' =>
t('The Uploaded image will be displayed.'),
'#default_value' => isset($bean->fid) ?
(array)file_load($bean->fid) : '',
'#media_options' => array(
'global' => array(
'types' => array('image'),
'schemes' => array('public', 'http'),
),
),
);

$options = array(
'imagelink_150' => 'Scale to 150px wide',
'imagelink_235' => 'Scale to 235px wide',
'imagelink_320' => 'Scale to 320px wide',
);

$form['image_style'] = array(
'#type' => 'select',
'#title' => t('Choose an Image size'),
'#options' => $options,
'#default_value' => $bean->image_style
);

$form['image_text'] = array(
'#title' => t('Text'),
'#type' => 'textarea',
'#default_value' => $bean->image_text,
);

$form['more_link'] = array(
'#title' => t('More Link'),
'#type' => 'textfield',
'#default_value' => $bean->more_link,
);

return $form;
}

Render and Display

Finally we tackle the render of the block. The view method in the plugin should only take care of the data aspect of the rendering. Be sure to use proper theme functions. The default view method expects you to return an render array.

Here is what we are using for the listing block type:

 

class listing_bean extends bean_plugin {
public function view($bean, $content, $view_mode = 'full', $langcode = NULL) {
$count = $bean->items_per_page['large_image'] + $bean->items_per_page['small_image'] + $bean->items_per_page['listing']; 

$query = new EntityFieldQuery();
$query
->entityCondition('entity_type', 'node')
->entityCondition('bundle', 'article')
->propertyCondition('status', 1)
->propertyOrderBy('created', 'DESC');
->range(0, $count);

if (!empty($bean->filters['term'])) {
$query->fieldCondition('field_article_type', 'tid', $$bean->filters['term']);
}

if (!empty($bean->filters['topic'])) {
$query->fieldCondition('field_topic_term', 'tid', $bean->filters['topic']);
}

if (!empty($bean->filters['audience'])) {
$query->fieldCondition('field_audience_term', 'tid', $bean->filters['audience']);
}

$result = $query->execute();

if (empty($result)) {
$content['nodes'] = array();
}
else {
$content['nodes'] = node_load_multiple(array_keys($result['node']));
}

$content['#theme'] = 'bean_list';
$content['more_link'] = array(
'text' => $bean->more_link['text'],
'path' => $bean->more_link['path'],
);
$content['items_per_page'] = array(
'large_image' => $bean->items_per_page['large_image'],
'small_image' => $bean->items_per_page['small_image'],
'listing' => $bean->items_per_page['listing'],
);

return $content;
}
}

The view method is passed 4 arguments.

  • bean: The bean object that you are rendering.
  • content: The current content array. If you have fields attached this block type (bundle) then the rendered field will be in this array.
  • view mode: The bean entity view mode that is being rendered.
  • langcode: The language being rendered.

For this block type we want total control over what is rendered so we ignore the current content array. We use EntityFieldQuery to query entities. It's very easy for us query fields that are attached to entities without knowing the table structure. For more information about this, see the documentation for the execute method.

We aren't doing a bunch in this method. Basically just loading the nodes based upon the selected criteria and passing on other settings to the theme layer. The details of the theme functions are beyond the scope of this post, but we will look at them in a future post.

Giving Power to the Editor

All this gives the editor power to create her own blocks for querying content, without needing to deal with a relatively complicated interface.

This is just one scenerio for the use of Beans. For Energy.gov, we created more then ten different block types. Each of these types have a different purpose and an individual instance can be placed anywhere a block can. Stay tuned for how we created the layout of the blocks on the individual pages, and how we themed the output without resorting to custom template files.

Neil Hastings