Framework to Extend hook_block in Drupal

New York State Senate

As you have probably already seen at Drupal.org, Advomatic recently launched the new site for the New York State Senate. This front page announcement has received some interest from other developers, and as promised, I'm detailing the custom block framework that we use there and on other sites.

First the background information. I assume that you already know something about blocks and regions. If not, you might want to review Drupal.org's documentation on block administration.

As a developer, one area of frustration I've frequently encountered in the past is how to control block placement and visibility from code. One of the strengths of Drupal is the abililty to administer most of the site from the site itself. This powerful ability, among others, has spelled doom for the webmaster. At the same time, there are some parts of building a site that a case can be made that would be better accomplished with code. Building blocks is arguably one such case.

Although it's certainly nice to be able to use the GUI to specify where a block appears, it's sometimes unwieldly for a developer to go through a dozen screens to enter the PHP block visibility. And even more difficult to go through and make changes later. And the worse piece of all is how to save those changes to a subversion or git repository (as many development shops must do).

Block configuration page

More background: The above screenshot shows a block configuration page. In this textarea, you might have a list of pages for a block to appear on (or not appear on), such as /node, <front>, or /admin*. It's often useful to have a block displayed on pages that are not so easily defined, such as only on node pages of a certain content type, on user pages (but only if they have more than three blog posts), or only on editorial view pages on the first wednesday of each month.

With core Drupal, site administrators must specify such challenging block visibility settings using this text area with its third setting, "Show if the following PHP code returns TRUE (PHP-mode, experts only)", and then only if they are able to enter PHP code in the first place. EDIT: And as JohnAlbin points out in a comment to this post, you don't really want to give administrators the ability to enter PHP code, or to edit a block that is currently using that and inadvertently change it.

In the course of developing sites, we at Advomatic have developed an internal system that addresses these concerns. This framework takes advantage of Drupal's core offerings, and extends it so that it's more usable for developers. Prior to this edit, it used hook_block to define our set of custom defined blocks. It then automatically filled each block visibility setting with

<?php
return my_module_block_display('$delta');
?>

so that the visibility is deferred back to the module. This allows us both to define and change visibility without needing to scour through dozens of administrative pages, to store these settings in a repository, and if we're using a development staging and production server deployment scheme, to synchronize those changes through our codebase.

Without further ado, here's my_module.module. (Note that you would also need to create a corresponding my_module.info file and place them both in /sites/all/modules/my_module. See documentation for more information on creating modules.)

<?php
// $Id$
/**
*  @file
*  My Module
*
*  This module defines several custom blocks.
*
*  To add a new block, you'll need to do the following:
*    1) Add the block to the $deltas list array in my_module_block $op 'list',
*        keyed as $delta => $info_title.
*    2) Define when the block will appear, in my_module_block_display.
*    3) Optionally create a theme file at
*        themes/my_module_block_view_content_[$delta].inc
*        and/or a template file at
*        themes/my_module_block_view_content_[$delta].tpl.php.
*    4) Create a functions here or in that file:
*        theme_my_module_block_view_content_[$delta].
*    5) Rebuild your theme to catch the new functions.
*    6) Visit /admin/build/block and place the new blocks in their required
*        regions.
*/
?>

The // $Id$ snippet isn't really needed here; it's just to encourage proper module development. If you were to contribute a module back to the Drupal community, this would be expanded to display the timestamp and username of the person committing the last change to the file. The rest of this is doxygen documentation telling us how to use the framework.

<?php
/**
*  Implements hook_block().
*/
function my_module_block($op = 'list', $delta = 0, $edit = array()) {
  static
$deltas;
  if (
is_null($deltas)) {
   
// We define the list of block deltas here.
    // We define the $delta string (corresponding to the $delta argument),
    // keyed to the block's 'subject' title.
   
$deltas = array(
     
'plain_block' => t('Plain block'),
     
'fancy_block' => t('Fancy block'),
     
'different_block' => t('Different block'),
    );
  }
  switch (
$op) {
    case
'list':
     
$blocks = array();
      foreach (
$deltas as $delta => $info) {
       
$blocks[$delta] = array(
         
'info' => $info,
        );
      }
      return
$blocks;
    case
'configure':
     
// Here's an example of how to offer specific block configuration.
      // In this case, we would enter the FAPI elements in the include file.
     
if ($delta == 'fancy_block') {
       
module_load_include('inc', 'my_module', "themes/my_module_block_view_content_$delta");
        return
my_module_block_configure_form_fancy_block();
      }
      break;
    case
'save':
     
// Again, we would need to save any special configuration options.
     
if ($delta == 'fancy_block') {
       
module_load_include('inc', 'my_module', "themes/my_module_block_view_content_$delta");
       
my_module_block_configure_form_save_fancy_block($edit);
      }
      break;
    case
'view':
     
$block = array(
       
'subject' => $deltas[$delta],
       
'content' => '',
      );
     
module_load_include('inc', 'my_module', "themes/my_module_block_view_content_$delta");
      if (
my_module_block_display('$delta')) {
       
// We delegate block content to theme functions defined later.
       
$block['content'] = theme('my_module_block_view_content_'. $delta);
      }
      return
$block;
  }
}
?>

