An sich ist es dem Computer egal, wie hübsch und strukturiert der Code formatiert ist, er wird ihn so oder so ausführen, wenn die Syntax und Semantik passt. Der Mensch tut sich jedoch erheblich leichter, wenn der Quelltext einem einheitlichen Programmierstil folgt, der das Wichtige vom Unwichtigen trennt und schnell Aufschluss darüber gibt, was wo wie passiert.
Eine Frage des guten Stils
So haben sich für diverse Programmiersprachen und Frameworks verschiedenere Coding Standards etabliert, die sich zum Teil widersprechen. Daher ist es unabdingbar, dass im Rahmen eines Projekts alle Entwickler nach dem gleichen Programmierstil entwickeln, damit jeder sich gleichermaßen im Quelltext zurechtfindet. Ferner muss die IDE entsprechend konfiguriert werden, damit eine Datei nicht nach jeder Bearbeitung komplett neu formatiert wird. In diesem Beitrag möchte ich beschreiben wie wir einen eigenen Coding Standard für den PHP CodeSniffer erstellt haben.
PHP CodeSniffer
Der PHP CodeSniffer, kurz PHPCS, ist ein flexibler PHP-Tokenizer, der prüft, ob der in PHP geschriebene Code einem bestimmten Coding Standard genügt. Wenn nicht, meldet er, in welcher Zeile welche Regelverletzung gefunden wurde. Somit ist er ein sehr wertvolles Werkzeug für die statische Code Analyse.
Typischerweise wird der CodeSniffer in die IDE, sowie Continous Intergration eingebunden. Von Haus aus unterstützt er eine Reihe von Coding Standards, z.B. Zend Framework, PEAR, PSR2. Auf GitHub finden sich weitere Regelsätze, z.B. für Symfony.
Der Befehl
% phpcs -i
gibt Auskunft darüber, welche Standards verwendet werden können.
Mittels
% phpcs --standard=PEAR .
werden rekursiv alle Dateien im aktuellen Verzeichnis auf die Konformität zum PEAR-Standard geprüft.
Nach eigenen Regeln
Gelegentlich gehen die Anforderungen eines Teams über bestehende Coding Standards hinaus. Ohne angepassten Code Sniffer zum Beispiel, läuft man Gefahr, dass die eigenen Regeln nicht wirklich beherzigt werden, beziehungsweise, dass viele manuelle Eingriffe nötig sind, um den Standard einzuhalten.
Wie schon Agent Smith in der Matrix zurecht sagte:
Nimm nie einen Menschen, wenn du eine Maschine dafür nehmen kannst.
Im Rahmen des Kundenprojekts MO4 wurde die bestehende Implementierung eines Symfony Regelsatzes für PHPCS vervollständigt, sowie ein eigener Standard entwickelt.
Der erste Schritt zum einem eigenen Regelsatz ist einfach. Man klont das PHP CodeSniffer Projekt von GitHub und wechselt in das Verzeichnis „PHP_CodeSniffer/CodeSniffer/Standards
„:
% git clone https://github.com/squizlabs/PHP_CodeSniffer.git % cd PHP_CodeSniffer/CodeSniffer/Standards
Dort findet man die mitgelieferten Implementierungen zu verschiedenen Coding Standards. Weitere Standards müssen hier abgelegt werden, damit PHPCS sie findet.
Jeder Standard benötigt eine Datei namens „ruleset.xml
„. Diese sollte mindestens den Namen des Standards beinhalten, Groß- und Kleinschreibung ist durchaus relevant. Darüber hinaus können Regeln – im Jargon Sniff genannt – aus anderen Standards importiert werden.
<?xml version="1.0"?> <ruleset name="MO4"> <description>The MO4 Symfony PSR-2 coding standard.</description> <!-- Include the whole Symfony PSR-2 standard --> <rule ref="Symfony"> <!-- exclude sniffs that are extended by this standard --> <exclude name="PEAR.Functions.FunctionCallSignature"/> <exclude name="PSR2.Namespaces.UseDeclaration"/> <exclude name="Generic.Formatting.MultipleStatementAlignment"/> </rule> <rule ref="Generic.Formatting.SpaceAfterCast"/> </ruleset>
Neben der Datei ruleset.xml
findet man bei den Coding Standards für PHPCS folgende Verzeichnisstruktur:
Sniffs
beherbergt die Implementierung von Sniffs in PHP. Unterhalb dieses Ordners werden die Sniffs üblicherweise thematisch in weitere Unterverzeichnisse gruppiert.- in
Tests
finden man die Unit-Tests zu den Sniffs in anloger Struktur, - und
Docs
ist für die Dokumentation der Sniffs vorgesehen.
Im Standard „Generic
“ findet man einen Grundstock an Regeln. Einige der dort angebotenen Sniffs heben sich sogar gegensätzlich auf, wie z.B. NoSpaceAfterCastSniff.php
und SpaceAfterCastSniff.php.
Daher ist von einem kompletten Import des Rulesets abzuraten.
Gibt es für die eigene Regel noch keinen passenden Sniff, kann jener implementiert werden. Das Framework von PHPCS unterstützt dabei das Test Driven Development. Unterhalb von Sniffs
und Tests
legt man die gleiche Verzeichnisstruktur an. Die Klasse für den Sniff benennt man mit dem Suffix „Sniff
„, die Klasse für den Test mit dem Suffix „UnitTest
„. Also z.B. für „UseArrayShortTag
“ jeweils „UseArrayShortTagSniff
“ und „UseArrayShortTagUnitTest
„. Das Einbehalten der Namenskonvention ist absolut notwendig, da sonst die Tests bzw. die Sniffs nicht gefunden werden!
Die Verzeichnisstruktur spiegelt sich im Klassennamen wieder, wie im folgenden Beispiel für „MO4/Sniffs/Formatting/UseArrayShortTagSniff.php“ zu entnehmen ist.
/** * Use the array short tag [...] instead of array(...) */ class MO4_Sniffs_Formatting_UseArrayShortTagSniff implements PHP_CodeSniffer_Sniff { /** * Registers the tokens that this sniff wants to listen for. * * @return array(int) */ public function register() { return array(T_ARRAY); } /** * Called when one of the token types is found. * * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file * @param int $stackPtr token position in file * * @return void */ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) { $phpcsFile->addError('Array short tag [ ... ] must be used', $stackPtr); } }
Jeder Sniff implementiert das Interface PHP_CodeSniffer_Sniff
, das die Implementierung von register()
und process()
vorschreibt. Die Funktion register()
liefert die Liste der PHP-Tokens zurück, für die der Sniff aktiv ist. PHPCS verwendet den PHP eigenen Tokenizer, hat jedoch weitere Tokens definiert und gruppiert. Eine vollständige Liste findet man in der Datei „Tokens.php
“ im PHPCS-Projekt.
Die Methode process()
wird jedes Mal vom PHPCS aufgerufen wenn er auf das entsprechende Token beim Parsen gestoßen ist. Der erste Parameter ist die aktuelle Datei, dargestellt als PHP_CodeSniffer_File
-Objekt, der zweite ist der Index in der Token-Liste.
Das obige Beispiel ist der denkbar einfachste Sniff den man sich vorstellen kann, da er die klassische Array Notation „array(...)
“ verbietet. Man sollte stattdessen die mit PHP 5.4 eingeführte „[...]
“ Notation verwenden.
Das nun folgende Beispiel zeigt eine Regel, welche besagt, dass bei Instantiierungen von Klassen, Klammern nach dem Klassennamen nicht fehlen dürfen.
public function register() { return array(T_NEW); } // must instantiate a Class like "new Bla()" instead of "new Bla" public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $allowedTokens = PHP_CodeSniffer_Tokens::$emptyTokens; $allowedTokens[] = T_STRING; $allowedTokens[] = T_NS_SEPARATOR; $next = $phpcsFile->findNext($allowedTokens, $stackPtr + 1, null, true); if (($next === false) || ($tokens[$next]['code'] !== T_OPEN_PARENTHESIS)) { $phpcsFile->addError('parenthesis missing after class name', $stackPtr); } }
Bei jedem new-Statement wird process()
aufgerufen. Über die Methode getTokens()
bekommen wir von der $phpcsFile
die Liste aller Tokens unserer Datei als Array, $stackPointer
zeigt auf das Element im Array. Jedes Element des Token-Arrays besteht aus den Feldern ‚type‘, ‚code‘, ‚content‘, ‚line‘, ‚column‘ sowie noch weiteren Feldern.
Mit der Funktion findNext()
können wir bequem durch die Liste der Tokens navigieren. In aufgeführten Beispiel navigiert der Aufruf der Funktion zum nächsten Token, das weder Kommentar, Whitespace, Namespace-Trenner noch Identifier ist. Falls das gefundene Symbol keine Klammer ist, kommt ein Fehler. Damit ist z.B. „new Bla;
“ unzulässig.
Neben findNext()
gibt es auch findPrevious()
, das in entgegengesetzte Richtung nach Tokens sucht.
Die Testklasse UseArrayShortTagUnitTest
implementiert die Methoden getErrorList()
und getWarningList()
. Zu jedem Test werden Input-Files eingelesen, die im Verzeichnis des Tests liegen und die Endung .inc
haben. Der Methoden geben jeweils die Anzahl der Fehler pro Zeile zurück, die der Sniff gefunden hat.
class MO4_Tests_Formatting_UseArrayShortTagUnitTest extends AbstractSniffUnitTest { protected function getErrorList($testFile = '') { switch ($testFile) { case 'UseArrayShortTagUnitTest.pass.inc': return array(); case 'UseArrayShortTagUnitTest.fail.inc': return array( 3 => 1, 4 => 1, 8 => 1, 10 => 1, ); } return null; } protected function getWarningList() { return array(); } }
Die Testsuite wird im PHP_CodeSniffer
Verzeichnis wie folgt aufgerufen:
% phpunit tests/AllTests.php
Den vollständigen Symfony Regelsatz, sowie den MO4 Conding Standard findet man unter:
Fazit
Wenn ein Projekt einem eigenen Coding Standard folgt, sollte man im Rahmen der Automatisierung ein dediziertes Regelset für PHP Code Sniffer erstellen. Dabei ist es nicht unbedingt erforderlich, das Rad neu zu erfinden. Das modulare Konzept ermöglicht bestehende Standards in Teilen oder Gänze zu importieren.
Eigene Sniffs können im Rahmen von Test Driven Development zielgerichtet implementiert werden, der eigene Projektcode dient dabei als Testbasis.
Es empfiehlt sich, zeitig den eigenen Standard auf den Entwickler-Rechner und der Continuous Integration zu installieren. Fängt man spät damit an, droht man von einer Fülle von Regelverletzungen erschlagen zu werden.
Ausblick
Eine weitere Erleichterung für die Arbeit von Entwicklern wäre die automatische Korrektur des Quelltextes anhand der eindeutigen Regelverletzungen, die der Code Sniffer meldet.
Hierzu gibt es diverse Entwicklungen, zum Beispiel das experimentelle Tool PHP-CS-Fixer. Dieses ist in der Lage, den Code gemäß PSR-1 oder PSR-2 zumindest teilweise zu fixen.
Im experimentellen Entwicklungszweig phpcs-fixer von PHP CodeSniffer wird ebenfalls an einer automatischen Korrektur der gemeldeten Fehler gearbeitet.
Schreibe einen Kommentar