Sunday, 22 February 2009

Phplocing your projects with Phing

When I started to play around with Ruby on Rails, my attention got somehow soon drawn to it's Rake stats task, which provides developers or more likely project managers with an overview of the actual project size. Exactly one month ago Sebastian Bergmann, of PHPUnit fame, started to implement a similar tool dubbed phploc which can give you an overview of the size for any given PHP project. As I wanted to automate the invocation of this handy tool and collect it's report output out of a Phing buildfile, I invested some time to develop a custom Phing task doing so. Thereby the following post will show you a possible implementation of this task and it's use in a buildfile.

Installing phploc

To setup phploc on your system simply install the phploc PEAR package available from the pear.phpunit.de channel as shown in the next commands. In case you already have installed PHPUnit via PEAR you can omit the channel-discover command.
sudo pear channel-discover pear.phpunit.de
sudo pear install phpunit/phploc

Implementing the phploc task

As I already blogged about developing custom Phing task I'm only going to show the actual implementation and not dive into any details; alternatively you can also grab it from this public GitHub repository.
<?php
require_once 'phing/Task.php';
require_once 'phing/BuildException.php';
require_once 'PHPLOC/Analyser.php';
require_once 'PHPLOC/Util/FilterIterator.php';
require_once 'PHPLOC/TextUI/ResultPrinter.php';

class PHPLocTask extends Task
{
protected $suffixesToCheck = null;
protected $acceptedReportTypes = null;
protected $reportDirectory = null;
protected $reportType = null;
protected $fileToCheck = null;
protected $filesToCheck = null;
protected $reportFileName = null;
protected $fileSets = null;

public function init() {
$this->suffixesToCheck = array('php');
$this->acceptedReportTypes = array('cli', 'txt', 'xml');
$this->reportType = 'cli';
$this->reportFileName = 'phploc-report';
$this->fileSets = array();
$this->filesToCheck = array();
}
public function setSuffixes($suffixListOrSingleSuffix) {
if (stripos($suffixListOrSingleSuffix, ',')) {
$suffixes = explode(',', $suffixListOrSingleSuffix);
$this->suffixesToCheck = array_map('trim', $suffixes);
} else {
array_push($this->suffixesToCheck, trim($suffixListOrSingleSuffix));
}
}
public function setFile(PhingFile $file) {
$this->fileToCheck = trim($file);
}
public function createFileSet() {
$num = array_push($this->fileSets, new FileSet());
return $this->fileSets[$num - 1];
}
public function setReportType($type) {
$this->reportType = trim($type);
}
public function setReportName($name) {
$this->reportFileName = trim($name);
}
public function setReportDirectory($directory) {
$this->reportDirectory = trim($directory);
}
public function main() {
if (!isset($this->fileToCheck) && count($this->fileSets) === 0) {
$exceptionMessage = "Missing either a nested fileset or the "
. "attribute 'file' set.";
throw new BuildException($exceptionMessage);
}
if (count($this->suffixesToCheck) === 0) {
throw new BuildException("No file suffix defined.");
}
if (is_null($this->reportType)) {
throw new BuildException("No report type defined.");
}
if (!is_null($this->reportType) &&
!in_array($this->reportType, $this->acceptedReportTypes)) {
throw new BuildException("Unaccepted report type defined.");
}
if (!is_null($this->fileToCheck) && !file_exists($this->fileToCheck)) {
throw new BuildException("File to check doesn't exist.");
}
if ($this->reportType !== 'cli' && is_null($this->reportDirectory)) {
throw new BuildException("No report output directory defined.");
}
if (count($this->fileSets) > 0 && !is_null($this->fileToCheck)) {
$exceptionMessage = "Either use a nested fileset or 'file' "
. "attribute; not both.";
throw new BuildException($exceptionMessage);
}
if (!is_null($this->reportDirectory) && !is_dir($this->reportDirectory)) {
$reportOutputDir = new PhingFile($this->reportDirectory);
$logMessage = "Report output directory does't exist, creating: "
. $reportOutputDir->getAbsolutePath() . '.';
$this->log($logMessage);
$reportOutputDir->mkdirs();
}
if ($this->reportType !== 'cli') {
$this->reportFileName.= '.' . trim($this->reportType);
}
if (count($this->fileSets) > 0) {
$project = $this->getProject();
foreach ($this->fileSets as $fileSet) {
$directoryScanner = $fileSet->getDirectoryScanner($project);
$files = $directoryScanner->getIncludedFiles();
$directory = $fileSet->getDir($this->project)->getPath();
foreach ($files as $file) {
if ($this->isFileSuffixSet($file)) {
$this->filesToCheck[] = $directory . DIRECTORY_SEPARATOR
. $file;
}
}
}
$this->filesToCheck = array_unique($this->filesToCheck);
}
if (!is_null($this->fileToCheck)) {
if (!$this->isFileSuffixSet($file)) {
$exceptionMessage = "Suffix of file to check is not defined in"
. " 'suffixes' attribute.";
throw new BuildException($exceptionMessage);
}
}
$this->runPhpLocCheck();
}
protected function isFileSuffixSet($filename) {
$pathinfo = pathinfo($filename);
$fileSuffix = $pathinfo['extension'];
return in_array($fileSuffix, $this->suffixesToCheck);
}
protected function runPhpLocCheck() {
$files = $this->getFilesToCheck();
$result = $this->getCountForFiles($files);

if ($this->reportType === 'cli' || $this->reportType === 'txt') {
$printer = new PHPLOC_TextUI_ResultPrinter;
if ($this->reportType === 'txt') {
ob_start();
$printer->printResult($result);
file_put_contents($this->reportDirectory
. DIRECTORY_SEPARATOR . $this->reportFileName,
ob_get_contents());
ob_end_clean();
$reportDir = new PhingFile($this->reportDirectory);
$logMessage = "Writing report to: "
. $reportDir->getAbsolutePath() . DIRECTORY_SEPARATOR
. $this->reportFileName;
$this->log($logMessage);
} else {
$printer->printResult($result);
}
} elseif ($this->reportType === 'xml') {
$xml = $this->getResultAsXml($result);
$reportDir = new PhingFile($this->reportDirectory);
$logMessage = "Writing report to: " . $reportDir->getAbsolutePath()
. DIRECTORY_SEPARATOR . $this->reportFileName;
$this->log($logMessage);
file_put_contents($this->reportDirectory . DIRECTORY_SEPARATOR
. $this->reportFileName, $xml);
}
}
protected function getFilesToCheck() {
if (count($this->filesToCheck) > 0) {
$files = array();
foreach ($this->filesToCheck as $file) {
$files[] = new SPLFileInfo($file);
}
} elseif (!is_null($this->fileToCheck)) {
$files = array(new SPLFileInfo($this->fileToCheck));
}
return $files;
}
protected function getCountForFiles($files) {
$count = array('files' => 0, 'loc' => 0, 'cloc' => 0, 'ncloc' => 0,
'eloc' => 0, 'interfaces' => 0, 'classes' => 0, 'functions' => 0);
$directories = array();

foreach ($files as $file) {
$directory = $file->getPath();
if (!isset($directories[$directory])) {
$directories[$directory] = TRUE;
}
PHPLOC_Analyser::countFile($file->getPathName(), $count);
}

if (!function_exists('parsekit_compile_file')) {
unset($count['eloc']);
}
$count['directories'] = count($directories) - 1;
return $count;
}
protected function getResultAsXml($result) {
$newline = "\n";
$newlineWithSpaces = sprintf("\n%4s",'');
$xml = '<?xml version="1.0" encoding="UTF-8"?>';
$xml.= $newline . '<phploc>';

if ($result['directories'] > 0) {
$xml.= $newlineWithSpaces . '<directories>' . $result['directories'] . '</directories>';
$xml.= $newlineWithSpaces . '<files>' . $result['files'] . '</files>';
}
$xml.= $newlineWithSpaces . '<loc>' . $result['loc'] . '</loc>';

if (isset($result['eloc'])) {
$xml.= $newlineWithSpaces . '<eloc>' . $result['eloc'] . '</eloc>';
}
$xml.= $newlineWithSpaces . '<cloc>' . $result['cloc'] . '</cloc>';
$xml.= $newlineWithSpaces . '<ncloc>' . $result['ncloc'] . '</ncloc>';
$xml.= $newlineWithSpaces . '<interfaces>' . $result['interfaces'] . '</interfaces>';
$xml.= $newlineWithSpaces . '<classes>' . $result['classes'] . '</classes>';
$xml.= $newlineWithSpaces . '<methods>' . $result['functions'] . '</methods>' . $newline;
$xml.= '</phploc>';
return $xml;
}
}

Hooking the phploc task into Phing

To use the task in your Phing builds simply copy it into the phing/tasks/my directory and make it available via the taskdef task. The next table shows the available task attributes and the values they can take to configure it's behaviour and output. As you will see it also provides the ability to generate reports in a XML format; I chose to implement this feature to have the possibilty to transform the report results into HTML documents by applying for example a XSLT stylesheet. This way they can provide more value to non-technical project members or can be made accessible in a CI system dashboard if desired.

NameTypeDescriptionDefaultRequired
reportTypestringThe type of the report. Available types are cli|txt|xml.cliNo
reportNamestringThe name of the report type without a file extension.phploc-reportNo
reportDirectorystringThe directory to write the report file to.falseYes, when report type txt or xml is defined.
filestringThe name of the file to check.n/aYes, when no nested fileset is defined.
suffixesstringA comma-separated list of file suffixes to check.phpNo

Supported Nested Tags:
  • fileset
The closing buildfile extract shows an example phploc task configuration and is also available at the public GitHub repository. Happy phplocing!
<?xml version="1.0"?>
<project name="example" default="phploc" basedir=".">
<taskdef name="phploc" classname="phing.tasks.my.PHPLocTask" />
<target name="phploc">
<tstamp>
<format property="check.date.time" pattern="%Y%m%d-%H%M%S" locale="en_US"/>
</tstamp>
<phploc reportType="txt" reportName="${check.date.time}-report"
reportDirectory="phploc-reports">
<fileset dir=".">
<include name="**/*.php" />
<include name="*.php" />
</fileset>
</phploc>
</target>
</project>

2 comments:

fede said...

Hi Raphael,

It will be nice to browse your code by project, considering the amount of stuff you have. I propose you use Redmine:

http://www.redmine.org/

(another excuse to use Ruby I guess)

Raphael Stolt said...

Hi Federico,

I guess by using it; I would have to invest some money into hosting it somewhere and therefore it's not so tempting. Though I guess I would really like the Ruby aspect.

Nevertheless I will try to utilize GitHub to bring some organization into it.