We can still use the block administration page to determine regions if desired, or we might specify one or two here anyway (and set the status as well). Note that in any case, we must still visit the block administration page, so my experience is it's easier to just go there and do that.

<?php
/**
*  Determine whether to display a specific block on the page.
*
*  @param $delta
*    The block to display.
*  @return
*    TRUE or FALSE, depending on whether the block should be displayed on this
*    page.
*/
function my_module_block_display($delta) {
  switch (
$delta) {
    case
'plain_block':
    case
'fancy_block':
     
// Display the Fancy and plain blocks only on the front page.
     
return drupal_is_front_page();
    case
'different_block':
     
// Only display the Different block on Page type node pages.
     
if (arg(0) == 'node' && arg(1) && is_numeric(arg(1))) {
       
$node = node_load(arg(1));
        return
$node->type == 'page';
      }
      break;
    case
'user:3':
     
// Here's an example of how you might use this framework with an existing
      // block. In this case, you would need to enter the following snippet
      // into the core "Who's Online" block visibility:
      // <?php return my_module_block_display('user:3'); ? >
      // @TODO: AGAIN, remove the space between the ? and >.
      // This hypothetical example would only display this block on the front
      // page for users who are logged in.
     
global $user;
      return (
$user->uid && drupal_is_front_page());
  }
  return
FALSE;
}
?>

The above is how we determine visibility for blocks.

<?php
/**
*  Implements hook_theme().
*/
function my_module_theme($existing, $type, $theme, $path) {
 
$items = array();
 
// We'll take the hook_block list of defined deltas, and build the theme
  // functions for each.
 
foreach (my_module_block('list') as $delta => $block) {
   
$theme_function = "my_module_block_view_content_$delta";
   
// First, include any specially defined files.
   
$file = module_load_include('inc', 'my_module', "themes/$theme_function");
   
// Define the block delta content theme function.
   
$items[$theme_function] = array(
     
'arguments' => array(),
    );
   
// If the module_load_include above was successful, then make sure our
    // definition includes that file.
    // Note that module_load_include returns NULL if successful, and FALSE
    // if not, so we have to test for an explicit FALSE rather than !$file.
   
if ($file !== FALSE) {
     
$items[$theme_function]['file'] = "themes/$theme_function.inc";
    }
    if (
file_exists(drupal_get_path('module', 'my_module') . "/themes/$theme_function.tpl.php")) {
     
$items[$theme_function]['template'] = "themes/$theme_function.tpl.php";
    }
  }
  return
$items;
}
?>

This section uses Drupal's theme system to dynamically load files with our required theme functions. It could be expanded to use template files (tpl.php) as well if we desired, which could further be overridden by future themes. Conversely, one could easily remove the $file references here and simply include the required theme functions directly in the my_module.module file.

Finally, you would create the theme functions and/or template files, corresponding to those spit out by the above function. Here's an example, which would go into /sites/all/modules/my_module/themes/my_module_block_view_content_fancy_block.tpl.php.

<?php
// $Id$
/**
*  @file
*  my_module_block_view_content_fancy_block.tpl.php
*
*  This is output when our fancy block is displayed.
*  Here we have some variables that have been defined for our use:
*
*  $message: This is an explanatory message configured in the block settings.
*  $data: This is a list of data we want displayed.
*/
?>

  <div class="message"><?php print $message; ?></div>
  <?php print theme('item_list', $data); ?>

And the following would go into /sites/all/modules/my_module/themes/my_module_block_view_content_fancy_block.inc, which would define the variables used in the previous template file, as well as allowing the message to be configured in the block configuration page:

<?php
// $Id$
/**
*  @file
*  my_module_block_view_content_fancy_block.inc
*
*  This defines variables for use when displaying and configuring the
*  Fancy block, defined by My Module.
*  OK, so this example is contrived, and the data is not really so interesting.
*  But you get the point...
*/
function template_preprocess_my_module_block_view_content_fancy_block(&$vars) {
 
$vars['message'] = variable_get('my_module_fancy_block_message', t('Here is some interesting data.'));
 
$vars['data'] = array();
 
$results = db_query_range("SELECT title, nid FROM {node}", 0, 1);
  while (
$result = db_fetch_object($results)) {
   
$vars['data'][] = l($result->title, 'node/'. $result->nid);
  }
}
function
my_module_block_configure_form_fancy_block() {
 
// This returns a form element that's called when configuring this block.
 
$form = array();
 
$form['message'] = array(
   
'#type' => 'textarea',
   
'#title' => t('Fancy block message'),
   
'#default_value' => variable_get('my_module_fancy_block_message', t('Here is some interesting data.')),
  );
  return
$form;
}
function
my_module_block_configure_form_save_fancy_block($edit) {
 
variable_set('my_module_fancy_block_message', $edit['message']);
}
?>

I hope that's not too arcane for folks. Please let me know any questions or comments you have about this framework!

Aaron Winborn is a developer with Advomatic. Besides development, Advomatic also offers a wide range of other Drupal services, including maintenance and clustered hosting. In addition to helping roll out sites such as the New York State Senate, Air America, and Mozilla, Aaron also contributes heavily to the Drupal community, including such modules as Embedded Media Field and Views SlideShow. He has written Drupal Multimedia, published by Packt Publishing, and is mentoring a Google Summer of Code project to help roll out the upcoming Media module. You can read his blogs at Advomatic and AaronWinborn.com.

Marco Carbone wrote 26 weeks 3 days ago

I really like this approach, especially for sites with a lot of custom blocks. Based on its success on New York Senate, I've been using this approach for another big project Advomatic is working on, but with a small modification. Rather than setting up the dynamic hook_theme function, which has the consequence of putting block functionality into theme functions, I moved it into hook_block:

<?php
   
case 'view':
     
// Include dynamic file and function based on block $delta.
     
include_once("includes/mymodule_blocks.block.$delta.inc");
      return array(
       
'subject' => call_user_func_array('mymodule_blocks_view_subject_'. $delta, array()),
       
'content' => call_user_func_array('mymodule_blocks_view_content_'. $delta, array()),
      );
?>

So for example, if I have a block with $delta = 'show_stuff', I'll have a file in sites/all/modules/mymodule/includes/mymodule_blocks.block.show_stuff.inc with a function called function mymodule_blocks_view_content_show_stuff() which returns the content of the block and a function mymodule_blocks_view_subject_show_stuff() which returns the subject. I manually add the theme functions it uses in hook_theme. This way, each block can still be included in a separate file and we can separate theme and functionality.

Dave Hansen-Lange wrote 26 weeks 2 days ago

Hey Marco,
No need for call_user_func_array, you can use variable functions:

http://php.mirrors.ilisys.com.au/manual/en/functions.variable-functions.php

JohnAlbin wrote 26 weeks 3 days ago

I see no reason to have your custom module’s hook_block give a default PHP Visibility setting.

PHP Visibility Settings are just an accident waiting to happen. Why let a mis-configured WYSIWYG editor or a site admin typo cause a Fatal PHP Error when malformed PHP is accidentally saved to that field?

Since you are going to all the trouble of putting your block in code (in general a good idea!), why not just move the PHP you have from the visibility setting into the hook_block's 'view' op?

<?php
   
case 'view':
     
// Include dynamic file and function based on block $delta.
     
include_once("includes/mymodule_blocks.block.$delta.inc");
     
$block = array(
       
'subject' => call_user_func_array('mymodule_blocks_view_subject_'. $delta, array()),
       
'content' => '',
      );
      if (
my_module_block_display($delta)) { // <-- much safer than PHP block visibility
       
$block['content'] = call_user_func_array('mymodule_blocks_view_content_'. $delta, array());
      };
      return
$block;
?>

If a block returns no content, Drupal won't display the block. My code above is what the PHP visibility setting is trying to emulate.

Aaron Winborn wrote 26 weeks 3 days ago

Thanks, John. That is very sound advice. I'll edit this post to reflect that.

Marco Carbone wrote 26 weeks 3 days ago

The problem with this suggestion is that it doesn't work for blocks provided by other modules. For example, we've been using this technique for views blocks by adding a simple PHP call to the very same function. That way all visibility code for *all* blocks with non-trivial display rules is in the same place.

Pasqualle wrote 26 weeks 3 days ago

Yes, avoid putting custom php into the database.

tip: The Context module has a much more robust visibility settings for blocks..

Contact Us

About Aaron Winborn

Aaron Winborn was Advomatic's first full time hire in 2006, and is a very active leader in the Drupal community. His first book, Drupal Multimedia is now available from Packt Publishing.

Advomatic on Twitter