 | Level: Intermediate Nicholas Chase (ibmquestions@nicholaschase.com), Freelance writer, Backstop Media Gina Deol (gdeol@binaryits.com), Vice President, E-commerce Development, Binary IT Solutions
05 Sep 2006 Throughout this "Understanding the Zend Framework" series, we use the PHP Zend Framework to create the Chomp online feed reader, and now it's time to do one last tweak to improve usability. This article shows how to use Ajax to add information to a page without reloading the entire page, and how to use the Zend Framework to easily streamline those requests by translating data to and from the JavaScript Object Notation (JSON).
Introduction
In Part 8 of this series, we added Yahoo!, Amazon, and Flickr results to the Chomp application. Now we're going to improve performance by loading only the data the user requests, when the user requests it. But first, let's see where we are.
How we got here
This nine-part "Understanding the Zend Framework" series chronicles the building of an online feed reader, Chomp, while explaining the major aspects of using the recently introduced open source PHP Zend Framework.
In Part 1, we talk about the overall concepts of the
Zend Framework, including a list of relevant classes and a general discussion of the MVC pattern. In Part 2, we expand on that to show how MVC can be implemented in a Zend Framework application. We also create the user registration and login process, adding user information to the database and pulling it back out again.
Parts 3 and 4 deal with the actual RSS and Atom feeds. In Part 3, we enable users to subscribe to individual feeds and to display the items listed in those feeds. We also discuss some of the Zend Framework's form-handling capabilities, validating data, and sanitizing feed items. Part 4 explains how to create a proxy to pull data from a site that has no feed.
The rest of the series involves adding value to the Chomp application. Part 5 explains how to use the Zend_PDF module to enable the user to create a customized PDF of saved articles, images, and search results. In Part 6, we use the Zend_Mail module to alert users to new posts. In Part 7, we look at searching saved content and
returning ranked results. In Part 8, we add an extra dimension to our feed reader by linking the online resources of Amazon.com, Yahoo!, and Flickr with our current application to create a robust mashup.
Now in the final part of the series, we will use Ajax and the Zend Framework's Zend_Json component to update part of the search results page automatically with specific requested information.
So what are Ajax and JSON, anyway?
Typically, people surf the Web by clicking on links to get more information. It most cases, this results in replacing and reloading the entire page, even if most of the page is unchanged. That is changing, however.
In many cases, there's no reason to replace an entire page just because you want an update to a sidebar. Instead, it would be much more convenient to leave the page in place and simply add information or alter information that is already there. To do that, developers use a combination of HTTP, JavaScript, and, in some cases, XML. This programmatic mashup was used in relative obscurity for years. However, it was given the cool name Ajax (for Asynchronous JavaScript and XML) and became a phenomenon. The Ajax process involves the browser sending off an HTTP request in the background. When it receives the data, it adds it to the page, typically as the contents of a div element.
Now, XML, while well suited for Web service requests in general, is not always the best way to execute this in a browser. It turns out that JavaScript internally represents objects as a simple string, which we can easily create and interpret manually. For example, look at an object you might create, as shown below.
Listing 1. Creating an object
var theFeed = new Object();
theFeed.title = "Chaos Magnet";
theFeed.url = "http://feeds.feedburner.com/ChaosMagnet";
|
This could instead be represented by the JSON string shown below.
{"title":"Chaos Magnet",
"url":"http://feeds.feedburner.com/ChaosMagnet"}
|
This may not seem like a big deal, but consider the fact that virtually every programming language has a routine to translate its own native objects into this form. This capability enables us to create a JavaScript object, send it as a JSON string, to a PHP routine, which translates it into a native PHP object. The PHP object is then translated back into a JSON string, which is returned as a Web service result to the JavaScript, which turns it back into a native JavaScript object.
We can even send and receive these strings as Ajax requests. But first, we need to do a little restructuring on the Chomp application.
Breaking out requests
As it stands right now, when the user performs a search, the viewSearchResults action in the FeedController not only creates the search results themselves but it also performs a request of the Amazon and Flickr Web services, and embeds them all in a single view. One of the problems with this involves performance because Web service requests are rarely fast, and including two of them in a single page can really slow things down.
Our first step in creating our Ajax request is to break this out into individual pieces. For example, we can move the Amazon and Flickr results from the main view, viewedSearchResults.php, to their own files, amazonView.php and flickrView.php.
Listing 2. amazonView.php
<h4>Were you looking for any of these books?</h4>
<ol>
<?php
foreach ($this->amazonHits as $result) {
echo "<li><img src='" .
$result->SmallImage->Url->getUri() .
"' width='" . $result->SmallImage->Width .
"' height='" . $result->SmallImage->Height . "' /> ";
echo "<a href='" . $result->DetailPageURL . "' title='" .
htmlentities($result->Title, ENT_QUOTES). " at Amazon.com'>";
echo "<strong>" . htmlentities($result->Title) .
"</strong></a>";
echo " (Ranked #" . $result->SalesRank . ")</li>";
}
?>
</ol>
|
This code is the same as it was in the main view. We simply moved it to its own file. We can perform the same step for the Flickr results (see Listing 3).
Listing 3. flickrView.php
<table>
<caption>Photos from <a
href="http://flickr.com/">Flickr</a></caption>
<tbody>
<?php
foreach ($this->flickrHits as $index=>$result) {
// Begin column
if ( $index % $this->columns == 0 ) {
echo '<tr>';
}
$thumbnail = '<img src="' . $result->Square->uri .
'" width="75" height="75" />';
echo '<td><a href="' . $result->Large->clickUri .
'" title="to Flickr">' . $thumbnail . '</a><br />';
echo '<small>by <a href="http://www.flickr.com/photos/' .
$this->escape($result->ownername) .
'" title="Owner">' . htmlentities($result->ownername) .
'</a> on ' . $result->datetaken .
'</small></td>';
// Close column
if ( $index % $this->columns == $this->columns - 1 ) {
echo '</tr>';
}
}
?>
</tbody>
</table>
|
Place both of these files (amazonView.php and flickrView.php) in the views directory.
Update the main view
Of course, we don't want to lose the data from this page altogether -- yet. Instead, we want to display the results of rendering those individual views. We can do that by including the rendered text just as we would include other data.
Listing 4. Streamlining viewSearchResults.php
...
$title = $feedTitle;
if($channelTitle != '')
$title = "$title > $channelTitle";
echo "<tr><td>#" . $i++ . ":</td>";
echo "<td><a href=\"$url\">$title</a></td>";
echo "<td>$score</td></tr>";
}
?>
</table>
<?php echo($this->renderedAmazon) ?>
<?php echo($this->renderedFlickr) ?>
</body>
</html>
|
To make this data available, we need to update the FeedController.
Listing 5. updating FeedController.php
public function viewSearchResultsAction()
{
...
$view = Zend::registry('view');
$view->title = "Search Results for: $query";
$view->hits = $hits;
$amazonView = Zend::registry('view');
require_once 'Zend/Service/Amazon/Query.php';
$key = 'YOUR_AMAZON_KEY_HERE';
$amazonQuery = new Zend_Service_Amazon_Query($key);
$amazonQuery->Category('Books')
->Keywords($filterGet->getRaw('query'))
->ResponseGroup('Medium');
$amazonView->amazonHits = $amazonQuery->search();
$view->renderedAmazon = $amazonView->render('amazonView.php');
$flickrView = Zend::registry('view');
$key = 'f50c3c5b6384493f20e69b70b9ff7d29';
$flickrQuery = new Zend_Service_Flickr($key);
$tags = explode(' ', $filterGet->getRaw('query'));
$flickrView->flickrHits = $flickrQuery->tagSearch($tags);
$view->renderedFlickr =
$flickrView->render('flickrView.php');
echo $view->render('viewSearchResults.php');
}
|
Here, we create two new view objects and render them with the data previously sent to the main view. This rendering process simply provides text, so we can set the rendered text as an attribute on the main view.
The result is a page that looks exactly like what we already had, as shown in Figure 1.
Figure 1. The reworked results
The difference is that now we can begin to manipulate how we use the individual pieces of data.
Creating the new links
The next step is to remove the data from the view altogether and replace it with links that will enable us to call it on demand. To do that, edit the viewedSearchResults.php file, as shown below.
Listing 6. Replacing content with links
...
echo "<tr><td>#" . $i++ . ":</td>";
echo "<td><a href=\"$url\">$title</a></td>";
echo "<td>$score</td></tr>";
}
?>
</table>
<center><p>
<a href="javascript:getMashup('amazon')"
target="_blank">Show related books</a>
|
<a href="javascript:getMashup('flickr')"
target="_blank">Show related photos</a>
</p></center>
</body>
</html>
|
These links are not yet active. We'll build the getMashup() function in a moment, but if you reload the search page, you can see the results in Figure 2.
Figure 2. Showing the new links
Creating the new mashup action
Before we create the JavaScript function that will request the new data, we need to create the action it will call. In this case, we're creating a new action, mashupAction, that accepts the type of mashup and the query, and returns the appropriate data, as shown below.
Listing 7. Adding the new action to FeedController.php
...
public function viewSearchResultsAction()
{
...
$view = Zend::registry('view');
$view->title = "Search Results for: $query";
$view->hits = $hits;
$view->query = $query;
echo $view->render('viewSearchResults.php');
}
public function mashupAction()
{
$filterGet = Zend::registry('fGet');
$type = $filterGet->getRaw('type');
if ($type == 'amazon'){
$amazonView = Zend::registry('view');
require_once 'Zend/Service/Amazon/Query.php';
$key = 'YOUR_AMAZON_KEY_HERE';
$amazonQuery = new Zend_Service_Amazon_Query($key);
$amazonQuery->Category('Books')
->Keywords($filterGet->getRaw('query'))
->ResponseGroup('Medium');
$amazonView->amazonHits = $amazonQuery->search();
echo $amazonView->render('amazonView.php');
}
if ($type == 'flickr'){
$flickrView = Zend::registry('view');
$key = 'YOUR_FLICKR_KEY_HERE';
$flickrQuery = new Zend_Service_Flickr($key);
$tags = explode(' ', $filterGet->getRaw('query'));
$flickrView->flickrHits = $flickrQuery->tagSearch($tags);
echo $flickrView->render('flickrView.php');
}
}
...
|
First, notice that we removed the extra information from the viewSearchResultsAction. We then create the mashupAction, which accepts two parameters using the GET filter, type, and query. Depending on the type, the action renders the Amazon view or the Flickr view.
You can see this action by pointing your browser to http://localhost/feed/mashup?type=flickr&query=pentagon or a URL with a similar query. You should see results similar to what's shown in Figure 3.
Figure 3. Showing the new mashup action
You can also change the type parameter to amazon to see the other data. We will include this data on the search results page, but only on demand.
Now we are ready to start adding Ajax to the mix.
Adding Ajax
The process of adding Ajax to a page used to be excruciating. Browser differences alone were enough to make many developers shy away. Fortunately, the process has been simplified by a number of different free and open source tools. In the case of this article, we're going to use the Prototype Framework. Download "just the JavaScript" from http://prototype.conio.net/ and save the file in the Web server's document root. At the time of this writing, the latest version is 1.4.0.
We can use this library by including it on the page and referencing the Ajax object, as shown below.
Listing 8. Adding Ajax to viewSearchResults.php
<html>
<head>
<script src="/prototype.js"></script>
<title><?php echo $this->escape($this->title);
?></title>
<script type="text/javascript">
function getMashup(mashupType){
var url = 'http://localhost/feed/mashup';
var myAjax = new Ajax.Request
(
url,
{
method: 'get',
parameters:
'type='+mashupType+'&query=<?php echo $this->query
?>',
onSuccess: renderResults
}
);
}
function renderResults(response){
var renderDiv = document.getElementById('mashupResults');
renderDiv.innerHTML = response.responseText;
}
</script>
</head>
<body>
[<a href='/'>Back to Main Menu</a>]<br>
<h1><?php echo $this->escape($this->title); ?></h1>
...
<center><p>
<a href="javascript:getMashup('amazon')">Show related books</a>
|
<a href="javascript:getMashup('flickr')">Show related photos</a>
</p></center>
<div id="mashupResults"></div>
</body>
</html>
|
First, we add the library to the page. (I renamed it prototype.js for maintainability.) Putting it in the document root enables us to reference it without hardcoding the actual location.
Next, we create the getMashup() function, which creates the new Ajax.Request. This request takes as arguments the URL -- notice that it is the mashup action for the FeedController -- and a set of parameters, including which method to use (GET or POST), parameters to send with the request, and the JavaScript action to execute when the response comes back.
In this case, that function is renderedResults(), which receives the HTTP response as an argument. That function simply gets a reference to the new div element we added, mashupResults, and sets its contents as the text of the response.
Because that text is HTML, the user can now click on one of the two links and see the results added to the page, as shown in Figure 4.
Figure 4. The Ajax result
Adding JSON
That process was certainly less painful than it used to be. In fact, it was downright easy. The only difficulty here is that what arrives in the browser is essentially one big blob of text. That doesn't matter now because we are just displaying it on the page, but it many cases, that is not what we want. So before we finish up, we need to take a look at a second way of dealing with these requests: JSON.
We'll start on the JavaScript side. Rather than sending our mashup request parameters as name-value pairs, let's create a JavaScript object that includes them as attributes.
Listing 9. Creating the JavaScript object request
<html>
<head>
<script src="/prototype.js"></script>
<script src="/json.js"></script>
<title><?php echo $this->escape($this->title);
?></title>
<script type="text/javascript">
function getMashup(mashupType){
var requestObject = new Object();
requestObject.type = mashupType;
requestObject.query = '<?php echo $this->query ?>';
var jsonRequest = requestObject.toJSONString();
alert(jsonRequest);
var url = 'http://localhost/feed/mashup';
var myAjax = new Ajax.Request(
url,
{
method: 'get',
parameters:
'request='+escape(jsonRequest),
onSuccess: renderResults
}
);
}
...
|
We're adding a second library, available from http://www.json.org/js.html. This library adds two new functions to JavaScript: toJSONString() and parseJSON().
We next create a simple object and set its attribute values. As before, the query is hardcoded by the PHP view-rendering process. We can then convert directly to this object to a JSON string and add it to the request as a parameter. If we set the page to output the string in a text box, we can see the results shown in Figure 5.
Figure 5. The JSON text
We then need to update the mashupAction() to look for this new data.
Listing 10. Accepting the JSON text in mashupAction()
...
public function mashupAction(){
$filterGet = Zend::registry('fGet');
$request = $filterGet->getRaw('request');
$requestObject = Zend_Json::decode($request,
Zend_Json::TYPE_OBJECT);
$type = $requestObject->type;
$query = $requestObject->query;
if ($type == 'amazon'){
$amazonView = Zend::registry('view');
require_once 'Zend/Service/Amazon/Query.php';
...
|
Instead of getting to separate requests for the type and query, we now extract a single string of text, $request, and use the Zend_Json component to convert it into a native PHP object. (If you leave out the second parameter for the decode() method, it returns an array rather than an object.) We can then extract the type and query attributes just as we would from any PHP object.
It turns out that returning a JSON object is a little bit trickier, but not because the technology is inherently complex. The Zend_JSON component also includes an encode() method, so it seems like we could simply just do something like below.
echo Zend_Json::encode($amazonQuery->search());
|
Unfortunately, while this does work -- and it does send back a JSON representation of the results -- it doesn't delve deep enough. For example, the resulting object will have a reference to the image URI object, but not its url attribute. In many cases, this will not be a problem, but because the Amazon response object is quite complex, we're better off simply extracting the information we need at creating our own object.
We can start by creating the Amazon classes, as shown below.
Listing 11. Creating the Amazon classes
class AmazonResults
{
public $results = array();
}
class AmazonResult
{
public $salesrank = null;
public $imgUri;
public $imgWidth;
public $imgHeight;
public $detailPage;
public $title;
public $salesRank;
}
|
Place these classes anywhere they can be available. From a PHP standpoint, you could place these classes in the FeedController.php file (outside of the class definition, of course), but from a Zend Framework point of view, you may want to place them in the models directory, which should reside in the same location as controllers and views.
The classes don't provide any functions; they exist solely to provide names for the objects they will represent. We can create anonymous objects, but you will find yourself in difficulty when you try it and several of them to a single JSON string.
That said, we can now create the new objects, as shown below.
Listing 12. Creating the Amazon result objects
...
if ($type == 'amazon'){
require_once 'Zend/Service/Amazon/Query.php';
$key = 'YOUR_AMAZON_KEY_HERE';
$amazonQuery = new Zend_Service_Amazon_Query($key);
$amazonQuery->Category('Books')
->Keywords($query)
->ResponseGroup('Medium');
$responseObject = new AmazonResults();
foreach ($amazonQuery->search() as $result) {
$resultObject = new AmazonResult();
$resultObject->imgUri =
$result->SmallImage->Url->getUri();
$resultObject->imgWidth =
$result->SmallImage->Width;
$resultObject->imgHeight =
$result->SmallImage->Height;
$resultObject->detailPage = $result->DetailPageURL;
$resultObject->title = htmlentities($result->Title);
$resultObject->salesRank = $result->SalesRank;
array_push($responseObject->results, $resultObject);
}
echo Zend_Json::encode($responseObject);
}
if ($type == 'flickr'){
$key = 'YOUR_FLICKR_KEY_HERE';
...
|
First, create the overall object as an instance of the AmazonResults class. Do that through each of the search results, extracting information we want from each and adding them to a new AmazonResult object. Each of those result objects is added to the results array for the main object. We can then encode the main object and send it back to the browser.
At present, a page simply outputs whatever string we send back. If you execute the request, you can see the actual JSON text on the page, as shown in Figure 6.
Figure 6. The returned JSON text
All we have to do now is fix the renderResults() function so it deals with the object we're sending it.
Listing 13. Handling the final response
...
function renderResults(response){
var responseObject = response.responseText.parseJSON();
var outputString = '<ol>';
for (i=0; i < responseObject.results.length; i++) {
thisResult = responseObject.results[i];
outputString += '<li><img src="' + thisResult.imgUri;
outputString += '" width="' + thisResult.imgWidth;
outputString += '" height="' + thisResult.imgHeight + '" /> ';
outputString += '<a href="' + thisResult.detailPage + '">';
outputString += '<strong>' + thisResult.title +
'</strong></a>';
outputString += ' (Ranked #' + thisResult.salesRank +
')</li>';
}
outputString += '</ol>';
var renderDiv = document.getElementById('mashupResults');
renderDiv.innerHTML = outputString;
}
...
|
First, we must get a reference to the actual object. To do that, we can use the parseJSON() method on the text of the response. Once we have the object, we can manipulate it just like any other JavaScript object. In this case, we create an output strength. To do that, we go through the results array, each time getting a reference to the individual element and retrieving its attributes. When we examine each of the results, we simply output them to the page as before.
In this case, we're using the same HTML we used before, but the important difference is that if we wanted to, we could use the information in different ways within the script.
Summary
And so we come to the end of our project. Over the course of this series, we started with nothing and used the Zend Framework to ease the process of creating the Chomp! online feed reader. The application performs traditional Web-related tasks, such as processing forms, as well as almost traditional tasks such as interpreting RSS and Atom feeds, and less traditional tasks such as generating PDF files dynamically.
Of course, the application itself is not finished. We created only a skeleton, which needs to be fleshed out with better graphic design, a potential database change or two, and perhaps a few new features, such as the very necessary ability to dynamically add a new feed that does not currently exist in the system.
But overall, the process has been fun, and has taken much, much less work than it would have taken without the Zend Framework. The Zend Framework itself is also changing and growing. By the time you read this, it will likely include new functionality touched on here, for even greater ease of programming.
Download | Description | Name | Size | Download method |
|---|
| Part 9 source code | os-php-zend9.source.zip | 12KB | HTTP |
|---|
Resources Learn
-
This series kicks off with "Understanding the Zend Framework, Part 1: The basics."
-
The Zend Framework Web site maintains the latest documentation, as well as installation FAQs.
-
You can see a good comparison of how much time Zend Framework will save you in "Ajax RSS reader," in which an RSS reader is built from scratch in PHP.
- Learn more about Zend Core for IBM, a seamless, easy-to-install, and supported PHP development and production environment that features integration with DB2 and Informix databases.
-
For an introduction to Ajax, check out the series titled "Mastering Ajax."
-
Java developers should be sure to see "Ajax for Java developers: Java object serialization for Ajax."
-
For another approach to Ajax and PHP, check out "Using Ajax with PHP and Sajax."
-
Developer Chris Laffra provides some interesting thoughts and experiments in Ajax and
mashup creations in "Considering Ajax, Part 2: Change your life with mashups."
-
A more in-depth discussion of stateless and sessioned architectures is discussed in "Resource-oriented vs. activity-oriented Web services."
-
"Understanding Web Services specifications, Part 1" provides an overview of SOAP, SOAP servers, and SOAP clients.
-
For a more technical description on communicating with Amazon, read the article about the
Amazon E-Commerce Service titled "Boost application development with Amazon Web Services, Part 1," which uses the Java Development Kit (JDK).
-
Why not explore the sites providing the services in this article? Amazon's database of books and retail goods, Yahoo!'s index of the Web, and Flickr's quality photographs are essential tools for any well-traveled surfer.
-
To better understand the goals of the feed-reader project, read "Introduction to Syndication, (RSS) Really Simple Syndication," by Vincent Luria.
-
The tutorial series "Learning PHP" takes you from the most basic PHP script to working with databases and streaming from the file system.
-
The PHP Function Reference is a valuable resource.
-
Thought Storms ModelViewController explains the MVC and the controversy and confusion surrounding it.
-
The phpPatterns Web site explains MVC from the PHP point of view.
-
View a screen cast that explains how to install Apache and PHP.
-
PHP.net is the central resource for PHP developers.
-
Check out the "Recommended PHP reading list."
-
Browse all the PHP content on developerWorks.
-
Expand your PHP skills by checking out IBM developerWorks' PHP project resources.
-
To listen to interesting interviews and discussions for software developers, check out developerWorks podcasts.
-
Using a database with PHP? Check out the Zend Core for
IBM, a seamless, out-of-the-box, easy-to-install PHP development and production environment that supports IBM DB2 V9.
-
Stay current with developerWorks' Technical events and webcasts.
-
Check out upcoming conferences, trade shows, webcasts, and other Events around the world that are of interest to IBM open source developers.
-
Visit the developerWorks Open source zone for extensive how-to information, tools, and project updates to help you develop with open source technologies and use them with IBM's products.
-
Watch and learn about IBM and open source technologies and product functions with the no-cost developerWorks On demand demos.
Get products and technologies
Discuss
About the authors  | |  | Nicholas Chase has been involved in Web site development for companies such as Lucent Technologies, Sun Microsystems, Oracle, and the Tampa Bay Buccaneers. Nick has been a high school physics teacher, a low-level radioactive waste facility manager, an online science fiction magazine editor, a multimedia engineer, an Oracle instructor, and the Chief Technology Officer of an interactive communications company. He is the author of several books, including XML Primer Plus (Sam's). |
 | |  | Gina Deol is in her last year as a bachelor's of science student in computer science and accounting information systems at Sacramento State University. She has worked as a Web development engineer and project coordinator, and she is currently vice president of e-commerce development for Binary IT Solutions, based in Sacramento. |
Rate this page
|  |