Quicknavi |
|
AJAX & the adventure php framework
1. Introduction
AJAX is noted for beeing a common technique to build web applications, that are more agile compared
to standard applications. It gives the possibility to change specific parts of the DOM of a HTML
page by using the XMLHTTPRequest implementation and some lines of java script code.
AJAX is not brand-new and there are a plenty of online arcticles about building AJAX applications.
At a glance, the following resources are worth being read:
Starting to write AJAX applications you will face with the problem to integrate AJAX widgets and
dynamic updates of certain areas of your web page into existing application structures and designs.
This fact often leads to the problem, that AJAX-featured applications tend to be not well-designed.
Im most cases, the client <-> server communication is done by requesting a file (e.g. a PHP file),
that sends back an XML string containing the information the client needs. In common for each AJAX
"action" one appropriate file is created. This does not only lead to complexity but also to
redundant code in the server side scripts. A second problem is security. Due to the fact, that the
application code merely resides on the client and hence can be analysed and manipulated easily, an
application tends to get insecure.
The two problems described above are well known problems. On this account many server and cliend
side AJAX frameworks have been built to solve the daily life problems a programmer is facing during
AJAX application development. However, building AJAX aplications is not as easy as it is described
in many of those "getting started" tutorials.
2. Which tools are included in the APF?
Basically the adventure php framework features no special AJAX component to use either on the client
or on the server side. Having no special module does not mean, that the APF does not have a way to
facilitate server side programming within AJAX applications.
To assist implementation of server side AJAX application parts, the front controller can be used.
Taking the front controller supports the programmer to build server side AJAX "actions" in a
standardized and powerful way. He is not forced to create several action files and reimplement
the functionality included in those files several times. Each AJAX action can be represented by one
special front controller action. Due to the fact that the front controller pattern is located in the
business layer, the front controller action can easily use existing application components (e.g.
business or data layer classes). Moreover using the front controller makes it easy to access the
parameters the action is called with. In other words, all modules known for building server side
applications can be involved.
Due to a wide range of java sctip AJAX frameworks the adventure php framework features no
java script libraries. For client side purpose any common AJAX framework can be used to create the
desired widgets. The following list shows a selection of famous java script frameworks:
3. My first AJAX application
To not getting you bored with theory, this chapter shows you, how to build a AJAX web application
with the adventure php framework from the scratch.
3.1. Requirements and considerations
Due to the fact, that we both are good software architects, we need to think about the requirements
of the software, we want to create. To keep things simple, the application to design should be
simple and easy to imagine. I think a news box would be a good example to show how an AJAX
application can be built with the adventure php framework.
Let me give you a brief description of the major features:
- news box can be included in any adventure php framework application
- news can be paged clockwise and vice versa
- news are read from several text files, each containing one news page, to keep maintenance easy
3.2. Design of the application
Compared to a standard application only the paging functionality is realized in AJAX style. The rest
of the module can be implemented as customary. The concepts used here (commonly known as patterns)
can be comprehended reading the
comment function tutorial. But the
focus of this article lies on the design of the AJAX part of the application.
In order to implement the paging functionality we need client and server addons. On the cliend side
we will use some java script code around the XMLHTTPRequest object to draw the widgets and to catch
user interaction. The server side is done by a special front controller action, that reads the
desired news page and gives it back to the client.
4. Implementation
To make sure, that the news box can be included in any APF style application we create a new module
named newspager. In the first instance, the folders and files must be created as
follows:
/apps/
modules/
newspager/
biz/
actions/
data/
pres/
documentcontroller/
templates/
As a next step, the files necessary must be created. As already mentioned above, we need to create
the following files:
-
biz/newspagerManager.php:
business component, that is used to get the news page content
-
biz/newspagerContent.php:
the domain object, that contains the content of one news page
-
biz/actions/newspagerAction.php:
file that contains the front controller action class
-
biz/actions/newspagerInput.php:
file that contains the front controller input class
-
data/newspagerMapper.php:
data layer component, that loads the newspager content from text files
-
pres/documentcontroller/newspager_v1_controller.php:
document controller, that generates the output of the module
-
pres/templates/newspager.html:
template file, that contains the style of the module and the additional java script code needed
to add AJAX style paging
In this tutorial we are going to use the bottom-up approch during implementation phase.
4.1. Data layer implementation
The data layer's duty is to load content from plain text files, map a news file into a domain
object and give it back to the business layer. The news files are located in the
./frontend/news/ folder located in parallel to the index.php file
of a project. Each file contains news text in a certain semantic: the first line is interpreted as
the headline, the second as a subheadline and the rest of the file is assumed to belong to the
content. On this account the domain object mentioned above has three properties: title, subtitle and
content. To have multilanguage support, each file should be named like
news_{LANG}_{PAGENUMBER}.txt
Here {LANG} must be any two letter ISO language code, {PAGENUMBER}
is a number beginning with 1. As an example, the news pager module can have the
following news files located in ./frontend/news/:
news_en_1.txt
news_en_2.txt
news_en_3.txt
news_en_4.txt
news_en_5.txt
news_it_1.txt
news_it_2.txt
news_it_3.txt
news_it_4.txt
The source code of the data layer class can be seen in the code box below:
<?php import('modules::newspager::biz','newspagerContent'); import('core::filesystem','filesystemHandler');
/** * @package modules::newspager::data * @class newspagerMapper * * Data layer component for loading the news page objects.<br /> * * @author Christian Achatz * @version * Version 0.1, 02.20.2008<br /> */ class newspagerMapper extends coreObject {
function newspagerMapper(){ }
/** * @public * * Loads a news page object.<br /> * * @param int $PageNumber; desire page number * @return newspagerContent $newspagerContent; newspagerContent domain object * * @author Christian Achatz * @version * Version 0.1, 02.02.2007<br /> */ function getNewsByPage($PageNumber){
// create filesystem handler $fM = new filesystemHandler('./frontend/news/');
// read all files located there $RawFiles = $fM->showDirContent();
// get files, that match the current language $Files = array(); $count = count($RawFiles);
for($i = 0; $i < $count; $i++){
if(substr_count($RawFiles[$i],'news_'.$this->__Language.'_') > 0){ $Files[] = $RawFiles[$i]; // end if }
// end for }
// throw error when page count is zero! $NewsCount = count($Files);
if($NewsCount == 0){ trigger_error('[newspagerMapper::__getFileNameByPageNumber()] No news files are given for language '.$this->__Language,E_USER_ERROR); exit; // end if }
// if page number is lower then zero, correct it! if($PageNumber <= 0){ $PageNumber = 1; // end if }
// if page number is higher then max, correct it! if($PageNumber > $NewsCount){ $PageNumber = $NewsCount; // end if }
// read content of file $NewsArray = file('./frontend/news/'.$Files[$PageNumber - 1]);
// initialize a new news content object $N = new newspagerContent();
// fill headline if(isset($NewsArray[0])){ $N->set('Headline',trim($NewsArray[0])); // end if }
// fill subheadline if(isset($NewsArray[1])){ $N->set('Subheadline',trim($NewsArray[1])); // end if }
// fill content $count = count($NewsArray); if($count >= 3){ $Content = (string)''; for($i = 2; $i < $count; $i++){ $Content .= $NewsArray[$i]; // end for } $N->set('Content',trim($Content));
// end if }
// set news count $N->set('NewsCount',$NewsCount);
// return object return $N;
// end function }
// end class } ?>
Calling the mapper class like
$M = new newspagerMapper(); $M->set('Context','sites::demosite'); $M->set('Language','en'); echo printObject($M->getNewsByPage(1));
is will return an newspagerContent object, that contains the information of the
first news page residing in the news_en_1.txt file. This object can later on be
used to deliver the xml requested by the java script code executed on the client machine or to
generate the output of the news box during rendering of the page.
4.2. Business layer implementation
4.2.1. Domain object
First of all, we must define the newspagerContent class, that was already used in
the data layer component. This class is a simple data storage class, that contains the information
of a certain news page. Due to the fact, that this class inherits from coreObject
no more get() and set() methods must be implemented. The source
code looks like this:
<?php /** * @package modules::newspager::biz * @class newspagerContent * * Domain object class.<br /> * * @author Christian Achatz * @version * Version 0.1, 02.20.2008<br /> */ class newspagerContent extends coreObject {
/** * @private * Headline of a news page. */ var $__Headline;
/** * @private * Subheadline of a news page. */ var $__Subheadline;
/** * @private * Content of a news page. */ var $__Content;
/** * @private * Number of news pages. */ var $__NewsCount;
function newspagerContent(){ }
// end class } ?>
The attribute $__NewsCount was introduced to control the pager widgets.
4.2.2. The manager
To be compilant to the classical thee tier architecture described in the pattern of the same name,
we introduce a business component, that contains the business logic of the application. In this case
the business layer component (often called manager) only contains of one singe method:
getNewsByPage(). The implementation is quite easy and so the php code looks as
follows:
<?php import('modules::newspager::data','newspagerMapper');
/** * @package modules::newspager::biz * @class newspagerManager * * Business component for loading the news page objects.<br /> * * @author Christian Achatz * @version * Version 0.1, 02.20.2008<br /> */ class newspagerManager extends coreObject {
function newspagerManager(){ }
/** * @public * * Loads a news page object.<br /> * * @param int $PageNumber; desire page number * @return newspagerContent $newspagerContent; newspagerContent domain object * * @author Christian Achatz * @version * Version 0.1, 02.02.2007<br /> */ function getNewsByPage($PageNumber = 1){
// get mapper $nM = &$this->__getServiceObject('modules::newspager::data','newspagerMapper');
// load and return news object return $nM->getNewsByPage($PageNumber);
// end function }
// end class } ?>
A unit test for the class and the underlaying data layer may look like this:
$M = new newspagerManager(); $M->set('Context','sites::demosite'); $M->set('Language','en'); echo printObject($M->getNewsByPage());
This statement will print out the content of the loaded newspagerContent object.
4.2.3. AJAX data source
As mentioned above, the client side java script codes needs a server side data source to obtain the
desired news page via xml. For this reason a front controller action is implemented, that delivers
the desired page. Due to the fact, that we have a business layer component (manager), the
action can make use of this class. The only task is then to transform the object returned by the
business layer component into valid xml and transfer it to the client machine. To indicate, which
page should be served, the front controller action url string does contains the parameter
page. Adventure php framework front controller actions normally contain three main
parts: the action class definition, the input class definition and the action configuration that
defines the parameters of the front controller action. Details can be seen on the
front controller page. Because this
example has no special requirements, the implementation is straight forward.
First of all, we have to implement the input object, that is filled with the url parameters present
in the url string when requesting the action. The class must inherit from the
FrontcontrollerInput class and the file only contains the following body:
<?php /** * @package modules::newspager::biz * @class newspagerInput * * Front controller input class.<br /> * * @author Christian Achatz * @version * Version 0.1, 02.20.2008<br /> */ class newspagerInput extends FrontcontrollerInput {
function newspagerInput(){ }
// end class } ?>
The action class definition encapsulates the functionality described a few lines above. As described
on the front controller page, each
action must implement the interface method run(), that is called during action
execution by the front controller. This function includes the main capacity and delivers the xml
string to the requestor. The source code may look like follows:
<?php import('modules::newspager::biz','newspagerManager');
/** * @package modules::newspager::biz * @class newspagerAction * * Front controller action implemenatation for AJAX style loading of a news page.<br /> * * @author Christian Achatz * @version * Version 0.1, 02.20.2008<br /> */ class newspagerAction extends AbstractFrontcontrollerAction {
function newspagerAction(){ }
/** * @public * * Implements the abstract run() method.<br /> * * @author Christian Achatz * @version * Version 0.1, 02.02.2007<br /> * Version 0.2, 05.02.2008 (language is now directly taken from the AJAX request)<br /> */ function run(){
// get desired page number and language $Page = $this->__Input->getAttribute('page'); $Language = $this->__Input->getAttribute('lang');
// get manager $nM = &$this->__getServiceObject('modules::newspager::data','newspagerManager');
// set language $nM->set('Language',$Language);
// load news object $N = $nM->getNewsByPage($Page);
// create xml $XML = (string)''; $XML .= '<?xml version="1.0" encoding="utf-8" ?>'; $XML .= '<news>'; $XML .= '<headline>'.$N->get('Headline').'</headline>'; $XML .= '<subheadline>'.$N->get('Subheadline').'</subheadline>'; $XML .= '<content>'.$N->get('Content').'</content>'; $XML .= '<newscount>'.$N->get('NewsCount').'</newscount>'; $XML .= '</news>';
// send xml header('Content-Type: text/xml; charset=iso-8859-1'); echo $XML;
// close application exit(0);
// end function }
// end class } ?>
To be able to write a unit test, or to call the action, the third part of an APF style front
controller action must be added: the configuration. To create this file, we must define, under which
namespace the action should be callable. For this example, we assume, that the action can be executed
using the url definition
http://dev.adventure-php-framework.org/~/modules_newspager_biz-action/Pager/page/1/lang/en
/~/modules_newspager_biz-action/Pager/page/{PAGE}/lang/{LANG}
where {PAGE} must be replaced by any integer and LANG a two letter
ISO language code. Hence, the namespace of the action is modules::newspager::biz
and though the configuration file for this action must be created under the folder
/apps/config/modules/newspager/biz/actions/{APPS__CONTEXT}/
Thereby {APPS__CONTEXT} is the folder path resulting from the current context of the
application. In this example, the context is sites::demosite, and consequently, the
full path must be
/apps/config/modules/newspager/biz/actions/sites/demosite/
The config file itself must be named
TESTSERVER_actionconfig.ini
because the current environment variable is set to TESTSERVER. In common, the action
definition file contains the following lines:
[Pager]
FC.ActionNamespace = "modules::newspager::biz::actions"
FC.ActionFile = "newspagerAction"
FC.ActionClass = "newspagerAction"
FC.InputFile = "newspagerInput"
FC.InputClass = "newspagerInput"
FC.InputParams = ""
As described on the
front controller page, the section
name corresponds to the action name, the section itself defines which files contain the action and
input classes. The sixth parameter can be used to specify standard parameters for the input object.
Calling the url
/~/modules_newspager_biz-action/Pager/page/1/lang/en
will display a XML similar to that:
<news>
<headline>APF news</headline>
<subheadline>New version 1.5 available!</subheadline>
<content>
We are proud to present the new version 1.5 of the adventure php framework. It includes
bugfixes and enhancements of the front controller component and a complete review of the
documentation plus translation of the entire documentation chapters into english language.
</content>
<newscount>4</newscount>
</news>
4.3. Presentation layer implementation
The presentation layer faces two main issues: create the news box on page creation and provide the
necessary java script code to update the news box in AJAX style. Sensibly, we first deal with the
generation of the news box, because this can be done with standard adventure php framework
appliances. Due to the fact, that the news box is created through dynamically generated content
(here: text files), we need a document controller and a template file. The template file defines the
structure of the box according to the MVC pattern and the document controller generates the view
content using the template file definition.
4.3.1. The template file
Due to the fact, that we have to display a box with three values filled in it, the template file
looks as follows:
<@controller
namespace="modules::newspager::pres::documentcontroller"
file="newspager_v1_controller"
class="newspager_v1_controller"
@>
<div style="width: 300px; float: right; border: 1px dashed #777BB4; margin: 10px;">
<table cellpadding="0" cellspacing="0">
<tr>
<td style="text-align: center; vertical-align: center; width: 15px; background-color: #777BB4; font-size: 16px;">
<span style="font-weight: bold;"><</span>
</td>
<td style="width: 270px; height: 80px; padding: 2px; vertical-align: top; text-align: left;">
<span style="font-variant: small-caps; font-size: 16px; font-weight: bold;">
<html:placeholder name="Headline" />
</span>
<br />
<span style="font-size: 12px; font-style: italic;"><html:placeholder name="Subheadline" /></span>
<br />
<br />
<span style="font-size: 11px;"><html:placeholder name="Content" /></span>
</td>
<td style="text-align: center; vertical-align: center; width: 15px; background-color: #777BB4; font-size: 16px;">
<span style="font-weight: bold;">></span>
</td>
</tr>
</table>
</div>
As the html code box shows, the template file contains a document controller specification and three
place holders to be filled with the content of the domain object.
4.3.2. The document controller
The corresponding document controller loads the newspagerContent object and filles
the place holders:
import('modules::newspager::biz','newspagerManager');
class newspager_v1_controller extends baseController {
function newspager_v1_controller(){ }
function transformContent(){
// get manager $nM = &$this->__getServiceObject('modules::newspager::data','newspagerManager');
// load default news page $N = $nM->getNewsByPage();
// fill place holders $this->setPlaceHolder('Headline',$N->get('Headline')); $this->setPlaceHolder('Subheadline',$N->get('Subheadline')); $this->setPlaceHolder('Content',$N->get('Content'));
// end function }
// end class }
4.3.3. The AJAX java script code
To add dynamic paging to the news box, some java script code must be added. Therefore, we add a
<script type="text/javascript">
</script>
to the newspager.html template file. As we have learned above, the XMLHTTPRequest
object is used to send a request via java script to retrieve dynamic information from a server
script to update a page part without reloading the page. For this reason we use the following java
script function to create an instance of the XMLHTTPRequest object:
function createXMLHttpRequest(){
if(window.ActiveXObject){
try{
// IE 6 and higher
xhttp = new ActiveXObject("MSXML2.XMLHTTP");
// end try
}
catch (e){
try{
// IE 5
xhttp = new ActiveXObject("Microsoft.XMLHTTP");
// end try
}
catch (e){
xhttp = false;
// end catch
}
// end catch
}
// end if
}
else if(window.XMLHttpRequest){
try{
// Mozilla, Opera, Safari ...
xhttp = new XMLHttpRequest();
// end try
}
catch (e){
xhttp = false;
// end catch
}
// end else if
}
// end function
}
// execute createXMLHttpRequest() on window load
window.onload = createXMLHttpRequest;
The window.onload tells the browser to execute the
createXMLHttpRequest after having loaded the page. In order to be able to
manipulate the content of the page, we have to upgrade the template definition with
ids for each tag. So we can access each tag by using the
var dom_node = document.getElementById('dom_node_id');
construct. Moreover, we must add an event handler to the two pager signs, so that the content of the
box can be changed. Therefore I defined two different java script functions (prev()
and next()), that wrap the functionality of getting forward or backward. Te next
code box shows the changes, we have to apply to the html code of the template file:
<div style="width: 300px; float: right; border: 1px dashed #777BB4; margin: 10px;">
<table cellpadding="0" cellspacing="0">
<tr>
<td style="text-align: center; vertical-align: center; width: 15px; background-color: #777BB4; font-size: 16px;">
<span id="newspager_prev_button" style="font-weight: bold; cursor: pointer; visibility: hidden;" onclick="prev();">
<
</span>
</td>
<td style="width: 270px; height: 80px; padding: 2px; vertical-align: top; text-align: left;">
<span id="newspager_headline" style="font-variant: small-caps; font-size: 16px; font-weight: bold;">
<html:placeholder name="Headline" />
</span>
<br />
<span id="newspager_subheadline" style="font-size: 12px; font-style: italic;">
<html:placeholder name="Subheadline" />
</span>
<br />
<br />
<span id="newspager_content" style="font-size: 11px;"><html:placeholder name="Content" /></span>
</td>
<td style="text-align: center; vertical-align: center; width: 15px; background-color: #777BB4; font-size: 16px;">
<span id="newspager_next_button" style="font-weight: bold; cursor: pointer;" onclick="next();">
>
</span>
</td>
</tr>
</table>
</div>
The functions mentioned above contain the following lines:
function next(){
// check if XMLHTTPRequest object is initialized correctly
if(!xhttp){
alert("An Error occured when trying to initialize the XMLHttpRequest object!");
return;
// end if
}
// increment page number
pagenumber = pagenumber + 1;
// create request
var request = "/~/modules_newspager_biz-action/Pager/page/" + pagenumber + "/lang/" + language;
// send request for next news
xhttp.open("GET",request,true);
xhttp.onreadystatechange = updateNewsContent;
xhttp.send(null);
// end function
}
function prev(){
// check if XMLHTTPRequest object is initialized correctly
if(!xhttp){
alert("An Error occured when trying to initialize the XMLHttpRequest object!");
return;
// end if
}
// decrement page number
pagenumber = pagenumber - 1;
// create request
var request = "/~/modules_newspager_biz-action/Pager/page/" + pagenumber + "/lang/" + language;
// send request for next or prev news page
xhttp.open("GET",request,true);
xhttp.onreadystatechange = updateNewsContent;
xhttp.send(null);
// end function
}
First of all, each function checks, wheather the XMLHTTPRequest object was created successfully.
Second, the page number is either incremented or decremented to indicate the page number to load.
After that, the front controller style request is constructed. Due to the fact, that xml http
requests only can be done for the same domain, the page is delivered, we can define the url string
relatively. The last code block then opens the defined url for a GET request, defines a event handler
for the readystatechange event and sends the request.
Some of you now may say, that the function definition should be more generic. The answer is: yes, it
may. But this example is there to demonstrate the way of how you can add AJAX support to your
application. As this module is quite simple, complexity drastically raises, when implementing more
substantial applications.
Let's now discuss the event handler function for updating the content view of the news box. This
method has two main tasks to execute: display the content of the XMLHTTPRequest request and control
the pager arrows, when the maximum or minimum number of pages has been reached. To do so, we have
to create the following java script code:
function updateNewsContent() {
// check for ready loadad and HTTP status 200 (OK)
if(xhttp.readyState == 4 && xhttp.status == 200){
// get request xml
var responseXML = xhttp.responseXML;
// insert xml values into the desired spans
var headline = responseXML.getElementsByTagName('news').item(0).childNodes[0].textContent;
document.getElementById("newspager_headline").innerHTML = headline;
var subheadline = responseXML.getElementsByTagName('news').item(0).childNodes[1].textContent;
document.getElementById("newspager_subheadline").innerHTML = subheadline;
var content = responseXML.getElementsByTagName('news').item(0).childNodes[2].textContent;
document.getElementById("newspager_content").innerHTML = content;
// extract news count out of the xml
var newscount = responseXML.getElementsByTagName('news').item(0).childNodes[3].textContent;
// enable prev and next button by default
document.getElementById("newspager_prev_button").style.visibility = "visible";
document.getElementById("newspager_next_button").style.visibility = "visible";
// disable next button if desired
if(pagenumber >= newscount){
document.getElementById("newspager_next_button").style.visibility = "hidden";
// end if
}
// disable prev button if desired
if(pagenumber <= 1){
document.getElementById("newspager_prev_button").style.visibility = "hidden";
pagenumber = 1;
// end if
}
// end if
}
As you can see in line 4, the function waits for the request being completed. If this is the case,
the content of the request is read from the xhttp object created before. The
responseXML attribute contains a DOM document, that can be accessed
via java script functions. The second code block updates the spans addressed by the given DOM ids.
To be able to control the pager buttons, we have to know the maximum number of pages and the current
page number. For this reason the news count is initially filled and is included in each response.
The initialisation of the newscount value is simply done by adding the declaration
block
// declare variable to be initialized with the XMLHttpRequest object
var xhttp;
// declare current page number
var pagenumber = 1;
// declace news count
var newscount = <html:placeholder name="NewsCount" />;
// declare current language
var language = "<html:placeholder name="NewsLanguage" />";
to the java script code and fill the place holders by append the following php code to the document
controller:
$this->setPlaceHolder('NewsLanguage',$this->__Language); $this->setPlaceHolder('NewsCount',$N->get('NewsCount'));
5. Lessons learned
This chapter is intended to list the pitfalls, that can appear during AJAX application development.
Comments
Do you want to add a comment to the article above, or do you want to post additional hints? So please click here. Comments already posted can be found below.
There are no comments belonging to this article.
|