Playing With ReactPHP

19th November 2012 - 14 minutes read time

I recently saw an implementation of a Twitter wall that used node.js to run searches on Twitter and post the results on a webpage. I had been wanting to create something using ReactPHP so I thought this was a good opportunity to have a go. ReactPHP, if you haven't heard of it, is an event-driven, non-blocking I/O that is essentially the PHP equivalent of node.js. The major difference is that ReactPHP is written in pure PHP with no extra components, whereas node.js is a collection of different programs, interfaces and languages. As a first attempt I wanted to create something simple so it needed to use simple JavaScript to load in the latest tweets for a given hashtag from a ReactPHP server. I have to warn that this is a simplistic implementation of ReactPHP, but it shows the basics of how to get started.

Building The Frontend

The first thing to do is create a HTML page that will display the tweets. When creating a simple ReactPHP server a port is normally assigned, in which case the first thing I had to do is figure out how to do a request to a certain port number in JavaScript (not port 80). Normally this would violate the same origin policy that JavaScript enforces but we can get around this using a technique called JSONP. This involved a cross domain request that returns a JSON string wrapped in a function callback. This is possible in jQuery using the getJSON() function.

The first parameter of this function is the full URL to our ReactPHP server (including the port), which must contain a parameter similar to 'jsoncallback'. This is what is used on the server side to send back the correct data format. The second parameter isn't used but it allows you to pass additional arguments. The third parameter is the callback function that does something with the returned data. We will return a JSON object with a property of 'content' that will contain the tweets.

  1. function loadtweets() {
  2. $.getJSON("http://www.tweetstream.local:4000?jsoncallback=?", {},
  3. function(data) {
  4. $('#stream').html(data.content);
  5. }
  6. );
  7. }

When we run this code it will send a request to the following URL structure.

http://www.tweetstream.local:4000?jsoncallback=jQuery18205617587673477829_1353020048954&_=1353020058958

What we do on the server side is return a string that wraps some JSON output inside a function with the same name as the jsoncallback parameter. The function is expanded, and the JSON it contains is then available to the return function. The only thing we do here is pass the entire content string into an element with an ID of 'stream'.

jQuery18205617587673477829_1353020048954('content':'text')

This function is basically wrapped in a call to setInterval(), which is used to poll the ReactPHP server every 5 seconds to see if there are any tweets. We could expand on this to try and keep the tweets already on the screen and just append the new ones to the top, but this will work for the purposes of this demonstration.

  1. <div id="stream">
  2. </div>
  3. <script src="jquery-1.8.2.min.js"></script><script>
  4. <!--//--><![CDATA[// ><!--
  5.  
  6. $(document).ready(function() {
  7.  
  8. // Run the loadtweets() function every 5 seconds.
  9. window.setInterval(function() {
  10. loadtweets();
  11. }, 5000);
  12.  
  13. function loadtweets() {
  14. $.getJSON("http://www.tweetstream.local:4000?jsoncallback=?", {},
  15. function(data) {
  16. $('#stream').html(data.content);
  17. }
  18. );
  19. }
  20. });
  21.  
  22. //--><!]]>
  23. </script>

Composer

With that in place we can now build our ReactPHP components. This is done using a tool called Composer, which is a dependency manager built in PHP. I won't go into Composer too much, but to get a copy just run the following command in the directory you want to work from.

curl -s https://getcomposer.org/installer | php

This will download a file called composer.phar, which is used to download various packages. All we need to do is tell Composer what packages we need to download. This is done using a file called composer.json where we list (in JSON format) the components we want for this project. Obviously we need to include ReactPHP but I have also included Zend Framework 1 so that I can use the Zend_Service_Twitter_Search class. Here is the composer.json file for this project. I could use a different Twitter class, but I am familiar with the Zend Framework implementation so I went with that one out of convenience.

  1. {
  2. "require": {
  3. "react/http": "0.2.*",
  4. "zendframework/zendframework1": "dev-release-1.12"
  5. }
  6. }

To get Composer to do something with this just call the composer file with the flag install like this.

composer install

This will download ReactPHP and Zend Framework 1 as well as a few other dependencies. You will now have a directory called vendor that will contain the downloaded packages as well as an autoloader.php file. To use anything from these directories just include the autoload.php file and start using the classes you need.

  1. autoload.php
  2. composer
  3. evenement
  4. guzzle
  5. react
  6. symfony
  7. zendframework

ReactPHP

We are finally ready to start using ReactPHP. The first step (aside from including the autoload.php file) is to instantiate a couple of objects. These are the event loop, socket and http objects. ReactPHP works by creating an internal event loop, which other systems can then plug into. The loop is then used to create a socket server, which is then in turn used to create a HTTP server. The HTTP server is essentially a wrapper around the socket object with some extra stuff to take care of headers and other HTTP related things.

The HTTP object is then set to fire an event when it receives and incoming request using the on() method. Inside this method we setup a closure to receive the incoming request and send corresponding output. This closure is where we find the jsoncallback wrapper name and return a json string containing all of the tweets we have loaded.

Finally, we set up the socket server to listen to port 4000 and run it.

  1. <?php
  2.  
  3. require 'vendor/autoload.php';
  4.  
  5. // load objects
  6. $loop = React\EventLoop\Factory::create();
  7. $socket = new React\Socket\Server($loop);
  8. $http = new React\Http\Server($socket, $loop);
  9.  
  10. // set up a request event
  11. $http-?>on('request', function ($request, $response) use() {
  12. $query = $request->getQuery();
  13.  
  14. if (!isset($query['jsoncallback'])) {
  15. // no jsoncallback parameter passes, so we quit.
  16. $response->writeHead(200, array('Content-Type' => 'text/plain; charset=utf-8'));
  17. $response->end('finish' . PHP_EOL);
  18. return;
  19. }
  20.  
  21. // Set up the correct headers
  22. $response->writeHead(200, array(
  23. 'Content-Type' => 'application/x-javascript; charset=utf-8',
  24. 'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
  25. 'Last-Modified' => gmdate("D, d M Y H:i:s") . ' GMT',
  26. 'Cache-Control' => 'no-store, no-cache, must-revalidate',
  27. 'Cache-Control' => 'post-check=0, pre-check=0',
  28. 'Pragma' => 'no-cache',
  29. ));
  30.  
  31. // Load in the tweets
  32. $searchResults = loadtweets();
  33.  
  34. // Generate the output
  35. $output = '';
  36. if ($searchResults !== FALSE) {
  37. foreach ($searchResults as $result) {
  38. $text = clean_tweet($result['text']);
  39. $output .= '<div class="tweet">';
  40. $output .= '<div class="screen_name">' . $result['from_user_name'] . '</div>';
  41. $output .= '<div class="profile_picture"><img src="' . $result['profile_image_url'] . '" /></div>';
  42. $output .= '<div class="tweet_contents">';
  43. $output .= '<p>' . $text . ' <span class="timestamp">on ' . $result['created_at'] . '</span></p>';
  44. $output .= '</div>';
  45. $output .= '</div>';
  46. }
  47. }
  48.  
  49. // JSON encode and wrap the output in the jsoncallback parameter
  50. $output = $query['jsoncallback'] . '(' . json_encode(array('content' => $output)) . ')' . PHP_EOL;
  51.  
  52. // End the response
  53. $response->end($output);
  54. });
  55.  
  56. // Listen to socket 4000
  57. $socket->listen(4000);
  58.  
  59. // Run the server
  60. $loop->run();

The clean_tweet() function above is taken from fiveminuteargument's PHP implementation of Remy Sharp's JavaScript Twitter parsing functions. These functions will add HTML links to the things like usernames and hashtags and means that the Twitter stream itself is a little more user friendly.

Of course we are missing the vital component of actually getting the tweets. We do this using the Zend_Service_Twitter_Search class which is part of Zend Framework. We already have access to this class thanks to our earlier actions with Composer so we don't need to include anything else. Here is the function in full, which will search for tweets in english using the hashtag #drupal.

  1. function loadtweets() {
  2. // Get twitter search object
  3. $twitterSearch = new Zend_Service_Twitter_Search('json');
  4.  
  5. // Set the correct language for the search
  6. $searchQuery = array(
  7. 'lang' => 'en'
  8. );
  9.  
  10. // Run the search
  11. $searchResults = $twitterSearch->search('#drupal', $searchQuery);
  12.  
  13. // Return the results
  14. return $searchResults;
  15. }

After saving this script to a file you can run it just like any other PHP script.

php TweetStream.php

You can check that this script is running by running it through curl via the following command. Without the jsoncallback parameter the script will just return 'finished' and quit but it's a good way of making sure that the right port is being listened to.

curl www.tweetstream.local:4000

Anything you echo out within the script will be output to the terminal instead of to the user awaiting a connection. This is a good idea to see that things are firing as expected. To stop the program again just Ctrl+c.

What we are doing here is no better than just creating a web based script that will pull the latest tweets down and return them to the browser. The power of ReactPHP is that we can separate searching for tweets from the user requesting the list of tweets. We can do this by using the addPeriodicTimer() method of the event loop object to create a timer that executes every 10 seconds. With this we can pull down the latest tweets in a controlled manner without relying on user interactions to run the script.

  1. $loop->addPeriodicTimer(10, function() use() {
  2. gettweets();
  3. });

The gettweets() function runs the twitter search and stores each of the results in a file.

  1. function gettweets() {
  2. // Get twitter search object
  3. $twitterSearch = new Zend_Service_Twitter_Search('json');
  4.  
  5. // Set the correct language for the search
  6. $searchQuery = array(
  7. 'lang' => 'en'
  8. );
  9.  
  10. // Run the search
  11. $searchResults = $twitterSearch->search('#drupal', $searchQuery);
  12.  
  13. // Store the results
  14. if ($searchResults !== FALSE && count($searchResults['results']) > 0) {
  15. foreach ($searchResults['results'] as $result) {
  16. if (!file_exists($cache_directory . $result['id'])) {
  17. $fp = fopen($cache_directory . $result['id'], 'w+');
  18. fwrite($fp, serialize($result));
  19. fclose($fp);
  20. }
  21. }
  22. }
  23. }

We can then change the loadtweets() function to pull the data out of the saved files and return it to the request event when needed. As we used the id of the tweet for the filenames we can reverse sort them to get a sorted list of tweets with the last one first. This means that searching for tweets and showing those tweets to the user are now two separate events.

  1. function loadtweets() {
  2. // Get a list of the files in the cache directory
  3. $files = array();
  4. foreach (new DirectoryIterator('cache') as $fileInfo) {
  5. if ($fileInfo->isDot()) {
  6. continue;
  7. }
  8. $files[] = $fileInfo->getFilename();
  9. }
  10.  
  11. // Reverse sort the files we have found
  12. rsort($files);
  13.  
  14. // Uncompress each of the files
  15. foreach ($files as $key => $file) {
  16. $files[$key] = unserialize(file_get_contents('cache/' . $file));
  17. }
  18.  
  19. // Return the result
  20. return $files;
  21. }

Future Plans

The next step in this is to create a socket server and connect to it using either the HTML web socket interface or something like socket.io.js. This would do away with the need to a timing interval to pull the latest results. As new items are found they would be pushed directly to the browser and then just pushed to the top of the list rather than re-loading the entire tweet steam all over again.

One thing that I found interesting whilst playing with this stuff is the paradigm shift it required. I have been using PHP in a web server environment for so long that I kept thinking about the application in a stateless state. One main revelation was that any variable or function I created in the global scope persisted in that scope until the program was stooped. This meant that anything I set during one request was still there when the next request was made.

I can see lots of potential for ReactPHP, even after only a few minutes messing about with simple sockets and ports. What I have accomplished here was done in a couple of hours, although I spent a while trying to figure out how to contact the ReactPHP server in JavaScript. Take a look at the ReactPHP website for more information and some more (better) implementations of ReactPHP. I am currently familiarising myself with the package and will definitely be revisiting it again in the future.

Comments

Permalink
Hello. Can you explain me why you write function() use() { ... } ? You don't give any $param via use(), so why that syntax ?

Vladimir L (Wed, 03/26/2014 - 23:47)

Permalink
It's a mistake. The use() isn't needed here but it was left in whilst I was building up an understanding of how to use React.

Add new comment

The content of this field is kept private and will not be shown publicly.