Drupal 10: Creating A Notification System Using The Message And ECA Modules

Drupal is a great platform to create communities of users where you can allow users to create their own content and interact with each other. One way of keeping users engaged on the site is by letting them know when other users are interacting with content they have created.

This is quite a common occurrence on sites like LinkedIn or Facebook, where you will receive a notification when a user comments or likes one of your posts. The same thing can be put together in Drupal with just a few modules (and a few lines of code).

In this article I will look at how to create a notifications system that will tell users about important events that they might be interested in. We will be using as many contributed modules as possible to do this, although there will be a small PHP class involved to tie some components together.

Installing The Required Modules

The following modules are required in order to get everything working together for the notifications system.

  • Message - This module allows a simple message template to be created that can then be used to create messages for users. We will use these messages as the notifications themselves. Templates types are used to define the types of interactions that we want to notify users about.
  • ECA - ECA or Event, Condition, Action, is a really powerful module that allows events to be reacted to and actions performed on those events. This will allow us to react to events happening on the site, and to generate notifications based on those events.
  • BPMN.IO - A module used by the ECA module to allow workflows to be created using the BPMN.io library.
  • Token - Drupal has a feature that gives user the ability to use a simple token replacement system in certain fields. This is useful if you want to dynamically print, for instance, the user's name in a field. The Token module is used to extend the core set of available tokens and is required for some of the advanced tokens we will be using here.

To add all of these modules to the site you need to run the following composer command.

composer require drupal/message drupal/eca drupal/bpmn_io drupal/token

Once they are in your codebase you can install the required modules using Drush in a single command.

drush en message eca eca_base eca_content eca_ui eca_modeller_bpmn bpmn_io token

The modules we are installing here are either the module itself, or a sub module required for certain functionality to be enabled. The ECA module is quire modular, but we need to enable the ECA Content module to allow interactions with content entities.

With everything installed and enabled we are now ready to configure the site.

Setting Up The Message Templates

The first thing we need to do is use the Message module to create a message template.

Message templates work in a similar way to other entities in Drupal, in that you get a edit screen with a few settings, and the ability to add fields to the message. What we need to do here is add a message that uses tokens to generate a message when an action is performed.

As an example notification I will use the action created when a user comments on an article. When this happens we will be creating a message of the type "new_comment_created" and assigning it to the user who will receive the notification.

Here is the basic setup of this message template.

The message template edit form, showing the template for a new comment being created.

With the template in place we now need to add a field to it. This field will form the data that is attached to the notification, which in this case is a reference to the comment that was created.

The admin interface of a message template, showing a comment reference field being part of the message.

Make sure you hide this comment reference field from display, it's only used to give the message some context. This single item of data is enough to get us the following information:

  • The user who commented on the article.
  • The original article where the commented was posted.
  • The author of the article itself, who will receive the notification.

The content of the message text is a formatted text area that can accept tokens. The full HTML of this field is as follows.

<p>
    <a href="[message:field_message_comment_ref:entity:entity:url:path]">[message:field_message_comment_ref:entity:author:name] commented on [message:field_message_comment_ref:entity:entity:title]</a>
</p>

We have used the following tokens in this markup

  • [message:field_message_comment_ref:entity:entity:url:path] - The URL to the original article.
  • [message:field_message_comment_ref:entity:author:name] - The name of the user who created the comment.
  • [message:field_message_comment_ref:entity:entity:title] - The title of the original article.

Once passed through the token replace feature this text should read something like this:

someuser commented on A New Article

With the entire text of the message being a link to the article where the comment was posted.

This is all we need in terms of the message templates, they don't do anything on their own, but are useful when created by other modules. Let's look at how we can generate a message for a user using this template.

Creating An Action

In order to connect the event with the message we need an action plugin. This plugin will accept the object that is at the heart of the notification and generate a message entity based on a message template. We can then use the ECA module to run this action when a certain event is triggered on the site.

The code has a number of checks involved here:

  • If the object passed to this action isn't a comment then something went wrong and we don't create the notification.
  • If the comment entity is an orphan (it has no parent entity) then do nothing as well. This might happen if we perform a migration and the comment entity is created before it can be linked to a page of content.
  • If the user who wrote the original page of content is also the user who wrote the comment then don't send the notification. This is in order to prevent unwanted noise as user's comment on their own articles already know they are doing that.

Here is the PHP class for the action plugin. This lives in a directory called src/Plugins/Action within a custom module. I have added comments to show what each part of the code is doing.

<?php

namespace Drupal\notifications\Plugin\Action;

use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\message\Entity\Message;
use Drupal\Core\Access\AccessResult;

/**
 * Creates a message.
 *
 * @Action(
 *   id = "notification_action_comment_created",
 *   label = @Translation("Comment is created"),
 *   type = "comment"
 * )
 */
class CommentCreated extends ActionBase {

  /**
   * {@inheritDoc}
   */
  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
    // The ECA module will perform an access check on the action, so we need
    // to ensure that all users are able to perform this action.
    $result = AccessResult::allowed();
    return $return_as_object ? $result : $result->isAllowed();
  }

  /**
   * {@inheritDoc}
   */
  public function execute($comment = NULL) {
    if (empty($this->configuration['comment'])) {
      $this->configuration['comment'] = $comment;
    }

    if ($comment === NULL) {
      // The comment entity hasn't been set, so we ignore the action.
      return;
    }

    if ($comment->getCommentedEntity() === NULL) {
      // This is an orphan comment, which might happen during migration.
      return;
    }

    // The owner of the message is the person who published the original
    // node being commented on.
    $user = $comment->getCommentedEntity()->get('uid')->referencedEntities()[0];
    $ownerUid = $user->id();

    // First, check to make sure we aren't notifying a user about their own
    // comment.
    if ($ownerUid === $comment->get('uid')->target_id) {
      return;
    }

    // Create the message entity, the owner of which is the person receiving
    // the notification.
    $message = Message::create([
      'template' => 'new_comment_created',
      'uid' => $ownerUid,
    ]);
    $message->set('field_message_comment_ref', $comment);
    $message->set('langcode', 'en');
    $message->save();
  }

}

Note that the key part here is that we are creating the message from the template "new_comment_created" and making the author of the article the owner of that message. This means that the message "belongs" to the correct user and they can view or delete it as they need.

Of course, this action doesn't do anything on its own, so let's create an ECA action that will run this action plugin.

The ECA Workflow

The ECA workflow for this event is pretty simple. We just need to have an event that is triggered when a comment is created, which then runs the comment is created notification action.

Here's a screenshot of the ECA workflow.

An ECA workflow showing a created comment generates a notification.

To create this workflow you need to add a start event that will listen to the comment being created. The template used here is "Insert content entity" and the type of entity you need to select is "Comment". You can drill down further if you want to restrict the type of comment being inserted.

Your ECA dialog should look something like this.

Drupal notifications, showing the ECA interface for listening for inserting a comment.

The notification is a task, and we want to apply the template "Notify author of the comment creation". Everything else is handled by the notification action we created earlier.

Drupal notifications, showing the ECA interface for notifying the user using our custom action.

With this ECA workflow in place a message is created when a user comments on another user's content.

I did consider making this more complex, with different events triggering different events. I think, however, that this way of doing things allows for a very modular notification system to be created. Plus, it also means that the action classes (like the CommentCreated action above) can be single responsibility classes. This means that we can also test the action classes and be more certain they work as intended.

There is also an argument here to move some of the checks added to the CommentCreated action to the ECA workflow, which is certainly possible to do this since the ECA module gives the ability to add checks to the workflow. Whilst this is possible, I added the checks directly to the CommentCreated action plugin as they could then be part of the unit testing of the action. It's down to personal preference really.

Notifications Page

In order to show the user what notifications they have received it is a good idea to create a page will all the notifications for that user. The simplest way of doing this is with the Views module as the messages module interacts well with views.

To setup a view you just need to ensure that you create a view using the messages as a base and then add the current user as a contextual filter. This means that when a user visits the page they will see the messages that belong to them. By ordering the notifications by descending created date we show the latest notifications first.

The setup of a Drupal notifications view, showing the fields added and the sort order by created date descending.

If we visit the /notifications page as a logged in user we will see something like the following (assuming that there are any messages to render).

The previously described notifications view, showing that some user has commented three times.

This is as simple as it gets. The notifications page we have created here can be adapted or styled however you would like. It can even be changed to a table view for a more administrative view of the user's notifications.

Read Status For Notifications

