Setting up a Notifications System in Symfony Projects

December 15th, 2016

Symfony / Case Studies / Events / Wiki // Myroslav

Setting Up a Notifications System in Symfony Projects

This information was initially presented at the #SymfonyCafeKyiv in December ‘16 by Myroslav Berlad, a Grossum software developer.

Notifications are a convenient thing for any kind of service (unless the system has been created to spam and annoy everyone). It’s good for users to know what’s new, whether there are pending requests or incoming messages, etc.

As the developers’ team has been working on a project, a need for a notification system arose.

We needed four types of notifications and the team agreed that writing our own solutions would take too much time, so we decided to use proven vendor API solutions.

  1. Push notifications (a message that pops up on a mobile device): GCM (google cloud messaging) - a mobile notification service developed by Google that enables third-party application developers to send notification data or information from developer-run servers to applications.
  2. Text messages (short message service, text messaging between mobile device users ): TurboSMS - a Ukrainian SMS sender service
  3. Email (messaging between computer device users): Mandrill is an email marketing service (that was merged with MailChimp)
  4. Web notifications & dynamic interface updates (targeted website user notifications / interface updates): Socket.io is a JavaScript library for real-time web applications. It enables real-time, bidirectional communication between web clients and servers. It has two parts: a client-side library that runs in the browser, and a server-side library for node.js.

How to implement a notification service?

There were two ways of doing this:

  • Symfony way: get some bundles, configure them, live happily ever after.
  • Our way: get some troubles. Cry.

How to do it Symfony-way?

A brief structure of our project’s architecture looks like this:

PUSH:

We decided to use RMSPushNotifications Symfony bundle for this. It allows sending notifications/messages for mobile devices and supports such platforms as iOS, Android (C2DM, GCM), Blackberry and Windows Phone (toast only).

composer require richsage/rms-push-notifications-bundle
 
use RMS\PushNotificationsBundle\Message\iOSMessage;
 
class PushDemoController extends Controller
{
   public function pushAction()
   {
       $message = new iOSMessage();
       $message->setMessage('Oh my! A push notification!');
       $message->setDeviceIdentifier('test012fasdf482asdfd63f6d7bc6d4293aedd5fb448fe505eb4asdfef8595a7');
 
       $this->container->get('rms_push_notifications')->send($message);
 
       return new Response('Push notification send!');
   }
}

Another bundle that we have used is EndroidGCM, which is a Google Cloud Messaging bundle used with Symfony projects. 

composer require endroid/gcm-bundle
 
<?php
public function gcmSendAction()
{
   $client = $this->get('endroid.gcm.client');
 
   $registrationIds = array(
       // Registration ID's of devices to target
   );
 
   $data = array(
       'title' => 'Message title',
       'message' => 'Message body',
   );
 
   $response = $client->send($data, $registrationIds);
 
   ...
}

TEXT MESSAGE / SMS

composer require kronas/smpp-client-bundle
$smpp = $this->get('kronas_smpp_client.transmitter');
$smpp->send($phone_number, $message);

EMAIL:

composer require hipaway-travel/mandrill-bundle
 
$dispatcher = $this->get('hip_mandrill.dispatcher');
       $message = new Message();
       $message
           ->setFromEmail('mail@example.com')
           ->setFromName('Customer Care')
           ->addTo('max.customer@email.com')
           ->setSubject('Some Subject')
           ->setHtml('<html><body><h1>Some Content</h1></body></html>')
           ->setSubaccount('Project');
       $result = $dispatcher->send($message);

SOCKET:

composer require gos/web-socket-bundle
 
var webSocket = WS.connect(“ws://127.0.0.1:8000”);
webSocket.on(“socket/connect”, function(session){
    //session is an Autobahn JS WAMP session.
console .log(“Successfully Connected!”);
})
webSocket.on(“socket/disconnect”, function(error){
    //error provides us with some insight into the disconnection: error.reason and error.code
console .log(“Disconnected for ” + error.reason + “ with code ” + error.code);
})
 
/**
    * This will receive any Subscription requests for this topic.
    *
    * @param ConnectionInterface $connection
    * @param Topic $topic
    * @param WampRequest $request
    * @return void
    */
   public function onSubscribe(ConnectionInterface $connection, Topic $topic, WampRequest $request)
   {
       //this will broadcast the message to ALL subscribers of this topic.
       $topic->broadcast(['msg' => $connection->resourceId . “ has joined ” . $topic->getId()]);
   }

Project architecture thoughts:

“Okay, this looks good. However, PHP is not a very good option to choose to run server daemon on. Also, it would be nice to post notifications with the same API.”

Additional parameters that we introduced for the project:

  • Reusable solution
  • Simple API for notifications
  • No PHP running as a daemon

Our way:

When we thought about the possible path for the messages to take, the solution turned out like this:

Symfony -> Rabbit -> Node -> Target

SYMFONY APPLICATION

To integrate Symfony2 and Symfony3 and RabbitMQ, we used php-amqplib (formerly known as oldsound/rabbitmq-bundle)

DATA HOSTED WITH ♥ BY PASTEBIN.COM - DOWNLOAD RAW - SEE ORIGINAL
composer require php-amqplib/rabbitmq-bundle
 
public function indexAction($name)
{
   $msg = array('user_id' => 1235, 'image_path' => '/path/to/new/pic.png');
   $this->get('old_sound_rabbit_mq.upload_picture_producer')->publish(serialize($msg));
}
 
rabbitmq-plugins enable rabbitmq_management
rabbitmqctl add_vhost SOME_VHOST
rabbitmqctl add_user SOME_USER SOME_PASS
rabbitmqctl set_permissions -p / SOME_USER “.*” “.*” “.*”
rabbitmqctl set_permissions -p SOME_VHOST SOME_USER “.*” “.*” “.*”
 
rabbitmqctl set_user_tags SOME_USER administrator
 
rabbitmqadmin declare exchange --vhost=SOME_VHOST name=send-sms type=direct -u SOME_USER -p SOME_PASS
rabbitmqadmin declare exchange --vhost=SOME_VHOST name=send-email type=direct -u SOME_USER -p SOME_PASS
rabbitmqadmin declare exchange --vhost=SOME_VHOST name=send-push type=direct -u SOME_USER -p SOME_PASS
rabbitmqadmin declare exchange --vhost=SOME_VHOST name=send-web-push type=direct -u SOME_USER -p SOME_PASS
 
rabbitmqadmin declare queue --vhost=SOME_VHOST name=send-sms durable=true -u SOME_USER -p SOME_PASS
rabbitmqadmin declare queue --vhost=SOME_VHOST name=send-email durable=true -u SOME_USER -p SOME_PASS
rabbitmqadmin declare queue --vhost=SOME_VHOST name=send-push durable=true -u SOME_USER -p SOME_PASS
rabbitmqadmin declare queue --vhost=SOME_VHOST name=send-web-push durable=true -u SOME_USER -p SOME_PASS
 
rabbitmqadmin --vhost=SOME_VHOST binding source=send-sms destination_type=queue destination=send-sms -u SOME_USER -p SOME_PASS
rabbitmqadmin --vhost=SOME_VHOST binding source=send-email destination_type=queue destination=send-email -u SOME_USER -p SOME_PASS
rabbitmqadmin --vhost=SOME_VHOST binding source=send-push destination_type=queue destination=send-push -u SOME_USER -p SOME_PASS
rabbitmqadmin --vhost=SOME_VHOST binding source=send-web-push destination_type=queue destination=send-web-push -u SOME_USER -p SOME_PASS
 
service rabbitmq-server restart

NODEJS

For the NodeJS part, we have decided to use AMQP 0-9-1 (e.g. RabbitMQ) library and client.

// Consumer
function consumer(conn) {
 var ok = conn.createChannel(on_open);
 function on_open(err, ch) {
   if (err != null) bail(err);
   ch.assertQueue(q);
   ch.consume(q, function(msg) {
     if (msg !== null) {
       console.log(msg.content.toString());
       ch.ack(msg);
     }
   });
 }
}

SMPP

SMPP client and server implementation in node.js.

var smpp = require('smpp');
var session = smpp.connect('smpp://example.com:2775');
session.bind_transceiver({
   system_id: 'YOUR_SYSTEM_ID',
   password: 'YOUR_PASSWORD'
}, function(pdu) {
   if (pdu.command_status == 0) {
       // Successfully bound
       session.submit_sm({
           destination_addr: 'DESTINATION NUMBER',
           short_message: 'Hello!'
       }, function(pdu) {
           if (pdu.command_status == 0) {
               // Message successfully sent
               console.log(pdu.message_id);
           }
       });
   }
});

MANDRILL / MAILCHIMP

For the email integration and notifications, we have used Mandrill (merged with Mailchimp). We used a node.js wrapper for the MailChimp API.

try {
   var api = new MailChimpAPI(apiKey, { version : '2.0' });
} catch (error) {
   console.log(error.message);
}
api.call('campaigns', 'list', { start: 0, limit: 25 }, function (error, data) {
   if (error)
       console.log(error.message);
   else
       console.log(JSON.stringify(data)); // Do something with your data!
});

SOCKET

Finally, we have arrived at the socket settings. We used socket.io, a node.js real-time framework server for this. 

var server = require('http').createServer();
var io = require('socket.io')(server);
io.on('connection', function(client){
 client.on('event', function(data){});
 client.on('disconnect', function(){});
});
server.listen(3000);

CHECK THE REQUIREMENTS:

  • Same notification API for all cases - CHECK
  • No PHP running as a daemon - CHECK

But what about a reusable solution?

Welcome, Docker! https://www.docker.com/

services:
    node:
        build: docker/node
        depends_on:
            - rabbitmq
ports:
- "80:80"
- "443:443"
links:
- rabbitmq
volumes:
- ./notification-server:/var/www/notification-server
working_dir: /var/www/notification-server
 
rabbitmq:
build: docker/rabbitmq
volumes:
    - ./var/rabbitmq:/var/lib/rabbitmq
ports:
- "5672:5672"
- "15671:15671"
- "15672:15672"
docker-compose up

Using Docker, we have created a container with all the necessary settings for our project. (Read more about working with Docker and here are some practical application examples.)

Okay, now we’re meeting all three requirements.

  • Same notification API for all cases - CHECK
  • No PHP running as a daemon - CHECK
  • Reusable solution - CHECK

However, a new question arises: should a new project handle notifications in its own way?

The short answer is: No.

These thoughts have led us to the creation of a solution that can be easily shared.

We present you...

Grossum Symfony Notification Bundle

Installation is fairly simple:

composer require grossum/notification-bundle

$userNotification = new MessageNotification();

   $userNotification
       ->setType(NotificationInterface::SOCKET_NOTIFICATION_TYPE_WEB_NOTIFICATION)
       ->setContent('You have created task to demo NotificationBundle')
       ->setMediaUrl('https://pbs.twimg.com/profile_images/564783819580903424/2aQazOP3.png')
       ->setTitle('You have created task to demo NotificationBundle')
       ->setCreatedAt(new \DateTime())
       ->setRecipientHashes(['sds12']);

   $this->disptacher->dispatch(
       'grossum.notification.event.send_notification',
       new NotificationCreatedEvent($userNotification)
   );
  grossum.notification.notification_sender.email:
    class: %grossum.notification.notification_sender.email.class%
    arguments:
      - "@old_sound_rabbit_mq.send_email_producer"
 
 grossum.notification.event_listener.email_produce:
    class: %grossum.notification.event_listener.email_produce.class%
    arguments:
      - "@grossum.notification.notification_sender.email"
    tags:
      - { name: kernel.event_listener, event: grossum.notification.event.send_email, method: produceNotifications }
EmailNotificationProduceListener
/**
   * @param NotificationSenderInterface $notificationSender
   */
  public function __construct(NotificationSenderInterface $notificationSender)
  {
      $this->notificationSender = $notificationSender;
  }
interface NotificationSenderInterface
{
   /**
    * @param NotificationInterface $notification
    */
   public function sendNotification(NotificationInterface $notification);
}
/**
    * {@inheritdoc}
    */
   public function sendNotification(NotificationInterface $message)
   {
       try {
           if ($message->isValid()) {
               $this->producer->publish(json_encode($message->exportData()));
           }
       } catch (\Exception $e) {
           //TODO: add logging
       }
   }
interface NotificationInterface
{
   const SOCKET_NOTIFICATION_TYPE_ENTITY_UPDATE = 'entity_update';
   const SOCKET_NOTIFICATION_TYPE_ENTITY_DELETE = 'entity_delete';
   const SOCKET_NOTIFICATION_TYPE_CHAT_MESSAGE = 'chat_message';
   const SOCKET_NOTIFICATION_TYPE_WEB_NOTIFICATION = 'web_notification';
   const PHONE_OS_TYPE_IOS = 'phone_ios';
   const PHONE_OS_TYPE_WINDOWS = 'phone_windows';
   const PHONE_OS_TYPE_ANDROID = 'phone_android';
   /**
    * @return array
    */
   public function exportData();
   /**
    * @return bool
    */
   public function isValid();
}

Grossum Notification Server

cp docker-compose.yml.dist docker-compose.yml docker-compose up

All requirements are met.

Our plans for the future of the bundle:

  • Write unit tests :)
  • Make flexible bundle configuration
  • Refactor NodeJS server part(simple, but can be better and cleaner)
  • Split node and docker into 2 separate repositories

CURRENT STATUS

  • The solutions seems to work :)
  • 3 projects use this solution, and hope more :)
  • Team received experience with RabbitMQ
  • Team received experience with NodeJS
  • Team received experience with splitting app into separate microservices
  • Team received experience with docker

Bonus:

You can use RabbitMQ, not as a part of GrossumNotificationServer, but you can consider it as your app message bus.

"A Message Bus is a combination of a common data model, a common command set, and a messaging infrastructure to allow different systems to communicate through a shared set of interfaces. Sending a message does not require both systems to be up and ready at the same time." Read more here.

Need help with a Symfony2 or Symfony3 project? We can help!

Author: Myroslav

Myroslav is Grossum's developer with experience in Symfony2/3 framework, EmberJS, node, docker, and other. He is fluent with REST API and CLI background tasks. He loves challenging tasks in projects and is not afraid to stand up to them. He is a speaker at #SymfonyCafeKyiv and other. In his free time, he likes to play League of Legends, read books, and play guitar.

Tags Symfony Cafe Development Hacks

See all blog

x

Grossum Startup Guide:
Five Things to Consider Before Web & Mobile Development

Get the Guide