Assertions ist ein nicht so häufig beachtetes Feature von PHP, dass allerdings -- richtig eingesetzt -- sehr hilfreich sein kann. Dabei geht es um Behauptungen über den logischen Zustand zum entsprechenden Zeitpunkt (der Wikipedia-Artikel liefert eine nette Beschreibung). Anders als bei der Prüfung von Benutzereingaben, bei dem man lieber nicht mit "Logik" rechnen sollte, geht man also davon aus, dass die Assertion nie fehlschlägt, zumindest solange man richtig mit diesem Code umgeht. Das es letzten Endes genau darum geht, zeige ich noch anhand eines kurzen Beispiels.

Assertions in PHP bestehen einzig aus der Funktion assert() und einer handvoll Optionen. assert() macht nun nichts anderes, als den übergebenen Parameter auszuwerten und je nachdem eine Warnung zu liefern, das Skript abzubrechen, oder eine definierte callback aufzurufen. Auf die technischen Details verzichte ich hier, denn diese lassen sich im Manual nachlesen. Was kann man nun mit Assertions anfangen?

Das Beispiel: Ich baue eine arithmetische Bibliothek und unter Anderem gibt es eine Funktion
[php]/**
* Divide $left / $right
*
* @param int $left
* @param int $right Cannot be 0
* @return float
*/
function div ($left, $right) {
return $left / $right;
}[/php]
Immer daran denken: Es ist nur ein Beispiel ;) Es fällt auf, dass keinerlei Prüfung der Parameter statt findet, aber ist das denn hier überhaupt notwendig? Gemäß dem Leitsatz "Alle Daten, die von Aussen kommen, müssen validiert werden!" könnte man hier sicherlich schon validieren, aber da die Funktion selbst auf keine Daten von Aussen zugreift und jemand, der die Funktion derart aufruft
[php]div($_GET['left'], $_GET['right']);[/php]
sowieso ein Kasten Bier ausgeben müsste, behaupte ich, dass keine Parameterprüfung statt finden sollte. Jedes mal, wenn div() aufgerufen wird, geschieht dies aus anderem Programmcode heraus, der für sich genommen getestet werden sollte. Kommen die Daten dabei von Aussen, dann liegt es eben auch in dessen Verantwortung sie zu prüfen. Ergo: Der mitdenkende Programmierer (und derjenige, der DocComments lesen kann; IDEs helfen ;)) wird sowieso dafür sorgen, dass keine fehlerhaften Werte übergeben werden. Würde div() nun noch selbst prüfen, käme es zu "Double Validation", also unnötiger, mehrfacher Prüfung der selben Werte auf den selben Sachverhalt. Das klingt zunächst unkritisch, kann aber -- geht man damit freizügig um -- bei vielen Funktions-/Methodenaufrufen zu einem Overhead führen. Und sowieso: Es ist unnötig!

Nun sind Entwickler auch nicht unfehlbar. Da kann es dann schon vorkommen, dass sich doch ein mal falsche Werte einschleichen. Schlecht ist es, wenn es nicht auffällt, denn manche Fehler können sich eine ganze Weile weiter tragen und dann zu einem Verhalten führen, der nur schwer zurück verfolgbar ist. Also muss ich doch noch sicher stellen, dass alles klar läuft, zumindest während andere Leute anhand meiner Bibliothek etwas entwickeln, aber ich möchte weiterhin auf explizite Parameterprüfung verzichten. Ich erweiter nun einfach die Funktion um Assertions und schaue mir an, was passiert:
[php]/**
* Divide $left / $right
*
* @param int $left
* @param int $right !=0
* @return float
*/
function div ($left, $right) {
\assert('\is_int($left);');
\assert('\is_int($right) && ($right != 0);');

return $left / $right;
}[/php]
Jetzt gibt es zwei Szenarien: Im Ersten ist die Auswertung der Assertions abgeschaltet. assert() wird mit einem String aufgerufen und kehrt sofort wieder zurück. Bei geringem Overhead, speziell im Vergleich zu expliziter Prüfung, die immer ausgewertet werden muss, passiert nichts weiter. Wenn ich (in diesem Fall als Verwender der Bibliothek) genau weiß, dass ich die Funktion korrekt verwende -- nach umfangreichen Tests direkt zum Release -- kann ich somit die unnötigen Prüfungen unterbinden. Feststellung daraus: Assertions gehören auf Produktivsystemen defintiv abgeschaltet!

Im zweiten Szenario sind Assertions aktiviert, der String wird ausgewertet und es wird eine (je nach Einstellung mehr oder weniger auffällige) Reaktion zurück geliefert. Während der Entwicklung sagt er mir also "Hey, da hast du scheinbar nicht richtig aufgepasst und mir Mist übergeben.". Anstatt undefinierbarem Verhalten (das Beispiel "devide by zero" ist insofern nur bedingt ein gutes Beispiel) bekomme ich direkt eine Rückmeldung samt der Stelle des Auftretens der ersten Ungereimtheit. So spart man sich langes Suchen, beziehungsweise vermindert das Risiko, dass dadurch Seiteneffekte unentdeckt bleiben. Das ich im Beispiel den DocComment angegeben habe, ist Absicht und nicht ganz unwichtig: Nun kann niemand (zumindest niemand, der eine anständige IDE verwendet, oder in der Lage ist eine API-Doc zu lesen) behaupten, dass die fehlgeschlagene Assertion besonders überraschend sei.

Das Beispiel behandelt nur die Parameter der Funktion und dies auch nur direkt nach Aufruf. Sind Objekte mit im Spiel, ist es häufig sinnvoll auch die verwendeten Objekt-Eingenschaften (speziell die des Objektes der aufgerufenen Methode) sicher zu stellen. Zusätzlich können auch Behauptungen dieser Art auf den Rückgabewert (vor der Rückgabe), oder auf Rückgabewerte aufgerufener Methoden (wobei die eigentlich selbst zugesichert sein müssten ;)) sinnvoll sein. Wie komplex die einzelnen Assertions dabei aussehen, ist fast egal, denn wenn Assertions nicht aktiv sind, ist es bloß ein "leerer" Funktionsaufruf mit einem String, der (copy-on-write sei dank) nicht mal zusätzlichen Speicher beansprucht.

[php]function assert ($assertion) {
if (assert_options(ASSERT_ACTIVE)) { // 0 == false
// Evaluate
}
}[/php]

So 100%ig folgenlos bleibt das Vorgehen nicht, denn es basiert auf einem Programmierstil, der vermutlich speziell der "get()/set()"-Fraktion etwas zuwider läuft: Kontrollverlust (und ein klein wenig Kaltschnäuzigkeit ;)). Mit diesem Vorgehen gebe ich ein stückweit die Kontrolle über die Parameter ab und vertraue darauf, dass der Entwickler, der auf meine Bibliothek aufbaut, selbst in der Lage ist damit umzugehen (sprich: Die Hinweise der IDE, die API-Docs, oder letzten Endes die DocComments selbst zu lesen). Und deswegen -- auch wenn das etwas leichtsinnig klingen mag-- kann es mir auch egal sein, denn schließlich gebe ich explizit vor, wie die Funktionen/Methoden zu verwenden sind... nur eben als Text, statt als Exception.

Zum Abschluss noch ein Hinweis: Assertions ersetzen in keinster Weise

  • Validierung von Benutzereingaben: Im besten Fall helfen sie ungenügende Validierung frühzeitig zu erkennen, letzten Endes gehört es aber explizit zu jeder Anwendung selbst ihre Eingaben zu prüfen. Spätestens wenn man auf einem Produktivsystem Assertions abschaltet, landet man sonst auf der Nase.
  • (Unit-)Tests: Assertions können diese allerdings unterstützen, denn schließlich handelt es sich dabei um eine Rückmeldung direkt aus dem Code heraus. Einen so tiefen Blick in den Code hinein können Testcases nie haben.
  • Type-Hints: Sie decken einen Teil des Aufgabengebietes von Assertions ab, aber nicht alles. Speziell wenn man für einzelne Parameter verschiedene Typen erlauben möchte, kommt man mit Type-Hints nicht weit. Hinzu kommt, dass null immer erlaubt ist und möglicherweise muss das übergebene Objekt selbst in einem bestimmten Zustand sein müssen (was allerdings auch das Objekt selbst sicher stellen könnte/sollte).