Upgrade Your Drupal Skills

We trained 1,000+ Drupal Developers over the last decade.

See Advanced Courses NAH, I know Enough

These Are A Few of My Favorite Things - Drupal 7 Hooks Part 2

Parent Feed: 
Printer-friendly version

In part 1 of this series of articles about my favorite new Drupal 7 hooks, we looked at the incredibly useful hook_page_alter(). I also stated that in this article I would write about another awesome new pair of hooks:  hook_query_alter() and hook_query_TAG_alter().

If you read my previous article: Drupal 6 vs Drupal 7 Database Primer - Part 1 then you know that in D7 we can build structured queries, also known as dynamic queries. Essentially, we're just creating a query object and by calling certain methods of that query object Drupal generates a SQL statement and executes it. Pretty straightforward, but here's an example as a quick refresher:

<?php
$query = db_select('users', 'u');

$query
  ->condition('u.uid', 0, '<>')
  ->fields('u', array('uid', 'name', 'status', 'created', 'access'))
  ->range(0, 50);

$result = $query->execute();
?>

There's an additional method we can call on this query object:

<?php
$query->addTag('mymodule');
?>

This method "tags" this query object with an arbitrary string, in this case the name of a module (which supports Drupal's loose namespace conventions). Our module or other modules can use the string - or strings as the method can accept any number of "tags" - assigned to this query object to alter that object.

As mentioned, D7 has provided us with two new hooks: hook_query_alter() and hook_query_TAG_alter(). These two hooks take a concept introduced all the way back in Drupal 4.6 which involved using the function db_rewrite_sql() and hook_db_rewrite_sql() to alter queries. This technique, for a variety of reason, was limited to node, taxonomy, and comment queries only. In D7, we can alter any query from any module so long as that query has been "tagged" and we know the tag's name. For that purpose, there are several utility methods we can call to help us:

<?php
// TRUE if this query object has this tag.
$query->hasTag('example');

// TRUE if this query object has every single one of the specified tags.
$query->hasAllTags('example1', 'example2');

// TRUE if this query object has at least one of the specified tags.
$query->hasAnyTag('example1', 'example2');
?>

Let's put all of this together and build a simple Drupal 7 module for demonstration purposes. First, let's outline what we want our module to do:

  1. Define a schema in our module's .install file using hook_schema() to store some arbitrary information that we can attach to nodes.
  2. Implement CRUD functionality for handling our data using hook_node_insert(), hook_node_update(), hook_node_delete(), and hook_node_load().
  3. Restrict or change the data loaded based on some condition using hook_query_alter() or hook_query_TAG_alter().
  4. Display our data when a node is loaded that meets our conditions using hook_node_view().

Ok. A good start. Let's think of a use-case so we know exactly what kind of data we'll be attempting to load with our nodes. For brevity's sake, we will attempt to simply store the IP address and time zone of the node's author when a new node is created. When a node is viewed by a user, we will check that user's permissions and if one condition is met, we will show both the IP address and time zone of that node's author. If another condition is met, however, we will alter our original query and only show the time zone value. Not really all that useful, but this example should illustrate some basic principles that module developers need to know in the field to handle complex business requirements.

Enough talk. Time to code.

First, our module's .info file which should be self-explanatory, but if you need a refresher you can read the official handbook page on D7 module .info files here:

; $Id$
name = Britesparkz
description = "Module for demonstrating various features in D7."
package = Development
core = 7.x
files[] = britesparkz.module
version = "7.x-1.x-dev"

Now for our module's .install file where we will define our schema:

<?php
/**
 * Implements hook_schema().
 */
function britesparkz_schema() {
  $schema['britesparkz'] = array(
    'description' => 'Stores node author IP address and time zone information.',
    'fields' => array(
      'nid' => array(
        'type' => 'int',
        'unsigned' => TRUE,
        'not null' => TRUE,
        'default' => 0,
        'description' => 'The {node}.nid for this authored node.',
      ),
      'uid' => array(
        'type' => 'int',
        'not null' => TRUE,
        'default' => 0,
        'description' => 'The {node}.uid for this authored node.',
      ),
      'ip_address' => array(
        'type' => 'varchar',
        'length' => 16,
        'not null' => TRUE,
        'default' => '',
        'description' => 'The IP address for this node author.',
      ),
      'timezone' => array(
        'type' => 'varchar',
        'length' => 32,
        'not null' => TRUE,
        'default' => '',
        'description' => 'The time zone for this node author.',      
      ),
    ),
    'indexes' => array(
      'node_author_ip' => array('ip_address'),
      'node_author_timezone' => array('timezone'),
      'uid' => array('uid'),
    ),
    'foreign keys' => array(
      'node' => array(
        'table' => 'node',
        'columns' => array(
          'nid' => 'nid', 
        ),
      ),
    ),  
    'primary key' => array('nid'),
  );

  return $schema;
}
?>

Here is the official handbook page on D7's Schema API for reference. Also, note that in D7 we no longer need a hook_install() to install our schema with drupal_install_schema(). If D7 sees an implementation of hook_schema() in a module's .install file, it will automatically install that schema now.

As you can see, we have defined a table with columns for storing a node's nid, the node author's uid, and the node author's respective IP address and time zone at the time the node was authored or updated. Now that we are all setup, we can get started with the actual module. First, we'll begin with defining a permission for our module using hook_permission():

<?php
/**
 * Implements hook_permission().
 */
function britesparkz_permission() {
  return array(
    'view node britesparkz info' => array(
      'title' => t('View node Britesparkz information'),
      'description' => t('Allows users to view Britesparkz information attached to nodes.'),
    ),
  );
}
?>

A simple implementation of a hook_permission(). We will use this permission to determine if the current user can view both the IP address and time zone. Otherwise, for this example, we will always default to displaying the node author's time zone.

Next, we need to begin work on the CRUD interface required for handling our data. We will use some Node API hooks to facilitate this. First, we need to actually store our data when a node is created or updated. For that we use hook_node_insert() and hook_node_update():

<?php
/**
 * Implements hook_node_insert().
 */
function britesparkz_node_insert($node) {
  $transaction = db_transaction();
  
  try {
    $record = new stdClass();
    $record->nid = $node->nid;
    $record->uid = $node->uid;
    $record->ip_address = ip_address();
    $record->timezone   = drupal_get_user_timezone();  
    
    drupal_write_record('britesparkz', $record);    
  }
  catch (Exception $e) {
    $transaction->rollback();
    watchdog_exception('britesparkz', $e);
    throw $e;
  }
}



/**
 * Implements hook_node_update().
 */
function britesparkz_node_update($node) {
  $transaction = db_transaction();
  
  try {
    $record = new stdClass();
    $record->nid = $node->nid;
    $record->uid = $node->uid;
    $record->ip_address = ip_address();
    $record->timezone   = drupal_get_user_timezone();  

    $result = db_query("SELECT nid FROM {britesparkz} WHERE nid = :nid", array(':nid' => $node->nid));
    
    $exists = $result->fetchObject();

    if ($exists) {
      drupal_write_record('britesparkz', $record, 'nid');  
    }
    else {
      drupal_write_record('britesparkz', $record);  
    }
  }
  catch (Exception $e) {
    $transaction->rollback();
    watchdog_exception('britesparkz', $e);
    throw $e;
  }
}
?>

We begin by telling Drupal that we want to use a transaction with the function db_transaction(). This is a good thing when doing insert and update operations for if anything should fail, the query does not execute at all and we don't have to worry about bad data possibly ending up in our database.

The rest is pretty standard fare. We populate a new object and write that object to our database table. That object's properties correspond to our table's column names so drupal_write_record() does all the dirty work for us.

But what if a node is deleted? Easy. We just delete the corresponding record from our database table using hook_node_delete():

<?php
/**
 * Implements hook_node_delete().
 */
function britesparkz_node_delete($node) {
  db_delete('britesparkz')
    ->condition('nid', $node->nid)
    ->execute();
}
?>

Now we need to actually "attach" our information to a node when that node is loaded by implementing hook_node_load(). That looks like this:

<?php
/**
 * Implements hook_node_load().
 */
function britesparkz_node_load($nodes, $types) {
  $query = db_select('britesparkz', 'b');
  $query
    ->condition('b.nid', array_keys($nodes), 'IN')
    ->fields('b', array('nid', 'timezone'))
    ->addTag('britesparkz');
    
  $result = $query->execute();
   
  foreach ($result as $record) {
    $nodes[$record->nid]->britesparkz = array(
      'ip_address' => isset($record->ip_address) ? $record->ip_address : NULL,
      'timezone' => $record->timezone,
    );
  }
}
?>

Here we use a dynamic query to fetch our information. Note that we only retrieve the nid and timezone here. That's because later we are going to use hook_query_alter() to add in the IP address field if the user has permission to view that. We have also "tagged" our query with the name of our module. We'll use that in our hook_query_alter() to target this query.

Now that our CRUD implementation is done, we need to actually display the attached information when a node is being viewed. For that we implement hook_node_view():

<?php
/**
 * Implements hook_node_view().
 */
function britesparkz_node_view($node, $view_mode, $langcode) {
  if (isset($node->britesparkz) && !empty($node->britesparkz)) {
    $node->content['britesparkz'] = array(
      '#theme' => 'item_list',
      '#title' => t('Britesparkz Info'),
      '#items' => array(        
        $node->britesparkz['timezone'],
      ),
      '#type' => 'ul',
    );
    
    if (isset($node->britesparkz['ip_address'])) {
      array_push($node->content['britesparkz']['#items'], $node->britesparkz['ip_address']);
    }
  }
}
?>

First, we check to see if our data was actually loaded with the node. If it was, we add our information to the node object's content array. This is a renderable array that Drupal parses and turns into markup. For simplicity, I have told Drupal to render our information using theme_item_list(), but I could have just as easily written my own theme function and passed the name of it for the #theme property. Lastly, we check if the IP address for this node has been set, and if so, add it to the item list.

That brings us now to hook_query_alter(). Everything else is in place, but as of yet we will never see the IP address regardless of our permissions. That's because we excluded that field from the tagged query we executed in hook_node_load(). Using hook_query_alter(), we can check the user's permissions and call our query object's addField() method to add the IP address field as such:

<?php
/**
 * Implements hook_query_alter().
 */
function britesparkz_query_alter(QueryAlterableInterface $query) {
  if ($query->hasTag('britesparkz')) {
    if (user_access('view node britesparkz info')) {
      $query->addField('b', 'ip_address');
    }
  }
}
?>

We target our query by calling the query object's hasTag() method and pass it the name of the tag we assigned it. Then we check the currently logged in user's permissions to see if they should be able to view both the IP address and timezone. If they have this permission, we call the query object's addField() method and pass it the IP address column name.

We could have also saved ourselves the trouble of having to call the hasTag() method by implementing hook_query_TAG_alter(). By replacing the "TAG" section of the function name with our tag's name, Drupal will automatically target the appropriate query for us:

<?php
/**
 * Implements hook_query_TAG_alter().
 */
function britesparkz_query_britesparkz_alter(QueryAlterableInterface $query) {
  if (user_access('view node britesparkz info')) {
    $query->addField('b', 'ip_address');
  }
}
?>

Hopefully, this article and the accompanying module help you to see just how powerful hook_query_alter() and hook_query_TAG_alter() can be. There's so many possibilities that I can't wait to see what module developers out in the field come up with using these hooks to bend Drupal to their will.

In the next article in my D7 series, we'll depart from hooks for the time being and look at some of my favorite new Drupal API functions.

Until next time, happy coding

Further reading:

Author: 
Original Post: 

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web