On this page
- What We Need, Part 1: Notifications
- What We Need, Part 2: Messaging
- Writing the Code, Part 1: Settings
- Writing the Code, Part 2: hook_notifications()
- Writing the Code, Part 3: hook_messaging()
- Writing the Code, Part 4.1: Logging Events
- Writing the Code, Part 4.2: Auto-Subscribing
- Writing the Code, Part 5: Adding "Subscribe" Links
- Testing and Debugging
- That's All, Folks
Adding new notification events and subscription types (2.x)
Let's say you've invented a new kind of entity called a "status." Users can post statuses to other users' profiles and to group pages. Then users should be able to subscribe to other users and groups so they get a notification when that user posts a message, as well as to individual statuses so they get a notification when someone comments on one.
This is approximately the case for the Facebook-style Statuses module, and it is my experience writing Notifications integration for that module that inspired me to contribute this tutorial. Even if your use case isn't that extensive, you can use this tutorial to learn how to create new subscription types (i.e. "subscribe to a user's status" and "subscribe to a status's comments") or event types ("status event" or "[user] stream event"). Notice already that the terminology can get confusing: make sure you pay attention to the difference between subscription types (usually $subs->type in code) and event types (usually $event->type or $event_type in code).
Also, be prepared: it's going to take a lot of code. Facebook-style Statuses' integration is a little over 1000 lines, and it doesn't do anything particularly special. You can take a look at the Notifications integration submodule here as an example, or use the code from the Notifications Content module (which comes with the Notifications package) as another example.
What We Need, Part 1: Notifications
Let's first consider what kind of information the Notifications framework will need for our plugin. Notifications does a lot of different things.
- If you have a new kind of entity, you will probably want to create a new tab of
admin/messaging/notificationswith settings for what subscription types are enabled and where "subscribe" links should appear -- for example, should users be able to subscribe to both other users' statuses and groups' statuses? Should a "subscribe" link show up on every status update? - If you provide new subscription types, you will probably want to create a new tab of
user/%user/notificationsfor each subscription type to list to which subscriptions of that type the user is subscribed. - You may also want to provide new permissions for any new subscription types.
- You will need to log a new notification when certain events occur -- for example, when a status is created.
- You need to describe the new subscription types you are making available.
- Given a subscription object, you need to provide a user-friendly name for it for administrative interfaces.
- Notifications needs to know which fields are important on the object relevant to your new subscription types. For example, the important field for a subscription to a status is the status ID, and the important fields for a subscription to a user or group is the user or node ID and whether we are subscribing to a user or node. This information is used to find subscriptions relevant to these objects. It's also used when a user adds a custom subscription at
user/%user/notifications/add. When a user is adding a custom subscription, you may also want to provide information to make a field autocompleted -- for example, a user should be able to type in a username or node title instead of the user or node ID -- and so you will need custom callbacks to help do that. - When a message is being prepared for sending, its tokens must be evaluated. To evaluate the tokens, Notifications needs to know which objects to use. For example, a [node-title] token will require a node object which you must provide.
- You need to describe the new event types you are making available.
- You need to describe any subscribe links that should appear on your objects (like "subscribe to comments on this status") as well as actually place the links on your objects in most cases.
- When an event is triggered, you need to determine whether a user has access to get a notification for that event.
- Notifications also gives you the ability to react when a subscription is saved or updated, and when an event is triggered or queued.
- If you want, you can also describe new digest formats. Default ones include "short" and "long."
Most of these things will be defined in one gigantic function: your hook_notifications() implementation.
What We Need, Part 2: Messaging
We also need to integrate with the Messaging module to make sure that there are messaging templates that are appropriate for the new events we've added. We do that by:
- Informing Messaging that each template exists
- Describing which fields each template should have (e.g. a subject, content area, and digest line)
- Providing default values for the new templates we've just created
- Reminding Messaging what kind of tokens we want to use
We can do all of these things in hook_messaging().
Writing the Code, Part 1: Settings
There are two kinds of settings pages you'll need if you're adding new subscription types. The first is the page for administrative, site-wide settings, and the second is the tabs on user profiles for each subscription type. So, you'll want to start with hook_menu().
Unfortunately, a lot of things in Notifications are inter-dependent, so we will come across some things that we won't nail down until later. The first of those is the subscription types you want to add! You should go ahead and figure out what kinds of things you want to subscribe to and just write them down somewhere temporary for now. Examples of subscription types include "thread" (subscribe to all comments on a specific node), "author" (subscribe to all nodes created by the author of the currently viewed node), "nodetype" (subscribe to all nodes of the same type as the currently viewed node), etc. So go ahead and decide on a machine name for your subscription type. Try to make it unambiguous -- "nodetype" is better than "type," for example, to avoid confusion with event types and subscription types.
Here's an example hook_menu() implementation for a module called example that adds a subscription type called customsub in example.module:
/**
* Implementation of hook_menu().
*/
function example_menu() {
$items = array();
// The global settings page.
$items['admin/messaging/notifications/example'] = array(
'title' => 'Example subscriptions',
'type' => MENU_LOCAL_TASK,
'page callback' => 'drupal_get_form',
'page arguments' => array('example_settings_form'),
'access arguments' => array('administer site configuration'),
'file' => 'example.pages.inc',
);
// List this user's customsub subscriptions.
$items['user/%user/notifications/customsub'] = array(
'type' => MENU_LOCAL_TASK,
'access arguments' => array('maintain own subscriptions'),
'title' => 'Customsub',
'page callback' => 'example_page_thread',
'page arguments' => array(1),
'weight' => 10,
'file' => 'example.pages.inc',
);
return $items;
}
Now we need to add two callbacks, example_settings_form() and example_page_thread(), in the file we specified (example.pages.inc). The settings you want to provide are basically up to you, but other modules that provide subscription types allow specifying which subscription types should be enabled on the site, and which of the enabled subscription types should have "subscribe" links displayed on the relevant entities. Here's our example:
/**
* Page callback for admin/settings/notifications/example.
* Display global settings for what subscriptions are enabled and where they are displayed.
*/
function example_settings_form($form) {
$form = array();
// Build the options array using the "description" property of the subscription type definition as specified in the "subscription types" case of hook_notifications().
// We haven't decided what our event type(s) are yet, so let's just say "node" for now. (You can fill this in with whatever you think is best -- just remember to check it later.)
$options = _notifications_subscription_types('long', array('event_type' => 'node'));
// The setting for which subscription types are enabled.
$form['example_type'] = array(
'#type' => 'checkboxes',
'#title' => t('Global options'),
'#options' => $options,
'#default_value' => variable_get('example_type', array()),
'#description' => t('Define the available subscription types that will be enabled globally'),
);
// The setting for which entities should have "subscribe" links.
$form['example_links'] = array(
'#type' => 'checkboxes',
'#title' => t('Show "subscribe to customsub" links on these entities'),
'#options' => array('customsub' => t('Customsub')),
'#default_value' => variable_get('example_links', array()),
);
return system_settings_form($form);
}
Don't forget to delete the variables example_type and example_links in your hook_uninstall() implementation in your .install file.
Here's the callback used by the Notifications Content submodule to list subscriptions to comments on individual nodes:
/**
* Page callback for user/%user/notifications/node.
* List thread subscriptions.
*
* For clarity in this tutorial: the relevant subscription type is "thread" and the relevant
* event type is "node."
*
* This function has been cleaned up a little from its actual implementation.
*/
function notifications_content_page_thread($account = NULL) {
global $user;
// Default to the current user.
if (is_null($account)) {
$account = $user;
}
// Get all node subscriptions.
$result = pager_query("
SELECT
s.sid, s.uid, s.type, s.event_type, s.conditions, s.send_interval, s.send_method, s.cron, s.module, s.status, s.destination, /* {notifications} columns */
f.value AS nid, /* {notifications_fields} columns */
n.type AS node_type, n.title /* {node} columns */
FROM {notifications} s
INNER JOIN {notifications_fields} f
ON s.sid = f.sid /* join on subscription ID */
LEFT JOIN {node} n
ON f.intval = n.nid /* join on node ID */
WHERE
s.uid = %d AND
s.type = 'thread' AND
s.event_type = 'node' AND
s.conditions = 1 AND /* there is only 1 field that matters: nid */
f.field = 'nid'
ORDER BY node_type ASC, n.title ASC
", NOTIFICATIONS_CONTENT_PAGER, 0, NULL, $account->uid); // NOTIFICATIONS_CONTENT_PAGER is a constant value of 20 (this is the number of results to display)
$subscriptions = array(); // Keep track of the subscription objects for further processing.
$list = array(); // A list of subscriptions that will appear on the page, keyed by a unique ID (usually the subscription ID, but node ID also works here).
$content_types = notifications_content_types('name'); // an associative array mapping machine names of content types to friendly names
while ($sub = db_fetch_object($result)) {
$subscriptions[$sub->nid] = $sub;
$list[$sub->nid] = '['. $content_types[$sub->node_type] .'] '. l($sub->title, 'node/'. $sub->nid);
}
if (empty($subscriptions)) {
$output = t('You are not currently subscribed to any active threads');
}
else {
$output = t('You are currently subscribed to the following threads:');
$defaults = array('type' => 'thread', 'event_type' => 'node');
$options = array('title' => t('Title'));
// A handy helper form that formats your list of subscriptions in a standard way.
$output .= drupal_get_form('notifications_user_form', $account, 'thread', $subscriptions, $list, $defaults, $options);
$output .= theme('pager', NULL, NOTIFICATIONS_CONTENT_PAGER);
}
return $output;
}
There you go -- we have our settings. Go ahead and browse to the pages you just created to get a feel for what you just accomplished.
Writing the Code, Part 2: hook_notifications()
The next step is to implement hook_notifications(), in which we will mostly provide a bunch of definitions for things related to subscriptions and events. That's right -- you will define a bunch of things in a single hook based on the $op parameter passed to it. Because this makes hook_notifications() huge, it has its own page dedicated to explaining it: http://drupal.org/node/407338
You should follow that link and follow the instructions there, then come back here.
Once you've finished implementing hook_notifications(), you will be able to manually subscribe to the new subscriptions you've defined at user/%user/notifications/add. You can check in the {notifications} and {notifications_fields} table in your database to confirm that this was successful.
Writing the Code, Part 3: hook_messaging()
Now that you've defined your subscriptions and events, we need to define the templates to use for the messages that are sent to users. We do this in hook_messaging(), which -- like hook_notifications() -- is a single hook where you will describe a lot of things. Since this means hook_messaging() is almost as large as hook_notifications(), it has its own page too: http://drupal.org/node/409614
You should follow that link and follow the instructions there, then come back here.
When you're done, your new templates should show up at admin/messaging/template. Click on them and make sure they have the right default text. Then save at least one and make sure it is correctly stored in the {messaging_message_parts} table in your database.
Writing the Code, Part 4.1: Logging Events
We've finally got all the configuration done, so it's on to the fun part: logging messages. Hopefully your module has some hooks you can implement so your Notifications integration can react at the right time. For example, if you are providing Notifications integration for a module called "widget" that allows people to make widgets, it might provide hook_widget_save($widget) that gets invoked when a widget is saved. Then, in your "example" module, you would implement that hook to alert Notifications like this:
/**
* Implementation of hook_widget_save().
*/
function example_widget_save($widget) {
$event = array(
'uid' => $widget->uid, // The ID of the user who took the action. Usually the current user.
'created' => $widget->created_time, // The time the action occurred.
'module' => 'example', // This doesn't seem to affect anything.
'oid' => $widget->wid, // The ID of the widget. Later, in hook_notifications(), this will be available to you so you can load the object if necessary.
'type' => 'widget', // The event type (not the subscription type).
'action' => 'insert', // The action that corresponds to the names you gave your message templates in hook_messaging().
'params' => array('wid' => $widget->wid), // Any additional parameters you want to have available when handling the event object in hook_notifications(), particularly in the "event load" and "query" cases.
);
notifications_event($event); // Store the event.
}
Woohoo! Now events should fire when you take action. You can go to user/%user/notifications/add to add custom subscriptions so you can test it out. You should get notifications formatted using the message templates you defined! If you don't, check out the "Testing and Debugging" section at the end of this tutorial.
Writing the Code, Part 4.2: Auto-Subscribing
Many users don't want to have to manually click a "subscribe" link or add a custom subscription in their profile -- they just want to get notifications on content they create or comment on. You can oblige by automatically subscribing them, usually using the same hooks as you used to send events in Part 4.1 above:
/**
* Implementation of hook_widget_save().
*/
function example_widget_save($widget) {
// ...snipped...
// Only do this if the autosubscribe module is enabled.
if (module_exists('notifications_autosubscribe')) {
global $user;
// Only do this if the user has enabled auto-subscribing.
if (notifications_user_setting('auto', $user)) {
// Don't allow subscribing to the same thing twice.
$has_subscription = notifications_user_get_subscriptions($user->uid, 'status', 'sid', $comment->sid);
if (!$has_subscription) {
// Build and save the subscription.
$subscription = array(
'uid' => $user->uid,
'type' => 'customsub', // The subscription type
'event_type' => 'widget', // The event type
'fields' => array('wid' => $widget->wid), // The fields that uniquely identify the object to which the user is subscribing (in this case, the widget)
);
notifications_save_subscription($subscription);
}
}
}
}
Try it out -- if you have the autosubscribe module enabled, and you enabled autosubscribing in your user settings, you should be automatically subscribed when you take the relevant action.
Writing the Code, Part 5: Adding "Subscribe" Links
Not all users will want to be autosubscribed, and some will want to subscribe to things without posting on them. (If only you could do that in the Drupal.org issue queues!) So you need to add your links somewhere to the object to which users can subscribe. For nodes and comments, you can do this by implementing hook_link(); for users, you can do this by implementing hook_user('view'); for a custom entity type, you will probably have your own way of adding links to it; or you can provide those links in a block.
Usually, just implementing the "node options" and "user options" cases of hook_notifications() should be enough, and Notifications should take care of the rest. If this doesn't work, however, or if you want to put your links somewhere special, you'll need to do that manually.
Testing and Debugging
Whew! That was a lot. If you got through it all and it worked the first time, I bow to your genius. If not, read on.
If you enable the "Logging" setting at both admin/messaging/notifications and admin/messaging/settings, Notifications and Messaging try to log what they're doing so you can see if the right values are being processed at admin/messaging/subscriptions/queue. Though the log is mostly just what database values are being processed, this is important because Notifications deletes events after they are processed so it's not particularly easy to go look at them directly in the database.
If your messages are being sent but they're using the wrong template, make sure that your template name is in the right format (see the documentation on hook_messaging()). Remember that subscriptions in the {notifications} table pretty much always use "notifications" as the module -- not the name of the module you are writing.
If you have other kinds of problems that aren't causing PHP errors or warnings, you will probably have to step backward through the code until you figure out what you're missing. If you have the Devel module installed, dsm(debug_backtrace()); is your friend -- it will show you the stack trace of what functions were called before the code ended up at a specific place. You will also probably have to use dsm() to print out the $subscription and $event objects at various points in their lifetime.
It also helps to look at examples. Notifications has two submodules that implement this API: Notifications Content and Notifications Taxonomy. It is at least worthwhile to take a look at those and see if anything jumps out at you if you're still having problems. There's also Facebook-style Statuses' Notifications submodule.
That's All, Folks
We're done creating new subscription and event types. Hooray!
If you're still feeling adventurous, try writing a plugin to replace Notifications' UI. That could use some documentation too.
Help improve this page
You can:
- Log in, click Edit, and edit this page
- Log in, click Discuss, update the Page status value, and suggest an improvement
- Log in and create a Documentation issue with your suggestion