Search code examples
phplaravelnotificationspusherratchet

Laravel + Ratchet: Pushing notifications to specific clients


I am new to websockets and I want to implement such service in my Laravel application. I have already read several posts/pages about this topic, but none explains what I need to do. All of them show how to create an "Echo" websocket server, where the server only responds to messages received from clients, which is not my case.

As a starting base I used the code provided at:

https://medium.com/@errohitdhiman/real-time-one-to-one-and-group-chat-with-php-laravel-ratchet-websocket-library-javascript-and-c64ba20621ed

Where the websocket server is ran from the command line or another console. The server has its own class to define it and imports the WebSocketController class (MessageComponentInterface), which contains the classic WebSocket server events (onOpen, onMessage, onClose, onError).

All that works fine as is but, how can I "tell" the WebSocket Server to send a message to a specific connection (client) from another class, which also belong to another namespace?. This is the case of a notification or event, where new web content must be sent to that specific client. There are no subscriptions nor publications on the way from the client side.

As @Alias asked in his post Ratchet PHP - Push messaging service I obviously cannot create a new instance of the Websocket server or its events management class, so what would be the best approach to send content or messages to the client?

As you all can see, the communication is only in one way: from the WebSocket Server to the client(s) and not the opposite. I already have a notification class and a listener class prepared for this but, I still don't know how to address the communication with clients from the handle() method:

namespace App\Listeners;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Events\NotificationSent;
use Illuminate\Queue\InteractsWithQueue;

class LogNotification
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
       //
    }

    /**
     * Handle the event.
     *
     * @param  NotificationSent  $event
     * @return void
     */
    public function handle(NotificationSent $event)
    {
       // Can content or message be sent to the client from here? how?
    }
}

Solution

  • Ok, after studying and researching a lot, I could post an answer to a similar question in:

    How to send a message to specific websocket clients with symfony ratchet?

    This is the solution I found, according to what I wrote in my second comment in this thread.

    I installed cboden/Ratchet package for the websocket server using composer. I needed to send notifications to a user/group of users, or update the UI, when an event is fired in the backend.

    What I did is this:

    1) Installed amphp/websocket-client package using composer.

    2) Created a separated class in order to instantiate an object that could get connected to the websocket server, send the desired message and disconnect:

    namespace App;
    use Amp\Websocket\Client;
    
    class wsClient {
    
       public function __construct() {
          //
       }
    
    
       // Creates a temporary connection to the WebSocket Server
       // The parameter $to is the user name the server should reply to.
       public function connect($msg) {
          global $x;
          $x = $msg;
          \Amp\Loop::run(
             function() {
                global $x;
                $connection = yield Client\connect('ws://ssa:8090');
                yield $connection->send(json_encode($x));
                yield $connection->close();
                \Amp\Loop::stop();
             }
          );
       }
    }
    

    3) The onMessage() event, in the handler class of the webSocket server, looks like this:

       /**
        * @method onMessage
        * @param  ConnectionInterface $conn
        * @param  string              $msg
        */   
       public function onMessage(ConnectionInterface $from, $msg) {
          $data = json_decode($msg);
          // The following line is for debugging purposes only
          echo "   Incoming message: " . $msg . PHP_EOL;
          if (isset($data->username)) {
             // Register the name of the just connected user.
             if ($data->username != '') {
                $this->names[$from->resourceId] = $data->username;
             }
          }
          else {
             if (isset($data->to)) {
                // The "to" field contains the name of the users the message should be sent to.
                if (str_contains($data->to, ',')) {
                   // It is a comma separated list of names.
                   $arrayUsers = explode(",", $data->to);
                   foreach($arrayUsers as $name) {
                      $key = array_search($name, $this->names);
                      if ($key !== false) {
                         $this->clients[$key]->send($data->message);
                      }
                   }
                }
                else {
                   // Find a single user name in the $names array to get the key.
                   $key = array_search($data->to, $this->names);
                   if ($key !== false) {
                      $this->clients[$key]->send($data->message);
                   }
                   else {
                      echo "   User: " . $data->to . " not found";
                   }
                }
             } 
          }
    
          echo "  Connected users:\n";
          foreach($this->names as $key => $data) {
             echo "   " . $key . '->' . $data . PHP_EOL;
          }
       }
    

    As you can see the user(s), you want the websocket server to send the message to, are specified as a string ($data->to) in the $msg parameter along with the message itself ($data->message). Those two things are JSON encoded so that the parameter $msg can be treated as an object.

    4) On the client side (javascript in a layout blade file) I send the user name to the websocket server when the client connects:

    var currentUser = "{{ Auth::user()->name }}";
    socket = new WebSocket("ws://ssa:8090");
    
    socket.onopen = function(e) {
       console.log(currentUser + " has connected to websocket server");
       socket.send(JSON.stringify({ username: currentUser }));
    };
    
    socket.onmessage = function(event) {
       console.log('Data received from server: ' + event.data);
    };
    

    So, the user name and its connection number are saved in the websocket server.

    5) The onOpen() method in the handler class looks like this:

       public function onOpen(ConnectionInterface $conn) {
          // Store the new connection to send messages to later
          $this->clients[$conn->resourceId] = $conn;
          echo " \n";
          echo "  New connection ({$conn->resourceId}) " . date('Y/m/d h:i:sa') . "\n";
       }
    

    Every time a client gets connected to the websocket server, its connection number or resourceId is stored in a array. So that, the user names are stored in an array ($names), and the keys are stored in another array ($clients).

    6) Finally, I can create an instance of the PHP websocket client (wsClient) anywhere in my project to send whatever data to any user(s):

    public function handle(NotificationSent $event) {
        $clientSocket = new wsClient();
        $clientSocket->connect(array('to'=>'Anatoly,Joachim,Caralampio', 'message'=>$event->notification->data));
    }
    

    In this case I am using the handle() method of a notification event Listener.

    Alright, this is for anyone wondering how to send messages from the PHP websocket server (AKA echo server) to one specific client or to a group of clients.