Saturday 31 March 2007

Using the Zend Framework plugin system for server-sided Ajax request identification

In an earlier article "Gluing Ajax with the Zend Framework" I'm using a doclet called @ajaxaction to mark actions that are handling Ajax requests.

If using the Prototype, jQuery or mootools libary, these requests differ from pure HTTP request by having a currently non-standard "X-Requested-With = XMLHttpRequest" header and their response mostly doesn't get rendered via a view. Unless you might use AXAH(Asychronous XHTML and HTTP) where the server might respond with snippets of XHTML to insert into the targeted DOM area.

The build filter should determine if the requested action is called in an Ajax context, by checking the request for the above-mentioned header and auditing if the action requested is marked as an Ajax action via a PHP doclet tag. If both off these contracts are met the request should be handled or otherwise be rejected for further controller dispatching.

Other usefull plugin scenarios would be verifying authentication, verifying the use of a valid session, tracking usefull client data and evaluating ACLs before any action will be dispatched by the controller.

If you are not using one of the mentioned javascript libaries you can add the Ajax identifing header by using the setRequestHeader method of the 'native' XMLHttpRequest object or a similar method of your preferred libary to enable the server-side identification of Ajax.

The following code fragment shows the custom plugin located in the recordshelf/libary/Recordshelf/Request/Ajax directory. It checks pre to every dispatch if the defined contracts(isAjaxRequest and isActionDoclettedAsAjaxHandler) are met and acts adequate.

If there is an Ajax request and the action matches against a specified doclet, here it is @ajaxaction, the action is released for dispatching by the controller. Otherwise a Http Status Code of 501 is returned to the client and the rejected action is logged.

<?php

class Recordshelf_Request_Ajax_Detection extends Zend_Controller_Plugin_Abstract {

protected $_request = null;
private $_doclet = null;

public function __construct($doclet) {

if($doclet == '') {

throw new Zend_Controller_Exception('No doclet tag provided.');

}

$this->_doclet = $doclet;

}

public function preDispatch($request) {

$this->_request = $request;

if($this->isAjaxRequest()) {

if(!$this->isActionDoclettedAsAjaxHandler($this->_doclet)) {

$action = $this->_request->getActionName().'Action';
$controller = ucfirst($this->_request->getControllerName()).'Controller';

Zend_Log::log("{$controller}->{$action} is not ajax docletted. Rejecting call.",
Zend_Log::LEVEL_ERROR);

$this->getResponse()->setHeader('Content-Type', 'text/txt')
->setHttpResponseCode(501)
->appendBody('Not Implemented')
->sendResponse();

exit(0);

}

}

}
/**
* Determines if request is in Ajax context.
* @return boolean
*/
private function isAjaxRequest() {

if($this->_request->getHeader('X-Requested-With') == '') {
return false;
}

return true;

}
/**
* Determines if requested action handles Ajax requests by checking for a provided custom doclet.
* @param string The custom doclet to validate against.
* @return boolean
*/
private function isActionDoclettedAsAjaxHandler($doclet) {

if($this->_request->getControllerName() != '') {

$controller = ucfirst($this->_request->getControllerName()).'Controller';

} else {

return false;

}

$action = $this->_request->getActionName().'Action';
$method = new ReflectionMethod($controller, $action);

$illuminator = new Recordshelf_Server_Reflection_Doclet($method->getDocComment());
$isAjaxDocletAvailable = $illuminator->hasDoclet($doclet);

if($isAjaxDocletAvailable) {

return true;

} else {

return false;

}

}

}
The plugin makes use of a custom Recordshelf_Server_Doclet class build on top of the PHP Reflection API to analyse the PHP doclets of the requested action method(s). Sadly the Zend_Server_Reflection doesn't currently support an full access to the doclets of a method, as Zend_Server_Reflection_Method only allows access to the textual part of the PHP doclet area, means everything without a preceding @. I'd like to see this IMHO basic feature added to the Zend_Server_Reflection component in future to avoid a fall back on the PHP Reflection API and to keep the use of reflection/introspection within the Zend Framework.

After the plugin has been installed, it has to be registered in the bootstrap file by chaining the registerPlugin method "fluently" on the Zend_Controller_Front instance. The plugin takes the doclet to identify/tag server-sided Ajax actions as an argument.
<?php
...
$controller->setControllerDirectory('/path/to/controllers')
->setRouter(new Zend_Controller_Router())
->registerPlugin(new Recordshelf_Request_Ajax_Detection('ajaxaction'));
...
?>
The custom Recordshelf_Server_Doclet class illuminates the PHPDoc comment of the requested action method, allows validation against any available doclet and provides access to all doclets found.
<?php

class Recordshelf_Server_Reflection_Doclet {

private $_doclets = null;
private $_comment = null;

public function __construct($comment) {

$this->_comment = $comment;
$this->_illuminate();

}
/**
* Worker method for illuminating the PHPDoc comment.
* @return null On empty PHPDoc comment and non-available doclets.
*/
private function _illuminate() {

if($this->_comment == '') {

$this->_doclets = array();
return null;

}

if(!$this->containsDoclets()) {

$this->_doclets = array();
return null;

}

$comment = trim(str_replace(array('/**', '*/', '*', '{@link'), '', $this->_comment));
$comment = trim(substr($comment, stripos($comment, '@'), strlen($comment)));

$doclets = explode('@', $comment);

array_shift($doclets);

foreach($doclets as $index => $doclet) {

$doclet = trim($doclet);

if(stripos($doclet, '(') && stripos($doclet, ')')) {

$value = substr($doclet, stripos($doclet, '('), stripos($doclet, ')'));
$value = str_replace(array('(',')', ' '),'', $value);
$values = explode(',', $value);

$tmp = null;

foreach($values as $index => $value) {

if(stripos($value, '=')) {

$docletValues = explode('=', $value);

$tmp[] = array('key' => trim($docletValues[0]), 'value' => trim($docletValues[1]));

} else {

$tmp[] = array('key' => trim($value));

}

}
$doclet = substr($doclet, 0, stripos($doclet, '('));
$this->_doclets[] = array(trim($doclet), $tmp);

} else {

$this->_doclets[] = trim($doclet);

}

}

}
/**
* Checks if provided PHPDoc comment contains any doclets.
* @return boolean
*/
private function containsDoclets()

if(stripos($this->_comment, '@')) {

return true;

}

return false;

}
/**
* Acessor for all found doclets.
* @return mixed An array containing all doclets.
*/
public function getDoclets() {

return $this->_doclets;

}
/**
* Checks if the provided specific doclet is available.
* @param string The name of the doclet.
* @return boolean
*/
public function hasDoclet($name) {

if($name == '') {

return false;

}

if(in_array($name, $this->_doclets)) {

return true;

} else {

return false;

}

}
}
The above stated Recordshelf_Server_Reflection_Doclet class already provides a basic skeleton and some features for the use of Annotations in the Zend Framework, like in the Stubbles framework, but is very far from beeing complete.

1 comment:

Anonymous said...

i'm just new to the zend framework so i'm still exploring its features and i've seen you have a nice point. i always thought of a way for the server to detect ajax requests and make the framework do some special treatment on those requests. i can feel your idea can provide a groundwork for realizing that.