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 reviewerOfYourTrust@example.com
*/
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.

No comments: