Integrating Node.js with PHP

It may be in vogue, but is there a practical reason to adopt Node.js? Lee Boynton shows how it can be used to add a real-time news feed to your PHP site.

Integrating Node.js with PHP

Lee Boynton

Node.js is a server-side solution for building applications in JavaScript which, in the few years it has been around, has already become quite popular. It is built on Chrome’s V8 JavaScript runtime, and it is especially well suited to building real-time websites with push capabilities thanks to its event-driven architecture whereby I/O calls are asynchronous.

This article aims to show you how you can start using Node to add real-time features to your PHP-based website. First, we shall look a bit more at what makes Node a good fit for real-time apps, before going on to demonstrate how to build a real-time news feed and incorporate it into your PHP website.

Thread-based vs Event-based

Traditionally PHP is served with Apache and the mod_php module. If you run the ‘top’ command on your Unix-based web server you will probably see a large number of Apache processes serving web clients. In this setup, each client request typically spawns a new Apache process until all of the available RAM is used up. Recently, nginx and php-fpm has emerged as the most efficient method of serving PHP websites, but even in this setup each client is served by a different PHP process. The key point here is that, from start to finish, a client request is using up a PHP process for the entire duration. If it takes a long time to process each request, the server’s resources can be used up very quickly.

In Node, a single Node process typically serves every client in an event loop. For long-running, expensive processes like accessing the file system, a database or a remote API, it is advocated to use asynchronous method calls instead of blocking ones. This is achieved through the use of callbacks which are triggered when an action like accessing the file system has finished. This means that a single Node process can continue to process new requests whilst the expensive operation is run in the background. When the expensive operation is complete, it goes back into the event loop queue to be processed further by Node.

In essence, Node can be viewed as a similar environment for building applications such as Python’s Twisted or EventMachine in Ruby. Node also has a built-in production-ready HTTP server so it does not need a separate server to run it such as Apache or Nginx, further enhancing its lean resource requirements (barring any memory leaks).

var http = require('http');
http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

The code example above shows how you can write an obligatory “hello world” web server in just a few lines of code. The first line demonstrates usage of the module system known as CommonJS which Node uses to include separate modules. The require function is built-in, and in this case imports Node’s HTTP module for use in the application. The second line creates a new web server object. Notice that the first parameter to the createServer method is an anonymous function. Most methods in Node accept a callback function as a parameter, and this is the key to building event-driven applications.

The next line of execution is line 5, which uses method chaining to call the listen method on the return value of the createServer method (the value returned is the HTTP module instance). The listen method causes the server to start accepting HTTP requests on port 1337 on localhost. The last line writes a message to the console to say that the server has started. Only when a request to the server is made is the anonymous function called, which sets the HTTP status code to 200 OK and sets the Content-Type header. The ‘Hello World’ message is finally written to the HTTP response body on line 4.

Why Should I Use Node.js Then?

Node’s event-driven model is especially suited to real-time applications such as games, news feeds and chat applications. In addition, it also allows you to use the same language on the frontend and backend. JavaScript is only becoming more popular as more rich client-side applications are built and web browsers get faster at executing JavaScript. Having to switch between languages can be frustrating.

Second, it has good support for WebSockets. Whilst it is possible to support WebSockets in PHP, Node’s asynchronous nature and its built-in HTTP server makes it a better fit. WebSockets are a way of maintaining a persistent connection to the browser in order to push data to the client quickly. Compared to previous solutions such as long polling or comet, WebSockets entail much lower latency, as there is no overhead of instantiating an HTTP connection each time some data needs to be sent. The downside to WebSockets is that it is an HTML5 feature, and as such is not as well-supported in browsers as plain old Ajax is. However, it is possible to gracefully fall back to alternative techniques such as long polling in browsers which do not support WebSockets.

Bear in mind though, Node is an immature platform compared to PHP. Originally created in 2009, it is still in its infancy and has not reached version 1.0 yet – if that matters to you. You may find that APIs you use change in the future, or be unable to find a framework which has the same feature set as your favourite PHP framework. Indeed, in my experience the third party libraries and frameworks available tend to consist of much smaller bundles of functionality that you need to piece together.

There is also a greater risk of memory leaks bringing your application to a grinding halt. Node processes typically run continually, whereas PHP processes tend to be respawned periodically to negate the effect of memory leaks.

The Integrating Part

The news feed will be integrated with a basic PHP website which handles user logins and sessions, using a common setup of php-fpm and nginx. We will be using JavaScript to communicate with a Node application on the server side and dynamically update the news feed without reloading the page. First though, a quick aside on sessions.

If you aren’t already doing so, you should be using a centralised storage area for your sessions (Figure 1). Memcached can easily be used for this task by using the built in session save handler in the PECL memcached extension. If you want the ability to restart the server storing your sessions without losing data, then Redis is a good bet. Either way, a centralised session storage enables you to load balance your application across multiple webservers. It also enables you to share session data with applications built with other programming languages.

Figure 1: Shared session architecture.

However, there is a small problem of parsing session data. Below shows the default serialisation format of a PHP session:

not|a:2:{i:0;s:4:"easy";i:1;a:1:{s:2:"to";s:5:"parse";}}

It may look like you can use string manipulation to parse it, but there could be edge cases which are tricky to solve. It would be nice if the session was serialised in the much-loved JSON format:

{"this":{"is": "easier", "to": "parse"}}

Much better. It is fairly easy to write your own session serialiser which will convert whatever you store in $_SESSION to JSON format, see my version.

Alternatively, you might want to consider msgpack which can be configured to serialise sessions, as in the code below, which also demonstrates how to use memcached as the session save handler.

<?php
ini_set('session.serialize_handler', 'msgpack');
ini_set('session.save_handler', 'memcached');
ini_set('session.save_path', 'sess1:11211');
?>

There is a third-party library available for Node which can serialise and deserialise msgpack, available in npm (Node Package Manager, contains a large number of modules which can be used in your application with just a simple npm install <package>). We shall now look at building the news feed using Node and incorporating this into a PHP application.

PHP App

The PHP application simply handles user logins and sessions. Sessions are stored in memcached, but could quite easily be stored in redis. The code below shows snippets of the simple one page application (for the full source go to github.com/lboynton/phphants-march-php).

<?php
require 'vendor/autoload.php';
$memcached = new Memcached();
$memcached->addServer('localhost', 11211);
$handler = new Lboy\Session\SaveHandler\Memcached($memcached);
session_set_save_handler(
    array($handler, 'open'),    
    array($handler, 'close'),
    array($handler, 'read'),
    array($handler, 'write'),
    array($handler, 'destroy'),
    array($handler, 'gc')
);
register_shutdown_function('session_write_close');
session_start();

$_SESSION['user']['username'] = $_GET['username'];
?>

<!-- Javascript -->
<script src="/components/jquery/jquery.js"></script>
<script src="/components/underscore/underscore.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script>
  $(document).ready(function() {
    var socket = io.connect();
    var template = _.template($('#js-news-template').html());
    socket.on('news', function (news) {
      var element = template({news: news});
      $(element).hide().prependTo('#js-news-container').slideDown();
    });
  });
</script>
<script type="text/template" id="js-news-template">
  <p class="well"><%- news %></p>
</script>

The first line of the script includes the Composer autoloader file, meaning that all dependencies are autoloaded automatically, obviating the need for separate include or require lines (Editor's note: for more on Composer, see Jefersson Nathan de O. Chaves' article). In this case, the session save handler is the only piece of external code which is required. However, it is very easy to include any third party packages available on packagist or elsewhere at a later date by using Composer which will also be automatically added to autoloading. Lines 3 and 4 set up a connection to memcached, whilst the session save handler is initialised and registered with PHP in lines 5-14. Users are logged in simply by specifying the username as a GET parameter, though in a real system this should be replaced with a more functional authentication system.

On the JavaScript side, we use some third party libraries : jQuery, underscore.js and the Socket.IO client, which is the browser part of the Node module we will be using to push data to the browser. The io.connect method call creates a connection to the Node application, which could use one of a number of transports such as websockets or Ajax depending on browser support. The socket.on method sets up an event handler which will render a news item on the page whenever a ‘news’ event is triggered by the server side.

Note: The application uses Composer for installing the session save handler and Bower, a package manager for installing client-side assets. To install Bower, run ‘npm -g install bower’. Then to install the assets, do ‘bower install’. Alternatively, update the script tags and stylesheets with local versions.

The application is served using nginx. Here's the nginx configuration file:

upstream node {
    server localhost:3000;
}

server {
    listen 8080;
    server_name php-node-demo.localhost;
    root /home/lee/public_html/php-node-demo;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_index index.php;
        fastcgi_pass  unix:/var/run/php5-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location ~ /socket.io {
        proxy_pass http://node;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Lines 1-3 define an upstream server which runs the Node app. Any request ending in .php will be sent to php-fpm (lines 15-20), whilst any requests with /socket.io in the URL will be passed to the Node app (lines 22-27). Lines 25 and 26 tell nginx to support protocol switching and enable websockets to be proxied to Node. This means that the PHP app and Node app are both run on the same port, as far as the client is concerned.

Node.js App

The Node.js app simply handles any client requests for the news feed. Snippets of it are displayed below (see github.com/lboynton/phphants-march-node for the full source).

io.set('authorization', function(handshake, callback) {
  var cookies = cookie.parse(handshake.headers.cookie);
  mcClient.get('sessions/' + cookies.PHPSESSID, function(error, result) {
    if (error) {
      callback(error, false);
    } else if (result) {
      handshake.session = JSON.parse(result);
      callback(null, true);
    } else {
      callback('Could not find session ID ' + cookies.PHPSESSID + ' in  memcached', false);
    }
  });
});

io.sockets.on('connection', function(socket) {
  var session = socket.handshake.session;
  sockets[session.user.username] = socket;
);

function getNews() {
  redis.blpop('news', 0, function(err, data) {
news = JSON.parse(data[1]);
if (typeof news.to !== 'undefined') {
  if (typeof sockets[news.to] !== 'undefined') {
sockets[news.to].emit('news', news.content);
  }
} else {
  io.sockets.emit('news', news.content);
}
process.nextTick(getNews);
  });
}

Socket.io is a library which supplies a single API for performing WebSocket communication between client and server. It also supports graceful fallback when WebSockets aren’t available in the browser, and other useful features in a messaging protocol such as heartbeats, timeouts and disconnection support which is not supported out of the box with the HTML5 WebSocket API.

The first snippetconfigures authorization for requests that Socket.io receives. When a new request comes in, it will parse the cookies in the request and attempt to retrieve a memcached key which corresponds to the value of the PHPSESSID cookie (the default name for the PHP session cookie). If found, it will store the parsed value of the memcached key in data.session which can be accessed later on.

The next snippet configures what should happen when Socket.io triggers the ‘connection’ event. This event is triggered during the initial connection from the client, once authorised. The session data that was previously retrieved from memcached can now be referenced via the socket.handshake variable. When the client connects, the socket instance is associated with the client’s username so that messages can be sent to individual users.

The last snippet contains a function to check for new news items in a redis queue. It uses the BLPOP command in redis which means if the queue is empty it will block until some data is appended to the queue before popping it. When some data can be popped off the queue, it is parsed as JSON before determining if the content should be sent to every connected client or just a single user. The news item content is sent to the correct socket by calling the emit() method on the socket which was associated with the user previously in the connection event handler.

Finally, Node’s process.nextTick() method is called with the getNews function as an argument. This means that the getNews function will be called the next time the event loop runs and will continue to check the redis queue for data until the application is stopped.

To install, use npm to download the required dependencies. Then run the application with node app.js.

You should now be able to open a web browser and navigate to http://localhost:8080/?username=bob and see the news feed application. Now open a second browser tab or window with a different username, for example http://localhost:8080/?username=sally.

Updating the Feed

Updating the feed is simply a case of pushing a new news item into the queue. Connect to the redis CLI interface using the command redis-cli. This is an interactive shell for redis, allowing you to send commands to the server directly. The code sample below shows how to push to the queue:

rpush news '{"content": "Testy test", "to": "bob"}'

In the web browser window you opened for user bob you should see the news item slide down from the top of the page. Alternatively, you can push news out to both bob, sally and any otherall connected clients by excluding the “to” parameter, as follows:

rpush news '{"content": "Everyone should see this"}'

In a real application you would push data into the queue from PHP, using for example the php-redis extension or predis.

Conclusion

This is just a simple example to demonstrate how Node can be integrated with your PHP application. There are a few limitations in the implementation. For example, it will only remember one connection from each user. Therefore if a user has multiple tabs or windows open to the news feed, only one page will update. This can be resolved by storing an array of sockets per user, and keeping track of each connection and disconnection to add or remove the sockets from the array. It is left to the reader to implement a better solution. It also showed off tools such as Composer and Bower which I highly recommend you consider using in your applications.

Lee Boynton is a developer based in Hampshire, UK, with a particular interest in real-time applications such as instant messaging and activity streams. He has knowledge of server side development and administration as well as frontend development. He works at local company Symbios Group and is also a member of PHP Hampshire for who he helps organise events.

Photo by iStockphoto.com/alkir

 

Comments

Add new comment