|
Article summary I
Encoded slashes lead to 404 error! Using a front controller action as a server side component, encoded slashes and backslashes lead to a 404 error if the "AllowEncodedSlashes = On" is not set in your VHOST configuration. |
/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:
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.txtThe source code of the data layer class can be seen in the code box below:
import('modules::newspager::biz','newspagerContent');
import('core::filesystem','filesystemHandler');
class newspagerMapper extends coreObject
{
/**
* @private
* Defines the dir, where the news content is located.
*/
var $__DataDir = null;
function newspagerMapper(){
}
function init($DataDir){
$this->__DataDir = $DataDir;
// end function
}
function getNewsByPage($PageNumber){
// create filesystem handler
$fM = new filesystemHandler($this->__DataDir);
// 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::getNewsByPage()] 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($this->__DataDir.'/'.$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
}
$M = new newspagerMapper();
$M->set('Context','sites::apfdocupage');
$M->set('Language','en');
echo printObject($M->getNewsByPage(1));
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
}
import('modules::newspager::data','newspagerMapper');
class newspagerManager extends coreObject
{
/**
* @private
* Defines the dir, where the news content is located.
*/
var $__DataDir = null;
function newspagerManager(){
}
function init($DataDir){
// cut trailing slash if necessary
if(substr($DataDir,strlen($DataDir) - 1) == '/'){
$this->__DataDir = substr($DataDir,0,strlen($DataDir) -1);
// end if
}
else{
$this->__DataDir = $DataDir;
// end else
}
// end function
}
function getNewsByPage($PageNumber = 1){
// get mapper
$nM = &$this->__getAndInitServiceObject('modules::newspager::data','newspagerMapper',$this->__DataDir);
// load and return news object
return $nM->getNewsByPage($PageNumber);
// end function
}
// end class
}
$M = new newspagerManager();
$M->set('Context','sites::apfdocupage');
$M->set('Language','en');
echo printObject($M->getNewsByPage());
class newspagerInput extends FrontcontrollerInput
{
function newspagerInput(){
}
// end class
}
import('modules::newspager::biz','newspagerManager');
class newspagerAction extends AbstractFrontcontrollerAction
{
function newspagerAction(){
}
function run(){
// get desired page number, language and data dir
$Page = $this->__Input->getAttribute('page');
$Language = $this->__Input->getAttribute('lang');
$DataDir = base64_decode($this->__Input->getAttribute('datadir'));
// get manager
$nM = &$this->__getAndInitServiceObject('modules::newspager::data','newspagerManager',$DataDir);
// 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
}
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
DEFAULT_actionconfig.inibecause the current environment variable is set to DEFAULT. 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.
/~/modules_newspager_biz-action/Pager/page/1/lang/enwill 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>
<@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.
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
}
<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.
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'));
AllowEncodedSlashes Onto your VHOST configuration. This apache bug cost me three days to find!
document.getElementById()or
foo.innerHTML = barSo adding AJAX features to your application means, that you have to think very carefully about your application or module design. I even claim, that writing AJAX applications needs much more understanding of good application design. Otherwise, this leads to bad software architecture!