Extending Custom Subscriptions with Notifications 4.x

We encourage users to post events happening in the community to the community events group on https://www.drupal.org.
ryan_courtnage's picture

Notifications version 6.x-4.x includes the Custom Subscriptions module. It allows for the creation of flexible event notification subscriptions, and exposes to the user a simple, no-nonsense interface for subscription management. Custom Subscriptions is also the recommended method for automatically subscribing users to notifications.

It’s for these reasons that I endeavoured to extend Custom Subscriptions to provide notifications for events triggered by my own custom module. Documentation for Notifications 4.x is somewhat lacking, so this became an exercise of reverse-engineering. I’ve documented the process and my learnings here.

Notifications & Custom Subscriptions already do a great job at making subscriptions to node-related events (ie: subscribe to the creation of nodes to a certain type). My goal is to extend Notifications and allow Custom Subscriptions to be created for events that have nothing to do with Drupal nodes. The events could come from a Drupal cron hook, or even something completely external to Drupal such as from a remote API pinging a webhook. For this exercise, we’ll be enabling the ability to define Custom Subscriptions for Watchdog events.

I suggest you follow along, but if you'd rather just download the module this HowTo describes, you can grab it here: Notifications for Watchdog

Let's get started!

1. Create a new module

Create a new module called notifications_watchdog and enable it. Make sure that you have Notifications 4.x with notifications_custom.module and dblog.module enabled. Open up notifications_watchdog.module in your editor.

2. Define your new Event Type

Visit the Custom Subscriptions admin @ admin/messaging/customsubs and click the link to Add new custom subscription. This is the interface we’d like to use to set up notifications for new Watchdog entries. If you haven’t enabled the Content Notifications module, you’ll notice that the “Event type” dropdown is empty, otherwise, it’ll have a single type of event called “Node”. Regardless, we’re interested in putting our own “Watchdog” event type in the dropdown.

Start by creating a function that implements hook_notifications():

<?php
/**
* Implementation of hook_notifications().
*/
function notifications_watchdog_notifications($op, &$arg0, $arg1 = NULL, $arg2 = NULL) {
  switch (
$op) {
   
  }
}
?>

There are several $op values that we’ll need to switch over. For now, we are interested in the op “event classes”. Inside of the switch statement in hook_notifications(), add the following:

<?php
 
case 'event classes':
    return array(
'watchdogevent' => t('Watchdog'));
?>

Refresh your “Add custom subscription” page and you should see your new “Watchdog” Event type appear. Go ahead and create your new custom subscription, selecting “Watchdog” for Event type. To make things easy in this exercise, click the checkbox for “Visible in user registration form”. This will make it easy for you to subscribe to the notifications on your user profile page.

Only local images are allowed.

Tip: An alternative way to subscribe users to the various Custom Subscriptions on your site is to use the bulk user update options @ admin/user/users

3. Define new subscription Fields for your event type

After saving your new subscription type, you’ll be taken to a page where you must choose one or more “Fields”. These are the event rules that must be met before a notification will be triggered.

Custom Subscriptions comes with one predefined Field type: “Action”. Using hook_notifications(), our module needs to define the actions that will trigger events. For the case of Watchdog, the only action that can really happen is Watchdog having a new log entry written. The key to describe our action to Notifications will be "log", Add the following to hook_notifications():

<?php
 
case 'event actions': // The actions the custom subscription should trigger on
     
return array(
       
'log' => t('Watchdog Entry'),
      );
?>

Being notified for every single Watchdog entry would be insane (or would it?), so we’ll want to add some additional field types, which will allow us to be a little more selective. Let’s allow a watchdog subscription to be restricted to a type and severity by adding to hook_notifications():

<?php
 
case 'subscription fields':
    
$fields['watchdogtype'] = array(
       
'name' => t('Watchdog Type'),
       
'field' => 'watchdogtype',
       
'type' => 'string',
       
'options callback' => 'notifications_watchdog_type_values',
      );
     
$fields['watchdogseverity'] = array(
       
'name' => t('Watchdog Severity'),
       
'field' => 'watchdogseverity',
       
'type' => 'string',
       
'options callback' => 'notifications_watchdog_severity_values',
      );
      return
$fields;
?>

The ‘options callback’ value is a function that returns an associative array of possible values for these field types. We’ll define them now:

<?php
// all available watchdog event types
function notifications_watchdog_type_values() {
 
$types = _dblog_get_message_types();
 
$options = array();
  foreach(
$types as $type) {
   
$options[$type] = $type;
  }
  return
$options;
}

// all available watchdog severity levels
function notifications_watchdog_severity_values() {
  return
watchdog_severity_levels();
}
?>

Refresh your Fields page for your subscription type. You should now be able to create a reasonably useful subscription. We’ll need to do some testing, so create a subscription that notifies whenever a ‘page not found’ log entry is written.

For testing, create a subscription as follows
* Action = Watchdog Entry
* Watchdog Type = page not found

Only local images are allowed.

