Server-Side Techniques with PHP and MySQL 90 Errors can happen on the client side, or on the server side. We made efforts to have the client protected by implementing a try/catch mechanism in key portions of the code. On the other hand, when an error happens on the server, that error doesn't propagate to the client as a client error. Instead, on the client we must manually analyze the input received from the server, and if it doesn't look like what we expected, we generate an error manually using throw. If the display_errors setting in php.ini is set to Off, when a PHP parse or fatal error happens, the error is logged only to the Apache error log file (Apache/logs/error.log), and the script's output will be void. So if we receive a void response, we also assume that something bad happened on the server, and we build a generic error message on the client. For example, if you try to load the page when no internet connection is available (so the remote server isn't reachable), then it should result in the following error being displayed (the error message will look differently if display_errors is set to Off in php.ini): Figure 3.14: An Error Message When No Internet Connection is Available The code in proxyping.php simply uses the parameters received though GET to access the random number generator server. One interesting detail to note in this script is the way we set the T page expiration headers. Setting page expiration is important because the server is always called using the same URL and query string, and the client browser may decide to cache the result—and we don't want that, because the results wouldn't be exactly random any more. <?php // load the error handling module require_once('error_handler.php'); // make sure the user's browser doesn't cache the result header('Expires: Wed, 23 Dec 1980 00:30:00 GMT'); header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); header('Cache-Control: no-cache, must-revalidate'); header('Pragma: no-cache'); You can find an excellent article on page caching and PHP at http://www.sitepoint.com/ article/php-anthology-2-5-caching . The remainder of proxyping.php simply uses the file_get_contents function to retrieve a response from the random number generator service, and output it for the client. Chapter 3 // retrieve the parameters $num = 1; // this is hardcoded on the server $min = $_GET['min']; $max = $_GET['max']; // holds the remote server address and parameters $serverAddress = 'http://www.random.org/cgi-bin/randnum'; $serverParams = 'num=' . $num . // how many random numbers to generate '&min=' . $min . // the min number to generate '&max=' . $max; // the max number to generate // retrieve the random number from foreign server $randomNumber = file_get_contents($serverAddress . '?' . $serverParams); // output the random number echo $randomNumber; ?> A Framework for Making Repetitive Asynchronous Requests Quite frequently when building AJAX applications, you will need your client script to retrieve data from the server at regular intervals. There are numerous example scenarios, and you will meet many in this book, and perhaps many more in your real-world projects. JavaScript offers four functions that can help achieving repetitive (or scheduled) functionality: setTimeout, setInterval, clearTimeout, and clearInterval, which can be used like this: // using setTimeout and clearTimeout timerId = window.setTimeout("function()", interval_in_milliseconds); window.clearTimeout(timeId); // using setInterval and clearInterval timerId = window.setInterval("function()", interval_in_milliseconds); window.clearInterval(timeId); setTimeout causes the function to be executed once, after the specified time period. setInterval executes the function repeatedly, at the mentioned interval, until clearInterval is used. In most AJAX scenarios we prefer using setTimeout because it offers more flexibility in controlling when the server is accessed. For a quick demonstration, we will extend the client that reads random numbers by making the following improvements: • When making a server request, we wait until the response containing the random number is received, and then we use setTimeout to restart the sequence (to make a new server request) after one second. This way, the interval between two requests is one second plus the time it takes to retrieve the random number. If you want to make the requests at exact periods, you must use setInterval, but in that case you need to check that the XMLHttpRequest object isn't busy waiting to complete the previous request (which can happen if the network is slow, or the server busy). • In this new example, we will also check for the server's availability from time to time. The random number generator service has a buffer of random numbers, which is used to serve the requests, and anyone can check the buffer's level at http://www.random.org/ cgi-bin/checkbuf . Our program will check this page every 10 requests, and will request new random numbers only if the buffer level is at least . 50% 91 Server-Side Techniques with PHP and MySQL The web application will look like Figure 3.15: Figure 3.15: Making Repetitive Asynchronous Requests This repetitive task must start somewhere. In our application, everything starts with process(). There, we decide what kind of server request to make; we can either ask for a new random number, or we can check for the buffer level of the random number generator server. We check for the buffer level every 10 requests, and by default we don't ask for new random numbers unless the buffer is higher than . The process is described in the flowchart given opposite: 50% 92 Chapter 3 Figure 3.16: Flowchart Describing the Process of Retrieving Random Numbers With the default code, setTimeout is only called to restart the process after successful HTTP requests; there is no setTimeout in the catch blocks. (Depending on your particular solution, you may want to try calling the server again after a while even if an error happens.) Time for Action—Implementing Repetitive Tasks 1. In the foundations folder, create a new folder named smartproxyping. 2. In the smartproxyping folder, create a file named smartproxyping.html: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html> <head> <title>Practical AJAX: Making Repetitive Asynchronous Requests</title> <script type="text/javascript" src="smartproxyping.js"></script> </head> <body onload="process()"> 93 Server-Side Techniques with PHP and MySQL Server, gimme some random numbers!<br/> <div id="myDivElement" /> </body> </html> 3. In the same folder, create smartproxyping.js: // holds an instance of XMLHttpRequest var xmlHttp = createXmlHttpRequestObject(); // holds the remote server address and parameters var serverAddress = "smartproxyping.php"; var getNumberParams = "action=GetNumber" + // get a new random number "&min=1" + // the min number to generate "&max=100"; // the max number to generate var checkAvailabilityParams = "action=CheckAvailability"; // variables used to check for server availability var requestsCounter = 0; // counts how many numbers have been retrieved var checkInterval = 10; // counts interval for checking server availability var updateInterval = 1; // how many seconds to wait to get a new number var updateIntervalIfServerBusy = 10; // seconds to wait when server busy var minServerBufferLevel = 50; // what buffer level is considered acceptable // creates an XMLHttpRequest instance function createXmlHttpRequestObject() { // will store the reference to the XMLHttpRequest object var xmlHttp; // this should work for all browsers except IE6 and older try { // try to create XMLHttpRequest object xmlHttp = new XMLHttpRequest(); } catch(e) { // assume IE6 or older var XmlHttpVersions = new Array("MSXML2.XMLHTTP.6.0", "MSXML2.XMLHTTP.5.0", "MSXML2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP"); // try every prog id until one works for (var i=0; i<XmlHttpVersions.length && !xmlHttp; i++) { try { // try to create XMLHttpRequest object xmlHttp = new ActiveXObject(XmlHttpVersions[i]); } catch (e) {} } } // return the created object or display an error message if (!xmlHttp) alert("Error creating the XMLHttpRequest object."); else return xmlHttp; } // call server asynchronously function process() { 94 Chapter 3 // only continue if xmlHttp isn't void if (xmlHttp) { // try to connect to the server try { // if just starting, or if we hit the specified number of requests, // check for server availability, otherwise ask for a new random number if (requestsCounter % checkInterval == 0) { // check if server is available xmlHttp.open("GET", serverAddress + "?" + checkAvailabilityParams, true); xmlHttp.onreadystatechange = handleCheckingAvailability; xmlHttp.send(null); } else { // get new random number xmlHttp.open("GET", serverAddress + "?" + getNumberParams, true); xmlHttp.onreadystatechange = handleGettingNumber; xmlHttp.send(null); } } catch(e) { alert("Can't connect to server:\n" + e.toString()); } } } // function called when the state of the HTTP request changes function handleCheckingAvailability() { // when readyState is 4, we are ready to read the server response if (xmlHttp.readyState == 4) { // continue only if HTTP status is "OK" if (xmlHttp.status == 200) { try { // do something with the response from the server checkAvailability(); } catch(e) { // display error message alert("Error reading server availability:\n" + e.toString()); } } else { // display status message alert("Error reading server availability:\n" + xmlHttp.statusText); } } } // handles the response received from the server function checkAvailability() { // retrieve the server's response var response = xmlHttp.responseText; 95 Server-Side Techniques with PHP and MySQL 96 // if the response is long enough, or if it is void, we assume we just // received a server-side error report if(response.length > 5 || response.length == 0) throw(response.length == 0 ? "Server error" : response); // obtain a reference to the <div> element on the page myDiv = document.getElementById("myDivElement"); // display the HTML output if (response >= minServerBufferLevel) { // display new message to user myDiv.innerHTML += "Server buffer level is at " + response + "%, " + "starting to retrieve new numbers. <br/>"; // increases counter to start retrieving new numbers requestsCounter++; // reinitiate sequence setTimeout("process();", updateInterval * 1000); } else { // display new message to user myDiv.innerHTML += "Server buffer is too low (" + response + "%), " + "will check again in " + updateIntervalIfServerBusy + " seconds. <br/>"; // reinitiate sequence setTimeout("process();", updateIntervalIfServerBusy * 1000); } } // function called when the state of the HTTP request changes function handleGettingNumber() { // when readyState is 4, we are ready to read the server response if (xmlHttp.readyState == 4) { // continue only if HTTP status is "OK" if (xmlHttp.status == 200) { try { // do something with the response from the server getNumber(); } catch(e) { // display error message alert("Error receiving new number:\n" + e.toString()); } } else { // display status message alert("Error receiving new number:\n" + xmlHttp.statusText); } } } // handles the response received from the server function getNumber() { // retrieve the server's response var response = xmlHttp.responseText; // if the response is long enough, or if it is void, we assume we just // received a server-side error report if(response.length > 5 || response.length == 0) Chapter 3 throw(response.length == 0 ? "Server error" : response); // obtain a reference to the <div> element on the page myDiv = document.getElementById("myDivElement"); // display the HTML output myDiv.innerHTML += "New random number retrieved from server: " + response + "<br/>"; // increase requests count requestsCounter++; // reinitiate sequences setTimeout("process();", updateInterval * 1000); } 4. In the same folder, create smartproxyping.php: <?php // load the error handling module require_once('error_handler.php'); // make sure the user's browser doesn't cache the result header('Expires: Wed, 23 Dec 1980 00:30:00 GMT'); // time in the past header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); header('Cache-Control: no-cache, must-revalidate'); header('Pragma: no-cache'); // retrieve the action parameter $action = $_GET['action']; // check availability or get new random number? if ($action == 'GetNumber') { $num = 1; // value is hardcoded because client can't deal with more numbers $min = $_GET['min']; $max = $_GET['max']; // holds the remote server address and parameters $serverAddress = 'http://www.random.org/cgi-bin/randnum'; $serverParams = 'num=' . $num . // how many random numbers to generate '&min=' . $min . // the min number to generate '&max=' . $max; // the max number to generate // retrieve the random number from foreign server $randomNumber = file_get_contents($serverAddress . '?' . $serverParams); // output the random number echo $randomNumber; } elseif ($action == 'CheckAvailability') { // address of page that returns buffer level $serverAddress = 'http://www.random.org/cgi-bin/checkbuf'; // received buffer level is in form 'x%' $bufferPercent = file_get_contents($serverAddress); // extract the number $buffer = substr($bufferPercent, 0, strlen($bufferPercent) - 2); // echo the number echo $buffer; } else { echo 'Error talking to the server.'; } ?> 5. In the same folder, create the error_handler.php file, which should be identical to its version from the previous exercises: <?php // set the user error handler method to be error_handler set_error_handler('error_handler', E_ALL); // error handler function 97 Server-Side Techniques with PHP and MySQL 98 function error_handler($errNo, $errStr, $errFile, $errLine) { // clear any output that has already been generated if(ob_get_length()) ob_clean(); // output the error message $error_message = 'ERRNO: ' . $errNo . chr(10) . 'TEXT: ' . $errStr . chr(10) . 'LOCATION: ' . $errFile . ', line ' . $errLine; echo $error_message; // prevent processing any more PHP scripts exit; } ?> 6. Load http://localhost/ajax/foundations/smartproxyping/ smartproxyping.html . The output should look like the one in Figure 3.15. What Just Happened? Our client, in this example, knows how to check from time to time if the server is available. The random number generator service provides the page http://www.random.org/cgi-bin/checkbuf —which you can use to check its buffer level. The JavaScript code in smartproxyping.js starts by defining a number of global variables that you use to control the program's behavior: // holds the remote server address and parameters var serverAddress = "smartproxyping.php"; var getNumberParams = "action=GetNumber" + // get a new random number "&min=1" + // the min number to generate "&max=100"; // the max number to generate var checkAvailabilityParams = "action=CheckAvailability"; // variables used to check for server availability var requestsCounter = 0; // counts how many numbers have been retrieved var checkInterval = 10; // counts interval for checking server availability var updateInterval = 1; // how many seconds to wait to get a new number var updateIntervalIfServerBusy = 10; // seconds to wait when server busy var minServerBufferLevel = 50; // what buffer level is considered acceptable These variables contain the data required to make server requests. getNumberParams contains the query string parameters needed to request a new random number, and checkAvailabilityParams contains the parameters used to check the server's buffer level. The other variables are used to control the intervals for making the asynchronous requests. A novelty in this exercise compared to the previous ones is that you have two functions that handle server responses— handleCheckingAvailability and handleGettingNumber. The roots of this happen to be in the process() function, which assigns one of these callback functions depending on the server action it requests. In this program, process() is not called only once as in other exercises; instead, it is called multiple times, and each time it must decide what action to make—should it ask for a new random number, or should it check the server's buffer level? The requestsCounter variable, which keeps a track of how many times we have retrieved a new random number since the last buffer check, helps us make a decision: Chapter 3 function process() { // if (requestsCounter % checkInterval == 0) { // check if server is available xmlHttp.open("GET", serverAddress + "?" + checkAvailabilityParams, true); xmlHttp.onreadystatechange = handleCheckingAvailability; xmlHttp.send(null); } else { // get new random number xmlHttp.open("GET", serverAddress + "?" + getNumberParams, true); xmlHttp.onreadystatechange = handleGettingNumber; xmlHttp.send(null); } // } The handleCheckingAvailability and handleGettingNumber functions are similar; they both are specialized versions of the handleRequestStateChange function you know from the previous exercises. Their role is to wait until the response has successfully been received from the server, and call a helper function ( checkAvailability and getNumber) to deal with the response as soon as the response is in. Notice the action query string parameter, which is used to tell the PHP script what kind of remote server request to make. On the server side, in smartproxyping.php, after loading the error-handling module, we read that action parameter and decide what to do depending on its value: <?php // load the error handling module require_once('error_handler.php'); // retrieve the action parameter $action = $_GET['action']; // check availability or get new random number? if ($action == 'GetNumber') { // If the action is GetNumber then we use the file_get_contents PHP function to read a new random number from the remote server: if ($action == 'GetNumber') { $num = 1; // value is hardcoded because client can't deal with more numbers $min = $_GET['min']; $max = $_GET['max']; // holds the remote server address and parameters $serverAddress = 'http://www.random.org/cgi-bin/randnum'; $serverParams = 'num=' . $num . // how many random numbers to generate '&min=' . $min . // the min number to generate '&max=' . $max; // the max number to generate // retrieve the random number from foreign server $randomNumber = file_get_contents($serverAddress . '?' . $serverParams); // output the random number echo $randomNumber; } 99 . caching and PHP at http://www.sitepoint.com/ article /php- anthology- 2-5 -caching . The remainder of proxyping .php simply uses the file_get_contents function to retrieve a response from the random. < ?php // set the user error handler method to be error_handler set_error_handler('error_handler', E_ALL); // error handler function 97 Server-Side Techniques with PHP and MySQL. setTimeout("process();", updateInterval * 100 0); } 4. In the same folder, create smartproxyping .php: < ?php // load the error handling module require_once('error_handler .php& apos;); // make sure