Ich mag Code. Nicht jeden natürlich, „Douchebag“ Code nervt, Code der aussieht wie ein schreiender Quasimodo oder Code der von einem Dr. Frankenstein in Ausbildung erstellt wurde sind bemitleidenswert und sehr viel Code stinkt. Code, der gefällt (wie beispielsweise der von Frigidor) ist clever ohne zu bluffen, kurz aber nicht kryptisch, tut etwas, ist lesbar und hat einige Wows drin.
Diese Bewunderung sollte in Zahlen gefasst werden: mit Statistiken, Diagrammen, Balken und Graphen. Dies dient zur Vorbereitung der „Continuous Integration“ mit Jenkins/Hudson, die in dieser Serie behandelt wird. Als Quellen haben vor allem jenkins-php und ein äusserst Lesbarer Artikel im Entwickler Magazin 01/11 gedient.
Hier wird nun gezeigt, wie mit einem Befehl:
- Die Unittests durchführt.
- Qualitative Softwaremetriken mit PHP Depend misst: Hierarchietiefe, Komplexität, …
- Unschöne Teile mit PHPMD identifiziert.
- Copy-Paste-Verbrecher aufspührt mit phpcpd.
- Den Coding-Style prüft mit dem PHP Code Sniffer.
- Quantitative Softwaremetriken mit phploc misst.
- Die PHPDoc-Doku erstellt.
- Die Resultate mit dem PHP Code Browser schön darstellt.
PEAR Tools installieren
PEAR stellt viele Hilfsprogramme zur Analyse zur Verfügung. Darum muss zuerst pear und danach diese Tools installiert werden. Am Besten als Root, wenn man nicht ein riesen Chaos verursachen möchte:
sudo pear channel-discover pear.pdepend.org
sudo pear channel-discover pear.phpmd.org
sudo pear channel-discover pear.phpunit.de
sudo pear channel-discover components.ez.no
sudo pear channel-discover pear.symfony-project.com
sudo pear install Text_Highlighter-0.7.1
sudo pear install pdepend/PHP_Depend
sudo pear install phpmd/PHP_PMD
sudo pear install phpunit/phpcpd
sudo pear install phpunit/phploc
sudo pear install PHPDocumentor
sudo pear install PHP_CodeSniffer
sudo pear install --alldeps phpunit/PHP_CodeBrowser
sudo pear install --alldeps phpunit/PHPUnit
Ant build Files
Um den Build zu steuern, eignet sich Ant sehr gut. Zwar ist es XML (Anwender sollen kein XML schreiben müssen!), stellt aber ein paar vernünftige Funktionen bereit. Für einen Build braucht es eine Properties- und eine XML-Datei, die am Besten in ein eigenes build Verzeichnis verfrachtet werden. In der Propertiesdatei können Variablen gesetzt und eventuell auf verschiedene Umgebungen reagiert werden. Bei mir heisst sie build.properties und sieht ziemlich einfach aus:
source=${basedir}/../htdocs/code
# Target directory
builddir=${basedir}/statistics
# Where are the Unittests
unittests=${basedir}/tests
testsuite=suite.php
# 10: Highest priority, 1: Lowest
niceness=1
# Set to a random String if you have no external libraries. PHPCPD accepts only ONE directory
extlib=inc/external
phpdocextlib=*${extlib}/*
extlibs=${extlib},\\.svn
# Files to consider
suffixes=php
# Ruleset for Code Sniffer
csruleset=${basedir}/conf/syncic
Hier sieht man auch schon eines der Hauptprobleme: Die Tools verwenden unterschiedliche Angaben um Verzeichnisse auszuschliessen: Pfade, RegExps, durch Komma getrennt, … Ziemlich mühsam.
Die Build-Datei build.xml ist etwas lange, aber gut verständlich. Zuerst werden die abstrakten Ziele eingerichtet und dann gibt es für jedes Tool ein Target.
<project name="Easy-Form" basedir="." default="build">
<property file="build.properties"/>
<nice newpriority="${niceness}"/>
<target name="build" depends="test, doc" />
<target name="clean">
<delete dir="${builddir}" />
</target>
<target name="prepare" depends="clean">
<mkdir dir="${builddir}/logs" />
<mkdir dir="${builddir}/logs/coverage" />
<mkdir dir="${builddir}/html" />
<mkdir dir="${builddir}/html/jdepend" />
<mkdir dir="${builddir}/html/unittests" />
<mkdir dir="${builddir}/html/experimental" />
<mkdir dir="${builddir}/html/doc" />
<mkdir dir="${builddir}/html/doc/api" />
<mkdir dir="${builddir}/html/doc/codebrowser" />
</target>
<target name="test" depends="test-unit, test-static" />
<target name="test-static" depends="prepare">
<parallel threadCount="2">
<sequential>
<antcall target="test-static-jdepend"/>
<antcall target="test-static-pmd"/>
</sequential>
<antcall target="test-static-cpd"/>
<antcall target="test-static-checkstyle"/>
<antcall target="test-static-phploc"/>
</parallel>
</target>
<target name="doc" depends="prepare">
<sequential>
<antcall target="doc-phpdoc"/>
<antcall target="doc-phpcb"/>
</sequential>
</target>
<!-- PHPUnit Tests -->
<target name="test-unit" depends="prepare">
<exec executable="phpunit" failonerror="true" dir="${unittests}">
<arg value="--log-junit" />
<arg value="${builddir}/logs/junit.xml" />
<arg value="--coverage-clover" />
<arg value="${builddir}/logs/coverage/clover.xml" />
<arg value="--coverage-html" />
<arg value="${builddir}/logs/coverage" />
<arg value="--testdox-html" />
<arg value="${builddir}/html/unittests/index.html" />
<arg value="--configuration" />
<arg value="${phpunitconf}" />
<arg value="${testsuite}" />
</exec>
</target>
<!-- Static Code Analysis -->
<target name="test-static-jdepend">
<exec executable="pdepend" failonerror="false" dir="${source}">
<arg value="--jdepend-xml=${builddir}/logs/jdepend.xml" />
<arg value="--jdepend-chart=${builddir}/html/jdepend/dependencies.svg" />
<arg value="--overview-pyramid=${builddir}/html/jdepend/pyramid-overview.svg" />
<arg value="--summary-xml=${builddir}/logs/jdepend-summary.xml" />
<arg value="--phpunit-xml=${builddir}/logs/jdepend-phpunit.xml" />
<arg value="--ignore=${extlibs}" />
<arg value="--suffix=${suffixes}" />
<arg value="${source}" />
</exec>
</target>
<target name="test-static-pmd">
<exec executable="phpmd" failonerror="false" dir="${source}">
<arg value="${source}" />
<arg value="xml" />
<arg value="codesize,design,naming,unusedcode" />
<arg value="--reportfile" />
<arg value="${builddir}/logs/pmd.xml" />
<arg value="--suffixes" />
<arg value="${suffixes}" />
<arg value="--exclude" />
<arg value="${extlibs}" />
</exec>
</target>
<target name="test-static-cpd">
<exec executable="phpcpd" failonerror="false" dir="${source}">
<arg value="--log-pmd" />
<arg value="${builddir}/logs/cpd.xml" />
<arg value="--suffixes" />
<arg value="${suffixes}" />
<arg value="--exclude" />
<arg value="${extlib}" />
<arg value="${source}" />
</exec>
</target>
<target name="test-static-checkstyle">
<exec executable="phpcs" failonerror="false" dir="${source}">
<arg value="--report=checkstyle" />
<arg value="--report-file=${builddir}/logs/checkstyle-result.xml" />
<arg value="--extensions=${suffixes}" />
<arg value="--ignore=${extlibs}" />
<arg value="--standard=${csruleset}" />
<arg value="${source}" />
</exec>
</target>
<target name="test-static-phploc">
<exec executable="phploc" failonerror="false">
<arg value="--log-csv" />
<arg value="${builddir}/logs/loc.csv" />
<arg value="--suffixes" />
<arg value="php" />
<arg value="--exclude" />
<arg value="${extlib}" />
<arg value="${source}" />
</exec>
</target>
<!-- Documentation -->
<target name="doc-phpdoc">
<exec executable="phpdoc" failonerror="false" dir="${source}">
<arg value="--directory"/>
<arg value="${source}" />
<arg value="--ignore" />
<arg value="${phpdocextlib}" />
<arg value="--target" />
<arg value="${builddir}/html/doc/api" />
<arg value="--output" />
<arg value="HTML:frames:earthli" />
<arg value="--title" />
<arg value="${ant.project.name}" />
</exec>
</target>
<target name="doc-phpcb">
<exec executable="phpcb" failonerror="false">
<arg value="--log"/>
<arg value="${builddir}/logs" />
<arg value="-i" />
<arg value="${extlibs}" />
<arg value="--output" />
<arg value="${builddir}/html/doc/codebrowser" />
</exec>
</target>
</project>
Ist alles richtig eingerichtet, kann der Build mit dem Kommando ant gestartet werden.
Hinweis: Das Coverage-XML und das Coverage-HTML müssen im gleichen Verzeichnis sein. Wenn der Clover-Report nicht funktioniert und ein 404 not found gibt, liegt es wahrscheinlich daran…
Code Sniffer einrichten
Der Stil des Codes (Einrücken, Benamsung, …) ist sehr persönlich. Sehrwahrscheinlich werden viele Warnungen gezeigt, mit denen man gar nicht einverstanden ist. Das ist aber kein Problem, denn der Codesniffer erlaubt es, eigene Codingstandards zu definieren. mit der neuen Version 1.3 (die mittels sudo pear install PHP_CodeSniffer-1.3.0RC2 installiert werden kann) geht das ziemlich einfach. Dafür brauch es in einem Grundverzeichnis eine ruleset.xml Datei und in einem Unterverzeichnis Sniff die eigenen Regeln, in PHP auscodiert. In ruleset.xml kann man sich frei an nestehenden Regeln bedienen. Meines sieht folgendermassen aus:
<ruleset name="Syncic Standard">
<rule ref="PEAR">
<exclude name="Generic.WhiteSpace.DisallowTabIndent"/>
<exclude name="PEAR.ControlStructures.ControlSignature"/>
</rule>
<rule ref="Generic.VersionControl.SubversionProperties"/>
<rule ref="Generic.Classes.DuplicateClassName"/>
<rule ref="Generic.Strings.UnnecessaryStringConcat"/>
<rule ref="Generic.PHP.DeprecatedFunctions"/>
<rule ref="Generic.PHP.ForbiddenFunctions"/>
<rule ref="Generic.PHP.NoSilencedErrors"/>
<rule ref="PEAR.WhiteSpace.ScopeIndent">
<properties>
<property name="indent" value="2"/>
</properties>
</rule>
</ruleset>
In dieser Datei übernehme ich vor allem den PEAR-Standard, konfiguriere etwas herum und mische ein paar allgemeine Regeln hinzu. Vorallem die Indentation mit Spaces wollte ich anpassen. Im Sniffs Verzeichnis habe ich meine eigenen Regeln. Beispielsweise ControlStructures/ControlSignatureSniff.php (angepasst vom PEAR Coding Standard:
/**
* Verifies that control statements conform to their coding standards.
*/
if (class_exists('PHP_CodeSniffer_Standards_AbstractPatternSniff', true) === false) {
throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_Standards_AbstractPatternSniff not found');
}
/**
* Verifies that control statements conform to their coding standards.
*/
class syncic_Sniffs_ControlStructures_ControlSignatureSniff extends PHP_CodeSniffer_Standards_AbstractPatternSniff {
/**
* Constructs a PEAR_Sniffs_ControlStructures_ControlSignatureSniff.
*/
public function __construct() {
parent::__construct(true);
}//end __construct()
/**
* Returns the patterns that this test wishes to verify.
*
* @return array(string)
*/
protected function getPatterns() {
return array(
'do {EOL...} while (...);EOL',
'while(...) {EOL',
'for(...) {EOL',
'if(...) {EOL',
'foreach(...) {EOL',
'} else if(...) {EOL',
'} elseif(...) {EOL',
'} else {EOL',
'do {EOL',
);
}//end getPatterns()
}//end class
?>
Was gewinnen wir damit?
Nach einem ant haben wir zum Einen ganz viele XML-Dateien. In einem späteren Beitrag werde ich zeigen, wie diese in ein „Continuous Integration“ System integriert werden können. Für Menscen Lesbar haben wir Folgendes:
Dokumentation unter statistics/html/doc/api/:
Eine Liste aller Style-Verstösse und Chaosattacken unter statistics/html/doc/codebrowser/:
Ein Coverage-Report der Unittests unter statistics/html/unittests/coverage/:
Zwei nette Diagramme von PHP-Depend (eine Abstraction Instability Chart und eine Overview Pyramid) unter statistics/html/jdepend:
Fazit
In einem nächsten Beitrag werde ich noch weitere Tools vorstellen und dann soll das alles ganz automatisch bei jeder Änderung geschehen… Stay tuned…
Ich interessier mich momentan zwar nicht mehr für PHP, aber du hast mich mit dem Eintrag dazu gebracht, nach ähnlichen Tools für Python zu googlen. Und ich bin mit pylint fündig geworden, tolle Sache 🙂 http://www.logilab.org/project/pylint Hab gleich mal ein kleines Projekt von mir refactored.
Python ist cool! Diese Sprache würde mich schon sehr lange reizen…
Hey, schön dass es Dich inspiriert hat. Ich wünsche Dir weiterhin viel Spass beim Coden…
Python ist wunderbar 🙂 Bin schon fast zum Fanboy geworden… Mit Frameworks wie Django entwickelt man auch viel schneller als mit PHP (kenne jedenfalls kein PHP-Framework das daran rankommt).