After saving, visit the Notifications tab in your My Account page and enable your new Subscription.

Tip: If you’re like me, you’ll want to know what just happened in the database.

  • Your ‘notifications’ table now has a row indicating your user’s subscription.
  • Your ‘notifications_fields’ table has now 2 rows, one for each field tied to your notification record.

Note that value in the "conditions" field in the {notifications} table must equal the number of rows for the notification in notifications_fields table. While developing, it’s possible for these to get out of whack: http://drupal.org/node/1052356

4. Triggering a Notification

Time to do some testing! We want to trigger an event whenever watchdog is being written to. We’ll use hook_watchdog() to do this:

<?php
/**
* Implementation of hook_watchdog()
*/
function notifications_watchdog_watchdog($log_entry) {
 
$event = array(
   
'module' => 'notifications_watchdog',
   
'type' => 'watchdogevent',
   
'action' => 'log',
  );
 
notifications_event($event, array('watchdog_entry' => $log_entry) );
}
?>

Take note of the 2nd parameter we pass to notifications_event(), “watchdog_entry”. This contains an object representing the thing we are creating an event for (a watchdog entry). It’s pretty important, and will be used in a couple of places, we need to tell Notifications a little more about it. In hook_notifications() add:

<?php
   
case 'object types': // objects that we pass with notifications_event()
       
$types['watchdog_entry'] = array(
         
'name' => t('Watchdog Entry'),
         
'key_field' => 'timestamp',
         
'load callback' => 'notifications_watchdog_load',
        );
        return
$types;
?>

This describes a little about our event’s object. Most importantly, the function we’ll call to load up the object (notifications_watchdog_load), and the key field to pass as an argument to this load function (timestamp). Here’s our simple load callback:

<?php
function notifications_watchdog_load($timestamp) {
  return
db_fetch_object(db_query('SELECT * FROM {watchdog} WHERE timestamp = %d', $timestamp));
}
?>

Note that we are using timestamp as the key_field for loading our watchdog entry. This is because when hook_watchdog gets invoked, the log entry is not yet written to the database, and we have no auto-increment field to work with. Using timestamp is obviously less than ideal, but it's suitable for this howto.

If we tried to trigger a Watchdog event right now, the notification would fail. This is because we need to add our ‘watchdogtype’ and ‘watchdogseverity’ fields to the query that Notifications uses to determine who should be notified. For this, we need to implement a new hook - hook_notifications_event():

<?php
/**
* Implementation of hook notifications_event()
*/
function notifications_watchdog_notifications_event($op, $event, $account = NULL) {
  switch (
$op) {
    case
'query':
     
$query[]['fields'] = array('watchdogtype' => $event->objects['watchdog_entry']->type);
     
$query[]['fields'] = array('watchdogseverity' => $event->objects['watchdog_entry']->severity);
      return
$query;
  }
}
?>

By the way, $event->params['objects'] will contain an array of all the objects that are passed in notifications_event().

It’s important to understand what we are doing here. Essentially we are adding to the WHERE clause for the Notifications query that determines if any users have a subscription. By adding these fields, we’ve added the highlighted areas into the Notifications query below:

Only local images are allowed.

That very last part “HAVING s.conditions = COUNT( f.sid )” is extremely important - what it’s saying is “make sure that ALL of the field conditions are met”

At this point, we should be able to create a watchdog entry, and have a notification sent out. If you've been following along, remember that you've added a custom subscription that sends notifications for 'page not found' watchdog messages on your site, and your user has subscribed to it.

Go ahead and try to access a non-existent page of your site (eg: user/goriders!). After you see the Page not found page, verify that a notification has been written to the notifications_queue table.
Hitting cron.php will clear the queue and send of the notification. The email you receive will not contain much information, in fact, it’ll be completely empty. This leads is to our next topic.

5. Creating email content

Your users can now subscribe to the custom event that you created, but the notification is pretty much useless. Let’s get some content in there.

Visit Message templates at admin/messaging/template and you’ll see some default template layouts. We’ll be creating a new template for the Watchdog Entry event type. To do this, we need to add to hook_notifications() to define the messaging template our event type uses:

<?php
   
case 'event types'// define email templates @ admin/messaging/notifications/events
       
$types['watchdogevent-log'] = array( // type-action
         
'type' => 'watchdogevent',
         
'action' => 'log',
         
'description' => t('Watchdog Entry'),
         
'template' => 'notifications-watchdog',
        );
        return
$types;
?>

Invoke hook_notifications_templates() to define our new template:

<?php
/**
* Implementation of hook_notifications_templates()
*/
function notifications_watchdog_notifications_templates($op, $type = 'all', $language = NULL) {
  switch (
$op) {
    case
'info':
     
$info = array();
      if (
$type == 'all' || $type == 'notifications-watchdog-log') {
       
$info['notifications-watchdog-log'] = array(
         
'module' => 'notifications_watchdog',
         
'name' => t('Notifications for Watchdog entries'),
         
'help' => t('The subject and main body will be provided by the event itself'),
         
'description' => t('For notifications resulting for Watchdog log entries.'),
         
'fallback' => 'notifications-event',
        );
      }
      return
$info;
  }
}
?>

