ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


Migrating Web-Based PHP Applications to Ajax

by Bill Lubanovic
05/10/2007

Web development is messy. Over the years, our toolbox has filled up with odd implements that are hard to use and don't fit well together. Web code has become a legacy problem. A typical web page is a tangle of HTML, JavaScript, and server-side scripts. User interface logic is interwoven with business rules and client-server communications. In most programming environments, we use documented APIs, so you just pass arguments to a function and get back the results. In the web environment, we've typically needed hacks like populating hidden fields in a form, and regenerating the whole page, even for a tiny change. Can we make the process more rational?

This article describes a makeover of a typical database-backed web form. We'll show some old code – a mixture of HTML, JavaScript, and PHP – and rebuild it with modern web techniques like Ajax, and modern tools like jQuery. The benefits will include:

The Old Grey Web

In the beginning was HTML, shortly followed by forms, client-side JavaScript, and server-side CGI scripts. You would fill in form variables and submit the form to the CGI script, or generate a long GET-style URL in JavaScript. JavaScript was close to undebuggable. Core variables like window were not part of the language, and the Microsoft-Netscape browser wars introduced gratuitous differences that continue to plague us. Things got a little better over time, as the W3C defined the DOM and developers built cross-platform DHTML libraries.

Some dynamic pages are easy, you query the database once and dump some nicely formatted HTML, but most are harder. Database values populate pull-down menus (HTML select and option tags) and choices (radio and checkbox tags). Some forms cover multiple pages, and the logic for maintaining overall state gets trickier. Every form submission returns a fully regenerated page, maintaining all the state of the previous submissions.

Traditional options on how to structure the application include:

The second option sounds better, but it still has problems: if an action changed the data underlying the form elements in the enclosing page, the whole page needs to be regenerated. We need a third option.

The New Toolbox

The common restriction is the whole-page design. No matter what you do, no matter how small the change, you submit it to the server and get back a brand new page (an iframe is like a mini-page, and its size can't be changed). A variety of JavaScript remoting techniques have tried to make the web client-server connection work more like a procedure call. I think the most useful contributions are:

innerHTML
This DOM attribute, introduced by Microsoft in IE4, is a de facto (not official W3C) standard supported by all modern browsers. With it, you get and set the contents between any HTML start and end tags, without a page refresh. This chunk of HTML:
<p id="changeme">Now you see it.</p>
can be modified by this chunk of JavaScript:
var obj = document.getElementById("changeme");
obj.innerHTML = "Now you don't";
to produce this:
<p id="changeme">Now you don't</p>
Although the DOM has functions to change page elements dynamically, innerHTML is simpler and faster. Unfortunately, innerHTML is a read-only attribute for many table elements in IE, forcing use of alternatives.
 
XMLHttpRequest
This is an API to send client requests to the server and receive the server responses. Microsoft invented it to make Outlook Web Access work more like a desktop application. It was included in IE5 and is supported by all modern browsers. Combined with innerHTML, you can call a server script and use the data returned to update any element on the page. Despite its utility, this function was hidden in plain sight for years (you wouldn't find it in JavaScript or DHTML books). Things changed when Google Mail, Maps, and Suggest demonstrated its responsiveness and introduced a new web application model.
 
Ajax
The dam burst in 2005, when Jesse James Garrett named the new model Ajax (Asynchronous JavaScript and XML), comprising XMLHttpRequest, the DOM, and other techniques. The brand and its timing were perfect, rejuvenating web development and rehabilitating JavaScript.
 
JSON
The old remoting frameworks used various formats for the client-server data stream, and XMLHttpRequest, as the name suggested, used XML. Doug Crockford designed the light and simple JSON (JavaScript Object Notation) format. It's trivial to parse JSON into JavaScript data structures (just eval(json_string)), and faster than deconstructing XML.
 
JavaScript libraries
High-quality JavaScript libraries have been developed to simplify new-model web development and bridge the inevitable cross-browser issues. I've chosen John Resig's jQuery over worthy competitors like Prototype, Dojo, or YUI for a number of reasons:

The Makeover

History class is over, and beauty class begins.

To make the presentation clear and short, our code examples ignore errors and possible security issues. The purpose is to show how jQuery and Ajax techniques can improve an old script. For production use, you would check function error returns, untaint input data, and follow the other rules of good web hygiene. With the new Ajax methods, an error in the client or server code can cause a silent failure. FireBug is a very handy tool for developing and debugging Ajax applications. The full-featured version is a Firefox plugin, but a light version is available for IE and other browsers.

Let's define our form's requirements:

  1. Get data from a people table in a database. The id column is the primary key.
  2. Display the names in a pull-down menu (a form select element).
  3. Let the user select a person from the menu.
  4. Display information about that person in a table: first name, last name, favorite dance, and favorite pie.

This example assumes the number of people will fit in an HTML select element without killing the browser. Larger data would require a paged table or something similar. The page should look something like this:


People

First Name First Name Dance Pie
Alfredo de Darc tango blueberry

Version 1: Original Code

In the original version we do everything in a single PHP script: write the static HTML, create the original list of people, and fill in the lower table if a person had been selected.

people1.php:
<?php
$cmd    = @$_REQUEST["cmd"];
$id     = @$_REQUEST["id"];
mysql_connect($server, $user, $password);
mysql_select_db("test");
?>
<html>
<head><title>Old Form</title>
<script>
// Get the selected user and retrieve his/her info
function user_info(sel)
        {
        var opt    = sel.options;
        var user_id    = opt[sel.selectedIndex].value;
        // Construct a GET URL, or create a hidden field for "cmd"
        var url  =  "people1.php?cmd=info&id=" + user_id;
        window.location.href = url;
        }
</script>
</head>
<body>
<form action="people1.php" method="post">
People<br>
<select name="people" onchange="user_info(this)">
<option value="">(select a person)
<?php
// Get all users and display every time script is called
$result = mysql_query("select id, fname, lname from people");
while ($row = mysql_fetch_array($result, MYSQL_ASSOC))
        echo "<option value='$row[id]'",
                $row["id"] == $id ? " selected" : "",
                ">$row[fname] $row[lname]\n";
?>
</select>
<?php
if ($cmd == "info")
        {
    $id     = mysql_real_escape_string($id);
        $result = mysql_query("select * from people where id='$id'");
        $info = mysql_fetch_array($result, MYSQL_ASSOC);
        }
else
        $info = array("fname"=>" ", "lname"=>" ", "dance"=>" ", "pie"=>" ");
echo <<< END
<br>
<table border=1>
<tr>
<td>First Name</td><td>Last Name</td><td>Dance</td><td>Pie</td></tr>
<tr>
<td>$info[fname]</td>
<td>$info[lname]</td>
<td>$info[dance]</td>
<td>$info[pie]</td>
</tr>
</table>

END;
?>
</form>
</body>
</html>

This isn't too hard to write or understand, but it already has problems. The good news is that the code is all in the same place. The bad news is the same as the good news.

New Design

Here's the plan:

  1. Split the initial script into three files: static content (HTML), client-side processing (JavaScript), and server-side processing (PHP).
  2. Include jquery.js and the new JavaScript file in the HTML file.
  3. Add a unique id attributes to all dynamic content tags.
  4. Define JavaScript functions to make Ajax-style server calls and update page elements.

We can merge the dynamic and static content in one of two ways:

Let's try both.

Version 2: Ajax Submit, HTML Return

In this method, PHP generates the HTML for the option tags, and we just stuff the HTML into the enclosing select. Now we start with an HTML file, and it's just a container:

people2.html:
<html>
<head><title>New Form Version 1</title>
<script src="jquery.js"></script>
<script src="people2.js"></script>
</head>
<body>
People<br>
<select id="people">
</select>
<br>
<table border=1>
<thead>
<tr><td>First Name</td><td>Last Name</td><td>Dance</td><td>Pie</td></tr>
</thead>
<tbody id="info">
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
</tbody>
</table>
</body>
</html>

Now let's look at the JavaScript file. jQuery's central function is $(), which returns a jQuery object. Its arguments may have many forms, but we'll use these now:

The jQuery call $(document).ready replaces window.onload. It's called when the DOM is ready, instead of waiting for all images to load. This avoids synchronization problems. All of the JavaScript functions that work with form elements can be in here.

The load() function calls the server script people.php and inserts its output into the select element with id people. This is the equivalent of the first chunk of the original version. It makes an Ajax connection to the server, and inserts the returned HTML into the element with id people:

(I've spread out the formatting to help distinguish the parentheses and curly brackets.)

people2.js:
$(document).ready
    (
    function()
        {
        // Call this when the DOM is ready:
        $("#people").load("people2.php?cmd=init");
        // Call this when a person is selected:
        $("#people").change(function()
            {
            // get the user's id from the selected option:
            var user_id = $(":selected").val();
            $("#info").load("people2.php?cmd=info&id=" + user_id);
            });
        }
    );

For this version, that's all the JavaScript we need. By the way, this approach has been called AHAH (Asynchronous HTML and HTTP). Now we'll look at the PHP script it calls. This version is like the original, but it only prints the HTML fragment for the current query:

people2.php:
  <?php
 
  $cmd    = @$_REQUEST["cmd"];
  $id    = @$_REQUEST["id"];
  mysql_connect($server, $user, $password);
mysql_select_db("test");
if ($cmd == "init")
        {
        $result = mysql_query("select id, fname, lname from people");
        echo "<option value=''>(select a person)\n";
        while ($row = mysql_fetch_array($result, MYSQL_ASSOC))
                echo "<option value='$row[id]'>$row[fname] $row[lname]\n";
        }
elseif ($cmd == "info")
        {
        $id     = mysql_real_escape_string($id);
        $result = mysql_query("select * from people where id='$id'");
        $info = mysql_fetch_array($result, MYSQL_ASSOC);
        echo <<< END
<tr>
<td>$info[fname]</td>
<td>$info[lname]</td>
<td>$info[dance]</td>
<td>$info[pie]</td>
</tr>

END;
        }
?>

Although you could call this approach AJAJ, thankfully no one does.

Version 3: Ajax Submit, JSON Return

In this alternative, PHP builds a data array from the database query and returns it to JavaScript in JSON format. jQuery converts this JSON string into a jQuery object and passes it to a callback function, which builds the HTML for the options and inserts it into the appropriate page element. This example is the same as people2.html, except it calls people3.js:

people3.html:
<html>
<head><title>New Form Version 1</title>
<script src="jquery.js"></script>
<script src="people3.js"></script>
</head>
<body>
People<br>
<select id="people">
</select>
<br>
<table border=1>
<thead>
<tr><td>First Name</td><td>Last Name</td><td>Dance</td><td>Pie</td></tr>
</thead>
<tbody id="info">
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
</tbody>
</table>
</body>
</html>

We're just going to fill the container in a different way. In this version, we call getJSON with three arguments:

A GET URL will be built from the URL and name:value arguments, and the returned JSON string will be converted into a JavaScript object and passed to the callback function.

people3.js:
 $(document).ready
        (
        function()
                {
        // Call this when the DOM is ready:
                $.getJSON("people3.php",
                        { cmd : "init" },
                        make_menu);
        // Call this when a person is selected:
                $("#people").change(function()
                        {
                        var user_id = $(":selected").val();
                        $.getJSON("people3.php",
                                { cmd : "info", id: user_id  },
                                make_info);
                        });
                }
        );
function make_menu(obj)
        {
        var str = "";
        var len = obj.length;
        str += "<option value=''>(select a person)\n";
        for (var i = 0; i < len; i++)
                {
                var user = obj[i];
                str += "<option value='" + user["id"] + "'>" +
                        user["fname"] + " " +
                        user["lname"] + "\n";
                }
        $("#people").html(str);
        }
function make_info(info)
        {
        var str = "<tr>";
    // You can get each value as info.name or info["name"].
    // Let's get the first name using the first way.
        str += "<td>" + info.fname + "</td>";
        str += "<td>" + info["lname"] + "</td>";
        str += "<td>" + info["dance"] + "</td>";
        str += "<td>" + info["pie"] + "</td>";
        str += "</tr>\n";
        $("#info").html(str);
        }

This version of the PHP script is even simpler than people2.php. Instead of creating HTML from the database return values, we just encode the PHP data array in JSON format and send it off:

people3.php:
<?php
$cmd    = @$_REQUEST["cmd"];
$id     = @$_REQUEST["id"];
mysql_connect($server, $user, $password);
mysql_select_db("test");
if ($cmd == "init")
        {
        $result = mysql_query("select id, fname, lname from people");
        $user_array = array();
        while($row = mysql_fetch_array($result, MYSQL_ASSOC))
                $user_array[] = $row;
        echo json_encode($user_array);
        }
elseif ($cmd == "info")
        {
        $id = mysql_real_escape_string($id);
        $result = mysql_query("select * from people where id='$id'");
        $info = mysql_fetch_array($result, MYSQL_ASSOC);
        echo json_encode($info);
        }
?>

The json_encode function is included with standard PHP starting with version 5.2. For earlier versions, see the PHP JSON manual section.

If you chose our friend Alfredo from the menu, the JSON returned would look like this:

{"id":"1","fname":"Alfredo","lname":"de Darc","dance":"tango","pie":"blueberry"}

That JSON string is converted to a JavaScript object by jQuery and passed to the make_menu function as the info argument.

Judging the Makeover

The main choice between these new versions is where to do the output formatting: in PHP (version 2) or in JavaScript (version 3). Another factor might be what other plans you have for the data. Instead of throwing info away after generating the HTML in version 3, you could save it in a global JavaScript variable and use it for other purposes later.

Are these new versions better than the original? Let's see if our original promises were kept:

Separating dynamic content from static content.
All the static content is in the HTML file, and the dynamic data from the PHP script are processed in JavaScript for page placement.
 
Separating content, style, and processing.
We didn't show it here, but a separate CSS file would be a nice orthogonal addition.
 
Web client-server communication via function calls.
Good old Ajax.
 
Partial page updates instead of flash-bang page reloads.
Ajax again.
 
Faster development and more maintainable code.
PHP now just gets data from the database and returns output chunks (HTML or JSON) rather than whole pages.
 
Faster load times and improved caching.
people2.html, people2.js, people3.html, and people3.js are static files that will be cached by the browser (and the web server, which will make the application more scalable). The whole output page is also cached, the only changes being performed by JavaScript within the browser. Finally, we avoid a database lookup in every call to people2.php or people3.php after the first one.

The most important benefit is that the new approach will scale much better with future requirements, such as adding a new person or editing the data of an existing person. And new requirements are as sure as death and taxes.

Bill Lubanovic started developing software with UNIX in the 70s, GUIs in the 80s, and the Web in the 90s. He now does web visualization work for a wind energy company.


Return to the PHP DevCenter.

Copyright © 2009 O'Reilly Media, Inc.