Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
1 Einleitung
2 Die Basis der Objektorientierung
3 Die Prinzipien des objektorientierten Entwurfs
4 Die Struktur objektorientierter Software
5 Vererbung und Polymorphie
6 Persistenz
7 Abläufe in einem objektorientierten System
8 Module und Architektur
9 Aspekte und Objektorientierung
10 Objektorientierung am Beispiel: Eine Web-Applikation mit PHP 5 und Ajax
A Verwendete Programmiersprachen
B Literaturverzeichnis
Stichwort
Ihre Meinung?

Spacer
 <<   zurück
Objektorientierte Programmierung von Bernhard Lahres, Gregor Rayman
Das umfassende Handbuch
Buch: Objektorientierte Programmierung

Objektorientierte Programmierung
2., aktualisierte und erweiterte Auflage, geb.
656 S., 49,90 Euro
Rheinwerk Computing
ISBN 978-3-8362-1401-8
Pfeil 10 Objektorientierung am Beispiel: Eine Web-Applikation mit PHP 5 und Ajax
  Pfeil 10.1 OOP in PHP
    Pfeil 10.1.1 Klassen in PHP
    Pfeil 10.1.2 Dynamische Natur von PHP
  Pfeil 10.2 Das entwickelte Framework – Trennung der Anliegen – Model View Controller
    Pfeil 10.2.1 Trennung der Daten von der Darstellung
  Pfeil 10.3 Ein Dienst in PHP
    Pfeil 10.3.1 Datenmodell
    Pfeil 10.3.2 Dienste – Version 1
  Pfeil 10.4 Ein Klient in Ajax
    Pfeil 10.4.1 Bereitstellung der Daten
    Pfeil 10.4.2 Darstellung der Daten
  Pfeil 10.5 Ein Container für Dienste in PHP
    Pfeil 10.5.1 Dispatcher
    Pfeil 10.5.2 Fabrik
    Pfeil 10.5.3 Dependency Injection
    Pfeil 10.5.4 Sicherheit
  Pfeil 10.6 Ein Klient ohne JavaScript
  Pfeil 10.7 Was noch übrigbleibt


Rheinwerk Computing - Zum Seitenanfang

10.5 Ein Container für Dienste in PHP  Zur nächsten ÜberschriftZur vorigen Überschrift

Wir haben unseren ersten »Webdienst« in der Datei kontakte.php implementiert. Manche Teile des Quelltextes befassen sich mit der Verwaltung der Kontakte, andere kümmern sich um die Verbindung mit der Datenbank, um die Authentifizierung des Benutzers oder um das Parsen der HTTP-Abfragen und die Kodierung der Daten im JSON-Format.

Bis auf die Auflistung der Kontakte in der Methode auflisten sind die aufgelisteten Anliegen nicht für unseren Dienst Kontakte spezifisch. Wenn wir sie von unserem ersten Dienst lösen, können wir sie bei anderen Diensten wiederverwenden.

Demonstrieren wir das an der zweiten Operation unseres Dienstes Kontakte: an der Operation speichern. Die Operation speichern wird die Änderungen, die wir im Browser vornehmen, in der Datenbank speichern. Sie wird (zuerst) zwei Parameter haben: den Parameter bearbeitet, der ein assoziatives Array von geänderten Einträgen enthalten wird, und den Parameter neu, der ein Array von neuen Werten enthalten wird. So wird bearbeitet[etg_id] den neuen Wert des Eintrages mit der Id etg_id enthalten, und neu[knt_id][ett_id] die Werte der neuen Einträge vom Kontakttyp mit der Id ett_id für den Kontakt mit der Id knt_id. Dem aufmerksamen Leser wird nicht entgangen sein, dass wir die INPUT-Felder in Listing 10.7 entsprechend genannt haben.

Die Methode speichern wird die bearbeiteten Einträge aktualisieren, oder, wenn der Wert leer ist, löschen und die neuen Einträge einfügen. Da die neuen Einträge vor dem Einfügen in die Datenbank keine eigenen Ids haben, müssen wir dem Klienten die neuerzeugten Ids zurückgeben – sonst könnte er die neuen Einträge nicht mehr aktualisieren.

Wir müssen noch darauf achten, dass wir ausschließlich die Kontakte des angemeldeten Benutzers bearbeiten und mehrwertige Einträge dort nicht zulassen, wo sie eindeutig sein müssen.

Hier unsere Methode speichern:

