Dec 11 2015
Dec 11

Drupal 8 logo

The Queue API in Drupal allows us to handle a number of tasks at a later stage. What this means is that we can place items into a queue which will run some time in the future and process each individual item at that point and at least once. Usually, this happens on CRON runs, and Drupal 8 allows for a quick set up for cronjob based queues. It doesn’t necessarily have to be CRON, however.

In this article, we will look at using the Queue API in Drupal 8 by exploring two simple examples. The first will see the queue triggered by Cron while the second will allow us to manually do so ourselves. However, the actual processing will be handled by a similar worker. If you want to follow along, clone this git repository where you can find the npq module we will write in this article.

The module we’ll work with is called Node Publisher Queue and it automatically adds newly created nodes that are saved unpublished to a queue to be published later on. We will see how later on can be the next CRON run or a manual action triggered by the site’s administrator. First, let’s understand some basic concepts about queues in Drupal 8.

The theory

There are a few components that make up the Queue API in Drupal 8.

The most important role in this API is played by the QueueInterface implementation which represents the queue. The default queue type Drupal 8 ships with is currently the DatabaseQueue which is a type of reliable queue that makes sure all its items are processed at least once and in their original order (FIFO). This is in contrast to unreliable queues which only do their best to achieve this (something for which valid use cases do exist).

The typical role of the queue object is to create items, later claim them from the queue and delete them when they have been processed. In addition, it can release items if processing is either not finished or another worker needs to process them again before deletion.

The QueueInterface implementation is instantiated with the help of a general QueueFactory. In the case of the DatabaseQueue, the former uses the DatabaseQueueFactory as well. Queues also need to be created before they can be used. However, the DatabaseQueue is already created when Drupal is first installed so no additional setup is required.

The Queue Workers are responsible for processing queue items as they receive them. In Drupal 8 these are QueueWorker plugins that implement the QueueWorkerInterface. Using the QueueWorkerManager, we create instances of these plugins and process the items whenever the queue needs to be run.

The Node Publish Queue module

Now that we’ve covered the basic concepts of the Queue API in Drupal 8, let’s get our hands dirty and create the functionality described in the introduction. Our npq.info.yml file can be simple:

name: Node Publish Queue
description: Demo module illustrating the Queue API in Drupal 8
core: 8.x
type: module

Queue item creation

Inside the npq.module file we take care of the logic for creating queue items whenever a node is saved and not published:

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Queue\QueueInterface;

/**
 * Implements hook_entity_insert().
 */
function npq_entity_insert(EntityInterface $entity) {
  if ($entity->getEntityTypeId() !== 'node') {
    return;
  }

  if ($entity->isPublished()) {
    return;
  }

  /** @var QueueFactory $queue_factory */
  $queue_factory = \Drupal::service('queue');
  /** @var QueueInterface $queue */
  $queue = $queue_factory->get('cron_node_publisher');
  $item = new \stdClass();
  $item->nid = $entity->id();
  $queue->createItem($item);
}

Inside this basic hook_entity_insert() implementation we do a very simple task. We first retrieve the QueueFactoryInterface object from the service container and use it to get a queue called cron_node_publisher. If we track things down, we notice that the get() method on the DatabaseQueueFactory simply creates a new DatabaseQueue instance with the name we pass to it.

Lastly, we create a small PHP object containing the node ID and create an item in the queue with that data. Simple.

The CRON queue worker

Next, let’s create a QueueWorker plugin that will process the queue items whenever Cron is run. However, because we know that we will also need one for manual processing that does the same thing, we will add most of the logic in a base abstract class. So inside the Plugin/QueueWorker namespace of our module we can have the NodePublishBase class:

/**
 * @file
 * Contains Drupal\npq\Plugin\QueueWorker\NodePublishBase.php
 */

namespace Drupal\npq\Plugin\QueueWorker;

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;


/**
 * Provides base functionality for the NodePublish Queue Workers.
 */
abstract class NodePublishBase extends QueueWorkerBase implements ContainerFactoryPluginInterface {

  /**
   * The node storage.
   *
   * @var \Drupal\Core\Entity\EntityStorageInterface
   */
  protected $nodeStorage;

