PHP CodeSniffer Hook für Mercurial unter Mac OS X 10.6

Klammersetzung in Klassen, Methoden und Conditions, Einrückungen mit Tabs oder Leerzeichen, Code-Dokumentation, usw"¦

Jeder Entwickler hat hier und da seine Gemeinsamkeiten mit anderen, aber auch unterschiedliche Ansichten darüber, wie der Code formatiert, dokumentiert oder strukturiert sein soll. Das ist durchaus ok, solange man für sich selbst entwickelt. In der Team-Entwicklung können diese kleinen Unterschiede in Formatierung, Dokumentation und Struktur des Codes zu mehr oder weniger großen Reibungsverlusten führen.

So landen immer wieder teilweise nicht oder unvollständig dokumentierte Quellcodes in der Version Control, die schon mal dazu führen, das für die ein oder andere Klasse die Code Completion kaum oder gar nicht funktioniert. Nicht weiter schlimm, wenn man derjenige ist, der den Code produziert hat und dessen Funktionalität genau kennt - zumindest für die nächsten 2 Wochen"¦
Für andere Entwickler, die mit im Team arbeiten, bedeutet das einen unnötigen Aufwand:

  • Nachschauen, was die Klasse kann.
  • Welche Methoden bietet sie?
  • Und was machen diese überhaupt?
  • ...

Je nach dem, wie weit die Ansichten über Coding Guidelines des Verfassers von den eigenen abweichen, fällt einem das Verstehen auf Anhieb gar nicht mal so leicht. Vielleicht sollte man genau jetzt über gemeinsame Code Richtlinien Nachdenken und diese auch konsequent einsetzen - schön und gut, solange Konsequenz nicht zu Inkonsequenz wird.

Hier und da ertappt man sich wahrscheinlich sogar selbst, wie man gegen seine eigenen Richtlinien verstößt und es mit den Coding Guidelines nicht ganz so genau nimmt. Lösen lassen sich solche Konflikte mit sogenannten Hooks für Mercurial (hg), einem mächtigen Tool eines mächtigen Source Control Managers. Hooks sind Aktionen, die bei bestimmten Events durchgeführt werden, vergleichbar mit einem Trigger für MySQL. Unter Mercurial können Hooks auf folgende Events folgen:

  • abgeschlossene Events
    • changegroup: wird ausgeführt, nachdem eine Gruppe von Changesets im Repository angekommen ist.
    • commit: wird nach einem commit in das Repository ausgeführt.
    • incoming: wird nach jedem eingehenden Changeset ausgeführt.
    • outgoing: wird ausgeführt, nachdem eine Gruppe von Changesets aus dem Repository ausgegangen ist.
    • tag: wird ausgeführt, nachdem ein Tag erzeugt wurde
    • update: wird nach einem update oder merge ausgeführt
  • kontrollierende Events
    • prechangegroup: wird ausgeführt, bevor eine Gruppe von Changesets im Repository angekommt.
    • precommit: wird vor einem commit in das Repository ausgeführt.
    • preoutgoing: wird ausgeführt, bevor eine Gruppe von Changesets aus dem Repository ausgeht.
    • pretag: wird ausgeführt, bevor ein Tag erzeugt wird
    • preupdate: wird vor einem update oder merge ausgeführt
    • pretxnchangegroup: wird ausgeführt, nachdem eine Gruppe von Changesets im Repository angekommen ist, jedoch bevor die Transaktion abgeschlossen ist.
    • pretxncommit: wird nach einem commit in das Repository ausgeführt, jedoch bevor die Transaktion abgeschlossen ist.

Alle mit 'pre' beginnende Events sind "kontrollierend", das heißt, sie können die zu verarbeitenden Daten überprüfen und den Event ggf. abbrechen.
Geradezu perfekt, um zu verhindern, dass 'schmutziger' Code versioniert wird ;-)
Wir wollen, dass 'schmutziger' Code gar nicht erst versioniert wird, also verwenden wir den Event 'pretxncommit'. Dieser hat gegenüber dem 'precommit' den Vorteil, dass bereits Revisions-Daten generiert wurden, welche wir auslesen und zur Analyse verwenden können.

Nun benötigen wir nur noch einen Mechanismus, der PHP-Dateien auf fehlerhaften Code prüft, auf Abweichungen des Haus eigenen Coding Standards. PHP CodeSniffer eignet sich wunderbar für diese Aufgabe. Neben bereits mitgelieferten Coding Standards wie PEAR oder Zend, besteht auch die Möglichkeit eigene Standards zu definieren.
In diesem Blog Beitrag beschränken wir uns aber vorerst auf die mitgelieferten Standards.
Um PHP CodeSniffer anwenden zu können, muss PHP und PEAR installiert sein. PHP sollte auf dem Mac bereits installiert sein, PEAR allerdings nicht, falls man es nicht bereits manuell installiert hat.
Als erstes installieren wir also PEAR über das Terminal:

# curl http://pear.php.net/go-pear | sudo php

Die Installation kann weitgehend mit <ENTER> und 'Y' fortgesetzt werden, wir gehen hier allerdings von folgendem "file Layout" aus:

  1. Installation prefix ($prefix) : /usr
 2. Temporary files directory         : /tmp
 3. Binaries directory                       : $prefix/bin
 4. PHP code directory ($php_dir) : $prefix/lib/php
 5. Documentation base directory   : $php_dir/docs
 6. Data base directory                     : $php_dir/data
 7. Tests base directory                   : $php_dir/tests

Nach der Installation von PEAR muss die php.ini angepasst oder sogar neu erstellt werden, falls /usr/lib/php noch nicht zu 'include_path' ergänzt wurde.

Der Aufruf

# php --info | grep include_path

sollte in etwa

include_path => .:/usr/lib/php => .:/usr/lib/php

oder ähnliches ausgeben. Der Pfad /usr/lib/php muss enthalten sein.
Ist das nicht der Fall, sollte mit dem Aufruf

# php --info | grep "Loaded Configuration File"

geprüft werden, welche php.ini verwendet wird. Lautet die Ausgabe

Loaded Configuration File => (none)

wird keine verwendet. In diesem Fall erstellen wir eine neue.

# sudo cp /etc/php.ini.default /etc/php.ini

Nun passen wir bei der neu angelegten oder bestehenden php.ini den Parameter 'include_path' an, indem wir die Datei mit einem Editor öffnen

# nano /etc/php.ini

und die Zeile (oder ähnlich)

;include_path = ".:/php/includes"

in

include_path = ".:/usr/lib/php"

ändern.

Jetzt können wir den PHP CodeSniffer installieren.

# sudo pear install PHP_CodeSniffer

Nach erfolgreicher Installation testen wir, ob es auch funktioniert:

# cd /tmp
# echo "<?php" >> test.php
# echo "?>" >> test.php
# phpcs --standard=zend test.php

sollte nun folgende Ausgabe liefern:


FILE: /tmp/test.php
--------------------------------------------------------------------------------
FOUND 1 ERROR(S) AND 0 WARNING(S) AFFECTING 1 LINE(S)
--------------------------------------------------------------------------------
 2 | ERROR | A closing tag is not permitted at the end of a PHP file
--------------------------------------------------------------------------------

Dies bedeutet, dass in Zeile 2 der Datei /tmp/test.php ein Fehler gefunden wurde. Schließende PHP-Tags sind laut Zend Coding Standard nicht erlaubt.

Nun können wir den Hook einrichten.
Hooks können auf Client Seite entweder global, oder Repository spezifisch eingebaut werden.
Um Hooks für ein bestimmtes Repository zu verwenden, müssen diese unter /PATH_TO_REPOSITORY/.hg/hgrc eingefügt werden. Wir erstellen jedoch ein globales Hook, welches generell für alle Repositories gültig ist.
Dafür passen wir die Datei ~/.hgrc an:

# echo "[hooks]" >> ~/.hgrc
# echo "pretxncommit.codeSniffer = hg status -n -0 | xargs phpcs --standard=zend" >> ~/.hgrc

Für diese Variante würde der Event 'precommit' ausreichen, da wir hier keine bestimmten Informationen verarbeiten. Für einen etwas ausgefeilteren Mechanismus sind weitere Informationen jedoch sehr hilfreich. Hier werden wir aber vorerst nicht weiter darauf eingehen.

Zum Abschluss testen wir den soeben erstellten Hook:

# cd /tmp
# hg init test_repo
# cd test_repo
# echo "<?php" >> test.php
# echo "?>" >> test.php
# hg add
# hg -v commit -m "test"

folgende Ausgabe sollte nun zu sehen sein:

running hook precommit.codeSniffer: hg status -n -0 | xargs phpcs --standard=zend

FILE: /tmp/test_repo/test.php
--------------------------------------------------------------------------------
FOUND 1 ERROR(S) AND 0 WARNING(S) AFFECTING 1 LINE(S)
--------------------------------------------------------------------------------
 2 | ERROR | A closing tag is not permitted at the end of a PHP file
--------------------------------------------------------------------------------

Abbruch: precommit.codeSniffer hook Beendet mit Status 1

Nach Korrektur der angekreideten Mängel sollte der commit dann ohne Probleme durchgehen.

Tags: