Skip to main content

skip to main content

developerWorks  >  Open source | Web development  >

Understanding the Zend Framework, Part 9: Adding interactivity with Ajax and JSON

developerWorks
Document options

Document options requiring JavaScript are not displayed

Sample code


My developerWorks needs you!

Connect to your technical community


Rate this page

Help us improve this content


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.



Back to top


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.



Back to top


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
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
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
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.



Back to top


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
Ajax result


Back to top


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
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
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.



Back to top


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.




Back to top


Download

DescriptionNameSizeDownload method
Part 9 source codeos-php-zend9.source.zip12KBHTTP
Information about download methods


Resources

Learn

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


Please take a moment to complete this form to help us better serve you.



 


 


Not
useful
Extremely
useful
 


Share this....

digg Digg this story del.icio.us del.icio.us Slashdot Slashdot it!



Back to top