  /**
   * Creates a new NodePublishBase object.
   *
   * @param \Drupal\Core\Entity\EntityStorageInterface $node_storage
   *   The node storage.
   */
  public function __construct(EntityStorageInterface $node_storage) {
    $this->nodeStorage = $node_storage;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $container->get('entity.manager')->getStorage('node')
    );
  }

  /**
   * Publishes a node.
   *
   * @param NodeInterface $node
   * @return int
   */
  protected function publishNode($node) {
    $node->setPublished(TRUE);
    return $node->save();
  }

  /**
   * {@inheritdoc}
   */
  public function processItem($data) {
    /** @var NodeInterface $node */
    $node = $this->nodeStorage->load($data->nid);
    if (!$node->isPublished() && $node instanceof NodeInterface) {
      return $this->publishNode($node);
    }
  }
}

Right off the bat we can see that we are using dependency injection to inject the NodeStorage into our class. For more information about dependency injection and the service container, feel free to check out my article on the topic.

In this base class we have two methods: publishNode() and the obligatory processItem(). The former publishes and saves a node that is passed to it. The latter loads the node using the node ID contained in the $data object and publishes it if it’s unpublished.

Now, let’s create a CronNodePublisher plugin that will use this logic on Cron runs:

namespace Drupal\npq\Plugin\QueueWorker;

/**
 * A Node Publisher that publishes nodes on CRON run.
 *
 * @QueueWorker(
 *   id = "cron_node_publisher",
 *   title = @Translation("Cron Node Publisher"),
 *   cron = {"time" = 10}
 * )
 */
class CronNodePublisher extends NodePublishBase {}

And that is all. We don’t need any other logic than what already is in our base class. Notice that, in the annotation, we are telling Drupal that this worker needs to be used by Cron to process as many items as it can within 10 seconds. How does this happen?

Whenever Cron runs, it uses the QueueWorkerManager to load all its plugin definitions. Then, if any of them have the cron key in their annotation, a Queue with the same name as the ID of the worker is loaded for processing. Lastly, each item in the queue is claimed and processed by the worker until the specified time has elapsed.

If we now save an unpublished node, it will most likely become published at the next Cron run.

The manual worker

Let’s create also the possibility for the Queue to be processed manually. First, let’s adapt the hook_entity_insert() implementation from before and change this line:

$queue = $queue_factory->get('cron_node_publisher');

to this:

$queue = $queue_factory->get('manual_node_publisher');

You can of course provide an admin screen for configuring which type of node publisher the application should use.

Second, let’s create our ManualNodePublisher plugin:

namespace Drupal\npq\Plugin\QueueWorker;

/**
 * A Node Publisher that publishes nodes via a manual action triggered by an admin.
 *
 * @QueueWorker(
 *   id = "manual_node_publisher",
 *   title = @Translation("Manual Node Publisher"),
 * )
 */
class ManualNodePublisher extends NodePublishBase {}

This is almost the same as with the CRON example but without the cron key.

Third, let’s create a form where we can see how many items are in the manual_node_publisher queue and process them all by the press of a button. Inside npq.routing.yml in the module root folder:

demo.form:
  path: '/npq'
  defaults:
    _form: '\Drupal\npq\Form\NodePublisherQueueForm'
    _title: 'Node Publisher'
  requirements:
    _permission: 'administer site configuration'

We define a path at /npq which should use the specified form that lives in that namespace and that we can define as such:

/**
 * @file
 * Contains \Drupal\npq\Form\NodePublisherQueueForm.
 */

namespace Drupal\npq\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Queue\QueueInterface;
use Drupal\Core\Queue\QueueWorkerInterface;
use Drupal\Core\Queue\QueueWorkerManagerInterface;
use Drupal\Core\Queue\SuspendQueueException;
use Symfony\Component\DependencyInjection\ContainerInterface;

class NodePublisherQueueForm extends FormBase {

  /**
   * @var QueueFactory
   */
  protected $queueFactory;

  /**
   * @var QueueWorkerManagerInterface
   */
  protected $queueManager;


