German (Deutsch) translation by Alex Grigorovich (you can also view the original English article)
In diesem Artikel lernen wir PHP-Ausnahmen von Grund auf kennen. Diese Konzepte werden in vielen großen, skalierbaren und objektorientierten Anwendungen und Frameworks verwendet. Nutzen Sie diese Sprachfunktion, um Ihre Fähigkeiten als Webanwendungsentwickler zu verbessern.
1 Zuerst ein Beispiel
Bevor wir mit allen Erklärungen beginnen, möchte ich zunächst ein Beispiel zeigen.
Sie möchten die Fläche eines Kreises anhand des angegebenen Radius berechnen. Diese Funktion erledigt das:
1 |
|
2 |
function circle_area($radius) { |
3 |
|
4 |
return pi() * $radius * $radius; |
5 |
|
6 |
}
|
Es ist sehr einfach, prüft jedoch nicht, ob der Radius eine gültige Zahl ist. Jetzt werden wir das tun und eine Ausnahme auslösen, wenn der Radius eine negative Zahl ist:
1 |
|
2 |
function circle_area($radius) { |
3 |
|
4 |
// radius can't be negative
|
5 |
if ($radius < 0) { |
6 |
throw new Exception('Invalid Radius: ' . $radius); |
7 |
} else { |
8 |
return pi() * $radius * $radius; |
9 |
}
|
10 |
|
11 |
}
|
Mal sehen, was passiert, wenn wir es mit einer negativen Nummer anrufen:
1 |
|
2 |
$radius = -2; |
3 |
|
4 |
echo "Circle Radius: $radius => Circle Area: ". |
5 |
circle_area($radius) . "\n"; |
6 |
|
7 |
|
8 |
echo "Another line"; |
Das Skript stürzt mit der folgenden Meldung ab:
1 |
|
2 |
<br /> |
3 |
<b>Fatal error</b>: Uncaught exception 'Exception' with message 'Invalid Radius: -2' in C:\wamp\www\test\test.php:19 |
4 |
Stack trace: |
5 |
#0 C:\wamp\www\test\test.php(7): circle_area(-2) |
6 |
#1 {main}
|
7 |
thrown in <b>C:\wamp\www\test\test.php</b> on line <b>19</b><br /> |
Da es sich um einen schwerwiegenden Fehler handelte, wurde danach keine Codeausführung mehr durchgeführt. Möglicherweise möchten Sie jedoch nicht immer, dass Ihre Skripte angehalten werden, wenn eine Ausnahme auftritt. Zum Glück können Sie sie "catch und damit umgehen.
Lassen Sie uns diesmal ein Array mit Radiuswerten erstellen:
1 |
|
2 |
$radius_array = array(2,-2,5,-3); |
3 |
|
4 |
foreach ($radius_array as $radius) { |
5 |
|
6 |
try { |
7 |
echo "Circle Radius: $radius => Circle Area: ". |
8 |
circle_area($radius) . "\n"; |
9 |
} catch (Exception $e) { |
10 |
echo 'Caught Exception: ', $e->getMessage(), "\n"; |
11 |
}
|
12 |
|
13 |
}
|
Jetzt erhalten wir diese Ausgabe:
1 |
|
2 |
Circle Radius: 2 => Circle Area: 12.566370614359 |
3 |
Caught Exception: Invalid Radius: -2 |
4 |
Circle Radius: 5 => Circle Area: 78.539816339745 |
5 |
Caught Exception: Invalid Radius: -3 |
Es gibt keine Fehler mehr und das Skript wird weiterhin ausgeführt. So fangen Sie Ausnahmen.
Vollbild
2 Was ist eine Ausnahme?
Ausnahmen gibt es schon seit geraumer Zeit in anderen objektorientierten Programmiersprachen. Es wurde erstmals in PHP mit Version 5 übernommen.
Per Definition wird eine Ausnahme "thrown", wenn ein außergewöhnliches Ereignis eintritt. Dies kann so einfach sein wie eine "division by zero" oder eine andere Art von ungültiger Situation.
1 |
|
2 |
throw new Exception('Some error message.'); |
Dies klingt möglicherweise ähnlich wie andere grundlegende Fehler, die Sie bereits oft gesehen haben. Ausnahmen haben jedoch einen anderen Mechanismus.
Ausnahmen sind tatsächlich Objekte, und Sie haben die Möglichkeit, sie zu "catch" und bestimmten Code auszuführen. Dies geschieht mithilfe von 'try-catch'-Blöcken:
1 |
|
2 |
try { |
3 |
|
4 |
// some code goes here
|
5 |
// which might throw an exception
|
6 |
|
7 |
} catch (Exception $e) { |
8 |
|
9 |
// the code here only gets executed
|
10 |
// if an exception happened in the try block above
|
11 |
|
12 |
}
|
Wir können jeden Code in einen 'try'-Block einschließen. Der folgende 'catch'-Block wird zum Abfangen von Ausnahmen verwendet, die möglicherweise aus dem try-Block ausgelöst wurden. Der catch-Block wird niemals ausgeführt, wenn keine Ausnahmen vorhanden waren. Sobald eine Ausnahme auftritt, springt das Skript sofort zum catch-Block, ohne weiteren Code auszuführen.
Weiter unten im Artikel finden Sie weitere Beispiele, die die Leistungsfähigkeit und Flexibilität der Verwendung von Ausnahmen anstelle einfacher Fehlermeldungen demonstrieren sollen.
3 Ausnahmen sprudeln
Wenn eine Ausnahme von einer Funktion oder einer Klassenmethode ausgelöst wird, geht sie an denjenigen, der diese Funktion oder Methode aufgerufen hat. Und das so lange, bis es die Spitze des Stapels erreicht ODER gefangen wird. Wenn es die Spitze des Stapels erreicht und nie aufgerufen wird, wird ein schwerwiegender Fehler angezeigt.
Hier haben wir zum Beispiel eine Funktion, die eine Ausnahme auslöst. Wir nennen diese Funktion eine zweite Funktion. Und schließlich rufen wir die zweite Funktion aus dem Hauptcode auf, um diesen Blaseneffekt zu demonstrieren:
1 |
|
2 |
function bar() { |
3 |
|
4 |
throw new Exception('Message from bar().'); |
5 |
|
6 |
}
|
7 |
|
8 |
function foo() { |
9 |
|
10 |
bar(); |
11 |
|
12 |
}
|
13 |
|
14 |
|
15 |
try { |
16 |
|
17 |
foo(); |
18 |
|
19 |
} catch (Exception $e) { |
20 |
echo 'Caught exception: ', $e->getMessage(), "\n"; |
21 |
}
|
Wenn wir also foo() aufrufen, versuchen wir, mögliche Ausnahmen abzufangen. Obwohl foo() keinen wirft, aber bar(), sprudelt es immer noch und wird oben abgefangen, sodass wir eine Ausgabe erhalten, die besagt: "Caught exception: Message from bar()."
4 Ausnahmen verfolgen
Da Ausnahmen auftauchen, können sie von überall her kommen. Um unsere Arbeit zu vereinfachen, verfügt die Exception-Klasse über Methoden, mit denen wir die Quelle jeder Ausnahme ermitteln können.
Sehen wir uns ein Beispiel an, das mehrere Dateien und mehrere Klassen umfasst.
Erstens haben wir eine Benutzerklasse und speichern sie als user.php:
1 |
|
2 |
class User { |
3 |
|
4 |
public $name; |
5 |
public $email; |
6 |
|
7 |
public function save() { |
8 |
|
9 |
$v = new Validator(); |
10 |
|
11 |
$v->validate_email($this->email); |
12 |
|
13 |
// ... save
|
14 |
echo "User saved."; |
15 |
|
16 |
return true; |
17 |
}
|
18 |
|
19 |
}
|
Es verwendet eine andere Klasse namens Validator, die wir in validator.php einfügen:
1 |
|
2 |
class Validator { |
3 |
|
4 |
public function validate_email($email) { |
5 |
|
6 |
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { |
7 |
throw new Exception('Email is invalid'); |
8 |
}
|
9 |
|
10 |
}
|
11 |
|
12 |
}
|
Aus unserem Hauptcode erstellen wir ein neues Benutzerobjekt, legen den Namen und die E-Mail-Werte fest. Sobald wir die save() -Methode aufrufen, wird die Validator-Klasse zum Überprüfen des E-Mail-Formats verwendet, wodurch möglicherweise eine Ausnahme zurückgegeben wird:
1 |
|
2 |
include('user.php'); |
3 |
include('validator.php'); |
4 |
|
5 |
|
6 |
$u = new User(); |
7 |
$u->name = 'foo'; |
8 |
$u->email = '$!%#$%#*'; |
9 |
|
10 |
$u->save(); |
Wir möchten jedoch die Ausnahme abfangen, sodass keine schwerwiegende Fehlermeldung angezeigt wird. Und dieses Mal werden wir uns die detaillierten Informationen zu dieser Ausnahme ansehen:
1 |
|
2 |
include('user.php'); |
3 |
include('validator.php'); |
4 |
|
5 |
try { |
6 |
|
7 |
$u = new User(); |
8 |
$u->name = 'foo'; |
9 |
$u->email = '$!%#$%#*'; |
10 |
|
11 |
$u->save(); |
12 |
|
13 |
} catch (Exception $e) { |
14 |
|
15 |
echo "Message: " . $e->getMessage(). "\n\n"; |
16 |
echo "File: " . $e->getFile(). "\n\n"; |
17 |
echo "Line: " . $e->getLine(). "\n\n"; |
18 |
echo "Trace: \n" . $e->getTraceAsString(). "\n\n"; |
19 |
|
20 |
}
|
Der obige Code erzeugt diese Ausgabe:
1 |
|
2 |
Message: Email is invalid |
3 |
|
4 |
File: C:\wamp\www\test\validator.php |
5 |
|
6 |
Line: 7 |
7 |
|
8 |
Trace: |
9 |
#0 C:\wamp\www\test\user.php(11): Validator->validate_email('$!%#$%#*')
|
10 |
#1 C:\wamp\www\test\test.php(12): User->save() |
11 |
#2 {main}
|
Ohne eine einzige Codezeile zu betrachten, können wir also feststellen, woher die Ausnahme stammt. Wir können den Dateinamen, die Zeilennummer, die Ausnahmemeldung und mehr sehen. Die Trace-Daten zeigen sogar die genauen Codezeilen, die ausgeführt wurden.
Die Struktur der Standard-Ausnahmeklasse wird im PHP-Handbuch gezeigt, in dem Sie alle Methoden und Daten sehen können, mit denen sie geliefert wird:



5 Ausnahmen erweitern
Da dies ein objektorientiertes Konzept ist und Exception eine Klasse ist, können wir es tatsächlich erweitern, um unsere eigenen benutzerdefinierten Ausnahmen zu erstellen.
Beispielsweise möchten Sie dem Benutzer möglicherweise nicht alle Details einer Ausnahme anzeigen. Stattdessen können Sie eine benutzerfreundliche Nachricht anzeigen und die Fehlermeldung intern protokollieren:
1 |
|
2 |
// to be used for database issues
|
3 |
class DatabaseException extends Exception { |
4 |
|
5 |
// you may add any custom methods
|
6 |
public function log() { |
7 |
|
8 |
// log this error somewhere
|
9 |
// ...
|
10 |
}
|
11 |
}
|
12 |
|
13 |
// to be used for file system issues
|
14 |
class FileException extends Exception { |
15 |
|
16 |
// ...
|
17 |
|
18 |
}
|
Wir haben gerade zwei neue Arten von Ausnahmen erstellt. Und sie können benutzerdefinierte Methoden haben.
Wenn wir die Ausnahme abfangen, können wir eine feste Nachricht anzeigen und die benutzerdefinierten Methoden intern aufrufen:
1 |
|
2 |
function foo() { |
3 |
|
4 |
// ...
|
5 |
// something wrong happened with the database
|
6 |
throw new DatabaseException(); |
7 |
|
8 |
}
|
9 |
|
10 |
|
11 |
try { |
12 |
|
13 |
// put all your code here
|
14 |
// ...
|
15 |
|
16 |
foo(); |
17 |
|
18 |
} catch (FileException $e) { |
19 |
|
20 |
die ("We seem to be having file system issues. |
21 |
We are sorry for the inconvenience."); |
22 |
|
23 |
} catch (DatabaseException $e) { |
24 |
|
25 |
// calling our new method
|
26 |
$e->log(); |
27 |
|
28 |
// exit with a message
|
29 |
die ("We seem to be having database issues. |
30 |
We are sorry for the inconvenience."); |
31 |
|
32 |
} catch (Exception $e) { |
33 |
|
34 |
echo 'Caught exception: '. $e->getMessage(). "\n"; |
35 |
|
36 |
}
|
Dies ist das erste Mal, dass wir uns ein Beispiel mit mehreren Catch-Blöcken für einen einzelnen Try-Block ansehen. Auf diese Weise können Sie verschiedene Arten von Ausnahmen abfangen, sodass Sie sie unterschiedlich behandeln können.
In diesem Fall wird eine DatabaseException abgefangen und nur dieser catch-Block wird ausgeführt. In diesem Blog können wir unsere neuen benutzerdefinierten Methoden aufrufen und dem Benutzer eine einfache Nachricht anzeigen.
Bitte beachten Sie, dass der catch-Block mit der Standard-Ausnahmeklasse der letzte sein muss, da unsere neuen untergeordneten Klassen auch weiterhin als diese Klasse betrachtet werden. Zum Beispiel wird 'DatabaseException' auch als 'Exception' betrachtet, sodass es dort abgefangen werden kann, wenn die Reihenfolge umgekehrt ist.
6 Umgang mit nicht erfassten Ausnahmen
Möglicherweise möchten Sie nicht immer in Ihrem gesamten Code nach Ausnahmen suchen, indem Sie alles in try-catch-Blöcke einschließen. Nicht erfasste Ausnahmen zeigen dem Benutzer jedoch eine detaillierte Fehlermeldung an, was auch in einer Produktionsumgebung nicht ideal ist.
Es gibt tatsächlich eine Möglichkeit, die Behandlung aller nicht erfassten Ausnahmen zu zentralisieren, sodass Sie die Ausgabe von einem einzigen Speicherort aus steuern können.
Dafür verwenden wir die Funktion set_exception_handler():
1 |
|
2 |
set_exception_handler('exception_handler'); |
3 |
|
4 |
function exception_handler($e) { |
5 |
|
6 |
// public message
|
7 |
echo "Something went wrong.\n"; |
8 |
|
9 |
// semi-hidden message
|
10 |
echo "<!-- Uncaught exception: " . $e->getMessage(). " -->\n"; |
11 |
|
12 |
}
|
13 |
|
14 |
|
15 |
throw new Exception('Hello.'); |
16 |
throw new Exception('World.'); |
Die erste Zeile weist PHP an, eine bestimmte Funktion aufzurufen, wenn eine Ausnahme auftritt und nicht abgefangen wird. Dies ist die Ausgabe:
1 |
|
2 |
Something went wrong. |
3 |
<!-- Uncaught exception: Hello. -->
|
Wie Sie sehen, wurde das Skript nach der ersten Ausnahme abgebrochen und die zweite nicht ausgeführt. Dies ist das erwartete Verhalten von nicht erfassten Ausnahmen.
Wenn Sie möchten, dass Ihr Skript nach einer Ausnahme weiter ausgeführt wird, müssen Sie stattdessen einen try-catch-Block verwenden.
7 Erstellen einer MySQL-Ausnahmeklasse
Zum Abschluss dieses Tutorials erstellen wir eine benutzerdefinierte MySQL-Ausnahmeklasse mit einigen nützlichen Funktionen und sehen, wie wir sie verwenden können.
1 |
|
2 |
class MysqlException extends Exception { |
3 |
|
4 |
// path to the log file
|
5 |
private $log_file = 'mysql_errors.txt'; |
6 |
|
7 |
|
8 |
public function __construct() { |
9 |
|
10 |
$code = mysql_errno(); |
11 |
$message = mysql_error(); |
12 |
|
13 |
// open the log file for appending
|
14 |
if ($fp = fopen($this->log_file,'a')) { |
15 |
|
16 |
// construct the log message
|
17 |
$log_msg = date("[Y-m-d H:i:s]") . |
18 |
" Code: $code - " . |
19 |
" Message: $message\n"; |
20 |
|
21 |
fwrite($fp, $log_msg); |
22 |
|
23 |
fclose($fp); |
24 |
}
|
25 |
|
26 |
// call parent constructor
|
27 |
parent::__construct($message, $code); |
28 |
}
|
29 |
|
30 |
}
|
Sie werden vielleicht bemerken, dass wir so ziemlich den gesamten Code in den Konstruktor eingefügt haben. Immer wenn eine Ausnahme ausgelöst wird, ist dies wie das Erstellen eines neuen Objekts, weshalb der Konstruktor immer zuerst aufgerufen wird. Am Ende des Konstruktors müssen wir auch den übergeordneten Konstruktor aufrufen.
Diese Ausnahme wird immer dann ausgelöst, wenn ein MySQL-Fehler auftritt. Anschließend werden die Fehlernummer und die Nachricht direkt von MySQL abgerufen und diese Informationen zusammen mit dem Zeitstempel in einer Protokolldatei gespeichert. In unserem Code können wir diese Ausnahme abfangen, dem Benutzer eine einfache Nachricht anzeigen und die Ausnahmeklasse die Protokollierung für uns übernehmen lassen.
Versuchen wir beispielsweise, eine Verbindung zu MySQL herzustellen, ohne Benutzer-/Kennwortinformationen anzugeben:
1 |
|
2 |
try { |
3 |
|
4 |
// attempt to connect
|
5 |
if (!@mysql_connect()) { |
6 |
throw new MysqlException; |
7 |
}
|
8 |
|
9 |
} catch (MysqlException $e) { |
10 |
|
11 |
die ("We seem to be having database issues. |
12 |
We are sorry for the inconvenience."); |
13 |
|
14 |
}
|
Wir müssen den Fehlerunterdrückungsoperator (@) vor dem Aufruf von mysql_connect() voranstellen, damit der Fehler dem Benutzer nicht angezeigt wird. Wenn die Funktion fehlschlägt, lösen wir eine Ausnahme aus und fangen sie dann ab. Nur unsere benutzerfreundliche Nachricht wird dem Browser angezeigt.
Die MysqlException-Klasse kümmert sich automatisch um die Fehlerprotokollierung. Wenn Sie die Protokolldatei öffnen, finden Sie folgende Zeile:
1 |
|
2 |
[2010-05-05 21:41:23] Code: 1045 - Message: Access denied for user 'SYSTEM'@'localhost' (using password: NO) |
Fügen wir unserem Beispiel mehr Code hinzu und geben Sie außerdem die richtigen Anmeldeinformationen an:
1 |
|
2 |
try { |
3 |
|
4 |
// connection should work fine
|
5 |
if (!@mysql_connect('localhost','root','')) { |
6 |
throw new MysqlException; |
7 |
}
|
8 |
|
9 |
// select a database (which may not exist)
|
10 |
if (!mysql_select_db('my_db')) { |
11 |
throw new MysqlException; |
12 |
}
|
13 |
|
14 |
// attempt a query (which may have a syntax error)
|
15 |
if (!$result = mysql_query("INSERT INTO foo SET bar = '42 ")) { |
16 |
throw new MysqlException; |
17 |
}
|
18 |
|
19 |
|
20 |
} catch (MysqlException $e) { |
21 |
|
22 |
die ("We seem to be having database issues. |
23 |
We are sorry for the inconvenience."); |
24 |
|
25 |
}
|
Wenn die Datenbankverbindung erfolgreich ist, aber die Datenbank 'my_db' fehlt, finden Sie dies in den Protokollen:
1 |
|
2 |
[2010-05-05 21:55:44] Code: 1049 - Message: Unknown database 'my_db' |
Wenn die Datenbank vorhanden ist, die Abfrage jedoch aufgrund eines Syntaxfehlers fehlschlägt, wird das im Protokoll angezeigt:
1 |
|
2 |
[2010-05-05 21:58:26] Code: 1064 - Message: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''42' at line 1 |
Bonus Aufbau einer DB-Klasse mit PHP Magic
Wir können das obige Codebeispiel noch sauberer machen, indem wir unsere eigene Datenbankklasse schreiben, die das Auslösen der Ausnahmen behandelt. Dieses Mal werde ich einige 'magische' Funktionen von PHP verwenden, um diese Klasse zu erstellen.
Am Ende soll unser Hauptcode folgendermaßen aussehen:
1 |
|
2 |
try { |
3 |
|
4 |
Database::connect('localhost','root',''); |
5 |
|
6 |
Database::select_db('test'); |
7 |
|
8 |
$result = Database::query("INSERT INTO foo SET bar = '42 "); |
9 |
|
10 |
} catch (MysqlException $e) { |
11 |
|
12 |
die ("We seem to be having database issues. |
13 |
We are sorry for the inconvenience."); |
14 |
|
15 |
}
|
Es ist schön und clean. Wir prüfen nicht bei jedem einzelnen Datenbankaufruf, ob Fehler vorliegen. Diese neue Datenbankklasse hat die Verantwortung, Ausnahmen auszulösen, wenn Fehler auftreten. Und da diese Ausnahmen in die Luft sprudeln, werden sie am Ende von unserem Fangblock erfasst.
Und hier ist die magische Datenbankklasse:
1 |
|
2 |
class Database { |
3 |
|
4 |
// any static function call invokes this
|
5 |
public static function __callStatic($name, $args) { |
6 |
|
7 |
// function to be called
|
8 |
$function = 'mysql_' . $name; |
9 |
|
10 |
// does the function exist?
|
11 |
if (!function_exists($function)) { |
12 |
// throw a regular exception
|
13 |
throw new Exception("Invalid mysql function: $function."); |
14 |
}
|
15 |
|
16 |
// call the mysql function
|
17 |
$ret = @call_user_func_array($function , $args); |
18 |
|
19 |
// they return FALSE on errors
|
20 |
if ($ret === FALSE) { |
21 |
// throw db exception
|
22 |
throw new MysqlException; |
23 |
}
|
24 |
|
25 |
// return the returned value
|
26 |
return $ret; |
27 |
}
|
28 |
|
29 |
}
|
Es hat nur eine Methode und wird immer dann aufgerufen, wenn wir eine statische Methode für diese Klasse aufrufen. Weitere Informationen zu diesem Konzept finden Sie hier: PHP-Überladung.
Wenn wir also Database::connect() aufrufen, ruft dieser Code automatisch mysql_connect() auf, übergibt die Argumente, prüft auf Fehler, löst bei Bedarf Ausnahmen aus und gibt den vom Funktionsaufruf zurückgegebenen Wert zurück. Es verhält sich im Grunde wie ein Mittelsmann und erledigt die Drecksarbeit.
Abschluss
Ich hoffe, Ihnen hat dieses Tutorial gefallen und Sie haben daraus gelernt. Jetzt sollten Sie dieses Thema besser verstehen. Versuchen Sie herauszufinden, ob Sie PHP-Ausnahmen in Ihrem nächsten Projekt verwenden können. Bis zum nächsten Mal!



