SnapShooter Backups Server, Database, Application and Laravel Backups - Get fully protected with SnapShooter

Real time notifications using Pusher in CakePHP 3

When users trigger actions that requires heavy processing power in our applications, the best practice is to put those actions into a worker queue and process them later. When they are done, we then notify users.

In this tutorial, we will demonstrate how to build the notification part of the process above. We will be using a service called Pusher, which allows us to send messages between server and client(application and browser) via WebSocket.

For demonstration purpose, we will pretend that we are building a report system. When a user clicks the "Generate Report" button, a job is sent to a worker queue for processing, when the job is completed, we will send the user a notification message.

Websocket vs Ajax

Before Websocket is implemented in major browsers. Ajax is used wildly for building real time web applications.

In case you are not aware the differences between them. We list out some key points below:

  • Websocket provides a bi-directional communication channel between user's client and server. Whereas Ajax is a uni-directional client-side polling mechanism.
  • When using ajax, we are sending requests to server periodically. Depends on the interval, it can result in large number of server hits, which can become the bottleneck easily. Whereas in websocket, a connection can last for a long time once it's established, which makes it a better fit for building real time web applications.

Pusher concept

Pusher is a web service helping develop real time web applications. The basic idea of Pusher is that, we send messages to Pusher server via its API, then Pusher will deliver those messages to our applications via websocket. Since Pusher takes care of all the infrastructure behind, it becomes very easy for us to build a real time web application.

Let's first clear some basic concepts in Pusher:

Channel:

The official definition of channel can found here. So we won't go into details here. There are three types of channels in Pusher, they are public channel, private channel and presence channel.

To help you pick up the concept quickly, we can imagine channels in Pusher as restricted groups. A channel can be accessed by everyone or certain people only depending on which type of channel we use.

Event:

The official definition of event can found here. So we won't repeat ourselves.

Event is a very common and easy to understand concept. It is similar to events in jQuery, we can attach a listener to an event and the listener will be trigger whenever the event fires. In Pusher, an event is fired via Pusher API.

Pusher client

It's time to get our hands dirty. Inevitably we will need to communicate Pusher in our application. So let's start with building our Pusher client.

Though Pusher provides an official PHP SDK, we prefer to keep our application code away from third party dependency as far as we can. To accomplish that, we can create a Client interface and provide an official PHP SDK implementation.

The Pusher client class only needs to do two things. And they are triggering Pusher events and authenticating private channel.

So we can create a Client interface as shown below:

<?php
 
namespace App\Utility\Pusher;
 
 
interface Client
{
    public function publish($channel, $event, array $data);
 
    public function authenticate($channelName, $socketId);
}

Before we create an implementation using Pusher PHP SDK, we need to install Pusher SDK source code, we can do so easily with Composer:

composer require pusher/pusher-php-server

The Pusher PHP SDK client is shown as below:

<?php
 
namespace App\Utility\Pusher;
 
 
class PusherSdkClient implements Client
{
    /**
     * @var \Pusher
     */
    private $pusher;
 
    /**
     * PusherSdkClient constructor.
     */
    public function __construct($appKey, $appSecret, $appId)
    {
        $this->pusher = new \Pusher($appKey, $appSecret, $appId);
    }
 
    public function publish($channel, $event, array $data)
    {
        return $this->pusher->trigger($channel, $event, $data);
    }
 
    public function authenticate($channelName, $socketId)
    {
        return $this->pusher->socket_auth($channelName, $socketId);
    }
 
}

As we can see, it is nothing more than a wrapper class.

Pusher credential

To access Pusher API, we need to supply the access credentials as shown in PusherSdkClient constructor.

We should never supply the credentials directly to the PusherSdkClient class. That way, our credentials will be exposed to anyone who gains access to the source code.

In CakePHP 3, we can store the credentials in a local configuration file (config/app_loca.php). Since it will be ignored by version control system, we are safe.

Let's create the Pusher API access credentials in config/app_local.php file as show below:

<?php
return [
    'Pusher' => [
        'appKey' => 'your-pusher-app-key',
        'appSecret' => 'your-pusher-app-secret',
        'appId' => 'your-pusher-app-id'
    ]
];

Pusher Client API

To listen to Pusher events sent by Pusher server, we need to create a JavaScript Pusher object that listens to Pusher events and reacts to them via JavaScript anonymous functions.

For demonstration purpose, we will simply alert a message containing a URL for downloading report from server.

<?php use Cake\Core\Configure; ?>
 
<script src="https://code.jquery.com/jquery-2.2.0.min.js"></script>
<script src="https://js.pusher.com/3.0/pusher.min.js"></script>
<script>
    var pusher = new Pusher('<?= Configure::read('Pusher.appKey');?>', {
    encrypted: true,
    authEndpoint: '<?= $this->Url->build(['controller' => 'Reports', 'action' => 'auth']);?>'
    });
     
    var channel = pusher.subscribe('private-account-1');
     
    channel.bind('report-ready', function (data) {
        alert('Your report is ready, download it from: ' + data.url);
    });
</script>

As you can see, we have kept the code to the minimum. First a Pusher object is created, then it creates a Channel object by subscribing to a private Pusher channel. At last we bind a "report-ready" listener to the Channel object.

Authenticating user

One thing we did not clear out in previous step is the authentication part. This is where we can put our custom authentication logic. For example, we can check if the login user has access to the Pusher channel he is requesting.

In previous step, we set the authentication end point to Reports::auth action, let's implement the action.

We will use PusherSdkClient class in ReportsController, the simplest way is to include via initialize method:

public function initialize()
{
    $this->pusherClient = new PusherSdkClient(
        Configure::read('Pusher.appKey'),
        Configure::read('Pusher.appSecret'),
        Configure::read('Pusher.appId')
    );
}

Next we implement the auth action. In auth action, we can inject our custom authentication logic. To reject a request, we can simply throw an exception.

For demonstration purpose, we will allow access to everyone.

public function auth()
{
    $this->autoRender = false;
 
    //custom authentication logic
 
    $result = $this->pusherClient->authenticate(
        $this->request->data('channel_name'),
        $this->request->data('socket_id')
    );;
 
    $this->response->type('json');
    $this->response->body($result);
}

Publishing events

This is the last step. We will publish a report-ready event from server.

Practically this event will be published by a job queue handler when a job is completed. We will skip the job queue part in this tutorial and show how to publish events only.

Publishing an event is straightforward since we have already created the Pusher client:

$this->pusherClient->publish(
    'private-account-1',
    'report-ready',
    [
        'url' => 'http://fake_url_to_download_pdf'
    ]
);

The End

In this tutorial, we demonstrate how to use Pusher in CakePHP 3 to build a real time notification system. Pusher utilises Websocket to send data between server and client's browsers, it is a very efficient way to build a real time application.