  /**
   * {@inheritdoc}
   */
  public function __construct(QueueFactory $queue, QueueWorkerManagerInterface $queue_manager) {
    $this->queueFactory = $queue;
    $this->queueManager = $queue_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('queue'),
      $container->get('plugin.manager.queue_worker')
    );
  }
  
  /**
   * {@inheritdoc}.
   */
  public function getFormId() {
    return 'demo_form';
  }
  
  /**
   * {@inheritdoc}.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    /** @var QueueInterface $queue */
    $queue = $this->queueFactory->get('node_publisher');

    $form['help'] = array(
      '#type' => 'markup',
      '#markup' => $this->t('Submitting this form will process the Manual Queue which contains @number items.', array('@number' => $queue->numberOfItems())),
    );
    $form['actions']['#type'] = 'actions';
    $form['actions']['submit'] = array(
      '#type' => 'submit',
      '#value' => $this->t('Process queue'),
      '#button_type' => 'primary',
    );
    
    return $form;
  }
  
  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    /** @var QueueInterface $queue */
    $queue = $this->queueFactory->get('manual_node_publisher');
    /** @var QueueWorkerInterface $queue_worker */
    $queue_worker = $this->queueManager->createInstance('manual_node_publisher');

    while($item = $queue->claimItem()) {
      try {
        $queue_worker->processItem($item->data);
        $queue->deleteItem($item);
      }
      catch (SuspendQueueException $e) {
        $queue->releaseItem($item);
        break;
      }
      catch (\Exception $e) {
        watchdog_exception('npq', $e);
      }
    }
  }
}


We are again using dependency injection to inject the QueueFactory and the manager for QueueWorker plugins. Inside buildForm() we are creating a basic form structure and using the numberOfItems() method on the queue to tell the user how many items they are about to process. And finally, inside the submitForm() method we take care of the processing. But how do we do that?

First, we load the Queue and instantiate a Queue worker (in both cases we use the manual_node_publisher id). Then we run a while loop until all the items have been processed. The claimItem() method is responsible for blocking a queue item from being claimed by another queue and returning it for processing. After it gets processed by the worker, we delete it. In the next iteration, the next item is returned and on like this until no items are left.

Although we have not used it, the SuspendQueueException is meant to indicate that during the processing of the item, the worker found a problem that would most likely make all other items in the queue fail as well. And for this reason it is pointless to continue to the next item so we break out of the loop. However, we also release the item so that when we try again later, the item is available. Other exceptions are also caught and logged to the watchdog.

Now if we create a couple of nodes and don’t publish them, we’ll see their count inside the message if we navigate to /npq. By clicking the submit button we process (publish) them all one by one.

This has been a demonstration example only. It’s always important to take into account the potential load of processing a large number of items and either limit that so your request doesn’t time out or use the Batch API to split them into multiple requests.

Conclusion

In this article we’ve looked at the Queue API in Drupal 8. We’ve learned some basic concepts about how it is built and how it works, but we’ve also seen some examples of how we can work with it. Namely, we’ve played with two use cases by which we can publish unpublished nodes either during Cron runs or manually via an action executed by the user.

Have you tried out the Queue API in Drupal 8? Let us know how it went!

Daniel Sipos

Meet the author

Daniel Sipos is a Drupal developer who lives in Brussels, Belgium. He works professionally with Drupal but likes to use other PHP frameworks and technologies as well. He runs webomelette.com, a Drupal blog where he writes articles and tutorials about Drupal development, theming and site building.
May 01 2015
May 01

In this article we are going to look at automated testing in Drupal 8. More specifically, we are going to write a few integration tests for some of the business logic we wrote in the previous Sitepoint articles on Drupal 8 module development. You can find the latest version of that code in this repository along with the tests we write today.

Drupal 8 logo

But before doing that, we will talk a bit about what kinds of tests we can write in Drupal 8 and how they actually work.

Simpletest (Testing)

Simpletest is the Drupal specific testing framework. For Drupal 6 it was a contributed module but since Drupal 7 it has been part of the core package. Simpletest is now an integral part of Drupal core development, allowing for safe API modifications due to an extensive codebase test coverage.

Right off the bat I will mention the authoritative documentation page for Drupal testing with Simpletest. There you can find a hub of information related to how Simpletest works, how you can write tests for it, what API methods you can use, etc.

By default, the Simpletest module that comes with Drupal core is not enabled so we will have to do that ourselves if we want to run tests. It can be found on the Extend page named as Testing.

Once that is done, we can head to admin/config/development/testing and see all the tests currently available for the site. These include both core and contrib module tests. At the very bottom, there is also the Clean environment button that we can use if any of our tests quit unexpectedly and there are some remaining test tables in your database.

How does Simpletest work?

When we run a test written for Simpletest, the latter uses the existing codebase and instructions found in the test to create a separate Drupal environment in which the test can run. This means adding additional tables to the database (prefixed by simpletest_) and test data that are used to replicate the site instance.

Depending on the type of test we are running and what it contains, the nature of this replication can differ. In other words, the environment can have different data and core functionality depending on the test itself.

What kinds of tests are there in Drupal 8?

There are two main types of tests that we can write for Drupal 8: unit tests using PHPUnit (which is in core now) and functional tests (using Simpletest). However, the latter can also be split into two different kinds: web tests (which require web output) and kernel tests (which do not require web output). In this article we will practically cover only web tests because most of the functionality we wrote in the previous articles is manifested through output so that’s how we need to test it as well.

Writing any type of test starts by implementing a specific class and placing it inside the src/Tests folder of the module it tests. I also encourage you to read this documentation page that contains some more information on this topic as I do not want to duplicate it here.

Our tests

As I mentioned, in this article we will focus on providing test coverage for some of the business logic we created in the series on Drupal 8 module development. Although there is nothing complicated happening there, the demo module we built offers a good example for starting out our testing process as well. So let’s get started by first determining what we will test.

By looking at the demo module, we can delineate the following aspects we can test:

That’s pretty much it. The custom menu link we defined inside the demo.links.menu.yml could also be tested but that should already work out of the box so I prefer not to.

For the sake of brevity and the fact that we don’t have too much we need to test, I will include all of our testing methods into one single class. However, you should probably group yours into multiple classes depending on what they are actually responsible for.

Inside a file called DemoTest.php located in the src/Tests/ folder, we can start by adding the following:

<?php

namespace Drupal\demo\Tests;

use Drupal\simpletest\WebTestBase;

/**
 * Tests the Drupal 8 demo module functionality
 *
 * @group demo
 */
class DemoTest extends WebTestBase {

  /**
   * Modules to install.
   *
   * @var array
   */
  public static $modules = array('demo', 'node', 'block');

  /**
   * A simple user with 'access content' permission
   */
  private $user;

  /**
   * Perform any initial set up tasks that run before every test method
   */
  public function setUp() {
    parent::setUp();
    $this->user = $this->drupalCreateUser(array('access content'));
  }
}

Here we have a simple test class which for every test it runs, will enable the modules in the $modules property and create a new user stored inside the $user property (by virtue of running the setUp() method).

For our purposes, we need to enable the demo module because that is what we are testing, the block module because we have a custom block plugin we need to test and the node module because our logic uses the access content permission defined by this module. Additionally, the user is created just so we can make sure this permission is respected.

For the three bullet points we identified above, we will now create three test methods. Keep in mind that each needs to start with the prefix test in order for Simpletest to run them automatically.

Testing the page

We can start by testing the custom page callback:

/**
 * Tests that the 'demo/' path returns the right content
 */
public function testCustomPageExists() {
  $this->drupalLogin($this->user);

  $this->drupalGet('demo');
  $this->assertResponse(200);

  $demo_service = \Drupal::service('demo.demo_service');
  $this->assertText(sprintf('Hello %s!', $demo_service->getDemoValue()), 'Correct message is shown.');
}

And here is the code that does it.

First, we log in with the user we created in the setUp() method and then navigate to the demo path. Simpletest handles this navigation using its own internal browser. Next, we assert that the response of the last accessed page is 200. This validates that the page exists. However, this is not enough because we need to make sure the text rendered on the page is the one loaded from our service.

For this, we statically access the \Drupal class and load our service. Then we assert that the page outputs the hello message composed of the hardcoded string and the return value of the service’s getDemoValue() method. It’s probably a good idea to write a unit test for whatever logic happens inside the service but for our case this would be quite redundant.

And that’s it with the page related logic. We can go to the testing page on our site, find the newly created DemoTest and run it. If all is well, we should have all green and no fails.

drupal 8 automatated tests

Testing the form

For the form we have another method, albeit more meaty, that tests all the necessary logic:

/**
 * Tests the custom form
 */
public function testCustomFormWorks() {
  $this->drupalLogin($this->user);
  $this->drupalGet('demo/form');
  $this->assertResponse(200);

  $config = $this->config('demo.settings');
  $this->assertFieldByName('email', $config->get('demo.email_address'), 'The field was found with the correct value.');

  $this->drupalPostForm(NULL, array(
    'email' => '[email protected]'
  ), t('Save configuration'));
  $this->assertText('The configuration options have been saved.', 'The form was saved correctly.');

  $this->drupalGet('demo/form');
  $this->assertResponse(200);
  $this->assertFieldByName('email', '[email protected]', 'The field was found with the correct value.');

  $this->drupalPostForm('demo/form', array(
    'email' => '[email protected]'
  ), t('Save configuration'));
  $this->assertText('This is not a .com email address.', 'The form validation correctly failed.');

  $this->drupalGet('demo/form');
  $this->assertResponse(200);
  $this->assertNoFieldByName('email', '[email protected]', 'The field was found with the correct value.');
}

The first step is like before. We go to the form page and assert a successful response. Next, we want to test that the email form element exists and that its default value is the value found inside the default module configuration. For this we use the assertFieldByName() assertion.

Another aspect we need to test is that saving the form with a correct email address does what it is supposed to: save the email to configuration. So we use the drupalPostForm() method on the parent class to submit the form with a correct email and assert that a successful status message is printed on the page as a result. This proves that the form saved successfully but not necessarily that the new email was saved. So we redo the step we did earlier but this time assert that the default value of the email field is the new email address.

Finally, we need to also test that the form doesn’t submit with an incorrect email address. We do so again in two steps: test a form validation failure when submitting the form and that loading the form again will not have the incorrect email as the default value of the email field.

Testing the block

/**
 * Tests the functionality of the Demo block
 */
public function testDemoBlock() {
  $user = $this->drupalCreateUser(array('access content', 'administer blocks'));
  $this->drupalLogin($user);

  $block = array();
  $block['id'] = 'demo_block';
  $block['settings[label]'] = $this->randomMachineName(8);
  $block['theme'] = $this->config('system.theme')->get('default');
  $block['region'] = 'header';
  $edit = array(
    'settings[label]' => $block['settings[label]'],
    'id' => $block['id'],
    'region' => $block['region']
  );
  $this->drupalPostForm('admin/structure/block/add/' . $block['id'] . '/' . $block['theme'], $edit, t('Save block'));
  $this->assertText(t('The block configuration has been saved.'), 'Demo block created.');

  $this->drupalGet('');
  $this->assertText('Hello to no one', 'Default text is printed by the block.');

  $edit = array('settings[demo_block_settings]' => 'Test name');
  $this->drupalPostForm('admin/structure/block/manage/' . $block['id'], $edit, t('Save block'));
  $this->assertText(t('The block configuration has been saved.'), 'Demo block saved.');

  $this->drupalGet('');
  $this->assertText('Hello Test name!', 'Configured text is printed by the block.');
}

For this test we need another user that also has the permission to administer blocks. Then we create a new instance of our custom demo_block with no value inside the Who field and assert that a successful confirmation message is printed as a result. Next, we navigate to the front page and assert that our block shows up and displays the correct text: Hello to no one.

Lastly, we edit the block and specify a Test name inside the Who field and assert that saving the block configuration resulted in the presence of a successful confirmation message. And we close off by navigating back to the home page to assert that the block renders the correct text.

Conclusion

In this article, we’ve seen how simple it is to write some basic integration tests for our Drupal 8 business logic. It involves creating one or multiple class files which simply make use of a large collection of API methods and assertions to test the correct behavior of our code. I strongly recommend you give this a try and start testing your custom code early as possible in order to make it more stable and less prone to being broken later on when changes are made.

Additionally, don’t let yourself get discouraged by the slow process of writing tests. This is mostly only in the beginning until you are used to the APIs and you become as fluent as you are with the actual logic you are testing. I feel it’s important to also mention that this article presented a very high level overview of the testing ecosystem in Drupal 8 as well as kept the tests quite simple. I recommend a more in depth look into the topic going forward.

