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

Throughout this "Understanding the Zend Framework" series, you used 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).

Share:

Nicholas Chase (ibmquestions@nicholaschase.com), Freelance writer, Backstop Media

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 (gdeol@binaryits.com), Vice President, E-commerce Development, Binary IT Solutions

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.



18 January 2011 (First published 05 September 2006)

Also available in Russian

Introduction

In Part 8 of this series, you added Yahoo!, Amazon, Twitter and Flickr results to the Chomp application. Now you'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 at.

How you 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 open source PHP Zend Framework.

Part 1 talked about the overall concepts of the Zend Framework, including a list of relevant classes and a general discussion of the MVC pattern. Part 2 expanded on that to show how MVC can be implemented in a Zend Framework application. You also created the user registration and login process, adding user information to the database and pulling it back out again.

Parts 3 and 4 dealt with the actual RSS and Atom feeds. In Part 3, you enabled users to subscribe to individual feeds and to display the items listed in those feeds. You also discussed some of the Zend Framework's form-handling capabilities, validating data, and sanitizing feed items. Part 4 explained 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 explained 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, you used the Zend_Mail module to alert users to new posts. In Part 7, you looked at searching saved content and returning ranked results. In Part 8, you added an extra dimension to your feed reader by linking the online resources of Amazon.com, Yahoo!, Twitter and Flickr with your current application to create a robust mashup.

Now in the final part of the series, you 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. 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 you can easily create and interpret manually. For example, look at an object you might create, as shown in Listing 1.

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 in Listing 2.

Listing 2. JSON string
{"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.

You can even send and receive these strings as Ajax requests. But first, you 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.

The first step in creating your Ajax request is to break this out into individual pieces. For example, you can move the Amazon and Flickr results from the main view, viewedSearchResults.php, to their own files, amazonView.php and flickrView.php (see Listing 3).

Listing 3. 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. You simply moved it to its own file. You can perform the same step for the Flickr results (see Listing 4).

Listing 4. 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, you don't want to lose the data from this page altogether — yet. Instead, you want to display the results of rendering those individual views. You can do that by including the rendered text just as you would include other data (see Listing 5).

Listing 5. 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, you need to update the FeedController (see Listing 6).

Listing 6. updating FeedController.php
    public function viewSearchResultsAction()
    {
...
        $view = Zend_Registry::get('view');
        $view->title = "Search Results for: $query";
        $view->hits = $hits;

        $amazonView = ZendRegistrt::get('view');
        require_once 'Zend/Service/Amazon/Query.php';

        $key = 'YOUR_AMAZON_KEY_HERE';
		$country = 'US';
        $secret = 'YOUR_AMAZON_SECRET KEY_HERE';
		$amazonQuery = new Zend_Service_Amazon_Query($key,$country,$secret);
        $amazonQuery->Category('Books')
                  ->Keywords($filterGet->getRaw('query'))
                  ->ResponseGroup('Medium');

        $amazonView->amazonHits = $amazonQuery->search();
        $view->renderedAmazon = $amazonView->render('amazonView.php');

        $flickrView = Zend_Registry::get('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, you create two new view objects and render them with the data previously sent to the main view. This rendering process simply provides text, so you can set the rendered text as an attribute on the main view.

The result is a page that looks exactly like what you already had, as shown in Figure 1.

Figure 1. The reworked results
Reworked results

The difference is that now you can begin to manipulate how you 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 you to call it on demand. To do that, edit the viewedSearchResults.php file, as shown in Listing 7.

Listing 7. 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. You'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 you create the JavaScript function that will request the new data, you need to create the action it will call. In this case, you're creating a new action, mashupAction, that accepts the type of mashup and the query, and returns the appropriate data, as shown in Listing 8.

Listing 8. Adding the new action to FeedController.php
...
    public function viewSearchResultsAction()
    {
...
        $view = Zend_Registry::get('view');
        $view->title = "Search Results for: $query";
        $view->hits = $hits;

        $view->query = $query;

        echo $view->render('viewSearchResults.php');
    }

    public function mashupAction()
    {
        $filterGet = Zend_Registry::get('fGet');
        $type = $filterGet->getRaw('type');

        if ($type == 'amazon'){
            $amazonView = Zend_Registry::get('view');
            require_once 'Zend/Service/Amazon/Query.php';

            $key = 'YOUR_AMAZON_KEY_HERE';
            $country = 'US';
            $secret = 'YOUR_AMAZON_SECRET KEY_HERE';
            $amazonQuery = new Zend_Service_Amazon_Query($key,$country,$secret);
            $amazonQuery->Category('Books')
                      ->Keywords($filterGet->getRaw('query'))
                      ->ResponseGroup('Medium');

            $amazonView->amazonHits = $amazonQuery->search();
            echo $amazonView->render('amazonView.php');
        }

        if ($type == 'flickr'){
            $flickrView = Zend_Registry::get('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 you removed the extra information from the viewSearchResultsAction. You 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. You will include this data on the search results page, but only on demand.

Now you 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, you're going to use the Prototype Framework. Download it and save the file in the Web server's document root. At the time of this writing, the latest version is 1.6.1 (see Resources for a link).

You can use this library by including it on the page and referencing the Ajax object, as shown in Listing 9.

Listing 9. 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, you add the library to the page. (it has been renamed it prototype.js for maintainability.) Putting it in the document root enables you to reference it without hardcoding the actual location.

Next, you 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 you 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

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 you are just displaying it on the page, but it many cases, that is not what you want. So before you finish up, you need to take a look at a second way of dealing with these requests: JSON.

You'll start on the JavaScript side. Rather than sending your mashup request parameters as name-value pairs, let's create a JavaScript object that includes them as attributes (see Listing 10).

Listing 10. 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
                                          }
                                         );

        }
...

You're adding a second library, available from http://www.json.org/js.html. This library adds two new functions to JavaScript: toJSONString() and parseJSON().

You next create a simple object and set its attribute values. As before, the query is hardcoded by the PHP view-rendering process. You can then convert directly to this object to a JSON string and add it to the request as a parameter. If you set the page to output the string in a text box, you can see the results shown in Figure 5.

Figure 5. The JSON text
JSON text

You then need to update the mashupAction() to look for this new data (see Listing 11).

Listing 11. Accepting the JSON text in mashupAction()
...
    public function mashupAction(){
        $filterGet = Zend_Registry::get('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::get('view');
            require_once 'Zend/Service/Amazon/Query.php';
...

Instead of getting to separate requests for the type and query, you 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). You can then extract the type and query attributes just as you 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 you could simply just do something like the following: 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, you're better off simply extracting the information you need at creating your own object.

You can start by creating the Amazon classes, as shown in Listing 12.

Listing 12. 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. You 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, you can now create the new objects, as shown in Listing 13.

Listing 13. Creating the Amazon result objects
...
        if ($type == 'amazon'){
            require_once 'Zend/Service/Amazon/Query.php';
			
			$country = 'US';
            $secret = 'YOUR_AMAZON_SECRET KEY_HERE';
            $amazonQuery = new Zend_Service_Amazon_Query($key,$country,$secret);
            $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 you 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. You can then encode the main object and send it back to the browser.

At present, a page simply outputs whatever string you 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 you have to do now is fix the renderResults() function so it deals with the object you're sending it (see Listing 14).

Listing 14. 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, you must get a reference to the actual object. To do that, you can use the parseJSON() method on the text of the response. Once you have the object, you can manipulate it just like any other JavaScript object. In this case, you create an output strength. To do that, you go through the results array, each time getting a reference to the individual element and retrieving its attributes. When you examine each of the results, you simply output them to the page as before.

In this case, you're using the same HTML you used before, but the important difference is that if you wanted to, you could use the information in different ways within the script.


Summary

And so you have come to the end of your project. Over the course of this series, you 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. You 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. You could even build mobile device aware versions of the application that can detect whether or not you are on a mobile device and render the page more appropriately.

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

DescriptionNameSize
Part 9 source codeos-php-zend9.source.zip51KB

Resources

Learn

Get products and technologies

Discuss

  • Join the developerWorks community, a professional network and unified set of community tools for connecting, sharing, and collaborating.

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Open source on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Open source, Web development
ArticleID=156990
ArticleTitle=Understanding the Zend Framework, Part 9: Adding interactivity with Ajax and JSON
publish-date=01182011