[PHP] Prepared Statements mit PDO (Teil 2 - Fehlerbehandlung)

[PHP] Prepared Statements mit PDO (Teil 2 - Fehlerbehandlung)

Referenzen:

Prepared Statements mit PDO (Teil 1 - Einführung)
SimpleLogger - Ein einfache Logger-Klasse

Vorwort

Im zweiten Teil der Reihe "Prepared Statements mit PDO" wollen wir uns dem Aspekt der Fehlerbehandlung widmen.

Es kann immer vorkommen, dass etwas nicht so abläuft, wie wir als Entwickler uns das vorgestellt haben. Zumindest sollten wir aber darauf vorbereitet sein, in diesem Fall angemessen zu reagieren. Eine fette Fehlermeldung und keine Möglichkeit für den Benutzer, etwas daran zu ändern, weil er technische Meldungen einfach nicht versteht, ist definitiv keine Lösung.

Natürlich beugt eine qualifizierte untechnische - also fachliche - Meldung dem Ausspähen unserer Applikationsstruktur vor. Was spätestens dann sinnvoll ist, wenn wir es bei dem Benutzer mit einer Person zu tun haben, die nur eins im Sinn hat: unsere Webapplikation für destruktive Zwecke zu verwenden und bspw. Schadcode einzuschleusen.

An allen Stellen, die nicht gerade eine simple Zuweisung von Variable A nach Variable B ist, könnte ein Fehler auftreten. Sei es im drastischen Fall der Verbindungsaufbau zum Datenbankserver oder in einer abgeschwächteren Variante, dass ein Benutzer Daten eingegeben hat, die nicht zum Typ einer Tabellen-Spalte passen.

Werden wir konkrekt

Zu allererst holen wir uns den Code aus dem ersten Teil und werden dann Schritt für Schritt aufzeigen, welche Probleme sich an welchen Stellen verbergen und wie wir angemessen darauf reagieren.

Hier zunächst der Code ohne irgendwelche Behandlungen:

PHP:
<?php
// Fehleranzeige aktivieren
ini_set('display_errors', 1);
error_reporting( E_ALL );

// Verbindungsdaten zur MySQL-Datenbank
$db_user = 'test';
$db_pass = '';

// Verbindung aufbauen
$db = new PDO('mysql:dbname=test;host=127.0.0.1', $db_user, $db_pass);

// Parameterlose Anfrage vorbereiten
$statement = $db->prepare('SELECT id FROM users WHERE username = :u_name AND passwd = :u_pass');

// Parameter-Bindung, die Daten kommen aus einem mit POST übertragenem Formular
$statement->bindParam(':u_name', strval($_POST['benutzername']));
$statement->bindParam(':u_pass', strval($_POST['passwort']));

// Ausführung der Anfrage
$statement->execute();

// Abholen der Daten als Objekt
$result = $statement->fetch(PDO::FETCH_OBJ);

// Anzeige der abgeholten Daten
printf("Ein Benutzer mit der ID %d wurde gefunden", $result->id);

Kleiner Exkurs in das Thema OOP und Exceptions

Wie die eine oder der andere evtl. schon wissen, ist objekt-orientierte Programmierung auch in PHP seit Version 4.x in einer primitiven Variante und in PHP 5 in überarbeiteter Engine vorhanden. Vom Thema OOP kommt man automatisch zum Begriff Exception-Handling, also Ausnahmen-Behandlung. Dabei handelt es sich um nichts anderes als den kompletten Abbruch des weiteren programmatischen Ablaufs mit dem Werfen einer Fehlermeldung. Wird diese geworfene Ausnahme (throw Exception) nicht abgefangen, schlägt sie bis zum Ursprung durch (der sog. main()) und führt zum Abbruch des kompletten Programmablauf, bei dem noch nicht einmal Applikations-Logging möglich ist.

Das Behandeln von Ausnahmen ist nach dem Schema Versuch => Irrtum aufgebaut. Man notiert einen sog. try-catch-Block nach diesem Muster:

PHP:
// Wir erwarten, dass ggf. Fehler auftreten können, daher behandeln wir alles als einen Versuch:
try
{
  // hier der Code, der Fehler verursachen kann und Ausnahmen wirft
}
catch(Exception $ex)
{
  // Ausgabe einer fachlichen Meldung ggf. abhängig der Ursache der Ausnahme und evtl. Logging
  die("Beim Verarbeiten ist ein Fehler aufgetreten");
  //Logger::logException($ex);
}

Ausnahmen in PDO

Man kann PDO dazu veranlassen, im Fehlerfall eine Exception zu werfen, denn standardmäßig wird es einfach abbrechen, ohne mitzuteilen, was der Grund für den Abbruch ist (siehe PDO::ERRMODE_SILENT). Die geworfene Exception sollte dann von uns im Code abgefangen und von einer technischen zu einer fachlichen Meldung umgewandelt werden. Die Verwendung des new-Operator auf die PDO-Klasse wirft allerdings schon eine Exception, wenn beim Verbindungsaufbau ein Fehler auftreten sollte (zB. fehlerhafte Angaben im DSN, falsche Datenbank-Credentials, oder schlicht, dass der Datenbankserver derzeit keine Verbindungsanfragen beantworten kann).

Dazu bauen wir zunächst einmal den kompletten Code so um, dass alle Exceptions abgefangen werden:

PHP:
<?php
// Fehleranzeige aktivieren
ini_set('display_errors', 1);
error_reporting( E_ALL );

// Verbindungsdaten zur MySQL-Datenbank
$db_user = 'test';
$db_pass = '';

try
{
  // Verbindung aufbauen
  $db = new PDO('mysql:dbname=test;host=127.0.0.1', $db_user, $db_pass);

  // Parameterlose Anfrage vorbereiten
  $statement = $db->prepare('SELECT id FROM users WHERE username = :u_name AND passwd = :u_pass');

  // Parameter-Bindung, die Daten kommen aus einem mit POST übertragenem Formular
  $statement->bindParam(':u_name', strval($_POST['benutzername']));
  $statement->bindParam(':u_pass', strval($_POST['passwort']));

  // Ausführung der Anfrage
  $statement->execute();

  // Abholen der Daten als Objekt
  $result = $statement->fetch(PDO::FETCH_OBJ);

  // Anzeige der abgeholten Daten
  printf("Ein Benutzer mit der ID %d wurde gefunden", $result->id);
}
catch(Exception $ex)
{
  // Siehe https://www.tutorials.de/resources/php-simplelogger-ein-einfache-logger-klasse.222/
  SimpleLogger::logException($ex);
  die("Beim Zugriff auf die Datenbank ist ein Fehler aufgetreten!");
}

Als nächstes teilen wir PDO mit, dass Fehler als Exception zu behandeln sind, wir stellen also vom Standard-Handling ERRMODE_SILENT auf ERRMODE_EXCEPTION um. Dazu gibt es die Methode setAttribute(). Dort geben wir an direkt nach dem Erzeugen des PDO-Objekts an, dass wir Ausnahmen haben wollen:

PHP:
try
{
  // Verbindung aufbauen
  $db = new PDO('mysql:dbname=test;host=127.0.0.1', $db_user, $db_pass);
  $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

Wenn nun bei den darauf folgenden PDO-Methoden ein Fehler auftritt, wird eine Exception geworfen, die im catch-Block abgefangen und von einer technischen Meldung (inkl. Stacktrace) in eine fachliche - für den Benutzer verständliche Meldung umgewandelt.

Wie bereits beim Exception-Exkurs erwähnt, sollte jedoch ein Logging-Mechanismus eingebaut werden, der uns als Entwickler die Möglichkeit gibt, den tatsächlichen technischen Fehler in Erfahrung zu bringen und den Fehler beheben zu können. Zu diesem Zweck kann meine SimpleLogger-Klasse verwendet werden, die ich in einem weiteren Beitrag vorstelle.

Behandeln von Fehlern, die nicht von PDO verursacht werden

Es kann vorkommen, dass man als Entwickler nicht an alle möglichen Fehlerursachen denkt - wir sind alle nur Menschen. Dazu kommt, dass nicht alle Fehler von PDO selbst verursacht werden müssen. Es gibt bspw. eine Ursache, auf die man beim Betrachten des Codes nur mit ausreichender Genauigkeit und Erfahrung kommt.

Folgende Frage sollte man beantworten können: Was passiert zum Beispiel, wenn ein Benutzer gar keine Eingabe vorgenommen hat?

Handelt es sich dann um einen Fehler, der dazu führt, dass die Datenbank-Operation überhaupt nicht ausgeführt werden soll, kann man diese Fehlerquelle ebenfalls als eine Exception werfen und behandeln. Jedoch kommt einem PHP hier nicht unbedingt entgegen. Statt dessen muss man einen sog. Fehlerhandler implementieren und registrieren, der auf die evtl. bereits bekannten E_NOTICE, E_WARNING usw. reagiert und in eine Exception unwandelt.

Hier wird ein Weg vorgestellt, der als Standard-Fehlerbehandlung angesehen werden kann. Wenn eine fehlende Benutzer-Eingabe jedoch nicht als Exception behandelt werden soll, muss der Fehler ggf. mit isset()-Prüfung gecheckt und im Code anders verfahren werden. Gehen wir jedoch erstmal davon aus, dass wir Mist gebaut haben und eine fehlende Eingabe als fataler Fehler und damit als Ausnahme behandelt werden muss. Dazu erstellen und registrieren unseren eigenen Fehler-Handler:

PHP:
/**
* ErrorHandler.php
* @author elad dot yosifon at gmail dot com
* @see http://php.net/manual/de/function.set-error-handler.php#112881
* Provides an exception based error handling
*/

/** Exceptions derived from ErrorException **/
class WarningException              extends ErrorException {}
class ParseException                extends ErrorException {}
class NoticeException              extends ErrorException {}
class CoreErrorException            extends ErrorException {}
class CoreWarningException          extends ErrorException {}
class CompileErrorException        extends ErrorException {}
class CompileWarningException      extends ErrorException {}
class UserErrorException            extends ErrorException {}
class UserWarningException          extends ErrorException {}
class UserNoticeException          extends ErrorException {}
class StrictException              extends ErrorException {}
class RecoverableErrorException    extends ErrorException {}
class DeprecatedException          extends ErrorException {}
class UserDeprecatedException      extends ErrorException {}

/**
* The error handler
* @param number $err_severity The level or error
* @param string $err_msg The message of the error
* @param string $err_file The file where the error has occured
* @param number $err_line The line where the error has occured
* @param array $err_context Detailed error description
*/
set_error_handler(function ($err_severity, $err_msg, $err_file, $err_line, array $err_context)
{
    // error was suppressed with the @-operator
    if (0 === error_reporting()) { return false;}
    switch($err_severity)
    {
        case E_ERROR:              throw new ErrorException            ($err_msg, 0, $err_severity, $err_file, $err_line);
        case E_WARNING:            throw new WarningException          ($err_msg, 0, $err_severity, $err_file, $err_line);
        case E_PARSE:              throw new ParseException            ($err_msg, 0, $err_severity, $err_file, $err_line);
        case E_NOTICE:              throw new NoticeException          ($err_msg, 0, $err_severity, $err_file, $err_line);
        case E_CORE_ERROR:          throw new CoreErrorException        ($err_msg, 0, $err_severity, $err_file, $err_line);
        case E_CORE_WARNING:        throw new CoreWarningException      ($err_msg, 0, $err_severity, $err_file, $err_line);
        case E_COMPILE_ERROR:      throw new CompileErrorException    ($err_msg, 0, $err_severity, $err_file, $err_line);
        case E_COMPILE_WARNING:    throw new CoreWarningException      ($err_msg, 0, $err_severity, $err_file, $err_line);
        case E_USER_ERROR:          throw new UserErrorException        ($err_msg, 0, $err_severity, $err_file, $err_line);
        case E_USER_WARNING:        throw new UserWarningException      ($err_msg, 0, $err_severity, $err_file, $err_line);
        case E_USER_NOTICE:        throw new UserNoticeException      ($err_msg, 0, $err_severity, $err_file, $err_line);
        case E_STRICT:              throw new StrictException          ($err_msg, 0, $err_severity, $err_file, $err_line);
        case E_RECOVERABLE_ERROR:  throw new RecoverableErrorException ($err_msg, 0, $err_severity, $err_file, $err_line);
        case E_DEPRECATED:          throw new DeprecatedException      ($err_msg, 0, $err_severity, $err_file, $err_line);
        case E_USER_DEPRECATED:    throw new UserDeprecatedException  ($err_msg, 0, $err_severity, $err_file, $err_line);
    }
});


Im Falle eines E_xxx-Fehlers wird nun eine entsprechende Exception geworfen, welche durch unseren catch-Block abgefangen und behandelt wird.

ANMERKUNG: In dem Code, den ich kopiert habe, werden auch E_xxx-Fehler behandelt, die eigentlich nicht an den benutzer-definierten Fehlerhandler übergeben werden. Dazu gehören unter anderem E_ERROR, E_CORE_xxx, E_PARSE und E_COMPILE_xxx. Das ist mir bewusst und ich lasse sie dennoch dort stehen. Wer nähere Informationen darüber haben will, sollte sich die Seite über set_error_handler() im Manual durchlesen.

Die Datei sollte so früh wie möglich im Code wie jedes anderes Script mittels require() oder include() eingebunden werden, es muss nichts weiter eingestellt oder konfiguriert werden.

Danach kann die Fehler-Anzeige ausgeschaltet werden (ini_set()-Zeile auskommentieren), jeglicher Fehler der auftritt wird dann durch das Exception-Handling behandelt und ggf. geloggt.

Was tun, wenn kein Datensatz zurückgegeben wurde

Der aufmerksame Leser hat evtl. bereits bemerkt, dass ein weiterer Fehler noch nicht behandelt wurde: Wenn beim fetch() kein Datensatz zurück kommt, liefert die Methode wie bereits erwähnt den Wert FALSE zurück. Dabei handelt es sich natürlich nicht um ein Objekt sondern einen boolschen Wert, der keine Mitgliedsvariable 'id' hat. Mit unserem derzeitigen Code würde hier ein technischer Fehler in Form der Exception

014-09-14 17:58:36 [FATAL]: Exception NoticeException occured: Trying to get property of non-object

auftreten. Um dies jedoch zu verhindern, bauen wir eine explizite Prüfung ein. Bevor eine Ausgabe der ID erfolgt, prüfen wir, ob $result gleich FALSE ist und werfen in diesem Fall eine eigene Exception. Dies soll dem Verständnis der Materie dienen und hat keinen tieferen Grund.

Bauen wir unsere Prüfung also ein:

PHP:
  // Abholen der Daten als Objekt
   $result = $statement->fetch(PDO::FETCH_OBJ);

   if($result === FALSE)
   {
      throw new UnexpectedValueException("Kein Datensatz entspricht den gewünschten Kriterien!");
   }
   // Anzeige der abgeholten Daten
   printf("Ein Benutzer mit der ID %d wurde gefunden", $result->id);

Finally {}

Und hier noch einmal das komplette Script zum direkten Ausprobieren:

PHP:
<?php// Benötigte externe Scripte
require 'ErrorHandler.php';
require 'SimpleLogger.php';

// Verbindungsdaten zur MySQL-Datenbank
$db_user = 'test';
$db_pass = '';

try
{
   // Verbindung aufbauen
   $db = new PDO('mysql:dbname=test;host=127.0.0.1', $db_user, $db_pass);
   $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

   // Parameterlose Anfrage vorbereiten
   $statement = $db->prepare('SELECT id FROM users WHERE username = :u_name AND passwd = :u_pass');

   // Parameter-Bindung, die Daten kommen aus einem mit POST übertragenem Formular
   $statement->bindParam(':u_name', strval($_POST['benutzername']));
   $statement->bindParam(':u_pass', strval($_POST['passwort']));

   // Ausführung der Anfrage
   $statement->execute();

   // Abholen der Daten als Objekt
   $result = $statement->fetch(PDO::FETCH_OBJ);

   if($result === FALSE)
   {
     throw new UnexpectedValueException("Kein Datensatz entspricht den gewünschten Kriterien!");
   }

   // Anzeige der abgeholten Daten
   printf("Ein Benutzer mit der ID %d wurde gefunden", $result->id);
}
catch (Exception $ex)
{
   // Logging siehe mein anderer Beitrag über die Klasse SimpleLogger
   SimpleLogger::logException($ex);
   die("Beim Datenbankzugriff ist ein Fehler aufgetreten!");
}

Damit schließt das Thema Fehlerbehandlung erstmal ab. Im nächsten Teil geht es um komplexere Konzepte wie die Verwendung eines ORM.
  • Gefällt mir
Reaktionen: Yaslaw
Autor
saftmeister
Aufrufe
4.220
First release
Last update
Bewertung
0,00 Stern(e) 0 Bewertungen

More resources from saftmeister

Zurück