Feb 11 2015
Feb 11

In this article I am going to show you how you can integrate Pushover with your Drupal site. I will illustrate a couple of examples of how you can use Pushover to notify yourself as soon as something happens on your site.

The code I write in this article is also available in this repository so you can just clone that if you want to follow along.

What is Pushover?

Pushover is a web and mobile application that allows you to get real time notifications on your mobile device. The way it works is that you install an app on your Android or Apple device and using a handy API you can send that app notifications. The great thing about this is that it happens more or less in real time (depending on your internet connection) as Pushover uses the Google and Apple servers to send the notifications.

The price is also very affordable. At a rate of $4.99 USD per platform (Android, Apple or desktop) paid only once, you can use it on any number of devices under that platform. And you also get a 5 day trial period for free the moment you create your account.

What am I doing here?

In this article I am going to set up a Pushover application and use it from my Drupal site to notify my phone of various events. I will give you two example use cases that Pushover can be handy with:

  • Whenever an anonymous user posts a comment that awaits administrative approval, I’ll send a notification to my phone
  • Whenever the admin user 1 logs into the site, I’ll send an emergency notification to my phone (useful if you are the only user of that admin account).

Naturally, these are examples and you may not find them useful. But they only serve as illustration of the power you can have by using Pushover.

Pushover account

First, a quick look at creating your Pushover account. To follow along, go on over to Pushover and sign up if you haven’t already and you can start your 5 day free trial. Then, go to Google Play or the App Store and install the app on your device. You’ll need to log in to your account and give the device a name. Mine is called simply Nexus (hint: I don’t have an iPhone).

Go back then to the Pushover website and you can already test it out by sending yourself a test notification to one or all of your active devices. You can even specify which sound it should make.

Next, if you want to use the API, you’ll need to create an application. This will generate for you an app_token to use later on. And that should be pretty much it.

Drupal

Now that the Pushover account creation is taken care of, the device app is installed (Android in my case) and I have my Pushover application, it’s time to see how I can achieve the goal set out in the beginning. As a brief roadmap, I will do the following:

  • Create a custom Drupal module
  • Add to it the Pushover class created by Chris Schalenborgh (a handy wrapper over the curl calls to the API)
  • Implement some hooks that will trigger the notifications based on certain conditions
  • Profit

The custom module I’ll be working with in this example is called pushover and it contains a pushover.info and a pushover.module file (as required). Additionally, I will create a lib/Pushover folder in it to store the external class I will use to connect to Pushover. There are other – more recommended ways – of importing external libraries into Drupal modules (see the Libraries API), but for the sake of brevity, this will work just fine.

The first thing I want to do in my pushover.module file is to import this external class. Based on my folder structure, I can do so with this line:

require_once(DRUPAL_ROOT . '/' . drupal_get_path('module', 'pushover') . '/lib/Pushover/Pushover.php');

Next, I want to create a reusable helper function that will return a pushable object. That is an object already populated with my own defaults (such as credentials) and that takes some parameters for the more common properties. But first, I want to put my Pushover account credentials into the settings.php file because they do not belong in my git repository:

$conf['pushover_credentials'] = array(
  'user_token' => 'uCpygdjfsndfi7233sdasdo33Yv',
  'app_token' => 'aKH8Nwsdasdanl342jmsdaBWgoVe',
);

Obviously neither of these tokens are now valid but if you are following along, you should replace them with yours: the user_token you get on the main page as you log in to the Pushover website and the app_token is the one generated for your application.

Then I can continue with my helper function I mentioned earlier:

/**
 * Helper function that returns a pushable object using the Pushover class
 * 
 * @param $vars
 * @return bool|Pushover
 */
function pushover_get_pushable($vars) {
  global $conf;
  if (isset($conf['pushover_credentials'])) {
    $push = new Pushover();
    $push->setToken($conf['pushover_credentials']['app_token']);
    $push->setUser($conf['pushover_credentials']['user_token']);
    $push->setTitle($vars['title']);
    $push->setMessage($vars['message']);
    if (isset($vars['url'])) {
      $push->setUrl($vars['url']);
    }
    if (isset($vars['device'])) {
      $push->setDevice($vars['device']);
    }
    $push->setTimestamp(time());

    return $push;
  }
  else {
    return FALSE;
  }
}

