Showing posts with label Reflection API. Show all posts
Showing posts with label Reflection API. Show all posts

Saturday, 7 April 2007

Rolling your own Phing task

To round off an older article on this blog called "Using the PHP 5 reflection API to keep track of unsolved refactorings" I wanted to automate the following task: collect and log some information about developer marked unsolved refactorings for a single class file or more common multiple files of an whole directory. And as I'm getting more and more acquainted with Phing I wanted to craft this custom task by using it's provided extension possibilities.

In the above mentioned article I outlined how to collect and introspect metadata of methods having a @unsolvedRefactoring in a single given PHP file. The PHP doclet @unsolvedRefactoring is an 'unoffical' convention used here to tag or mark methods as improvable via suitable refactorings and is assumed for the ongoing train of thoughts.

So here is an example of a method marked as containing an unsolved refactoring.

<?php

class RecordshelfController extends Zend_Controller_Action {

...

/**
* @unsolvedRefactoring A longer note stretching over several lines,
* which might log your thoughts at the moment of writing
* this method & something else.
*/
public function saveAction() {
...
}

...
}
The possibility to track unsolved refactorings might be most usefull as a reminder in stress or time pressure situations, but shouldn't influence the developers discipline to refactor weak spots as early as possible. Simply because to much refactorings add to a later refactoring queue might be lessen their importance and might support the unwanted spread of broken windows in the code base.

To build the desired custom task I had to derivate from the provided Task class of Phing and stuff it with mutator methods for each used target attribute of the buildfile task and the main task functionality. The forthcoming buildfile shows the way to make the custom task accessible to the using targets by defining the task and providing a mapping to it's defintion class ExtractOpenRefactoringTask.
<?xml version="1.0" ?>
<project name="own-phing-task" basedir="." default="unsolved-refactoring-analyzer-multiple">

<!-- Define the custom task, defined and located in/at classname -->
<taskdef name="extractUnsolvedRefactoring" classname="phing.tasks.my.tools.ExtractUnsolvedRefactoringsTask"/>

<property name="include.directory.zend" value="${project.basedir}\library\Zend\ZendFramework-0.9.1\library"/>
<property name="include.directory.custom" value="${project.basedir}\library\Recordshelf"/>

<!-- Use a single PHP file -->
<target name ="unsolved-refactoring-analyzer-single">
<extractOpenRefactoring doclet="unsolvedRefactoring"
outfile="unsolved-refactorings"
type="txt"
file="${project.basedir}/examples/Deeper/Rush.php"
includepath="/path/to/include"/>
</target>

<!-- Use a file set of multiple PHP files -->
<target name="unsolved-refactoring-analyzer-multiple">
<extractUnsolvedRefactoring doclet="unsolvedRefactoring"
outfile="unsolved-refactorings"
type="html"
includepath="${include.directory.zend};${include.directory.custom}">
<fileset dir="${project.basedir}/application" >
<include name="**/*.php" />
</fileset>
</extractUnsolvedRefactoring>
</target>

</project>
The custom class can be located under /classes/phing/tasks/my/tools. The extractUnsolvedRefactoring task requires several mandatory attributes which are each mapped to properties of the custom task class. These attributes and their default values are listed in the following table.

NameTypeDescriptionDefaultRequired
docletstringEvery alphanumeric PHP doclet, without a preceding @.unsolvedRefactoringYes
outfilestringThe log file name to collect open or sheduled refactorings.unsolvedRefactoringsYes
typestringThe type to use for the outfile. Available options are txt|xml|html.txtYes
fileFilePath to the file that should be examined for open or sheduled refactorings.txtNo, if using a nested fileset
includepathstringPath to add to PHP's include_path, for resolving dependencies.n/aYes

So now it's time to delve into the ExtractUnsolvedRefactoringTask class which performs the actual work. As mentioned before every attribute of the target definition in the buildfile is mapped to a property of the class and will be set via mutator methods. These properties are crucial to steer the task.

The method findUnsolvedRefactorings does delegate all the work, analyzing the PHP files and extracting the metadata, to the UnsolvedRefactoringsExtractor(formerly know as UnsolvedRefactoringsProcessor) class and stores the results in an internal array. Finally this "collecting" array is persisted to a log file via the generateLogFile method. At this point only logging to an text, xml or html file is provided, but other output formats like csv might be achieved easy. The Html logging or report is generated via a Xsl stylesheet applied on the on the in-memory Xml result set of this task and has to be located in the same directory of the custom task classes.

