Development

AjaxAndJSON

You must first sign up to be able to contribute.

Version 18 (modified by ProtoBurger, 10 years ago)
--

Introduction

I'd like to show an other way to play with AJAX using symfony. Right now if you look at the askeet or ajax tutorial you basically write ajax with remote_function using the update option which let you update a whole div or HTML tag with the result from a view. This is very nice, but what if you want to update more than one HTML element ? Isn't it the worst to refresh a whole div tag with heavy HTML code for only one integer value ? What we figured out, when it comes to creating more interactive web 2.0 application(like chat apps), is that it's more than refreshing a div tag with HTML code that you might want to do... This wiki page will try to show you how to use prototype.js ajax/JSON features to prove symfony is indeed a good choice for a web 2.0 framework.

Our goal

So what do we want ? This is a basic example taken from http://openrico.org/ javascript updater demo. Suppose you have a basic HTML body like this:

<div id="content">
<span id="title">basic letter</span>
<p Hello Mr, <span id=name>name_here</span> we are proud to announce that symfony has been approuved for web <span id='version'>version_here</span> application coding.</p>
<p id='thanks'>thanks_here</p>
</div>

The question is how to populate some database information in this HTML code ? We would make a simple ajax call that can populate title, name etc.. at one time.

Basic module action

Let's create a module:

symfony module publishing

Now create an action that will simply return the content we wrote just before. Edit indexSucess.php and type:

<div id="content">
<span id="title">basic letter</span>
<p Hello Mr, <span id=name>name_here</span> we are proud to announce that symfony has been approuved for web <span id='version'>version_here</span> application coding.</p>
<p id='thanks'>thanks_here</p>
</div>
<?php echo link_to_remote("refresh", array("url" => "/publishing/refresh", 'complete'=>'updateJSON(request,json)') ); ?>

Notice the last line: we have added a refresh link that call an ajax function to refresh the page. Now if you simply call the action with the url "http://localhost/publishing" you just get the HTML code and the refresh button that does nothing. Let's ignore the ", json" part of the code. I'll explain it later.

Refresh action

So now let's create the refresh action. Just add this to your "actions.class.php" :

/**
   * Executes refresh action
   *
   */
  public function executeRefresh()
  {
    $this->output = '[["title","My basic letter"],["name","Mr Brown"], ["version","2.0"], ["thanks","gracefully"]]';
  }

I know this is a little cryptic. But this notation is called JSON.http://www.json.org and it is the abbrevation for Javascript Simple Object Notation in which { is used for defining an object and [ for defining an array. There are many ways to encode and decode an object or an array from PHP to JSON or from JSON to PHP. There's even a C php extension that does lightspeed encoding but it's not in the scope of this wiki page. For now we're going to produce it "by hand" even if I recommend you to use PHP-JSON extension if you are considering doing JSON Ajax with symfony on a production environment. So what we wrote here is a simple array of key/value pairs that we'd like javascript to populate in our page.

Create a template for this action: (refreshSuccess.php)

<?php echo $output ?>

You also need to tell symfony not to use any layout in the view.yml file of your application. So our action won't use any layout and only JSON text will be received:

all:
  has_layout:off

Javascript to populate the data

So now we need a bit of javascript code ( yes we need it :) !) but prototype.js is here to help us !

Create a file named "updaterJSON.js" in your web/js directory with this content:

function updateJSON(request, json){
	var responses = json;
	if (!json){
	  //if you don't use the json tips then evaluate the renderedText instead
	  var responses = eval('(' + request.responseText + ')');
	}
	var resSize = responses.length;
	for (var i = 0; i < resSize; i++)
	{
	   Element.update(responses[i][0], responses[i][1]);
	}
}

So a bit of explanation, remember what we wrote in our template :

 link_to_remote("refresh", array("url" => "/publishing/refresh", 'complete'=>'updateJSON(request,json)') );