Once a notification has been seen it should be flagged as being read. This is a standard piece of functionality that allows the users to keep track of what is new and what they have already seen in the notifications list. This read status can also be used to generate counts of the number of unread messages, which shows users how many news messages they have.

There are a couple of ways in which this effect can be achieved, but perhaps the best approach is to use the Flag module to do this. The Flag module gives us the ability to add arbitrary flags against any entity in the site, including messages. As such we can add a flag as metadata to the message, which will show if the message has been read or not.

To install the Flag module we just need to require it with composer and when install it with Drush.

composer require drupal/flag
drush en flag

How you setup the flags is down to preference. You can either add a flag as the message is generated to show that it's "unread", or add a flag to the message to say that it has been "read" after the user has interacted with it. The flag should always be created as a personal flag, as it needs to belong to the same user who the message belongs to.

My preference is to set the flag on the message after the user has read it. This keeps the message creation nice and simple as it means we don't need to add additional steps to add the flags at the same time as the message. This read status is then used to influence the interface and show the read/unread status to the user as they view their notifications stream.

In this case we would create a flag called "message_read" and attach this to the message after the user has read them. The following code would be used to generate the flag in this way.

$flagging = $flaggingStorage->create([
  'flag_id' => 'message_read',
  'entity_type' => 'message',
  'entity_id' => $message->id(),
  'uid' => $user->id(),
  'created' => time(),
  'session_id' => NULL,
]);
$flagging->save();

How you go about triggering the read status flag is up to you. I have had success in using a click event to trigger an ajax callback to mark the notification as read as the user click on it. Once the ajax callback has completed it's job the user is taken to the page that the notification is referencing. This takes a little bit of code to get working, and is complex enough to be its own article, but if there is some interest in this I will do a follow-on article looking at this aspect.

With the flag in place we can modify our original notifications view to incorporate this data, this can be as simple as adding a the read status to the notifications list.

A modified notifications page, showing the read/unread status of the notifications.

Also a good idea here is to order the notifications by their flag status first, and then by their created date. This will show the unread notifications at the top of the list, followed by the read notifications below, with each part of the list ordered by the created date.

Purging Notifications

Once we have the notifications being created some thought should be taken to when those messages are deleted. The Message module has thought about this and it has a purge system built in that you can configure to clear out messages from your database. This is the configuration interface for messages purge settings.

The message module purge settings form, showing the ability to purge message once a quota has been reached and the message is a certain number of days old.

Note that these are the default settings from the Messages module; you will likely want to set these a lot higher depending on the system you are running. The "Quota" method is quite heavy handed and will simply delete any message once the message table reaches a certain size. 

Interestingly, these settings are driven by a plugin system and so you can create your own MessagePurge plugin to change how messages are deleted. By default, the Message module treats the messages created as a single collection of messages, but you might want to treat them on a per user basis so that users with a small number of notifications don't get their messages purged through the action of users with large numbers of notifications.

Purge actions can also be configured on individual messages, which is useful if you want your more important notifications to bypass this purge process.

Further Improvements

We don't need to stop here as there are a number of other improvements we could make to this system. As we have used the Message module to create the messages for the notifications we can also use the community of other Message modules to add extra functionality to the site.

For example, we could use the Message Digest module to create a weekly list of any new notifications that the user has had. This will be sent as an email to the user and will allow them to return to the site to see what the detail of the notifications are.

It's also important to remember that not all users want to receive notifications for everything, so it's a good idea to give users the ability to opt out of notifications, or at least, some notification types.

Conclusion

We now have a full working notifications system, built with just a few modules and some custom code. The custom code is an action plugin to generate the message and some code to add a flag status to the message, all the rest of this uses the installed modules and core Drupal functionality. Any extra functionality required can be added using the community ecosystem of modules around the Message module.

Once you have the basics of this system in place you can expand the notifications to include all manner of different actions. The example above has been creating a notification for when a comment is created, but we could extend this to include when an item of content is liked, or when content makes its way through the workflow system and is published. Be careful not to overwhelm the user with lots of notifications, or, if you have lots, then give them the ability to turn off some of them to reduce the amount of noise generated by the system.

Please let me know if you would like me to collect the configuration and code together and package it as a notifications module on Drupal.org. The site I created this system for had some very specific requirements, but if this gets enough interest I will look at creating a more generic set of actions that can be used to notify users of different events. Comment below or contact me through the site contact form to let me know!

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
2 + 8 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.