So with no further deviation and yabbing here comes the code for the ExtractUnsolvedRefactoringTask class.
<?php

require_once('phing/Task.php');
require_once('UnsolvedRefactoringsExtractor.php');

class ExtractUnsolvedRefactoringsTask extends Task {

protected $supportedTypes = array();
protected $doclet = null;
protected $file = null;
protected $filesets = array();
protected $type = null;
protected $outfile = null;
protected $includepath = null;
protected $applicationDirectory = null;

private $unsolvedRefactorings = array();
private $unsolvedTotal = null;
private $xslFile = 'transformation.xsl';

public function init() {
$this->supportedTypes = array('txt', 'html' , 'xml');
$this->fileset = array();
$this->unsolvedRefactorings = array();
$this->unsolvedTotal = 0;
$this->xslFile = 'transformation.xsl';
}

public function setDoclet($str = 'unsolvedRefactoring') {
if(substr($str, 0, 1) == "@") {
$this->doclet = substr($str, 1, strlen($str));
} else {
$this->doclet = $str;
}
}

public function setFile(PhingFile $file) {
$this->file = $file;
}

public function createFileSet() {
$num = array_push($this->filesets, new FileSet());
return $this->filesets[$num-1];
}

public function setType($str = 'txt') {
$this->type = $str;
}

public function setOutfile($str = 'unsolvedRefactorings') {
$this->outfile = $str;
}

public function setIncludepath($str) {
$this->includepath = get_include_path();
$this->includepath.= PATH_SEPARATOR . $str;
set_include_path($this->includepath);
}

public function main() {
if(!isset($this->file) && count($this->filesets) == 0) {
throw new BuildException("Missing either a nested fileset or attribute 'file' set");
}

if(!isset($this->doclet)) {
throw new BuildException("Missing the unsolved refactoring identifying doclet tag attribute");
} else {
print("Using doclet @{$this->doclet}\n");
}

if(!isset($this->outfile)) {
throw new BuildException("Missing the outfile attribute");
}

if(!isset($this->includepath)) {
throw new BuildException("Missing the includepath attribute");
}

if($this->includepath == '') {
throw new BuildException("Missing value for the includepath attribute");
}

if(!isset($this->type)) {
throw new BuildException("Missing the type attribute");
}

if(!in_array($this->type, $this->supportedTypes)) {
throw new BuildException("Unkown type attribute {$this->type} set");
}

if(!@unlink($this->outfile.'.'.$this->type)) {
throw new BuildException("Unable to delete old log file {$this->outfile}.{$this->type}");
}

// process a single file
if($this->file instanceof PhingFile) {
$this->findUnsolvedRefactorings($this->file->getPath());

// process multiple files in a file set
} else {
$project = $this->getProject();

foreach($this->filesets as $fs) {
$ds = $fs->getDirectoryScanner($project);
$files = $ds->getIncludedFiles();
$dir = $fs->getDir($this->project)->getPath();

if($this->applicationDirectory === null) {
$this->applicationDirectory = $fs->getDir($this->project);
}
foreach($files as $file){
$this->findUnsolvedRefactorings($dir.DIRECTORY_SEPARATOR.$file);
}
}
}

print("\nFound methods docletted with @{$this->doclet}: {$this->unsolvedTotal}\n");

if($this->unsolvedTotal > 0) {
$this->generateLogFile();
print("Logged results to {$this->outfile}.{$this->type}\n");
}

}

protected function findUnsolvedRefactorings($file) {

if(($classname = $this->getCallableClassName($file)) !== null) {
print("+ Inspecting class {$classname}\n");
$extractor = new UnsolvedRefactoringsExtractor($file, $classname, $this->doclet);
$extractions = $extractor->getMethodsWithUnsolvedRefactorings();

if(is_array($extractions)) {
$this->unsolvedRefactorings[]['class'] = $classname;
$this->unsolvedRefactorings[count($this->unsolvedRefactorings) - 1]['file'] = $file;
$this->unsolvedRefactorings[count($this->unsolvedRefactorings) - 1]['unsolved'] = $extractions;
}
$this->unsolvedTotal+= count($extractions);
} else {
return false;
}

}
/**
* @unsolvedRefactoring That method is too long, handling too much and is bloated.
*/
protected function generateLogFile() {
if($this->unsolvedRefactorings !== null) {

if($this->type == "xml" || $this->type == "txt") {
$fhandle = fopen($this->outfile.'.'.$this->type, 'a');
if(!fhandle) {
throw new BuildException("Unable to open {$this->outfile}.{$this->type} for writing");
}
}

foreach($this->unsolvedRefactorings as $index => $refactoring) {
if($this->applicationDirectory != null) {
$refactoring['file'] = str_replace($this->applicationDirectory.DIRECTORY_SEPARATOR,
'',
$refactoring['file']);
}

if($this->type == "xml" || $this->type == "html") {
if($index == 0) {
$xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'."\n";

if($this->applicationDirectory != null) {
$xmlContent.= '<unsolved-refactorings directory="'.$this->applicationDirectory.'"';
$xmlContent.= ' amount-of-unsolved-refactorings="'.$this->unsolvedTotal.'">'."\n";
} else {
$xmlContent.= '<unsolved-refactorings';
$xmlContent.= ' amount-of-unsolved-refactorings="'.$this->unsolvedTotal.'">'."\n";
}
}

$xmlContent.= '<class name="'.$refactoring['class'].'"';
$xmlContent.= ' file="'.$refactoring['file'].'">'."\n";

foreach($refactoring['unsolved'] as $unsolved) {

if(isset($unsolved['notice'])) {
$xmlContent.= '<method startLine="'.$unsolved['startLine'].'"';
$xmlContent.= ' endLine="'.$unsolved['endLine'].'"';
$xmlContent.= ' notice="'.htmlspecialchars($unsolved['notice']).'">';
} else {
$xmlContent.= '<method startLine="'.$unsolved['startLine'].'"';
$xmlContent.= ' endLine="'.$unsolved['endLine'].'"';
$xmlContent.= ' notice="-">';
}
$xmlContent.= $unsolved['method'];
$xmlContent.= '</method>'."\n";
}
$xmlContent.= '</class>'."\n";

} else {

if($index > 0) {
fwrite($fhandle, "\nUnsolved refactorings for {$refactoring['class']}");
fwrite($fhandle, "({$refactoring['file']}):\n");
} else {
fwrite($fhandle, "Unsolved refactorings for {$refactoring['class']}");
fwrite($fhandle, "({$refactoring['file']}):\n");
}

foreach($refactoring['unsolved'] as $unsolved) {
fwrite($fhandle, "+ {$unsolved['method']} Line: {$unsolved['startLine']} - ");
fwrite($fhandle, "{$unsolved['endLine']}");

if(isset($unsolved['notice'])) {
$notice = str_replace("\n","", $unsolved['notice']);
fwrite($fhandle, " Notice: [{$notice}]");
}
fwrite($fhandle,"\n");
}
}
}

if($this->type == "xml" || $this->type == "html") {
$xmlContent.= '</unsolved-refactorings>'."\n";
}

if($this->type == "xml") {
fwrite($fhandle, $xmlContent);
} elseif($this->type == "html") {

$xml = new DOMDocument;
$xml->loadXML($xmlContent);
$xsl = new DOMDocument;

// Get path xsl file relative to the custom phing task
$pathToXslFile = Phing::getResourcePath("phing/tasks/my/tools/{$this->xslFile}");

if(!$xsl->load($pathToXslFile)) {
throw new BuildException("Unable to open {$pathToXslFile} for transformation to html");
}

$proc = new XSLTProcessor;
$proc->importStyleSheet($xsl);
$proc->transformToURI($xml, $this->outfile.'.html');
}

if($this->type == "xml" || $this->type == "txt") {
fclose($fhandle);
}
}
}

private function getCallableClassName($file) {
$content = file_get_contents($file);

$pattern = "/\s+class\s+([A-z_0-9]+)\s+/";
$times = preg_match($pattern, $content, $matches);

$content = null;

if($times === 0 || $times === false) {
return null;
} else {
return $matches[1];
}
}
}
The next code 'snippet' shows the modified UnsolvedRefactoringsExtractor class, which atcually retrieves the metadata of the methods marked as improveable via the @unsolvedRefactoring doclet and this one has to be located in the directory of the custom task class as well.
<?php
class ClassFileNotReadableException extends Exception {

}
/**
* Formerly known as UnsolvedRefactoringsProcessor class.
*/
class UnsolvedRefactoringsExtractor {

private $_classToInspect = NULL;
private $_fileToInspect = NULL;
private $_methodsToRefactor = NULL;
private $_refactoringTag = NULL;

public function __construct($fileToInspect, $classToInspect, $refactoringTag = 'unsolvedRefactoring') {
$this->_fileToInspect = $fileToInspect;
$this->_classToInspect = $classToInspect;
$this->_refactoringTag = $refactoringTag;
$this->_methodsToRefactor = array();
}

private function setClassToInspect($classToInspect) {
$this->_classToInspect = $classToInspect;
}

private function isClassFileReadable() {
if(file_exists($this->_fileToInspect)) {
return true;
}
return false;
}

private function inspectClass() {
if(!$this->isClassFileReadable()) {
throw new ClassFileNotReadableException($this->_fileToInspect . ' is not readable.');
}
include_once($this->_fileToInspect);
$this->getUnsolvedRefactorings(new ReflectionClass($this->_classToInspect));
}

private function getUnsolvedRefactorings($class) {
foreach($class->getMethods() as $method) {

// exclude inherited methods
if($class->getName() === $method->getDeclaringClass()->getName()) {

if($this->isRefactoringTagPresent($method->getDocComment())) {
$this->_methodsToRefactor[]['method'] = $method->getName();
$this->_methodsToRefactor[count($this->_methodsToRefactor) - 1]['startLine'] =
$method->getStartLine();
$this->_methodsToRefactor[count($this->_methodsToRefactor) - 1]['endLine'] =
$method->getEndLine();

if($this->getNotice($method->getDocComment()) != null) {
$this->_methodsToRefactor[count($this->_methodsToRefactor) - 1]['notice'] =
$this->getNotice($method->getDocComment());
}

}
}
}
}

private function isRefactoringTagPresent($docComment) {
$pattern = '/\s+@'.$this->_refactoringTag.'\b/';

if(preg_match($pattern, $docComment, $matches) > 0) {
return true;
}
return false;
}

private function getNotice($docComment) {
$doclets = $this->extractDoclets(trim(str_replace(array('**','*', '/', ' ', '\n'), '', $docComment)));

foreach($doclets as $doclet) {
if($doclet['doclet'] == $this->_refactoringTag) {

if(array_key_exists('description', $doclet)) {
return $doclet['description'];
}
}
}
return null;
}

private function extractDoclets($docCommentCleanedUp) {
$doclets = explode("@", $docCommentCleanedUp);
array_shift($doclets);

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

if($this->isDocletWithDescription($doclet)) {
$doclets[$index] = array('doclet' => $this->getDocletTag($doclet),
'description' => $this->getDocletDescription($doclet));
} else {
$doclets[$index] = array('doclet' => $doclet);
}
}
return $doclets;
}

private function getDocletDescription($singleDoclet) {
return substr($singleDoclet, stripos($singleDoclet, " ") + 1);
}

private function getDocletTag($singleDoclet) {
return substr($singleDoclet, 0, stripos($singleDoclet, " "));
}

private function isDocletWithDescription($singleDoclet) {
if(stripos($singleDoclet, " ")) {
return true;
} else {
return false;
}
}

public function getMethodsWithUnsolvedRefactorings() {
try {
$this->inspectClass();
} catch (ClassFileNotReadableException $e) {
return $e->getMessage();
}
if($this->classContainsUnsolvedRefactorings()) {
return $this->_methodsToRefactor;
}
return null;
}

public function classContainsUnsolvedRefactorings() {
if(count($this->_methodsToRefactor) > 0) {
return true;
}
return false;
}

public function getInspectedClassName() {
return $this->_classToInspect;
}
}
// needed to resolve heritage hierachies in the relected files/classes
function __autoload($classname) {
$filename = str_replace('_', '/', $classname).'.php';
require($filename);
}

With everything in place it is now possible to run the custom task by calling phing. If your buildfile looks like the one above a simple call to phing will find all unsolved refacorings, otherwise you have to provide the target which crawls for them.

phing unsolved-refactoring-analyzer-multiple

This is how the output of the custom Phing task should look like, indicating which classes were introspected and how many unsolved refactorings where found.

