Building a Live-score Widget Using PHP Web Sockets

The introduction of web sockets makes it possible for web applications to handle near real-time data without resorting to "hacks" such as long-polling.

One example of an application requiring up-to-the-minute data is sports scores. Even now, many websites which display this information use Flash applications, since Actionscript provides the facility to communicate over socket-based connections. However, web sockets allow us to replicate this functionality using only HTML and Javascript. That's what we're going to build in this tutorial, along with a lightweight "server" in PHP.

image

Installation and Setup

We'll base the example around the Ratchet library, which provides a PHP implementation of web sockets.

Create the following composer.json file, which both installs this dependency and sets up an autoloader for the code we're going to write:

{
    "require": {
        "cboden/Ratchet": "0.2.*"
    },
    "autoload": {
        "psr-0": {
            "LiveScores": "src"
        }
    }    
}

Now set up the directory structure:

[root]
    bin
    src
        LiveScores
    public
        assets
            css
                vendor
            js
                vendor
    vendor

You'll probably want to clone the repository, which contains a number of CSS / JS / image assets, as well as all the code from this tutorial. If you'd like to build it from scratch alongside this article, all you need to do is copy the public/assets/*/vendor folders from the cloned/downloaded package into your own at the appropriate locations.

Naturally, don't forget to run php composer.phar update, preceded by curl -sS https://getcomposer.org/installer | php if you don't have composer installed.

We'll start by building a class which resides on the server and acts as a sort of message broker – accepting connections and sending messages. Later, we'll also use it to maintain information about the games in progress. This is a skeleton implementation, to show how a generic message broker might operate:

// src/LiveScores/Scores.php

<?php namespace LiveScores;

use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class Scores implements MessageComponentInterface {

    private $clients;    

    public function __construct() 
    {    
        $this->clients = new \SplObjectStorage;
    }

    public function onOpen(ConnectionInterface $conn) 
    {
        $this->clients->attach($conn);
    }

    public function onMessage(ConnectionInterface $from, $msg) 
    {            
        foreach ($this->clients as $client) {
            if ($from !== $client) {
                // The sender is not the receiver, send to each client connected
                $client->send($msg);
            }
        }
    }

    public function onClose(ConnectionInterface $conn) 
    {
        $this->clients->detach($conn);
    }

    public function onError(ConnectionInterface $conn, \Exception $e) 
    {     
        $conn->close();
    }


}

Important points to note;

  • The class needs to implement MessageComponentInterface in order to act as a "message broker"
  • We're maintaining a list of all clients that have connected to the server as a collection
  • When a client connects, the onOpen event gets fired, where we add the client to our collection
  • When a client disconnects (onClose), we do the opposite
  • The interface also requires us to implement a simple error handler (onError)

Next up, we need to create a server daemon to instantiate our new class and start listening to connections. Create the following file:

// bin/server.php

<?php
use Ratchet\Server\IoServer;
use Ratchet\WebSocket\WsServer;
use LiveScores\Scores;

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

$server = IoServer::factory(
    new WsServer(
        new Scores()
    )
    , 8080
);

$server->run();

This should all be pretty self-explanatory; WsServer is an implementation of the more generic IoServer which communicates using web sockets, and we'll set it listening on port 8080. You're free to choose a different port, of course – provided it's not blocked by your firewall – but 8080 is usually a pretty safe bet.

Maintaining State

We'll let the server keep track of the current state of the games; no need to commit it to storage, we'll simply keep it in memory for optimum performance. Each time an event takes place in one of the games, we'll update the scores on the server and then broadcast the event to all listening clients.

First, though, we need to generate the fixtures (i.e. the list of games). For simplicity we'll do it at random, and just keep this set of fixtures active for the duration of the daemon's execution.

// src/LiveScores/Fixtures.php
<?php namespace LiveScores;

class Fixtures {

    public static function random()
    {
        $teams = array("Arsenal", "Aston Villa", "Cardiff", "Chelsea", "Crystal Palace", "Everton", "Fulham", "Hull", "Liverpool", "Man City", "Man Utd", "Newcastle", "Norwich", "Southampton", "Stoke", "Sunderland", "Swansea", "Tottenham", "West Brom", "West Ham");

        shuffle($teams);

        for ($i = 0; $i <= count($teams); $i++) {
            $id = uniqid();
            $games[$id] = array(
                'id' => $id,
                'home' => array(
                    'team' => array_pop($teams),
                    'score' => 0,
                ),
                'away' => array(
                    'team' => array_pop($teams),
                    'score' => 0,
                ),
            );
        }

        return $games;
    }


}  

Note that we're assigning each game a unique identifier, which we'll use later to indicate which game an event has taken place in. Going back to our Scores class:

// src/LiveScores/Scores.php

public function __construct() {

    // Create a collection of clients
    $this->clients = new \SplObjectStorage;

    $this->games = Fixtures::random();
}

Because a client could call upon our widget at any stage during a game, it's important that they get up-to-the-minute information. One way to do this is simply to "reply" to a new connection request by sending the current state of the games, then rendering the list of games and their scores client-side.

Here's the onOpen implementation, which does just that:

// src/LiveScores/Scores.php

public function onOpen(ConnectionInterface $conn) {
    // Store the new connection to send messages to later
    $this->clients->attach($conn);

    // New connection, send it the current set of matches
    $conn->send(json_encode(array('type' => 'init', 'games' => $this->games)));

    echo "New connection! ({$conn->resourceId})\n";
}

Note that the message we're sending is actually a JSON object, with the type of event set as a property. There's no requirement to send messages using JSON – you can send any format you wish – but doing it in this way allows us to send different types of structured messages.

The HTML

Because we're going to load in the current scores over a web socket and render them using Javascript, the HTML for the page to start with is very simple:

<div id="scoreboard">

    <table>

    </table>

</div>

Once rendered, a row in the score-table will look like this:

<tr data-game-id="SOME-IDENTIFIER">
    <td class="team home">
        <h3>HOME TEAM NAME</h3>
    </td>
    <td class="score home">
        <div id="counter-0-home"></div>
    </td>
    <td class="divider">
        <p>:</p>
    </td>
    <td class="score away">
        <div id="counter-0-away"></div>
    </td>
    <td class="team away">
        <h3>AWAY TEAM NAME</h3>
    </td>
</tr>

The counter-*-* elements are placeholders for a JS plugin we're going to use to render a fancy score widget later.

The JavaScript

Now let's start building the JS. The first thing to do is open a web socket:

var conn = new WebSocket('ws://localhost:8080');

You may need to substitute the hostname and / or the port number, depending on where your "server" is running.

Next, attach an event handler to the connection, which fires whenever a message is received:

conn.onmessage = function(e) {    

The message itself is provided as a data property to the event e. Because we're sending messages in JSON format, we'll need to parse it first:

var message = $.parseJSON(e.data);

Now we can examine the type, and call the appropriate function:

switch (message.type) {
    case 'init':
        setupScoreboard(message);
        break;
    case 'goal':
        goal(message);
        break;
}

The setupScoreboard function is pretty straightforward:

function setupScoreboard(message) {

    // Create a global reference to the list of games
    games = message.games;

    var template = '<tr data-game-id="{{ game.id }}"><td class="team home"><h3>{{game.home.team}}</h3></td><td class="score home"><div id="counter-{{game.id}}-home" class="flip-counter"></div></td><td class="divider"><p>:</p></td><td class="score away"><div id="counter-{{game.id}}-away" class="flip-counter"></div></td><td class="team away"><h3>{{game.away.team}}</h3></td></tr>';

    $.each(games, function(id){        
        var game = games[id];                
        $('#scoreboard table').append(Mustache.render(template, {game:game} ));        
        game.counter_home = new flipCounter("counter-"+id+"-home", {value: game.home.score, auto: false});
        game.counter_away = new flipCounter("counter-"+id+"-away", {value: game.away.score, auto: false});
    });

}

In this function we're simply iterating through the array of games, using Mustache to render a new row to be added to the scoreboard table, and instantiating a couple of animated counters for each one. The games array is going to store the current state of the games client-side, and includes references to those counters so we can update them as required.

Next up, the goal function. The message we receive over the web socket to indicate a goal will be a JSON object with the following structure:

{
    type: 'goal',
    game: 'UNIQUE-ID',
    team: 'home'
}

The game property contains the unique identifier, and team is either "home" or "away". Using these bits of information, we can update the relevant score in the games array, find the appropriate counter object and increment it.

function goal(message) {    
    games[message.game][message.team]['score']++;
    var counter = games[message.game]['counter_'+message.team];
    counter.incrementTo(games[message.game][message.team]['score']);
}

All that remains is some way of indicating that a goal has been scored. In order to keep things simple, we'll just add that to the client; clicking a team's name will indicate that they've scored. In practice you'd have a separate application or page, but the principle is the same. We'll simply add a click handler as follows, which sends a simple JSON message over the web socket:

$(function () {

    $(document).on('click', '.team h3', function(e){
        var game = $(this).parent().parent().attr('data-game-id');        
        var team = ($(this).parent().hasClass('home')) ? 'home' : 'away';
        conn.send(JSON.stringify({ type: 'goal', team: team, game: game }));
    });

});

The server "listens" for these messages, and if it receives word of a goal it updates its record. All messages received are immediately re-broadcast to all connected clients.

// src/LiveScores/Scores.php
public function onMessage(ConnectionInterface $from, $msg) {

    foreach ($this->clients as $client) {        
        $client->send($msg);            
    }

    $message = json_decode($msg);

    switch ($message->type) {
        case 'goal':
            $this->games[$message->game][$message->team]['score']++;
            break;
    }

}

Finally, to get it up-and-running, you'll need to launch the server from the command-line:

php bin/server.php

That's it – try opening a couple of windows side-by-side, and clicking a team name to indicate a goal. You should see the scoreboard update straight away!

Conclusion

In this article, I've demonstrated a simple HTML and Javascript "live scores" widget using web sockets. It has its limitations; normally you'd expect to see the goalscorer and the time each goal was scored, as well as additional information such as bookings and sending-offs. However, because we're using a JSON object to represent an event, such features should be relatively straightforward to add. A live demo of this tutorial is available.

(Note: The Javascript and styles for the counters are thanks to Chris Nanney, and come from this post.)

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Dom

    Any chance to see a live demo of this tut?

  • geopelia

    Just the guide I needed, thank you!! will try and share the results.

  • Taylor Ren

    Will it work on a shared VHOST environment?

    • Anonymous

      As long as you have SSH access to the instance so you can run commands that way, it should.

  • Daniel

    I’ll give you a thumbs up just for giving my club Norwich a positive scoreline :)

  • Michael Tea

    Can I use database as a datasource to connect with Ratchet?

  • Anonymous

    Good tutorial. Nice clean PHP and good use of composer. Thanks for this.

  • Anonymous

    Having just written this comment, just realised there is a small issue with the code on Github. :)

    setupScoreboard(message);

    is never fired.

    I just quickly hacked it to make it work by adding :

    conn.onopen = function(e) {

    var message = $.parseJSON(‘{“type”:”init”,”games”:{“52613fc668b94″:{“id”:”52613fc668b94″,”home”:{“team”:”West Ham”,”score”:0},”away”:{“team”:”Man City”,”score”:0}},”52613fc668b9b”:{“id”:”52613fc668b9b”,”home”:{“team”:”Sunderland”,”score”:0},”away”:{“team”:”Newcastle”,”score”:0}},”52613fc668b9f”:{“id”:”52613fc668b9f”,”home”:{“team”:”Cardiff”,”score”:0},”away”:{“team”:”Hull”,”score”:0}},”52613fc668ba2″:{“id”:”52613fc668ba2″,”home”:{“team”:”Swansea”,”score”:0},”away”:{“team”:”West Brom”,”score”:0}},”52613fc668ba4″:{“id”:”52613fc668ba4″,”home”:{“team”:”Man Utd”,”score”:0},”away”:{“team”:”Southampton”,”score”:0}},”52613fc668ba6″:{“id”:”52613fc668ba6″,”home”:{“team”:”Norwich”,”score”:0},”away”:{“team”:”Liverpool”,”score”:0}},”52613fc668ba8″:{“id”:”52613fc668ba8″,”home”:{“team”:”Aston Villa”,”score”:0},”away”:{“team”:”Crystal Palace”,”score”:0}}}}’);
    setupScoreboard(message);
    };

    and then the app works great! Thanks again.

  • Paul Freeland

    Thanks for this, nice clear well written article.

  • gopher

    Why i’m in xampp, and put this code, visit: http://localhost:8088/LukasWhite-LiveScores/public/, just don’t work, the pictures not shown, only displayed the title “Live Scores”.

    • Lukas White

      If it’s not showing anything other than the title, it means it hasn’t managed to get the current scores. This either means it can’t access the web socket – perhaps check the port number – or the daemon’s not running. Have you done “php bin/server.php”?

      • gopher

        Thank you, it’s the port problem. By the way, How to make daemon in linux?

  • Kaushik

    How can we setup in window xampp system? any code available on gitHub? on vendoer directory found on gitHub code.
    https://github.com/phpmasterdotcom/LukasWhite-LiveScores

    • TalksOnWeb

      You need to make sure you have PHP 5.3.9 as the minimum. Then you need to have SSL set on your local dev environment. If you don’t know how to do that, there’s a good tutorial on youtube which walks you through it in minutes. Hope this helps

  • Adil

    HTML5′s websocket API isn’t supported by older browsers. Can you give us a work around for that? If I implement this tutorial with an application, how do I provide support for people with older browsers?

    • Adil

      Hey, I figured it out! I’ll be creating a tutorial and posting the link here later on. Hopefully, the comment section will not be closed by then. For those who want to try themselves. You need the web-socket-js library. It will help with the old browsers.

  • Adil

    Here’s the tutorial on how to make your socket application work with older browsers. Please go to http://talksonweb.com/socket-programming-with-old-browsers-supported/