Chat server with PHP and websockets

This example mount a multichannel chat server using Sandstone topic creation feature.

Server script

You will first need to create the topic class.

This class must extends Eole\Sandstone\Websocket\Topic, and then implements these methods:

  • onSubscribe: called when someone join this topic
  • onPublish: called when someone publish a message to this topic
  • onUnSubscribe: called when someone unsubscribes

The logic behing broadcasting messages to all subscribing client of a topic is a logic that differs depending on what you want to implement.

In our case, the logic would be:

  • onSubscribe: I want to receive message from this topic
  • onPublish: I send a message on this topic
  • onUnSubscribe: I don’t want to receive messages from this topic anymore

In other hand, the Eole\Sandstone\Websocket\Topic class provides a method to broadcast a message to every subscribing client, example:

$this->broadcast([
    'type' => 'message',
    'message' => $event,
]);

Then let’s create the ChatTopic class:

ChatTopic.php

php
class ChatTopic extends Eole\Sandstone\Websocket\Topic
{
    /**
     * Broadcast message to each subscribing client.
     *
     * {@InheritDoc}
     */
    public function onPublish(Ratchet\Wamp\WampConnection $conn, $topic, $event)
    {
        $this->broadcast([
            'type' => 'message',
            'message' => $event,
        ]);
    }

    /**
     * Notify all subscribing clients that a new client has subscribed to this channel.
     *
     * {@InheritDoc}
     */
    public function onSubscribe(Ratchet\Wamp\WampConnection $conn, $topic)
    {
        parent::onSubscribe($conn, $topic);

        $this->broadcast([
            'type' => 'join',
            'message' => 'Someone has joined this channel.',
        ]);
    }

    /**
     * Notify all subscribing clients that a new client has unsubscribed from this channel.
     *
     * {@InheritDoc}
     */
    public function onUnSubscribe(Ratchet\Wamp\WampConnection $conn, $topic)
    {
        parent::onUnSubscribe($conn, $topic);

        $this->broadcast([
            'type' => 'leave',
            'message' => 'Someone has left this channel.',
        ]);
    }
}

This class will be used for a chat topic, i.e general or technical.

To create one topic, this could do the job:

$app->topic('chat/general', function ($topicPattern) {
    return new ChatTopic($topicPattern);
});

What will happens here:

When a client subscribes to chat/general topic, Sandstone will call your callback to get a new instance of your ChatTopic, and provides the string "chat/general" to your instance.

This same instance will be used for each future client subscribing to this topic.

In the same way, a message Someone has joined this channel. will be broadcasted to all subscribing clients on new subscriptions.

When someone send a message, it is just broadcasted to every subscribing clients.

But we don’t want to only one channel, we want to allow to create dynamic channels:

$app->topic('chat/{channel}', function ($topicPattern) {
    return new ChatTopic($topicPattern);
});

This works the same way as Silex routes.

Note: See more at Topic route parameters to know how to retrieve topic route arguments.

So let’s create the server script:

chat-server.php

php
require_once 'vendor/autoload.php';
require_once 'ChatTopic.php';

// Instanciate Sandstone
$app = new Eole\Sandstone\Application();

// Sandstone requires JMS serializer
$app->register(new Eole\Sandstone\Serializer\ServiceProvider());

// Register and configure your websocket server
$app->register(new Eole\Sandstone\Websocket\ServiceProvider(), [
    'sandstone.websocket.server' => [
        'bind' => '0.0.0.0',
        'port' => '25569',
    ],
]);

// Add a route `chat/{channel}` and its Topic factory (works same as mounting API endpoints)
$app->topic('chat/{channel}', function ($topicPattern) {
    return new ChatTopic($topicPattern);
});

// Encapsulate your application and run websocket server
$websocketServer = new Eole\Sandstone\Websocket\Server($app);

$websocketServer->run();

Now the chat server works.

How to test it with javascript

There is a Javascript library, Autobahn|JS, which provides an implementation of the WAMP protocol.

Note: Be careful to use the 0.8.x version of the library in order to work with WAMP v1.

Following the documentation, we can use our chat with:

front-end.html

<html>
    <body>
        <script src="http://autobahn.s3.amazonaws.com/js/autobahn.min.js"></script>
        <script>

            /**
             * Called when connected to chat server.
             *
             * @param {wampSession} session Object provided by autobahn to subscribe/publish/unsubscribe.
             */
            function onSessionOpen(session) {
                // Subscribe to 'chat/general' topic
                session.subscribe('chat/general', function (topic, event) {
                    console.log('message received', topic, event);
                });

                // Publish a message to 'chat/general' topic
                session.publish('chat/general', 'Hello friend !');
            }

            /**
             * Called on error.
             *
             * @param {Integer} code
             * @param {String} reason
             * @param {String} detail
             */
            function onError(code, reason, detail) {
                console.warn('error', code, reason, detail);
            }

            // Connect to chat server
            ab.connect('ws://localhost:25569', onSessionOpen, onError);
        </script>
    </body>
</html>

Then, run your chat server:

php chat-server.php

And go to this page. You should receive chat messages in your Javascript console.