class Kontakte { 
... 
public function speichern($bearbeitet = array(), $neu = array()) { 
    $db = self::db(); 
    try { 
        // Geänderte Einträge speichern 
        $stmt = $db->prepare(" 
            UPDATE eintrag e  
            INNER JOIN kontakt k ON k.knt_id = e.knt_id 
            SET e.etg_wert = ?  
            WHERE etg_id = ? AND k.bnz_id = ? 
        "); 
        if (!$stmt)  
             throw new SQLException($db->error, $db->errno); 
        $benutzerid = $this->benutzerId(); 
        $stmt->bind_param("sii",  
               $wert, $eintragid, $benutzerid);         
        foreach($bearbeitet as $eintragid => $wert) { 
            if (trim($wert)) $stmt->execute(); 
        } 
        $stmt->close(); 
         
        // Leere Einträge löschen 
        $stmt = $db->prepare(" 
            DELETE FROM eintrag e  
            USING eintrag e  
            INNER JOIN kontakt k ON e.knt_id = k.knt_id 
            WHERE e.etg_id = ? 
            AND k.bnz_id = ? 
        "); 
        if (!$stmt)  
            throw new SQLException($db->error, $db->errno); 
        $benutzerid = $this->benutzerId(); 
        $stmt->bind_param("ii", $eintragid, $benutzerid); 
         
        foreach($bearbeitet as $eintragid => $wert) { 
            if (!trim($wert)) $stmt->execute(); 
        } 
        $stmt->close(); 
         
        // Neue Einträge einfügen 
        $stmt = $db->prepare(" 
            INSERT INTO eintrag (knt_id, ett_id, etg_wert) 
            SELECT ?, ?, ? 
            FROM kontakt k 
            WHERE k.knt_id = ? 
            AND k.bnz_id = ? 
            AND NOT EXISTS ( 
              SELECT * FROM eintrag e  
              INNER JOIN eintragstyp t ON e.ett_id = t.ett_id  
              WHERE e.knt_id = ? AND e.ett_id = ?  
                    AND t.ett_eindeutig) 
        "); 
        if (!$stmt)  
            throw new SQLException($db->error, $db->errno);         
        $stmt->bind_param("iisiiii",  
           $kontaktid, $eintragstypid, $wert, 
           $kontaktid, $benutzerid, $kontaktid, $eintragstypid); 
        $eingefuegt = array(); 
        foreach ($neu as $kontaktid => $eintraege) { 
            foreach ($eintraege as $eintragstypid => $werte) { 
                foreach ($werte as $wertid => $wert) { 
                    if (trim($wert)) { 
                        $stmt->execute(); 
                        if ($stmt->affected_rows > 0) {                               
                           $eingefuegt[$kontaktid] 
                              [$eintragstypid][$wertid]  
                              = $stmt->insert_id; 
                        } 
                    } 
                } 
            } 
        } 
        $stmt->close(); 
        $db->close(); 
        return $eingefuegt; 
         
    } catch (Exception $e) { 
        if ($stmt) $stmt->close(); 
        if ($db) $db->close(); 
        throw $e; 
    } 
}  
... 
}

Listing 10.8    Die Funktion »speichern« in »kontakte.php«

Nun haben wir unsere neue Methode, doch wie ruft sie der Klient auf? Erinnern Sie sich noch an das Listing 10.4, in dem wir HTTP-Parameter an die Methode auflisten übergaben, um dann ihr Ergebnis im JSON-Format auszugeben?

In den folgenden beiden Abschnitten 10.5.1 und 10.5.2 werden wir ein Verfahren vorstellen, das uns in gleicher Weise die Einbindung der Methoden zum Auflisten und zum Speichern erlaubt.


Rheinwerk Computing - Zum Seitenanfang

10.5.1 Dispatcher  Zur nächsten ÜberschriftZur vorigen Überschrift

Zurzeit bewirkt der Aufruf der PHP-Seite kontakte.php den Aufruf der Methode auflisten. Wir könnten eine andere PHP-Seite programmieren, die die Methode speichern aufruft. Allerdings wäre das nicht besonders elegant, wenn wir für jede Methode jedes Dienstes eine neue PHP-Datei erstellen müssten.

Geschickter ist es, den Namen der Methode in einem HTTP-Parameter zu übergeben und diesen Parameter in einem Dispatcher zu verwenden, um zu entscheiden, welche Methode aufgerufen wird.

Der Dispatcher könnte vereinfacht etwa so aussehen:

$operation = $_GET['$operation']; 
if ($operation == 'auflisten') { 
  ... 
  $ergebnis = $kontakte->auflisten($kontaktid); 
} else if ($operation == 'speichern') { 
  ... 
  $ergebnis = $kontakte->speichern($bearbeitet, $neu); 
} 
echo json_encode($ergebnis);

Wenn wir dies tun, dürfen wir nicht vergessen, den Ajax-Aufruf in kontakte.js zu ändern:

$.getJSON( 
    'kontakte.php?$operation=auflisten', 
    kontakteAuflisten);

Wiederholungen vermeiden!

Bernhard: Wir haben jetzt zwar die Notwendigkeit, für jede Methode eine neue PHP-Datei erstellen zu müssen, aber so richtig elegant ist diese Lösung auch nicht. Wir haben einen Dispatcher, den wir für jede neue Methode des Dienstes anpassen müssen. So müssen wir die Tatsache, dass unser Dienst eine bestimmte Methode hat, an mehreren Stellen deklarieren: in der Klasse, die den Dienst implementiert, und dann noch in dem Dispatcher. Das geht doch gegen das Prinzip, dass man Wiederholungen vermeiden soll.

Gregor: In der Tat. So sollten wir den Dispatcher nicht lassen. Denn PHP bietet uns eine Möglichkeit, mit der wir ihn viel eleganter implementieren können. Wir können mit Reflexion die aufzurufende Methode finden, ihre Parameter erforschen und sie dann aufrufen. Den Quelltext des Dispatchers brauchen wir dann nicht an jede neue Methode anzupassen.

Bernhard: Na, dann aber los!

Reflexion in PHP

Hier der Dispatcher, der die Methode über Reflexion aufruft:

$kontakte = new Kontakte(); 
$operation = $_GET['$operation']; 
 
$refDienst = new ReflectionObject($kontakte);  
$refMethode = $refDienst->getMethod($operation); 
$refParameter = $refMethode->getParameters(); 
 
$aufrufParameter = Array(); 
foreach($refParameter as $p => $param) { 
    if ($_SERVER['REQUEST_METHOD'] == 'POST'  
      && array_key_exists($param->getName(), $_POST)) { 
        $wert = $_POST[$param->getName()]; 
    } else if (array_key_exists($param->getName(), $_GET)) { 
        $wert = $_GET[$param->getName()]; 
    } else if ($param->isDefaultValueAvailable()) {  
        $wert = $param->getDefaultValue();  
    } else { 
        $wert = null; 
    } 
    $aufrufParameter[$p] = $wert; 
} 
$ergebnis =  
    $refMethode->invokeArgs($kontakte, $aufrufParameter);  
 
if ($ergebnis !== null) { 
    header('Content-type: text/plain; charset=UTF-8'); 
    echo json_encode($ergebnis); 
}

Listing 10.9    Ein Dispatcher, der Reflexion verwendet

Ab Zeile holen wir uns das Reflexionsobjekt [Den Begriff »Reflexion« haben wir in Abschnitt 9.1.2, »Lösungsansätze zur Trennung von Anliegen«, eingeführt. Reflexion bezieht sich grundsätzlich darauf, dass wir auf Informationen zugreifen, die zur Struktur des Programms selber gehören. In diesem Fall greifen wir auf die Information zu, welche Methoden ein Objekt zur Verfügung stellt und welche Parameter diese Methoden haben. ] zu unserem Dienst, und zwar zu der Methode, die wir aufrufen möchten, und zu ihrer Parameterliste. Dann füllen wir die Liste $aufrufParameter mit den Werten aus dem HTTP-Aufruf. Wenn kein entsprechender HTTP-Parameter übergeben worden ist, verwenden wir in Zeile , falls angegeben, den Standardwert des Parameters. Dies wäre zum Beispiel bei der Methode auflisten für den Parameter $kontaktid die –1. Falls wir weder einen HTTP-Parameter noch den Standardwert finden, verwenden wir NULL für den Wert des Parameters.

Schließlich rufen wir die Methode in Zeile auf und geben das Ergebnis im JSON-Format zurück.

Bernhard: Einen Makel hat der Dispatcher allerdings noch immer. Er verwendet stets ein Exemplar der Klasse Kontakte als Dienst. So müssten wir für jeden Dienst einen eigenen Dispatcher bauen.

Gregor: Stimmt. Lass uns also den Dispatcher so ändern, dass der Name des Dienstes auch als ein HTTP-Parameter übergeben wird. So können wir den Dispatcher vollständig von einem konkreten Dienst abkoppeln.
Rheinwerk Computing - Zum Seitenanfang

10.5.2 Fabrik  Zur nächsten ÜberschriftZur vorigen Überschrift

Da wir den Dispatcher von der Klasse Kontakte trennen wollen, ist es sinnvoll, ihn auch in eine eigene PHP-Datei auszulagern. Legen wir also die Datei dispatcher.php an:

class Dispatcher { 
 
  public static function httpRequestBehandeln() {  
      $dienstName = $_GET['$dienst']; 
      $operationsName = $_GET['$operation'];         
      $dienst = self::dienstErstellen($dienstName); 
      $ergebnis =  
          self::methodeAufrufen($dienst, $operationsName);                 
      if ($ergebnis !== NULL) { 
          header('Content-type: text/plain; charset=UTF-8'); 
          echo json_encode($ergebnis); 
      } 
  } 
 
  private static function dienstErstellen($dienstName) {  
      require_once("dienst.$dienstName.php"); 
      $refDienst = new ReflectionClass($dienstName); 
      return $refDienst->newInstance();         
  } 
 
  private static function methodeAufrufen($dienst, $operation) { 
      ... 
  } 
} 
 
Dispatcher::httpRequestBehandeln();

Listing 10.10    Die Datei »dispatcher.php«

Fabrikmethode

Der Dispatcher bietet eine einzige öffentliche Methode, httpRequestBehandeln , in der wir mit der Fabrikmethode dienstErstellen ein Exemplar des benötigten Dienstes erstellen und die gewünschte Methode aufrufen.

In der Fabrikmethode dienstErstellen gehen wir davon aus, dass die Klasse des Dienstes in einer Datei mit dem Namen dienst.NameDesDienstes.php programmiert wurde. Diese Namenskonvention ist nur eine kleine Sicherheitsmaßnahme, um zu verhindern, dass jede öffentliche Methode jeder Klasse unserer Anwendung als ein Dienst über den Dispatcher aufgerufen werden kann.

Selbstverständlich müssen wir jetzt die Datei kontakte.php in dienst.Kontakte.php umbenennen und den Ajax-Aufruf in kontakte.js anpassen.


Rheinwerk Computing - Zum Seitenanfang

10.5.3 Dependency Injection  Zur nächsten ÜberschriftZur vorigen Überschrift

Bernhard: Jetzt haben wir also die Infrastruktur, die es uns ermöglicht, PHP-Dienste einfach zu schreiben. Dabei brauchen wir uns keine Gedanken mehr über die Übertragung der Parameter und der Ergebnisse zu machen, denn das macht ja der Dispatcher.Lass uns jetzt den Dienst Kontakte fertigschreiben, um die Methode benutzerId richtig zu implementieren.

Gregor: Gute Idee! Wie wollen wir das machen?

Bernhard: Lass uns einen neuen Dienst implementieren, es geht jetzt so einfach. Der Dienst Sicherheit wird eine Methode einloggen haben, mit der sich der Benutzer anmelden kann. Seine Daten können dann in der Websession gespeichert werden, und die Methode benutzerId der Klasse Kontakte könnte die Daten aus der Session einfach verwenden.

Gregor: Gute Idee! Allerdings würde der neue Dienst auch einen Zugriff auf die Datenbank brauchen. Der ist aber zurzeit in der Klasse Kontakte implementiert. Ich möchte den Quelltext auf keinen Fall kopieren. Wir sollten ihn in eine Oberklasse für alle Dienste verschieben, um ihn in allen Diensten verwenden zu können.

Bernhard: Wirklich in allen Diensten? Was ist, wenn bestimmte Dienste nicht auf die Datenbank zugreifen müssen? Was, wenn bestimmte Dienste auf eine andere Datenbank zugreifen möchten? Ich finde, dass die Oberklasse nicht der ideale Platz für die Erstellung der Datenbankverbindung ist.

Gregor: Du hast recht. Zum Glück haben wir noch eine andere Stelle, an der wir die Datenbankverbindung den Diensten, die sie brauchen, zur Verfügung stellen können. Wir haben den Dispatcher. Er kann die Klasse des Dienstes untersuchen und wenn dieser eine Datenbankverbindung braucht, ihm eine bereitstellen.

Dependency Injection

Die in der Diskussion beschriebene Technik, haben wir unter dem Namen Dependency Injection bereits in Abschnitt 7.2.7 vorgestellt. Wenn wir sie im Dispatcher umsetzen, werden unsere Dienste nicht mehr die Datenbankverbindung selbst erstellen müssen. Sie werden nur deklarieren müssen, dass sie eine Datenbankverbindung benötigen, und der Dispatcher wird ihnen eine zur Verfügung stellen. Unser Dispatcher wird so langsam zu einem Container für die Dienste.

Grundsätzlich haben wir zwei Möglichkeiten, wie wir hier die Dependency Injection implementieren können. Wir können dem Dienst die Datenbankverbindung als einen Parameter des Konstruktors übergeben, oder wir übergeben sie dem bereits erstellen Exemplar des Dienstes über eine Setter-Methode. [Man könnte sie sogar als eine öffentliche Variable des Dienstes setzen. Öffentliche Objektvariablen verursachen aber auch in PHP einen Quelltextgeruch, den nicht jeder mag. ]

Beide Vorgehensweisen haben ihre Vor- und Nachteile. Die Konstruktor-Variante kommuniziert sehr deutlich, dass der Dienst die eingefügte Ressource benötigt. Wenn die Ressource im Laufe der Lebenszeit des Dienstes nicht ausgetauscht werden darf, ist es auch vorteilhaft, sie im Konstruktor zu übergeben, denn ein Konstruktor kann für ein Objekt nur einmal aufgerufen werden.

Bei der Variante mit der Setter-Methode können wir die Ressource bei einem Dienst zwischendurch ändern. So kann der Container dasselbe Exemplar eines Dienstes zu unterschiedlichen Zeiten in unterschiedlichen Kontexten verwenden.

Wenn die Übergabe und eventuelle spätere Verwaltung der Ressource beim Empfänger komplexer ist und sich nicht in einem Schritt bewerkstelligen lässt, verwendet man oft auch die dritte Variante der Dependency Injection: Bei dieser implementiert das empfangende Objekt eine definierte Schnittstelle, in der die Operationen für die Übergabe und die Verwaltung der Ressource beim Empfänger beschrieben werden.

In unserer Anwendung werden wir nur mit einer Datenbank arbeiten und daher die zugewiesene Datenbankverbindung nie ändern. Wir können auch davon ausgehen, dass ein Dienst, der auf die Datenbank zugreift, dies bei fast allen seinen Operationen machen wird, also braucht er sie für seine gesamte Lebenszeit. Daher spricht alles dafür, den Exemplaren solcher Dienste die Datenbankverbindung direkt in ihrem Konstruktor zu übergeben. [Die Implementierung der Konstruktor-Variante und der Setter-Variante ist in PHP etwa gleich einfach. In anderen Programmiersprachen und anderen Containern kann die Implementierung der verschiedenen Varianten unterschiedlich komplex sein. Das kann die Wahl der Variante beeinflussen. ]

Wie aber erkennen wir, welchen Diensten wir die Datenbankverbindung übergeben sollen? Wir könnten dies in der Konfiguration unserer Anwendung spezifizieren oder die Dienste so programmieren, dass der Dispatcher selbst entdecken kann, ob einem Dienst die Datenbankverbindung übergeben werden soll.


Convention over Configuration

Bestimmte Einstellungen von Softwarekomponenten konfigurierbar zu machen ist sehr nützlich, weil die Komponente so in mehreren Umgebungen eingesetzt werden kann, ohne selbst geändert werden zu müssen. Allerdings können Konfigurationsdateien auch ziemlich lästig werden und die Verwendung der Komponente verkomplizieren – vor allem dann, wenn man die Komponente auf die »übliche« Art verwenden möchte.

Das Designprinzip Convention over Configuration besagt, dass man Software so entwickeln soll, dass sie sich nach einer Konvention richtet und nur Abweichungen von der Konvention durch die Konfiguration bestimmt werden müssen.

So kann man zum Beispiel in einem Werkzeug für objekt-relationales Mapping der Benennungskonvention folgen, die besagt, dass die Exemplare einer Klasse in einer Tabelle gespeichert werden, die denselben Namen trägt wie die Klasse.

In diesem Falle würde man zum Beispiel die Exemplare der Klasse Kunde in der Tabelle KUNDE speichern. Dies in einer Konfigurationsdatei bestimmen zu müssen wäre eine Verletzung des Prinzips der Vermeidung der Wiederholungen, denn der Name der Tabelle ist bereits durch den Namen der Klasse bestimmt.

Es kann aber Fälle geben, in denen man von der Konvention abweichen muss – schließlich handelt es sich um eine Konvention und nicht um ein Gesetz. Und genau nur solche Abweichungen von der Konvention muss man durch die Konfiguration bestimmen.

Durch diese Vorgehensweise behalten wir die volle Flexibilität der Softwarekomponente, und gleichzeitig reduzieren wir enorm die Komplexität ihrer Konfiguration.


Auch in unserer Anwendung definieren wir also eine Konvention:

1. Die Werte der Parameter des Konstruktors, die nicht mit einem Unterstrich beginnen, werden durch die Parameter des HTTP-Aufrufs bestimmt.
       
2. Wenn der Konstruktor eines Dienstes einen Parameter mit dem Namen $_db hat, übergeben wir über diesen Parameter die Datenbankverbindung an den Konstruktor. [In anderen Programmiersprachen könnte man eine Konvention definieren, die sich nach den Typen der Parameter richtet. Da man in PHP keine Typen der Parameter deklarieren kann, müssen wir uns an den Namen der Parameter orientieren – also geht es hier um eine Namenskonvention. ]
       

Wir werden also die Parameter der Konstruktoren der Dienste genauso behandeln wie die Parameter der Methoden.

Bernhard: Wir haben jetzt eine Konvention definiert. Um allerdings dem Prinzip Convention over Configuration zu folgen, sollten wir aber noch die Möglichkeit schaffen, Abweichungen zu konfigurieren.

Gregor: Ja, das sollten wir in der Tat. Wir werden das sofort tun, wenn wir die erste Abweichung brauchen.

Bernhard: Das wird wohl nicht in diesem Buch sein, nicht wahr?

Gregor: Das hoffe ich. Wenn aber jemand unser kleines Framework in einer komplexeren PHP-Anwendung verwenden möchte, kann er oder sie damit rechnen, dass wir ihn/sie nicht im Stich lassen!

Hier also unser angepasster Dispatcher:

class Dispatcher { 
  private $db; 
  public function __construct() { 
    $this->db = new mysqli('localhost', 'buch', 'buch', 'buch'); 
    $this->db->set_charset("utf8"); 
  } 
 
  public function __destruct() {  
    if ($this->db) $this->db->close(); 
  } 
  public function httpRequestBehandeln() {  
    $dienstName = $_GET['$dienst']; 
    $operationsName = $_GET['$operation']; 
    $dienst = self::dienstErstellen($dienstName); 
    $ergebnis = self::methodeAufrufen($dienst, $operationsName); 
    if ($ergebnis !== NULL) { 
      header('Content-type: text/plain; charset=UTF-8'); 
      echo json_encode($ergebnis); 
    } 
  } 
 
  private function dienstErstellen($dienstName) { 
    require_once("dienst.$dienstName.php"); 
    $refDienst = new ReflectionClass($dienstName); 
    $refConstructor = $refDienst->getConstructor(); 
    if (!$refConstructor) return $refDienst->newInstance(); 
    $initParameter = self::parameterSetzen($refConstructor); 
    return $refDienst->newInstanceArgs($initParameter);  
  } 
 
  private function parameterSetzen($operation) { 
    $aufrufParameter = Array(); 
    $refParameter = $operation->getParameters(); 
    foreach($refParameter as $p => $param) { 
      $parameterName = $param->getName(); 
      if ($parameterName == '_db') { 
        $wert = $this->db; 
      } else if ($parameterName[0] != '_'  
             && $_SERVER['REQUEST_METHOD'] == 'POST' 
             && array_key_exists($parameterName, $_POST)) { 
                $wert = $_POST[$parameterName]; 
      } else if ($parameterName[0] != '_' 
             && array_key_exists($parameterName, $_GET)) { 
        $wert = $_GET[$parameterName]; 
      } else if ($param->isDefaultValueAvailable()) { 
        $wert = $param->getDefaultValue(); 
      } else { 
        $wert = null; 
      } 
      $aufrufParameter[$p] = $wert; 
    } 
    return $aufrufParameter; 
  } 
 
  private function methodeAufrufen($dienst, $operation) { 
    $refDienst = new ReflectionObject($dienst); 
    $refMethode = $refDienst->getMethod($operation); 
    $aufrufParameter = self::parameterSetzen($refMethode); 
    return $refMethode->invokeArgs($dienst, $aufrufParameter);  
  } 
} 
 
$dispatcher = new Dispatcher();  
$dispatcher->httpRequestBehandeln();

Listing 10.11    Dispatcher mit Dependency Injection

In Zeile sehen Sie, dass wir den Aufruf des Dispatchers geändert haben. Vorher haben wir die Methode httpRequestBehandeln als eine Klassenmethode verwendet, jetzt verwenden wir sie als Methode eines konkreten Exemplars des Dispatchers.

Vorher hat die Klasse Dispatcher keine Objektvariablen verwendet, deren Exemplare hatten also keinen Objektzustand, und wir konnten alle ihre Methoden als Klassenmethoden aufrufen. Jetzt aber verwalten die Exemplare des Dispatchers eine Datenbankverbindung. Sie erstellen sie in ihrem Konstruktor und schließen sie dann in ihrem Destruktor . Wir müssen also die Methoden eines konkreten Exemplars verwenden.

Damit die Dependency Injection für den bereits implementierten Dienst Kontakte funktioniert, müssen wir auch ihn ändern. Er soll nicht mehr seine eigene Datenbankverbindung erstellen, und auf keinen Fall darf er die übergebene Datenbankverbindung schließen:

class Kontakte { 
   // Diese Methode brauchen wir nicht mehr  
   // private static function db(); 
 
  private $db; 
 
  public function __construct($_db) {  
    $this->db = $_db; 
  } 
 
  public function auflisten($kontaktid = -1) {  
    // $db = self::db(); Wir erstellen die Verbindung nicht mehr  
    try { 
        $stmt = $this->db->prepare("...");  
          ... 
        $stmt->close(); 
      // $db->close(); Wir schließen die Verbindung nicht  
    } catch (Exception $e) { 
      if ($stmt) $stmt->close(); 
      // if ($db) $db->close(); Auch hier nicht  
      throw $e; 
   }
... 
}

Listing 10.12    Anpassungen im Dienst »Kontakte«

Die Methode db , mit der wir früher die Datenbankverbindung erstellt haben, können wir jetzt löschen. Stattdessen programmieren wir einen Konstruktor mit dem Parameter _db . In den Fachmethoden brauchen wir dann die Datenbankverbindung nicht mehr selbst erstellen , und wir dürfen sie auch nicht am Ende schließen , denn sie gehört dem Dispatcher. Wenn wir die Datenbankverbindung in der Methode brauchen, steht sie uns als Objektvariable zur Verfügung.


Rheinwerk Computing - Zum Seitenanfang

10.5.4 Sicherheit  topZur vorigen Überschrift

Jetzt können wir uns endlich der Sicherheit widmen. Wir werden einen neuen Dienst implementieren, mit dem wir Benutzer erstellen und authentisieren können – einen Dienst, mit dem unsere Anwendung beim Einloggen und Ausloggen der Benutzer verwendet wird.

In typischen Webanwendungen ist die Erstellung eines Benutzerkontos ein Prozess, der aus mehreren Schritten besteht:

Zuerst wählt der Benutzer einen Benutzernamen und ein Kennwort aus und gibt seine Daten, wie zum Beispiel die E-Mail-Adresse, ein. Anschließend muss er in der Regel die Richtigkeit der eingegebenen E-Mail-Adresse noch bestätigen, um sein neues Konto zu aktivieren.

Diesen ganzen Prozess zu implementieren würde den Rahmen dieses Kapitels sprengen, daher vereinfachen wir uns an dieser Stelle die Arbeit. Bei uns wird ein neues Benutzerkonto immer dann angelegt, wenn sich ein Benutzer mit einem noch nicht angelegten Benutzernamen anmeldet. Bei der ersten Anmeldung bestimmt der Benutzer seinen Benutzernamen und auch sein Kennwort. Seine E-Mail-Adresse werden wir nicht kennen, also können wir ihm nicht helfen, wenn er sein Kennwort vergisst. Aber hey, dies ist eine Beispielanwendung, und es gibt wohl gute Gründe, warum echte Webanwendungen einen komplizierteren Anmeldeprozess haben.

Unser Sicherheitsdienst wird nach einer erfolgreichen Anmeldung die Benutzerdaten in der Websession speichern und TRUE zurückgeben. Schlägt die Anmeldung fehl, gibt der Dienst FALSE zurück. Der Dispatcher wird die Benutzerdaten aus der Websession lesen und sie an andere Dienste übergeben. Wie? Selbstverständlich durch Dependency Injection!

Hier also unser Sicherheitsdienst:

class Sicherheit { 
  private $db; 
  public function __construct($_db) { 
    $this->db = $_db; 
  } 
 
  public function ausloggen() {  
    unset($_SESSION['benutzer']); 
    return TRUE; 
  } 
 
  public function einloggen($benutzername, $kennwort) { 
    if (!trim($benutzername) || !trim($kennwort))  
      return FALSE;  
    $kennworthash = md5("Adressbuch.$benutzername.$kennwort"); 
    $benutzerid = NULL; 
 
    try { 
      $stmt = $this->db->prepare(" 
            SELECT bnz_id, bnz_kennworthash FROM benutzer 
            WHERE bnz_benutzername = ? 
        "); 
 
      if (!$stmt) { 
        throw new SQLException($this->db->error,  
                               $this->db->errno); 
      } 
      $stmt->bind_param("s", $benutzername); 
      $stmt->bind_result($benutzerid, $hash); 
      $stmt->execute(); 
       
      if ($stmt->fetch()) { 
        // Der Benutzer existiert 
        if ($hash != $kennworthash) {  
          // Aber das Kennwort ist falsch 
          $benutzerid = NULL; 
        } 
      } else { 
        // Der Benutzer existiert nicht, wir legen ihn an 
        $stmt->close(); 
        $stmt = $this->db->prepare("  
           INSERT INTO benutzer (bnz_benutzername, 
                                 bnz_kennworthash) 
           VALUES (?, ?)"); 
        if (!$stmt) { 
          throw new SQLException($this->db->error,  
                                 $this->db->errno); 
        } 
        $stmt->bind_param("ss", $benutzername, $kennworthash); 
        if (!$stmt->execute()) { 
          throw new SQLException($this->db->error,  
                                 $this->db->errno); 
        } 
        if ($stmt->affected_rows > 0) { 
          $benutzerid = $stmt->insert_id; 
        } 
      } 
      $stmt->close(); 
       
      if ($benutzerid) { 
        $_SESSION['benutzer'] = Array(   
          'id' => $benutzerid, 
          'name' => $benutzername); 
        return TRUE; 
      } else { 
        unset($_SESSION['benutzer']);  
        return FALSE; 
      }             
    } catch (Exception $e) { 
      if ($stmt) $stmt->close(); 
      throw $e; 
    } 
  } 
}

Listing 10.13    Der Dienst »Sicherheit« in »dienst.Sicherheit.php«

Das Ausloggen ist sehr einfach, wir löschen nur die Daten aus der Websession. Die Methode einloggen ist etwas komplizierter, denn hier müssen wir

  • überprüfen, ob der Benutzername und das Kennwort überhaupt eingegeben wurden ,
  • wenn der Benutzername bereits belegt ist, das Kennwort überprüfen ,
  • wenn ein unbelegter Benutzername eingegeben wurde, einen neuen Benutzer erstellen
  • und schließlich die Benutzerdaten in der Websession speichern oder, wenn das Einloggen fehlgeschlagen ist, die Daten eines vorher angemeldeten Benutzers aus der Session löschen .

Wie schon besprochen, stehen noch zwei Änderungen an: Wir müssen den Dispatcher ändern, so dass er die Benutzerdaten den Diensten, die sie brauchen, injiziert. Und wir müssen den Dienst Kontakte so ändern, dass er die Daten empfangen kann.

Hier die Anpassungen am Dispatcher:

public function httpRequestBehandeln() { 
  session_start();  
  ... 
  session_write_close();  
} 
 
private function parameterSetzen($operation) { 
  $aufrufParameter = Array(); 
  $refParameter = $operation->getParameters(); 
  foreach($refParameter as $p => $param) { 
    $parameterName = $param->getName(); 
    if ($parameterName == '_benutzerId') {  
      if ($_SESSION && array_key_exists('benutzer', $_SESSION))  
        $wert = $_SESSION['benutzer']['id']; 
      } else { 
        $wert = NULL; 
      } 
    } else if ($parameterName == '_db') { 
      $wert = $this->db; 
    } else if (... 
  ... 
}

Listing 10.14    Anpassungen am Dispatcher

Wir müssen sicherstellen, dass der HTTP-Aufruf mit einer Session assoziiert wird, also müssen wir in der Methode httpRequestBehandeln die Session am Anfang starten und sie am Ende beenden sowie in den dauerhaften Sessionspeicher schreiben .

In der Methode parameterSetzen übergeben wir die Id des eingeloggten Benutzers an den aufgerufenen Konstruktor oder die Methode, die einen Parameter mit dem Namen _benutzerId hat. [Gut, dass wir in unserer Namenskonvention bestimmt haben, dass die Namen von Parametern, die von dem HTTP-Aufruf übernommen werden, nicht mit einem Unterstrich beginnen dürfen. So können wir jetzt sicher sein, dass keine Operation und kein Konstruktor eines bestehenden Dienstes den Namen _benutzerId bereits zu einem anderen Zweck verwendet. ]

Die Anpassungen im Dienst Kontakte sind auch sehr einfach:

class Kontakte { 
 
    private $db; 
    private $benutzerId; 
     
    public function __construct($_db, $_benutzerId) { 
        $this->db = $_db; 
        $this->benutzerId = $_benutzerId; 
    } 
 
    private function benutzerId() { 
        return $this->benutzerId; 
    } 
 
    ... 
}

Listing 10.15    Anpassungen am Dienst »Kontakte«



Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

 <<   zurück
  Zum Rheinwerk-Shop
Neuauflage: Objektorientierte Programmierung






Neuauflage:
Objektorientierte Programmierung

Jetzt Buch bestellen


 Ihre Meinung?
Wie hat Ihnen das Openbook gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Rheinwerk-Shop: Java ist auch eine Insel






 Java ist auch
 eine Insel


Zum Rheinwerk-Shop: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Rheinwerk-Shop: C++ Handbuch






 C++ Handbuch


Zum Rheinwerk-Shop: Einstieg in Python






 Einstieg in Python


Zum Rheinwerk-Shop: IT-Handbuch für Fachinformatiker






 IT-Handbuch für
 Fachinformatiker


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2009
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern