Building An Advanced Notification System For WordPress

About The Author

Carlo is a freelance front-end designer and developer. In the last years he’s been writing for ioProgrammo, an Italian printed computer magazine, and … More about Carlo ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

Depending on the website’s architecture, you could store the metadata in a category, a tag, a custom taxonomy or a custom field. In this article Carlo Daniele will show you how to let your website’s subscribers decide when they want notifications, and linked to a particular location. Today you are going to add several functionalities to WordPress’ core, and the CMS allows you to declare our own custom functions in the main file of a plugin.

A lot of tools enable us to distribute a website’s content, but when we need to promptly reach a target group, an email notification system might be the best option. If your website is not frequently updated, you could notify all subscribers each time a post is published. However, if it’s updated frequently or it covers several topics, you could filter subscribers before mailing them.

If you opt for the latter, you could set up a user meta field that stores a bit of information to identify the subscribers to be notified. The same bit of information would label the posts you’re publishing. Depending on the website’s architecture, you could store the metadata in a category, a tag, a custom taxonomy or a custom field. In this article we’ll show you how to let your website’s subscribers decide when they want notifications, and linked to a particular location.

Can I Use A Plugin?

If WordPress is your CMS, you can choose from a number of plugins, such as the comprehensive JetPack or the more specialized Subscribe 2.

Subscribe 2 settings page
Subscribe 2’s settings page. (View large version)

Jetpack is easy to use, whereas Subscribe 2 is specialized and full-featured. Both plugins enable you to send email notifications to subscribers whenever a post is published. Unfortunately, neither allows you to notify specific users about specific content. And we want to select posts based on custom fields, mailing them to specific groups of users. Unfortunately, no plugin seems able to help us with this.

Things To Do

We are going to add several functionalities to WordPress’ core, and the CMS allows us to declare our own custom functions in the main file of a plugin. We’re not going to dive deep into plugin development, but you can get the information you need directly from the Codex.

We have to accomplish the following tasks:

  1. add two meta fields to the user’s profile, the first of which stores the name of a location and the second of which determines whether the user will receive emails;
  2. add a custom meta box to the post-editing page containing the location-related custom field;
  3. select the users to be notified and send an email to them.

Add Meta Fields To User Profiles

WordPress stores user data in the wp_users and wp_usermeta tables.

WordPress database description
WordPress’ database description. (Image: Codex)

Here, wp_users holds the list of all website users, while wp_usermeta contains all meta data associated with each user’s profile. Meta data is registered as key-value pairs in the meta_key and meta_value fields.

WordPress generates a bunch of meta data, such as nickname, first_name, last_name, description and wp_capabilities. Much of this data is automatically assigned to each user’s profile, and a user is able to edit it later from their profile page.

To perform our first task, we’ll add two meta fields to the profile pages of users. These fields will store the name of a geographic location and will allow the user to activate (or deactivate) the notification feature.

In the main file of the plugin, let’s define a global associative array whose elements consist of the names of US states:


$smashing_notification_states = array( 'AL' => 'Alabama', 'AK' => 'Alaska', 'AZ' => 'Arizona', 'AR' => 'Arkansas', 'CA' => 'California', 'CO' => 'Colorado', 'CT' => 'Connecticut', 'DE' => 'Delaware', 'FL' => 'Florida', 'GA' => 'Georgia', 'HI' => 'Hawaii', 'ID' => 'Idaho', 'IL' => 'Illinois', 'IN' => 'Indiana', 'IA' => 'Iowa', 'KS' => 'Kansas', 'KY' => 'Kentucky', 'LA' => 'Louisiana', 'ME' => 'Maine', 'MD' => 'Maryland', 'MA' => 'Massachusetts', 'MI' => 'Michigan', 'MN' => 'Minnesota', 'MS' => 'Mississippi', 'MO' => 'Missouri', 'MT' => 'Montana', 'NE' => 'Nebraska', 'NV' => 'Nevada', 'NH' => 'New Hampshire', 'NJ' => 'New Jersey', 'NM' => 'New Mexico', 'NY' => 'New York', 'NC' => 'North Carolina', 'ND' => 'North Dakota', 'OH' => 'Ohio', 'OK' => 'Oklahoma', 'OR' => 'Oregon', 'PA' => 'Pennsylvania', 'RI' => 'Rhode Island', 'SC' => 'South Carolina', 'SD' => 'South Dakota', 'TN' => 'Tennessee', 'TX' => 'Texas', 'UT' => 'Utah', 'VT' => 'Vermont', 'VA' => 'Virginia', 'WA' => 'Washington', 'WV' => 'West Virginia', 'WI' => 'Wisconsin', 'WY' => 'Wyoming' );

Thanks to this array, we will generate a select menu to avoid input errors by users. Now, we need to add two form fields to the user’s profile page. To do this, we will use two action hooks:


add_action( 'show_user_profile', 'smashing_show_user_meta_fields' );
add_action( 'edit_user_profile', 'smashing_show_user_meta_fields' );

Here, show_user_profile is triggered when a user is viewing their own profile, while edit_user_profile is triggered when a user is viewing another user’s profile.

The callback function prints the markup.


/**
 * Show custom user profile fields.
 *
 * @param obj $user The user object.
 */
function smashing_show_user_meta_fields( $user ) { 
    global $smashing_notification_states;
    ?>
    <h3><?php _e( 'Smashing profile information', 'smashing' ); ?></h3>
    <table class="form-table">
        <tr>
            <th scope="row"><?php _e( 'State', 'smashing' ); ?></th>
            <td>
                <label for="state">
                    <select name="state">
                        <option value="" <?php selected( get_user_meta( $user->ID, 'state', true ), "" ); ?>>Select</option>
                        <?php foreach ($smashing_notification_states as $key => $value) { ?>
                            <option value="<?php echo $key; ?>" <?php selected( esc_attr( get_user_meta( $user->ID, 'state', true ) ), $key ); ?>><?php echo $value; ?></option>
                        <?php } ?>
                    </select>
                    <?php _e( 'Select state', 'smashing' ); ?>
                </label>
            </td>
        </tr>
        <tr>
            <th scope="row"><?php _e( 'Notifications', 'smashing' ); ?></th>
            <td>
                <label for="notification">
                    <input id="notification" type="checkbox" name="notification" value="true" <?php checked( esc_attr( get_user_meta( $user->ID, 'notification', true ) ), 'true' ); ?> />
                    <?php _e( 'Subscribe to email notifications', 'smashing' ); ?>
                </label>
            </td>
        </tr>
    </table>
<?php }        

This table contains two custom meta fields. The first is a select menu whose options are generated by a foreach loop that iterates over the $smashing_notification_states global array. This way, the user doesn’t have to type the name of their state, but instead chooses it from a dropdown list.

As you can see, we’re calling the selected() function twice from inside two <option> tags; selected() is a WordPress function for comparing two strings. When the strings have the same value, the function prints selected=‘selected’; otherwise, it echoes an empty string.

The first time we call selected(), we’re comparing the current option’s value (‘state’) with an empty string (which means no state was selected). When iterating over the $smashing_notification_states array, we’re comparing the value of each element to the current value of the ‘state’ meta field. This way, we can automatically select the option corresponding to the existing ‘state’ value.

The second meta field to be added to users’ profiles is a checkbox. Its value will be ‘true’ or ‘false’ depending on whether the user chooses to receive notifications. Similar to selected(), checked() prints out the string checked=‘checked’ when its two arguments have the same value. Of course, checked() applies to checkboxes and radio buttons.

Now that we’ve got the fields, we can save the user’s input. We need two action hooks to store the user data:


add_action( 'personal_options_update', 'smashing_save_user_meta_fields' );
add_action( 'edit_user_profile_update', 'smashing_save_user_meta_fields' );

Here, personal_options_update is triggered when the user is viewing their own profile page, while edit_user_profile_update is triggered when a user with sufficient privileges is viewing another user’s profile page. We have two hooks but just one callback:


/**
 * Store data in wp_usermeta table.
 *
 * @param int $user_id the user unique ID.
 */
function smashing_save_user_meta_fields( $user_id ) {

    if ( !current_user_can( 'edit_user', $user_id ) )
        return false;

    if( isset($_POST['state']) )
        update_user_meta( $user_id, 'state', sanitize_text_field( $_POST['state'] ) );

    if( !isset($_POST['notification']) ) 
        $_POST['notification'] = 'false';

    update_user_meta( $user_id, 'notification', sanitize_text_field( $_POST['notification'] ) );

}

This function verifies whether the user is allowed to edit_user, and if current_user_can is true, it checks the data and saves it in the wp_usermeta table.

Custom meta fields
The custom meta fields added to the user’s profile page.

Custom Meta Box And Custom Fields

We have to decide what kind of content should be included in the notification to subscribers. This decision will depend on your website’s architecture. In this example, we’ll go for regular posts, but you could choose a custom post type instead. The choice depends on your needs.

That being said, we are going to build a custom meta box containing a set of custom fields. These fields will be used to store an address, city, state and some other data related to location. Two other custom fields will enable and disable notifications on a per-post basis, and they will register the number of emails sent to users whenever a new post has been published. Let’s put another action hook to work:


add_action( 'add_meta_boxes', 'smashing_add_meta_box' );
function smashing_add_meta_box(){

    $screens = array( 'post' ); // possible values: 'post', 'page', 'dashboard', 'link', 'attachment', 'custom_post_type'

    foreach ($screens as $screen) {
        add_meta_box(
            'smashing_metabox',                     // $id - meta_box ID
            __( 'Venue information', 'smashing' ),  // $title - a title for the meta_box container
            'smashing_meta_box_callback',           // $callback - the callback that outputs the html for the meta_box
            $screen,                                // $post_type - where to show the meta_box. Possible values: 'post', 'page', 'dashboard', 'link', 'attachment', 'custom_post_type'
            'normal',                               // $context - possible values: 'normal', 'advanced', 'side'
            'high'                                  // $priority - possible values: 'high', 'core', 'default', 'low'
            );
        }
}

Here, add_meta_box accepts seven arguments: a unique ID for the meta box, a title, a callback function, a value for screen, the context (i.e. the part of the page where to show the meta box), and priority and callback arguments. Because we are not setting a value for the callback argument parameter, the $post object will be the only argument passed to smashing_meta_box_callback. Finally, let’s define the callback function to print out the meta box:


/*
 * Print the meta_box
 *
 * @param obj $post The object for the current post
 */
function smashing_meta_box_callback( $post ){
    global $smashing_notification_states;

    // Add a nonce field
    wp_nonce_field( 'smashing_meta_box', 'smashing_meta_box_nonce' );

    $address = esc_attr( get_post_meta( get_the_ID(), 'address', true ) );
    $city = esc_attr( get_post_meta( get_the_ID(), 'city', true ) );
    $state = esc_attr( get_post_meta( get_the_ID(), 'state', true ) );
    $zip = esc_attr( get_post_meta( get_the_ID(), 'zip', true ) );
    $phone = esc_attr( get_post_meta( get_the_ID(), 'phone', true ) );
    $website = esc_attr( get_post_meta( get_the_ID(), 'website', true ) );
    $disable = esc_attr( get_post_meta( get_the_ID(), 'disable', true ) );
    ?>
<table id="venue">
    <tbody>
        <tr>
            <td class="label"><?php _e( 'Address', 'smashing' ); ?></td>
            <td><input type="text" id="address" name="venue[address]" value="<?php echo $address; ?>" size="30" /></td>
        </tr>
        <tr>
            <td><?php _e( 'City', 'smashing' ); ?></td>
            <td><input type="text" id="city" name="venue[city]" value="<?php echo $city; ?>" size="30" /></td>
        </tr>
        <tr>
            <td><?php _e( 'State', 'smashing' ); ?></td>
            <td>
                <select name="venue[state]">
                    <option value="" <?php selected( $state, "" ); ?>>Select</option>
                    <?php foreach ($smashing_notification_states as $key => $value) { ?>
                        <option value="<?php echo $key; ?>" <?php selected( $state, $key ); ?>><?php echo $value; ?></option>
                    <?php } ?>
                </select>
            </td>
        </tr>
        <tr>
            <td><?php _e( 'Disable notification', 'smashing' ); ?></td>
            <td><input id="disable" type="checkbox" name="venue[disable]" value="true" <?php checked( $disable, 'true' ); ?> /></td>
        </tr>
    </tbody>
</table>
<?php
}

First, we’re initializing the global array and registering a nonce field. We then add two simple text fields. The name attribute is set in the form of an array element, while the value is set to the corresponding custom field’s value. Finally, the main custom fields are added.

Just like with the user’s meta data, we add a select menu whose options are echoed, iterating over the elements in the $smashing_notification_states global array. Once we have built the select menu, let’s continue with a checkbox to enable and disable the single post notification.

Now we have to save the data: Our action hook is save_post. We’ll perform a number of tasks with the callback function. Take a look at the inline documentation for more information.


add_action( 'save_post', 'smashing_save_custom_fields' );

/*
 * Save the custom field values
 *
 * @param int $post_id the current post ID
 */
function smashing_save_custom_fields( $post_id ){

    // Check WP nonce
    if ( !isset( $_POST['smashing_meta_box_nonce'] ) || ! wp_verify_nonce( $_POST['smashing_meta_box_nonce'], 'smashing_meta_box' ) )
        return;

    // Return if this is an autosave
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
        return;

    // check the post_type and set the correspondig capability value
    $capability = ( isset( $_POST['post_type'] ) && 'page' == $_POST['post_type'] ) ? 'edit_page' : 'edit_post';

    // Return if the user lacks the required capability
    if ( !current_user_can( $capability, $post_id ) )
        return;

    if( !isset($_POST['venue']['disable']) ) 
        $_POST['venue']['disable'] = 'false';

    // validate custom field values
    $fields = ( isset( $_POST['venue'] ) ) ? (array) $_POST['venue'] : array();
    $fields = array_map( 'sanitize_text_field', $fields );

    foreach ($fields as $key => $value) {
        // store data
        update_post_meta( $post_id, $key, $value );
    }
}

Our custom meta box is up and running, and it looks like this:

Custom meta box
The custom meta box showing the location details. (View large version)

Building The Notification System

If you were working with custom post types, you would need the publish_{$post_type} hook (i.e. publish_recipes, publish_events, etc.). But since we are working with posts, publish_post is the hook for us:


add_action( 'publish_post', 'smashing_notify_new_post' );

/*
 * Notify users sending them an email
 *
 * @param int $post_ID the current post ID
 */
function smashing_notify_new_post( $post_ID ){
    global $smashing_notification_states;

    $url = get_permalink( $post_ID );
    $state = get_post_meta( $post_ID, 'state', true );

    if( 'true' == get_post_meta( $post_ID, 'disable', true ) )
        return;

    // build the meta query to retrieve subscribers
    $args = array(
            'meta_query' => array(
                    array( 'key' => 'state', 'value' => $state, 'compare' => '=' ),
                    array( 'key' => 'notification', 'value' => 'true', 'compare' => '=' )
                ),
            'fields' => array( 'display_name', 'user_email' )
        );
    // retrieve users to notify about the new post
    $users = get_users( $args );
    $num = 0;
    foreach ($users as $user) {

        $to = $user->display_name . ' <' . $user->user_email . '>';

        $subject = sprintf( __( 'Hei! We have news for you from %s', 'smashing' ), $smashing_notification_states[$state] );

        $message = sprintf( __( 'Hi %s!', 'smashing' ), $user->display_name ) . "\r\n" .
        sprintf( __( 'We have a new post from %s', 'smashing' ), $smashing_notification_states[$state] ) . "\r\n" .
        sprintf( __( 'Read more on %s', 'smashing' ), $url ) . '.' . "\r\n";   

        $headers[] = 'From: Yourname <you@yourdomain.com>';
        $headers[] = 'Reply-To: you@yourdomain.com';

        if( wp_mail( $to, $subject, $message, $headers ) )
            $num++;
    }
    // a hidden custom field
    update_post_meta( $post_ID, '_notified_users', $num );
    return $post_ID;
}

Once again, we declare the global array $smashing_notification_states. The two variables $url and $state will store the post’s permalink and state. The succeeding condition checks the value of the disable custom field: If it’s ‘true’, we exit the function. We have to retrieve from the database all users whose state meta field has the same value as the state custom field of the current post, and we use the get_users() function to accomplish this.

The wp_mail function accepts five arguments: recipient(s), subject, message, headers, attachments. The recipients could be passed as an array or as a comma-separated strings of addresses. So, we could have passed to the function all of the addresses together, but doing so would have made them publicly visible (this is the way wp_mail() works).

So, we’ll iterate over the $users array and call wp_mail repeatedly (which shouldn’t be done with a huge number of emails, as we’ll see in a moment). In case of success, wp_mail returns true. The counter is incremented by 1, and the loop continues with the next user.

When the foreach cycle ends, the current value of $num is registered in the hidden _notified_users custom field (notice the underscore preceding the name of the custom field).

Unfortunately, a loop iterating over and over hundreds of times could considerably slow down the script, as pointed out in the reference on the PHP mail() function:

"It is worth noting that the mail() function is not suitable for larger volumes of email in a loop. This function opens and closes an SMTP socket for each email, which is not very efficient. For the sending of large amounts of email, see the » PEAR::Mail, and » PEAR::Mail_Queue packages."

We could work around this, passing to the function the email addresses as BCCs, setting them in the headers, as shown here:


function smashing_notify_new_post( $post_ID ){
    global $smashing_notification_states;

    $url = get_permalink( $post_ID );
    $state = get_post_meta( $post_ID, 'state', true );

    if( 'true' == get_post_meta( $post_ID, 'disable', true ) )
        return;

    // build the meta query to retrieve subscribers
    $args = array(
            'meta_query' => array(
                    array( 'key' => 'state', 'value' => $state, 'compare' => '=' ),
                    array( 'key' => 'notification', 'value' => 'true', 'compare' => '=' )
                ),
            'fields' => array( 'display_name', 'user_email' )
        );
    // retrieve users to notify about the new post
    $users = get_users( $args );

    $num = 0;

    $to = 'Yourname <you@yourdomain.com>';

    $subject = sprintf( __( 'Hei! We have news for you from %s', 'smashing' ), $smashing_notification_states[$state] );

    $message = __( 'Hi ', 'smashing' ) . "\r\n" .
        sprintf( __( 'We have a new post from %s', 'smashing' ), $smashing_notification_states[$state] ) . "\r\n" .
        sprintf( __( 'Read more on %s', 'smashing' ), $url ) . '.' . "\r\n";  

    $headers[] = 'From: Yourname <you@yourdomain.com>';
    $headers[] = 'Reply-To: you@yourdomain.com';

    foreach ($users as $user) {
        $headers[] = 'Bcc: ' . $user->user_email;
        $num++;
    }

    if( wp_mail( $to, $subject, $message, $headers ) )
        update_post_meta( $post_ID, '_notified_users', $num );

    return $post_ID;
}

As you can see, in case of wp_mail()’s success, we update the _notified_user custom field with $num’s value. However, in the code above, $num stores the number of retrieved users, not the number of times we call wp_mail().

Finally, if none of the solutions presented fit your needs, you could consider a third-party email notification system, such as MailChimp or FeedBurner, which enable you to deliver notifications from a website’s feed.

A Note About Status Transitions

We hooked the smashing_notify_new_post callback to the publish_post action. This hook is triggered each time the status of an existing post is changed to publish. Unfortunately, publish_post is not fired when a new post is published. So, to send notifications, first save the post as “draft” (or “pending”). If you prefer to email subscribers each time a post is published, consider calling the save_post action instead:


add_action( 'save_post', 'smashing_notify_new_post' );
/*
 * Save the custom field values
 *
 * @param int $post_id the current post ID
 */
function smashing_notify_new_post( $post_ID ){
    global $smashing_notification_states;

    if( 'publish' != get_post_status( $post_ID ) )
        return;

    ...
}

Check the Codex for further information about status transitions and the save_post action hook.

A Confirmation Message

When you work with the publish_post action hook, you will soon realize that testing your scripts can get a little tricky. When a new post is published, WordPress loads a script that saves data and, when it is done, redirects the user to the post-editing page. This double redirection does not allow variable values to be printed on the screen.

A confirmation message could be a good workaround. This solution allows us to check a variable’s values and to give the publisher useful information: specifically, the number of times wp_mail has been called (or the number of users to be notified).

Remember the $num variable? Its value was stored in a hidden custom field, _notified_users. Now we have to retrieve that value and print it out in a message using a filter hook.

Thanks to the post_updated_messages filter, we can customize WordPress confirmation messages and output them to the screen whenever a new post is saved or published (the Codex does not provide a reference for this filter hook, only an example of usage). Here is the callback function we can use to customize the message when a post is published:


add_filter( 'post_updated_messages', 'smashing_updated_messages' );

/**
 * Post update messages.
 *
 * See /wp-admin/edit-form-advanced.php
 *
 * @param array $messages Existing post update messages.
 *
 * @return array Amended post update messages with new update messages.
 */
function smashing_updated_messages( $msgs ){

    $post = get_post();
    $post_type = get_post_type( $post );
    $post_type_object = get_post_type_object( $post_type );

    $num = get_post_meta( $post->ID, '_notified_users', true );

    if ( $post_type_object->publicly_queryable ) {
        $msgs[$post_type][6] .= ' - ' . $num . __( ' notifications sent.', 'smashing' );
    }
    return $msgs;
}
A custom message
When a post is published, a custom message is printed informing the author about the number of emails sent to users.

wp_mail Function And SMTP

WordPress’ wp_mail() function works the same way as PHP’s mail() function. Whether an email has been successfully sent will depend on php.ini’s settings, but most hosts include SMTP in their services. If you aren’t able to set that up, you could choose an external SMTP service and use it in tandem with the WP Mail SMTP plugin, which routes your emails through an SMTP service.

WP Mail SMTP configuration panel
WP Mail SMTP’s settings page. (View large version)

Be careful when you save data: the “from” field should have the same value as your account’s email address; otherwise, the server might respond with an error message.

Be aware that a plugin is not necessary: WordPress allows for the possibility of overwriting php.ini’s settings from within a script, with the phpmailer_init action hook. This hook allows us to pass our own parameters to the PHPMailer object. See the Codex for more information on this.

Designing Better Emails

Just like the PHP mail() function, wp_mail()’s default Content Type is text/plain. And just like the mail() function, wp_mail() allows us to set the Content Type to text/html. You can specify a different Content Type using the wp_mail_content_type filter or by setting the following headers:


$headers[] = "MIME-Version: 1.0";
$headers[] = "Content-Type: text/html; charset=ISO-8859-1";

Of course, a number of plugins allow you to manage your email’s Content Type from the administration panel. WP Better Emails is just one, but it’s one of the most appreciated. This plugin forces WordPress to send HTML emails, but it’s not its only feature. It also allows administrators to build their own email templates and to send arbitrary emails for testing purposes.

Finally, the following image shows what will be delivered to a Gmail user’s inbox.

Description of the image.
The result in Gmail: The content has been generated by our code; SMTP parameters have been saved on WP Mail SMTP’s settings page; and the template is provided by WP Better Emails. (View large version)

Conclusion

Our notification system is ready to be used. In building it, we’ve toured many of WordPress’ core features: user meta fields, custom meta boxes, custom queries, the mailing from script and more. If you’re interested, download the full code (ZIP file), and remember to switch each occurrence of the you@yourdomain.com string with your own email address.

You can expand on this a lot more. You could integrate this system with a third-party email management application such as MailChimp or Mad Mimi. You could design flashy email templates. Or you could create even more personalized notifications.

Other Resources

Further Reading

Smashing Editorial (dp, al, ml, mrn)