Gimme-Gimme-Gimme

ABBA suchte dereinst nach einen Mann für die Nachtschicht. PHP kann damit wenig anfangen, genauso wenig wie mit den falschen Typen, wobei mit “Typen” jetzt nicht der Mann gemeint ist.

Oft schreibt man Funktionen oder Methoden und macht sich nur wenig Gedanken über den Rückgabewert im Fehlerfall, also dann, wenn die Funktion nicht die Aktion durchführen konnte wofür sie geschrieben wurde. Ich selber habe es bisher auch so gehandhabt das ich im Fehlerfall einen boolschen Wert oder vergleichbares zurück gegeben habe. Das Ergebnis ist, ich muss bei jeden Funktionsaufruf prüfen ob auch der Variablen-Typ zurück kommt den ich erwarte. Erwarte ich z.B. ein Array, kommt im Fehlkerfall aber ein boolsches False zurück, muss ich das abfangen.

Warum? Weil es ansonsten unter Umständen Fehlermeldungen hagelt und das Script abbricht. Im Backend kann man es noch verschmerzen, ich nenne das immer den “Weisste bescheid” Zustand. Im Frontend ist es aber mehr als peinlich und sogar kontraproduktiv wenn der Seitenaufbau nach dem Blogheader mit einer Fehlermeldung abbricht.

Stellen wir uns mal vor wir schreiben ein Widget für die Sidebar. Im Code für das Widget sind folgende zwei Funktionen:

/**
 * Create a ordered- or unordered list from values
 *
 * @param	string	$type	ol for ordered lists or ul for unordered list
 * @param	array	$values	Array with list items
 * @return	string			HTML list
 */
function get_list( $type = 'ol', $values = array() ) {

	$type = ( in_array( $type, array( 'ol', 'ul' ) ) ) ?
		$type : 'ol';

	foreach ( $values as &$val )
		$val .= ' item';

	$format = "<{$type}>" . str_repeat( '<li>%s</li>', sizeof( $values ) ) . "</{$type}>";

	return vsprintf( $format, $values );

}

/**
 * Returns an array or boolean depending on parameter
 *
 * @param	boolean	$fail	Returns an array if set to true, else returns boolean false.
 * @return Ambigous <boolean, multitype:string >
 */
function get_list_values( $fail = false ) {

	return ( true == $fail ) ?
		false : array( 'A', 'B', 'C' );

}

Die eine Funktion (get_list_values()) erzeugt ein Array mit Elementen, die andere Funktion (get_list()) erzeugt aus einem Array mit Werten eine HTML-Liste. Der Aufruf beider Funktionen könnte exemplarisch so aussehen:

$vals = get_list_values();
$list = get_list( 'ol', $vals );

echo $list;

Soweit funktioniert das ganze recht ordentlich. Nun provozieren wir jedoch mal einen Fehler und rufen die Funktion get_list_values() mit den Parameter 1 auf. Jetzt werden wir mit einen Warning: Invalid argument supplied for foreach() in ... begrüßt und das Script bricht an dieser Stelle ab.

Um solche Fehlermeldungen zu vermeiden, hätte ich vorher prüfen müssen ob $vals ein Array ist.

$vals = get_list_values(1);

if ( is_array( $vals ) ) {
  $list = get_list( 'ol', $vals );
  echo $list;
} else {
  // do some error handling
}

Das erscheint bisher alles recht logisch. Die frage ist nur, was soll man an dieser Stelle für ein Error-Handling machen? Einen Log-Eintrag? Eine eigene Fehlermeldung ausgeben? Nichts?
Nichts scheint mir eigentlich die beste Lösung zu sein. Mit einer eigenen Fehlermeldung kann der Besucher nichts anfangen.Und ein Log-Eintrag hilft dem Besucher auch nicht weiter. Ein Log-Eintrag hilft dem Programmierer bzw. den Admin an dieser Stelle ebenso wenig, da es unklar ist warum die Funktion get_list_values() gescheitert ist. Das kann uns nur die Funktion selber sagen, daher wäre ein Log-Eintrag innerhalb der Funktion sinnvoller untergebracht.