Note that ‘fallback’ is ‘notifications-event’. This is a default event template that comes with Notifications.

Refresh the Messaging Templates page. You should now see your new template. Note that the “Parts” column is empty for our new template. If we don’t define any parts, our template will just inherit the parts from the fallback template. We’ll want to customise some of the parts in our template. At least a Subject and Content. Add the following to hook_notifications_templates():

<?php
 
case 'parts':
      switch (
$type) {
        case
'notifications-watchdog-log':
          return array(
           
'subject' => t('Subject'),
           
'main' => t('Content'),
       
'digest' => t('Digest Line'),
          );
      }
      break;
?>

Refresh the page, and you should see your template’s parts listed in the Parts column.

Only local images are allowed.

Now click into your new new message template to have a look at the parts you’re defining (expand the “Default” fieldset if it’s collapsed). You should see textareas for your newly added parts. We could just edit these fields in the form and put in the content we want, but it’s useful to have your module provide some sane defaults. Let’s go ahead and write some default content for each part by adding to hook_notifications_templates():

<?php
   
case 'defaults':
      switch (
$type) {
        case
'notifications-watchdog-log':     
          return array(
           
'subject' => t('Watchdog on [site-name]', array(), $language->language),
           
'main' => array(
             
'A Watchdog entry which you subscribe to has been written.',
             
'Time: [watchdog-time]',
             
'Type: [watchdog-type]',
             
'Message: [watchdog-message]',
             
'View this entry: [watchdog-event-url]',
            ),
      
'digest' => array(
             
'[watchdog-type] @ [watchdog-time] : [watchdog-message]',
             
t('View [watchdog-event-url]', array(), $language->language),
            ),
          );
      }
      break;
?>

Everything written in square brackets is meant to be replaced by dynamic token values. We’ll get to that after we test. Hit an invalid URL in your site again, then hit cron.php. This time, the email notification should be a little more useful. Almost finished, we now just need to define values for the [tokens] we’re using in the email.

6. Using Tokens to create dynamic message content

Token.module already provides a bunch of global tokens (eg: [site-url]), and a number of tokens for general objects like node, user, comment, etc. We’re free to use these in our email templates without doing anything else. For our module, however, we want to create a bunch of watchdog-specific tokens from our watchdog entry object.

We need to go back to hook_notifications_templates() and tell Notifications about the new token types/contexts we want to use. We use one called “watchdog_tokens”:

<?php
   
case 'tokens':
     
$tokens = array();
     
$tokens[] = 'watchdog_tokens';
      return
$tokens;
?>

Now it’s just a matter of using the Token hooks: hook_token_list() and hook_token_values(). It’s pretty straight forward. Token.module has excellent documentation if you have questions.

<?php
/<strong>
*
Implementation of hook_token_list()
*/
function
notifications_watchdog_token_list($type = 'all') {
 
$tokens = array();
  if (
$type == 'watchdog_tokens' || $type == 'all') {
   
$tokens['watchdog_tokens']['watchdog-time']    = t('The time of the watchdog event.');
   
$tokens['watchdog_tokens']['watchdog-type']    = t('The type of watchdog event.');
   
$tokens['watchdog_tokens']['watchdog-message']    = t('The message the watchdog event contained.');
   
$tokens['watchdog_tokens']['watchdog-event-url']    = t('The URL to view the watchdog event.');
  }
  return
$tokens;
}

/</
strong>
*
Implementation of hook_token_values()
*/
function
notifications_watchdog_token_values($type, $object = NULL, $options = array()) {
  switch (
$type) {
    case
'watchdog_entry':
      if (
$watchdog_entry = $object) {
       
$values['watchdog-time'] = format_date($watchdog_entry->timestamp);
       
$values['watchdog-type'] = $watchdog_entry->type;
       
$values['watchdog-message'] = $watchdog_entry->message;
       
$values['watchdog-event-url'] = l('admin/reports/event/'.$watchdog_entry->wid, 'admin/reports/event/'.$watchdog_entry->wid, array('absolute' => TRUE));
        return
$values;
      }
      break;
  }
}
?>

The important thing to note is that the 2nd parameter passed to hook_token_values() ($object) is our watchdog log object from notifications_watchdog_load().

Hope someone finds this useful. I'm looking forward to seeing all sorts of contributed notification options added to Custom Subscriptions!

Cheers

Comments

Thank you so much for taking

quadbyte's picture

Thank you so much for taking the time to post this. It's a super starting point for my own customization.

Thank you!

chingis's picture

Thanks for good tutorial!

Yeah, thanks very much, this

Steven Jones's picture

Yeah, thanks very much, this is really useful.

Thanks so much

Quaranta's picture

Thanks for the effort of putting this tutorial together. Like quadbite said, its an excellent starting point!