Full example of websocket application working with a RestApi

Example of a fully working rest api working together with a websocket server.

Note: there is a Docker image for this example. Check zareba_pawel/php-websockets-sandstone.

Use case

We want a rest api endpoint to post articles, and a multichannel chat where we can talk, and be notified when an article is published.

How to do it with Sandstone:

Rest Api stack: A rest api will allow to POST articles by calling POST rest-api.php/api/articles, then the rest api will respond with a http status CREATED.
Websocket stack: In a same way, a multichannel chat server in mounted by running php websocket-server.php, we can enter and publish message to channels (i.e channel/general).
Push server: When someone will publish an article using rest api, an event will be sent to websocket server thread using Push server, and this event will be publish to all chat channels to notice chat users that an article has just been published.

Installation

Create your composer.json:

composer.json

json
{
    "require": {
        "eole/sandstone": "2.0.x"
    },
    "autoload": {
        "psr-4": {
            "": "src/"
        }
    }
}

Then run composer update.

Project structure

Silex\Application

Eole\Sandstone\Application

src/MyApp.php

php
class MyApp extends Eole\Sandstone\Application
{
    public function __construct()
    {
        parent::__construct([
            'debug' => true,
        ]);

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

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

        // Register Push Server and ZMQ bridge extension
        $this->register(new \Eole\Sandstone\Push\ServiceProvider());
        $this->register(new \Eole\Sandstone\Push\Bridge\ZMQ\ServiceProvider(), [
            'sandstone.push.server' => [
                'bind' => '127.0.0.1',
                'host' => '127.0.0.1',
                'port' => 5555,
            ],
        ]);

        // Register serializer metadata
        $this['serializer.builder']->addMetadataDir(
            __DIR__,
            ''
        );
    }
}

rest-api.php

php
require_once 'vendor/autoload.php';

use Symfony\Component\HttpFoundation\JsonResponse;

$app = new MyApp();

// Creating an api endpoint at POST api/articles
$app->post('api/articles', function () use ($app) {
    // Persisting article...
    $articleId = 42;

    $event = new ArticleEvent();

    $event->id = $articleId;
    $event->title = 'Unicorns spotted in Alaska';
    $event->url = 'http://unicorn.com/articles/unicorns-spotted-alaska';

    // Dispatch an event on article creation
    $app['dispatcher']->dispatch('article.created', $event);

    return new JsonResponse($articleId, 201);
});

// Send all 'article.created' events to push server
$app->forwardEventToPushServer('article.created');

$app->run();

websocket-server.php

php
require_once 'vendor/autoload.php';

$app = new MyApp();

// 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 start websocket server
$websocketServer = new Eole\Sandstone\Websocket\Server($app);

$websocketServer->run();

Implement ChatTopic class

In this example, we used a ChatTopic class.

In our case, we want:

  • ChatTopic just broadcast all incoming chat messages to every subscribers,
  • broadcast a message when an article has been posted.

So we need to listen to article.created event:
The topic class has to implement Symfony\Component\EventDispatcher\EventSubscriberInterface to automatically listen to events (to be used with forwardEventToPushServer).

So let's implement ChatTopic class:

src/ChatTopic.php

php
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

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

    /**
     * Subscribe to article.created event.
     *
     * {@InheritDoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            'article.created' => 'onArticleCreated',
        ];
    }

    /**
     * Article created listener.
     *
     * @param ArticleEvent $event
     */
    public function onArticleCreated(ArticleEvent $event)
    {
        $this->broadcast([
            'type' => 'article_created',
            'message' => 'An article has just been published: '.$event->title.', read it here: '.$event->url,
        ]);
    }
}

About serialization

When an object is sent to websocket server from rest api stack, or is sent by rest api response to http client, Sandstone uses JMS serializer to serialize it at one side, and deserialize at the other side.

In this example, an instance of ArticleEvent is sent to websocket server.

So this is how the class and its serializer metadata look like:

src/ArticleEvent.php

php
use Symfony\Component\EventDispatcher\Event;

class ArticleEvent extends Event
{
    /**
     * @var int
     */
    public $id;

    /**
     * @var string
     */
    public $title;

    /**
     * @var string
     */
    public $url;
}

src/ArticleEvent.yml

yaml
ArticleEvent:
    exclusion_policy: NONE
    properties:
        id:
            type: integer
        title:
            type: string
        url:
            type: string

That's why we needed to add in MyApp:

src/MyApp.php

// Register serializer metadata
$this['serializer.builder']->addMetadataDir(
    __DIR__,
    ''
);

Now, let's run the thing

The rest api endpoint will be hit with:

POST rest-api.php/api/articles

HTTP/1.1 201 Created
Content-Length: 2
Content-Type: application/json

42

Run the websocket server and push server with:

php websocket-server.php

# Websocket server is starting
[info] Initialization... []
[info] Bind websocket server {"bind":"0.0.0.0","port":"25569"}
[info] Bind push server {"bind":"127.0.0.1","host":"127.0.0.1","port":5555}

# Someone connects to websocket server
[info] Connection event {"event":"open"}
[info] Authentication... []
[info] Anonymous connection []

# the connected guy subscribes to a chat topic
[info] Topic event {"event":"subscribe","topic":"chat/general"}

# Websocket server received an event from rest api
[info] Push message event {"event":"article.created"}