Setting up a Notifications System in Symfony Projects

Symfony Case Studies Events Wiki Grossum Possum

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.

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. Text messages (short message service, text messaging between mobile device users ): TurboSMS - a Ukrainian SMS sender service Email (messaging between computer device users): Mandrill is an email marketing service (that was merged with MailChimp) 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.

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...

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! Describe your project to us

Author: Grossum Possum Grossum Possum. He loves using Symfony2 for his projects and learning to implement Symfony3. He also likes developing web and mobile applications using other programming languages and frameworks, such as PHP and Java. In his free time, Grossum Possum likes to write about his experiences in the blog. Grossum Possum. He loves using Symfony2 for his projects and learning to implement Symfony3. He also likes developing web and mobile applications using other programming languages and frameworks, such as PHP and Java. In his free time, Grossum Possum likes to write about his experiences in the blog.