In this function I instantiate a new Pushover object if there are credentials in my settings.php file. Otherwise, I fail silently by returning false. The function takes some parameters that are set on the object: title and message are mandatory whereas the url and device are not. The device is actually the name of the device to which you want to restrict this notification.

Additionally, I set the current timestamp and then return the object.

Next, it’s time to use this function inside some hooks. The first one is going to be hook_comment_insert():

/**
 * Implements hook_comment_insert().
 */
function pushover_comment_insert($comment) {

  // Send a push notification if a new comment is created by an anonymous user
  // and it is not yet published.
  if ($comment->status == 0 && $comment->is_anonymous == TRUE) {
    global $base_url;
    $vars = array(
      'title' => 'New comment on ' . variable_get('site_name') . '!',
      'message' => 'Subject: ' . $comment->subject,
      'url' => $base_url . '/node/' . $comment->nid . '#comment-' . $comment->cid,
      'device' => 'Nexus'
    );
    $pushable = pushover_get_pushable($vars);
    if ($pushable) {
      $pushed = $pushable->send();
      if ($pushed == false) {
        watchdog('Pushover', t('A comment has been created but there was an error pushing that over.'), array(), WATCHDOG_ERROR, NULL);
      }
    }
  }
}

In here I check if the commenter is anonymous and the status is 0 (to be on the safe side). If that’s the case, I build the array of parameters for my helper function with some information about the site and comment and use the send() method to send the notification. You’ll notice that I restricted this to the Nexus device.

At the end, I check whether the notification went out successfully (the send() method returns false if the Pushover service does not return the success status code of 1. If something went wrong, I quickly log it to the watchdog.

So now if an anonymous user writes a comment, I get a push notification with the site name, comment subject and URL. Nifty.

Now let’s turn to the second example in which I implement an emergency notification if my admin user logs into the site. If it’s not me, I’ll know something is wrong and I probably got hacked. I do this inside a hook_user_login() implementation:

/**
 * Implements hook_user_login().
 */
function pushover_user_login(&$edit, $account) {
  // If the admin user logs in, send a push notification.
  if ($account->uid == 1) {
    $whitelist = array('1.1.1.1');
    if (!in_array(ip_address(), $whitelist)) {
      global $base_url;
      $vars = array(
        'title' => 'Admin user sign in',
        'message' => 'Admin user has logged into this site: ' . variable_get('site_name') . '!',
        'url' => $base_url,
      );
      $pushable = pushover_get_pushable($vars);
      if ($pushable) {
        $pushable->setPriority(2);
        $pushable->setRetry(30);
        $pushable->setExpire(60);
        $pushed = $pushable->send();
        if ($pushed == false) {
          watchdog('Pushover', t('An admin user has logged into the site but there was an error pushing this over.'), array(), WATCHDOG_ERROR, NULL);
        }
      }
    }
  }
} 

In here, I first check if the user logging in is the one with the id of 1 (the main admin user). Then I create an array with whitelisted IPs to check against the user logging in. I don’t want to get notified if I log in from home or from the office (1.1.1.1 is just an example ip address).

Then just like before, I get my pushable object with the usual variables (no device this time, I want this to go out to all my devices). On top of those, I set a priority of 2 (marking it an emergency notification), a retry value of 30 seconds and an expire value of 60 seconds. The latter 2 in combination with the priority make it so that if left unacknowledged by my phone, the notification gets resent every 30 seconds for a total duration of 60 seconds. For more information about the possible options you have with Pushover, make sure you check out their API docs.

And there it is. I will now get an emergency notification if someone logs in with my admin account. Of course, not very good if many people can log in with that account, but you get the point.

Conclusion

In this tutorial I showed you how you can use Pushover from your Drupal site to send notifications to your phone when certain events occur. I covered 2 examples but I’m sure you can find more. I would like to also mention that I found a contrib Drupal module called Pushover which uses Rules in order to send out Pushover notifications. I haven’t really used it, but make sure to check it out if you want to use Pushover and your site is already making use of the Rules module. Or, you know, you hate writing code.

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