function get_list_values( $fail = false ) {

	if ( true == $fail ) {
		write_to_error_log( sprintf( 'Function %s was called with parameter $fail set to %s', __FUNCTION__, $fail ) );
		return false;
	}

	return array( 'A', 'B', 'C' );

}

Dadurch sind wir schon mal einen kleinen Schritt weiter. Der Fehler wird da geloggt, wo er tatsächlich auftritt, nicht da wo er zu einen weiteren Fehler führt. Das eigentliche Problem besteht jedoch weiterhin. Denn die Funktion gibt ja noch einen boolschen Wert zurück. Wir müssen also weiterhin prüfen ob das Ergebnis des Funktionsaufrufes tatsächlich von den erwarteten Typs ist. Das ist hinderlich, denn dadurch ist folgendes Konstrukt nicht möglich:

echo get_list( 'ol', get_list_values( 1 ) );

Würde die Funktion nun einen Wert vom erwarteten Typ zurück geben, könnten wir obiges Konstrukt ohne weiteres verwenden. Im Fehlerfall erhalten wir dann anstatt einer Liste lediglich ein leeres Ergebnis. Also ändern wir die Funktion ein klein wenig:

function get_list_values( $fail = false ) {

	if ( true == $fail ) {
		write_to_error_log( sprintf( 'Function %s was called with parameter $fail set to %s', __FUNCTION__, $fail ) );
		return array();
	}

	return array( 'A', 'B', 'C' );

}

Das Logging des Fehlers bleibt, er geht also nicht verloren. Dafür haben wir im Frontend keine Fehlermeldung mehr und der Seitenaufbau bricht auch nicht mehr ab.

Dies können wir nun auf alle Funktionen übertragen. Wenn die Funktion im Erfolgsfall ein Array zurück gibt, sollte sie dies auch im Fehlerfall machen, dann halt ein leeres Array. Erwarten wir ein Objekt als Rückgabewert, so sollte im Fehlerfall ein leeres Objekt zurück kommen. Allerdings sollte man speziell bei Objekten die Eigenschaften vorbelegen und nicht einfach nur ein leeres Objekt zurück geben da oft direkt auf die Objekteigenschaften zugegriffen wird und man, anders als bei Arrays, nicht mit empty() prüfen kann ob das Objekt überhaupt irgendwelche Eigenschaften enthält. Eine Abfrage mit empty() liefert bei Objekten immer false zurück, da ein Objekt niemals “leer” ist.

function get_some_object( $fail = false ) {

	$object = new stdClass();
	$object->foo = false;
	$object->bar = '';
	$object->baz = null;

	if ( true == $fail )
		return $object;

	$object->foo = true;
	$object->bar = 'FooBarBaz';
	$object->baz = $object;

	return $object;

}
$objects = array();
var_dump( $objects );

$objects[] = get_some_object();
$objects[] = get_some_object( true );
$objects[] = new stdClass();

array_walk( $objects, function( $object ) { var_dump( empty( $object ) ); } );

/*
Ausgabe:
array (size=0)
  empty

boolean false
boolean false
boolean false
*/

Der Grund warum man sich damit beschäftigen sollte ist jedoch nicht nur eine saubere Ausgabe im Frontend, sondern auch das Debuggen. Im ersten Beispiel wurde der Fehler nämlich still und heimlich an einen ganz anderen Ort verlagert. Obwohl er in der Funktion get_list_values() aufgetreten ist, wird er erst in get_list() beim Aufruf der foreach-Schleife wirksam. In Beispielen ist es immer einfach den Fehler zu finden. Sie sind von Natur aus kurz und übersichtlich. In größeren und längeren Scripten ist es jedoch oft nicht so einfach heraus zu finden wann und wo eine Variable durch einen Funktionsaufruf einen Wert zugewiesen bekommen hat. Liegen zwischen der Wertzuweisung und der Verarbeitung des Wertes noch andere Zwischenschritte in der der fehlerhafte Wert nicht zu einen Fehler führt, wird die Suche noch umständlicher.
Deshalb sollte man Fehlerfälle immer sofort dort behandeln, wo sie auftreten anstatt sie in einen Pseudozustand umzuwandeln und sie ggf. auch noch als Rückgabewert zurück zu geben.