own-phing-task > unsolved-refactoring-analyzer-multiple:
Using doclet @unsolvedRefactoring
+ Inspecting class IndexController
+ Inspecting class LoginController
+ Inspecting class RecordshelfController
+ Inspecting class TourController
+ Inspecting class UserController
+ Inspecting class WishlistController
+ Inspecting class Dj_Set
+ Inspecting class Record_Playlist
+ Inspecting class Record_Shelf
+ Inspecting class Record_Wishlist
+ Inspecting class Record
+ Inspecting class User
+ Inspecting class Utilities

Found methods docletted with @unsolvedRefactoring: 4
Logged results to unsolved-refactorings.html

As you can see after an successfull execution of the unsolved-refactoring-analyzer-multiple target there will be a log or report file located in the base directory of the buildfile. This file will contain the collected information(methodname, start and end line, notes made by the developer while recognizing the need for a refactoring) about developer tagged @unsolvedRefactorings in every inspected class file.

So here is an example of a generated Html report summarizing all found results.

Unsolved refactoring report

The above Html report is generated via this Xsl stylesheet, which can be easily customized to change the appearance of the report.
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:template match="/">
<html>
<head>
<title>Unsolved refactoring report</title>
<style type="text/css">
.box {
font-size: 11px;
padding: 3px;
border: 1px solid #000;
}
.text {
font-size: 11px;
text-indent: 1px;
margin: 1px;
}
.comment {
text-indent: 1px;
margin: 1px;
}
.headline {
font-weight: bold;
}
.unsolvedRefactoring {
background-color: #CE6700;
}
.intro {
font-size: 11px;
text-indent: 1px;
padding-bottom: 5px;
font-weight: bold;
}
body {
font-family : Verdana, Geneva, Arial, Helvetica, sans-serif;
padding-left: 10px;
}
</style>
</head>
<body>
<div class="intro">
<xsl:value-of select="//@amount-of-unsolved-refactorings"/>unsolved refactorings in
<xsl:value-of select="//@directory"/>
</div>
<table class="text" width="70%" border="0">
<xsl:for-each select="unsolved-refactorings/class">
<!-- Information about the method hosting class -->
<tr class="headline">
<td colspan="3" class="box" valign="top">
<xsl:value-of select="./@name"/> [<xsl:value-of select="./@file"/>]
</td>
</tr>
<!-- Apply xsl template for method entities -->
<xsl:apply-templates />
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>

<xsl:template match="method">
<!-- Meta information about method with unsolved refactoring -->
<tr>
<td width="15%" class="unsolvedRefactoring box" valign="top">
<b><xsl:value-of select="."/></b>
</td>
<td class="box" width="15%" valign="top">
Line <xsl:value-of select="@startLine"/> - <xsl:value-of select="@endLine"/>
</td>
<td class="box" width="70%" valign="top">
<xsl:value-of select="@notice"/>
</td>
</tr>
</xsl:template>
</xsl:stylesheet>

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.

Wednesday, 31 January 2007

Using the PHP 5 reflection API to keep track of unsolved refactorings

When I'm developing software with php I try to keep myself to the TDD flow of Red, Green and Refactor.

But sometimes the refactoring isn't obvious at that current time or there is to much stress to complete this task right away so I leave a meta tag in PHPDoc syntax to mark the method I have to get back on later. These tags are defined in an own coding convention so they are unknown for tools like PHPDocumentor. So I played around with the very suitable reflection API to write an unsolved refactoring processor.

The used coding or meta tag conventions define several tags like unsolvedRefactoring and needsCodeReview for methods who need an other pair of eyes. The code review meta tag might be used by an prosessor to email the person which should join the code review. This mail might contain a simple note with an filepath to the class or even the whole method body and so on.

The following class uses these conventions to mark the smelling or non-reviewed methods.

class ExampleWithUnsolvedRefactorings {

/**
* Puuh. That's an empty method and a strong smelling method.
* @param integer The smelling scale.
* @unsolvedRefactoring Your hints to remove the smell.
*/
public function smellingMethod($in) {
// omitted method body
}
/**
* This method is smelling too.
* @unsolvedRefactoring The thoughts to remove the smell.
*/
public function anOtherOdourfullMethod() {
// omitted method body
}
/**
* This methods needs a code review.
* @needsCodeReview [email protected]
*/
public function seekFeedbackMethod() {
// omitted method body
}
}
As mentioned above these tags can't be processed with tools like PHPDocumentor so I had to come up with an own class processor or a refactoring tracker.

There for I used the reflection API of PHP 5 to extract the metatags from the target class. It provides several classes like ReflectionClass, ReflectionMethod with lots of handy methods to inspect a class and it's contained structure.

So crafted this class to get the defined methods of a the test class and extract the ones with unsolved refactorings. At this point this solution isn't able to extract the hints stated after the metatags which might contain thoughts about the steps of this refactoring. The review tags are not handled by this example.
class ClassFileNotReadableException extends Exception {

}
class UnsolvedRefactoringsProcessor {

private $_refactoringTag = NULL;
private $_classToInspect = NULL;
private $_methodsToRefactor = NULL;

public function __construct($classToInspect, $refactoringTag = 'unsolvedRefactoring') {
$this->_classToInspect = $classToInspect;
$this->_refactoringTag = $refactoringTag;
$this->_methodsToRefactor = array();
}

private function setClassToInspect($classToInspect) {
$this->_classToInspect = $classToInspect;
}

private function isClassFileReadable() {
if(file_exists($this->_classToInspect.'.php')) {
return true;
}
return false;
}

private function inspectClass() {
if(!$this->isClassFileReadable()) {
throw new ClassFileNotReadableException($this->_classToInspect . ' is not readable.');
}
include($this->_classToInspect.'.php');
$this->getUnsolvedRefactorings(new ReflectionClass($this->_classToInspect));
}

private function getUnsolvedRefactorings($class) {
foreach($class->getMethods() as $method) {
if($this->isRefactoringTagPresent($method->getDocComment())) {
$this->_methodsToRefactor[]['method'] = $method->getName();
$this->_methodsToRefactor[count($this->_methodsToRefactor) - 1]['startLine'] =
$method->getStartLine();
$this->_methodsToRefactor[count($this->_methodsToRefactor) - 1]['endLine'] =
$method->getEndLine();
}
}
}

private function isRefactoringTagPresent($docComment) {
$pattern = '/\s+@'.$this->_refactoringTag.'\b/';
if(preg_match($pattern, $docComment, $matches) > 0) {
return true;
}
return false;
}

public function getMethodsWithUnsolvedRefactorings() {
try {
$this->inspectClass();
} catch (ClassFileNotReadableException $e) {
return $e->getMessage();
}
if($this->classContainsUnsolvedRefactorings()) {
return $this->_methodsToRefactor;
}
return $this->_classToInspect . ' contains no open refactoring tasks.';
}

public function classContainsUnsolvedRefactorings() {
if(count($this->_methodsToRefactor) > 0) {
return true;
}
return false;
}

public function getInspectedClassName() {
return $this->_classToInspect;
}

}//end of class
The methods inspectClass and getUnsolvedRefactorings are using the reflection API to extract the methods of an class including their doc comments, start and end line. The doc comments are inspected against the defined meta tag and all methods with an unsolved refactoring are collected in an array which is used for further processing. My current solution simply prints all found unsolved refactoring to the screen. But it can easily be used to write it to an log or even better to automate the processing and logging of custom defined tags via an own Phing task. I guess that's a good exercise for my next phing exploration.

So here is my simple test driver for the above class which inspects the ExampleWithUnsolvedRefactorings class for unsolved refactorings.
include('UnsolvedRefactoringsProcessor.php');

$processor = new UnsolvedRefactoringsProcessor('ExampleWithUnsolvedRefactorings');

echo "Unsolved Refactorings for {$processor->getInspectedClassName()}:";

$openRefactorings = $processor->getMethodsWithUnsolvedRefactorings();

if(is_array($openRefactorings)) {
foreach($openRefactorings as $aRefactoring) {
echo "\n+ {$aRefactoring['method']} ";
echo "Line: {$aRefactoring['startLine']} - {$aRefactoring['endLine']}";
}
} else {
echo "\n" . $openRefactorings;
}
The result for the unsolved refactoring in the ExampleWithUnsolvedRefactorings class looks like this:
Unsolved Refactorings for ExampleWithUnsolvedRefactorings:
+ smellingMethod Line: 10 - 12
+ anOtherOdourfullMethod Line: 17 - 19
The solution I crafted in the above code snippets is far from beeing productive code but I quess you got the point and for me there is much potential for further explorations. I will pick up this topic again on my next session about Phing the PHP build tool. In work life a sophisticated solution might help you to keep track of smelly areas in your codebase and might support parts of a workflow or step in an process like code reviews.