This line says create a refresh link(<a> tag) which will call the url /publishing/refresh through an XmlHttpRequest?(ajax). On complete event the javascript function updateJSON(request,json) will be called. The request will contain the xmlhttprequest itself. Now the updateJSON javascript call 'request.responseText' to retrieve the text from the symfony view template(the JSON text we output from the refreshSuccess.php). For javascript to understand the JSON and decode it to an object/array just call eval('(' + originalRequest.responseText + ')') . This will return an array of array with key/value pairs. The script updateJSON is a very quick one. This wiki is not about learning javascript so we used only a simple array definition to be able to iterate over key/value pairs. But you must know that JSON is more close to object than array and that you can imagine generating a binding to an object instead, and even create a real Class with prototype.js and instantiate them through JSON with a bit of javascript code... But let's stay focused to our tutorial for now... What the javascript does is basically, iterating over key/value pairs, fetching the Element key from the page and setting its value to value...

javascript include

For our javascript to work we need to include it and also take care that prototype.js is included before it as we use it in updaterJSON.js. To accomplish this you'll need to modify the view.yml in your module's config directory:

all:
  has_layout:off
  javascripts: [/sf/js/prototype/prototype, updaterJSON]

That's it. Now just browse to "http://localhost/publishing", press the refresh link and watch the magic :) ...

troubleShooting

Element.update from updaterJSON doesn't work with IE
Why ?
"with IE you can't have two id with the same name for modification with Element.update"

json header

But wait what about the json parameter we've ignored till now ? In fact, prototype.js has a very handy feature for JSON. When using an AJAX function, it always looks for the header "X-JSON" and will evaluate it automatically and pass it as a second argument to your javascript callback function. Note that for now there exists a bug in symfony preventing this feature from working. I made a patch #252 and #272 for this issue. As soon as it's fixed you'll be able to replay the tutorial but this time change those:

  • delete the refreshSuccess.php template. We don't need a view for JSON header !(action response will be faster)
  • change your executeRefresh method like this:
    /**
       * Executes refresh action
       *
       */
      public function executeRefresh()
      {
        $output = '[["title","My basic letter"],["name","Mr Brown"], ["version","2.0"], ["thanks","gracefully"]]';
        $this->getResponse()->setHttpHeader("X-JSON", '('.$output.')');
        return sfView::HEADER_ONLY;
      }
    

You should notice two things here. Symfony will camel-case the X-JSON to X-Json but it doesn't seem to bother prototype.js for now, also we'll need to enclose the $output with braces so prototype 1.4 can acknowledge it's the json server work ... Doing these will do the trick. So, what are the advantages? Debugging! When outputted with an header, the logs would show you the result header is X-JSON and then when in development environment you can simply follow your request and see where the bug might be. Developing AJAX is an asynchronous nightmare and a javascript call with symfony is a bit hard to follow so seeing the header is a must.

json without header

Its not clear yet which other AJAX frameworks support the X-JSON header. So it seems that with YUI its more reliable to simply use application/json as the content type, which aside from being a cleaner way to return json data also addresses some security issues. Here is an example helper class that from which all JSON enabled modules should extend. As you can see the code is also able to return the data as HTML in case this method is used on a request that is made directly (not via XMLHTTPRequest) using the debug frontend. This requires the existence of a global _json.php template. This way you can also use the debug panel when debugging JSON requests.

sfJSONActions.class.php

class sfJSONActions extends sfActions
{
  /**
   * Return in JSON when requested via AJAX or as plain text when requested directly in debug mode
   *
   */
  public function returnJSON($data)
  {
    $json = json_encode($data);

    if (SF_DEBUG && !$this->getRequest()->isXmlHttpRequest()) {
      sfLoader::loadHelpers('Partial');
      $json = get_partial('global/json', array('data' => $data));
    } else {
      $this->getResponse()->setHttpHeader('Content-type', 'application/json');
    }

    return $this->renderText($json);
  }
}

_json.php global template

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>JSON response</title>
<link rel="shortcut icon" href="/favicon.ico" />
<script language="JavaScript" type="text/javascript" src="<?php echo javascript_path('/sf/sf_web_debug/js/main.js') ?>" />
<link rel="stylesheet" type="text/css" media="all" href="<?php echo stylesheet_path('/sf/sf_web_debug/css/main.css') ?>" />
</head>
<body>
json:<br>
<?php echo json_encode($sf_data->getRaw('data')); ?>
<br>
data structure:<br>
<pre>
<?php print_r($sf_data->getRaw('data')); ?>
</pre>


With the participation of Noel, thanks for reading, hope my english isn't too bad and that this might be the start for other wiki about AJAX/object binding with symfony as server. Feel free to ask on irc : benoitm on #symfony