Push to an Existing Site
Enhance your existing work without wide-spread changes.

The Problem

The previous tutorials were cool and all, but their focus was creating a long lived application where users interacted with each other entirely over WebSockets with no persistence. That's a lot of work to incorporate into an already existing site. Code would need to be ported out of your repository and into a new Ratchet application. A whole new testing phase would need to happen to make sure previously functional pages still work.

Goal

When a user, be it yourself in your admin or a user posting a comment on your blog, does a POST through a form submission or AJAX we want that change to immediately be pushed to all other visitors on that page. We will add real-time updates to our site without disrupting your code base or affecting its current stability.

For this tutorial we're going to pretend (reads: boilerplate code missing) you're publishing a blog article on your web site and the visitors will see the story pop up as soon as you publish it.

Network Architecture

  1. Step 1

    A client makes a request and receives a response from the web server and renders the page. It then establishes an open WebSocket connection (client 2 and 3 do the same thing).

  2. Step 2

    Client 1 does a POST back, through form submission or AJAX, to the web server. (Notice the still open WebSocket connection)

  3. Step 3

    While the server is handling the POST request (saving to database, etc) it sends a message directly to the WebSocket stack with a ZeroMQ transport.

  4. Step 4

    The WebSocket stack handles the ZeroMQ message and sends it to the appropriate clients through open WebSocket connections. The web browsers handle the incoming message and update the web page with Javascript accordingly.

This workflow is unobtrusive and it's easy to introduce into existing web sites. The only changes to the site are adding a bit of ZeroMQ to the server and a Javascript file on the client to handle incoming message from the WebSocket server.

Requirements

ZeroMQ

To communicate with a running script it needs to be listening on an open socket. Our application will be listening to port 8080 for incoming WebSocket connections...but how will it also get updates from another PHP script? Enter ZeroMQ. We could use raw sockets, like the ones Ratchet is built on, but ZeroMQ is a library that just makes sockets easier.

ZeroMQ is a library (libzmq) you will need to install, as well as a PECL extension for PHP bindings. Installation is easy and is provided for many operating systems on their web site.

React/ZMQ

Ratchet is a WebSocket library built on top of a socket library called React. React handles connections and the raw I/O for Ratchet. In addition to React, which comes with Ratchet, we need another library that is part of the React suite: React/ZMQ. This library will bind ZeroMQ sockets to the Reactor core enabling us to handle both WebSockets and ZeroMQ sockets. To install, your composer.json file should look like this:

{
    "autoload": {
        "psr-4": {
            "MyApp\\": "src"
        }
    },
    "require": {
        "cboden/ratchet": "0.4.*",
        "react/zmq": "0.2.*|0.3.*"
    }
}

Start your coding

Let's get to some code! We'll start by stubbing out our class application. We're going to useWAMP for its ease of use with the Pub/Sub pattern. This will allow clients to subscribe to updates on a specific page and we'll only push updates to those who have subscribed.

<?php
namespace MyApp;
use Ratchet\ConnectionInterface;
use Ratchet\Wamp\WampServerInterface;

class Pusher implements WampServerInterface {
    public function onSubscribe(ConnectionInterface $conn, $topic) {
    }
    public function onUnSubscribe(ConnectionInterface $conn, $topic) {
    }
    public function onOpen(ConnectionInterface $conn) {
    }
    public function onClose(ConnectionInterface $conn) {
    }
    public function onCall(ConnectionInterface $conn, $id, $topic, array $params) {
        // In this application if clients send data it's because the user hacked around in console
        $conn->callError($id, $topic, 'You are not allowed to make calls')->close();
    }
    public function onPublish(ConnectionInterface $conn, $topic, $event, array $exclude, array $eligible) {
        // In this application if clients send data it's because the user hacked around in console
        $conn->close();
    }
    public function onError(ConnectionInterface $conn, \Exception $e) {
    }
}

Save this in /src/MyApp/Pusher.php. We just made the methods required forWAMP and made sure no one tries to send data, closing the connection if they do. We're making a push application and not accepting any incoming messages from WebSockets, those will all be coming from AJAX.

Editing your blog submission

Next we're going to add a little ZeroMQ magic into your existing web site's code where you handle a new blog post. The code here may be a little basic and archaic compared to the advanced architecture your actual blog is, sitting on Drupal or WordPress, but we're focusing on the fundamentals.

<?php
    // post.php ???
    // This all was here before  ;)
    $entryData = array(
        'category' => $_POST['category']
      , 'title'    => $_POST['title']
      , 'article'  => $_POST['article']
      , 'when'     => time()
    );

    $pdo->prepare("INSERT INTO blogs (title, article, category, published) VALUES (?, ?, ?, ?)")
        ->execute($entryData['title'], $entryData['article'], $entryData['category'], $entryData['when']);

    // This is our new stuff
    $context = new ZMQContext();
    $socket = $context->getSocket(ZMQ::SOCKET_PUSH, 'my pusher');
    $socket->connect("tcp://localhost:5555");

    $socket->send(json_encode($entryData));

After we logged your blog entry in the database we've opened a ZeroMQ connection to our socket server and delivered a serialized message with the same information. (note: please do proper sanitization, this is just a quick and dirty example)

Handling ZeroMQ messages

Let's go back to our application stub class. As we left it, it was only handling WebSocket connections. As you saw in our last code snippet, we opened a connection to localhost on port 5555 that we sent data to. We're going to add handling for that ZeroMQ message as well as re-sending it to our WebSocket clients.

<?php
namespace MyApp;
use Ratchet\ConnectionInterface;
use Ratchet\Wamp\WampServerInterface;

class Pusher implements WampServerInterface {
    /**
     * A lookup of all the topics clients have subscribed to
     */
    protected $subscribedTopics = array();

    public function onSubscribe(ConnectionInterface $conn, $topic) {
        $this->subscribedTopics[$topic->getId()] = $topic;
    }

    /**
     * @param string JSON'ified string we'll receive from ZeroMQ
     */
    public function onBlogEntry($entry) {
        $entryData = json_decode($entry, true);

        // If the lookup topic object isn't set there is no one to publish to
        if (!array_key_exists($entryData['category'], $this->subscribedTopics)) {
            return;
        }

        $topic = $this->subscribedTopics[$entryData['category']];

        // re-send the data to all the clients subscribed to that category
        $topic->broadcast($entryData);
    }

    /* The rest of our methods were as they were, omitted from docs to save space */
}

Tying it all together Creating our executable

So far we've covered all the logic of sending, receiving, and handling messages. Now, we're going to bind it all together and create our executable script that manages everything. We're going to build our Ratchet application with I/O, WebSockets, Wamp, and ZeroMQ components and run the event loop.

<?php
    require dirname(__DIR__) . '/vendor/autoload.php';

    $loop   = React\EventLoop\Factory::create();
    $pusher = new MyApp\Pusher;

    // Listen for the web server to make a ZeroMQ push after an ajax request
    $context = new React\ZMQ\Context($loop);
    $pull = $context->getSocket(ZMQ::SOCKET_PULL);
    $pull->bind('tcp://127.0.0.1:5555'); // Binding to 127.0.0.1 means the only client that can connect is itself
    $pull->on('message', array($pusher, 'onBlogEntry'));

    // Set up our WebSocket server for clients wanting real-time updates
    $webSock = new React\Socket\Server('0.0.0.0:8080', $loop); // Binding to 0.0.0.0 means remotes can connect
    $webServer = new Ratchet\Server\IoServer(
        new Ratchet\Http\HttpServer(
            new Ratchet\WebSocket\WsServer(
                new Ratchet\Wamp\WampServer(
                    $pusher
                )
            )
        ),
        $webSock
    );

    $loop->run();

Save the code as /bin/push-server.php and run it:

$ php bin/push-server.php

Client side Getting real-time updates

Now that our server side code is complete as well as up and running it's time to get those real-time posts! What you do with those updates specifically is beyond the scope of this document, we're just going to put those messages into the debug console.

<script src="https://gist.githubusercontent.com/cboden/fcae978cfc016d506639c5241f94e772/raw/e974ce895df527c83b8e010124a034cfcf6c9f4b/autobahn.js"></script>
<script>
    var conn = new ab.Session('ws://localhost:8080',
        function() {
            conn.subscribe('kittensCategory', function(topic, data) {
                // This is where you would add the new article to the DOM (beyond the scope of this tutorial)
                console.log('New article published to category "' + topic + '" : ' + data.title);
            });
        },
        function() {
            console.warn('WebSocket connection closed');
        },
        {'skipSubprotocolCheck': true}
    );
</script>

Finally, open the page you placed this Javascript in with one browser window and from another browser post a blog entry to "kittensCategory" and watch your console's log from the first. Once that is working, your next steps are to incorporate the data received into some DOM manipulation goodness.

When that's working locally (assuming localhost has been your development environment) you can change the localhost references and possibly the bindings to your proper server hostnames/IP addresses.