Building a Live-score Widget Using PHP Web Sockets

Share this article

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.)

Frequently Asked Questions (FAQs) about Building a Live Score Widget Using PHP Web Sockets

How Can I Implement Real-Time Updates in My Live Score Widget?

Real-time updates are crucial for a live score widget. To implement this, you can use WebSockets, a technology that provides full-duplex communication channels over a single TCP connection. In PHP, Ratchet is a popular library for handling WebSocket connections. You can use it to push updates to the client whenever a score changes. This way, users will always see the most recent scores without needing to refresh the page.

What Are the Security Considerations When Using WebSockets?

When using WebSockets, it’s important to consider security. One way to secure your WebSocket connections is by using WSS (WebSocket Secure), which encrypts the data transmitted between the client and server. Also, validate all data sent over the WebSocket connection to prevent injection attacks. Lastly, consider using authentication to ensure that only authorized users can connect to your WebSocket server.

How Can I Scale My Live Score Widget to Handle More Users?

To scale your live score widget, you can use a load balancer to distribute WebSocket connections across multiple servers. This way, if one server becomes overloaded, the load balancer can redirect new connections to a less busy server. Also, consider using a shared database or a distributed cache to store scores, so all servers can access the same data.

Can I Use AJAX Instead of WebSockets for My Live Score Widget?

While AJAX can be used to update scores, it’s not as efficient as WebSockets for real-time updates. AJAX requires the client to periodically send requests to the server to check for updates, which can lead to unnecessary network traffic and delay in updates. On the other hand, WebSockets allow the server to push updates to the client as soon as they occur, providing a more real-time experience.

How Can I Make My Live Score Widget Mobile-Friendly?

To make your live score widget mobile-friendly, consider using responsive design techniques. This means your widget should adjust its layout based on the screen size of the device. You can use CSS media queries to apply different styles for different screen sizes. Also, ensure that your scores are easy to read on small screens, and consider using touch-friendly controls for any interactive elements.

How Can I Add a Leaderboard to My Live Score Widget?

A leaderboard can be a great addition to your live score widget. You can create a leaderboard by storing the scores in a database and then querying the database to retrieve the top scores. Display these scores in a table or list, and update the leaderboard in real-time using WebSockets whenever a score changes.

Can I Use the Live Score Widget for Different Sports?

Yes, you can use the live score widget for different sports. You just need to customize the data structure to accommodate the scoring system of each sport. For example, for soccer, you might need to track goals, while for basketball, you might need to track points, rebounds, and assists.

How Can I Test My Live Score Widget?

Testing is an important part of developing a live score widget. You can use unit tests to test individual functions, and integration tests to test how these functions work together. Also, consider using load testing to see how your widget performs under heavy traffic.

How Can I Improve the Performance of My Live Score Widget?

To improve the performance of your live score widget, consider using techniques like caching and compression. Caching can reduce the load on your database by storing frequently accessed data in memory. Compression can reduce the size of the data sent over the network, making updates faster.

How Can I Customize the Look of My Live Score Widget?

You can customize the look of your live score widget using CSS. With CSS, you can change the colors, fonts, and layout of your widget to match your website’s design. You can also use CSS animations to add visual effects, like highlighting a score when it changes.

Lukas WhiteLukas White
View Author

Lukas is a freelance web and mobile developer based in Manchester in the North of England. He's been developing in PHP since moving away from those early days in web development of using all manner of tools such as Java Server Pages, classic ASP and XML data islands, along with JavaScript - back when it really was JavaScript and Netscape ruled the roost. When he's not developing websites and mobile applications and complaining that this was all fields, Lukas likes to cook all manner of World foods.

PHPreal timeweb socket
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week