Kapitel 9. Implementierung des Domain-Models: Einträge und Autoren

Inhaltsverzeichnis

9.1. Einleitung
9.2. Domain-Model und Datenbankzugriffs-Pattern
9.3. Erforschung der Domain-Objekte
9.4. Erforschung des Entry-Data-Mappers
9.5. Auswahl der Werkzeuge für die Implementierung
9.5.1. Domain-Objekte
9.5.2. Validierungs- und Filter-Regeln
9.5.3. Datenbankzugriff
9.6. Implementierung
9.6.1. Unit-Tests zur Testsuite hinzufügen
9.6.2. Die Domain-Objekte
9.6.3. Die Data-Mapper
9.6.4. Lazy-Loading von Domain-Objekten
9.6.5. Doppelten Entitäten mit einer Identity-Map vorbeugen
9.7. Fazit

9.1. Einleitung

In Kapitel 3: Das Model erläuterte ich einige Konzepte rund um das Model, welches den Zustand und die Geschäftsregeln einer Entität repräsentiert (zum Beispiel jene, die Einträge in einem Blog verwaltet). Wir haben Glück, da unsere Entitäten in diesem Kapitel extrem simpel sind. Dadurch können wir eine Menge abstrakter Konzepte rund um das Model ignorieren, die für Webanwendungen mit weit komplexeren Systemen von Bedeutung sind. Doch selbst unter unseren einfachen Umständen müssen wir zu Beginn einige Herausforderungen an unser Design lösen.

Lassen Sie mich gleich zu Anfang erwähnen, dass dieses Kapitel nicht die Verwendung von Zend_Db, Zend_Db_Table oder Zend_Db_Table_Row in größerem Umfang lehrt. Wenn wir im Verlauf des Buches auf diese Klassen stoßen, wird ihr Einsatz erklärt, doch der Fokus dieses Kapitels liegt darauf, Zend_Db_Table als Basis für den Entwurf und die Implementierung des Models für diesen Blog zu verwenden. Einen Bereich des Models werden wir uns besonders genau ansehen: die Einträge.

9.2. Domain-Model und Datenbankzugriffs-Pattern

Wenn wir über Models sprechen, gibt es einige Standardbegriffe, derer wir uns bedienen können. Zuallererst muss jedes Model einer Domain zugehörig sein, quasi einem Gesamtsystem, in dem das Model operiert. In unserer Anwendung ist das schlicht "blogging". Innerhalb dieser Domain setzt sich das Model aus einem oder mehreren Domain-Objekten zusammen. Ein Domain-Objekt ist repräsentiert eine Entität, dessen Eigenschaften und die Geschäftsregeln (auch bekannt als Domain-Logik), die auf das Objekt angewendet werden. Innerhalb unserer Domain "blogging" kann es also Domain-Objekte geben, die Entry-Entitäten (= Einträge) repräsentieren. Der Vollständigkeit halber kann eine Entität auch als ein eindeutig identifizierbarer Teil der Domain mit einem Satz von Verhaltensweisen definiert werden. Zum Beispiel sollten alle Einträge einen einzigartigen Titel und Inhalt besitzen, aber die tatsächlich einzigartige Eigenschaft wird die Id sein. Alle Einträge haben zudem einen Satz prozeduraler Verhaltensweisen - sie werden geschrieben, validiert und veröffentlicht.

Wenn Sie sich die Erklärung genauer ansehen, wird Ihnen auffallen: es steht nirgendwo, dass ein Model aus einem einzelnen Objekt besteht. In einem Klimamodell hätten wir tausende interagierende Entitäten, Faktoren, Verhaltensweisen, Randbedingungen etc. Wenn wir von einem Model sprechen, beziehen wir uns also eigentlich auf all die Entitäten, die in dem Model der Domain enthalten sind, und darauf, wie sie sich verhalten und miteinander agieren. Im Umfeld von Zend Framework werden Sie oft feststellen, dass Entitäten als Models bezeichnet werden, z.B. beim Entry-Model, dem Author-Model etc. In fast allen Fällen handelt es sich dabei um Domain-Objekte innerhalb eines einzelnen Domain-Models.

Wir haben die Domain-Logik erwähnt. Es handelt sich dabei um einen allgemeineren Begriff für Geschäftsregeln und Verhalten - doch es geht nicht immer um Geschäftslogik. Oft wird unsere Domain-Logik Bedingungen beschreiben, die auf die Eigenschaften der Domain-Objekte zutreffen, z.B. für die Validierung und das Filtern von Eigenschaften. Ich erwähne das, um zu betonen, dass die Validierung eine Aufgabe des Models ist, nicht des Controllers oder der View. Wenn wir uns in einem späteren Kapitel Formulare ansehen, werden Sie sehen, wie das zum Tragen kommt.

Das Hauptproblem, auf das Entwickler häufig beim Konzeptionieren der Model-Schicht ihrer Anwendung stoßen, ist die Frage, wie nahe an der "Oberfläche" die Speicherschicht des Models liegen soll.

In sehr einfachen Anwendungen mag es reichen, wenn wir direkt mittels Zend_Db SQL-Abfragen absetzen oder Domain-Objekte erstellen, die - der Zweckmäßigkeit halber - Zend_Db_Table erweitern (welches das Table-Data-Gateway-Pattern von Martin Fowler aus seinem Buch "Patterns Of Enterprise Application Architecture" (kurz POEAA) implementiert). Das bringt den Speichermechanismus, eine relationale Datenbank, an die Oberfläche. Die Anwendung kann somit direkt darauf zugreifen. Dasselbe kann über Zend_Db_Table_Row gesagt werden, das Fowlers Row-Data-Gateway-Pattern implementiert, oder über ActiveRecord von Ruby On Rails, was (mit einigen Verbesserungen, die es einem Data-Mapper ähnlicher machen) Fowlers Active-Record-Pattern implementiert. Diese drei Pattern haben eines gemeinsam: sie kapseln direkt den Datenbankzugriff, oft indem sie von einer Basis-Klasse erben, die an eine einzelne Datenbanktabelle oder -Zeile gebunden ist.

In vielen nicht so simplen Anwendungen kann das Domain-Model aber an Komplexität gewinnen. Vielleicht stellt sich heraus, dass das Domain-Model, das in Form von Objekten und Objekt-Eigenschaften dargestellt wird, nicht einfach auf Datenbanktabellen umgelegt werden kann. Ein einfaches Beispiel dafür ist ein Blog-Eintrag. Der Eintrag scheint eine simple Entität zu sein, die einer Entries-Tabelle in einem Datenbankschema zugewiesen werden kann, aber sie enthält auch eine Referenz (z.B. einen Fremdschlüssel) für einen Autor. Aus Sicht unseres Domain-Objekts bedeutet das, dass das Objekt für den Eintrag ein Autoren-Objekt enthält. Warum? Weil das Model aus einer objektorientierten Perspektive entworfen wird und es sich dabei um das offensichtlichste Design handelt.

Aus Sicht des Datenbankschemas würden Autoren in einer eigenen Tabelle abgespeichert werden. Das heißt, dass unser Entry-Objekt nicht auf genau eine einzige Datenbanktabelle abgebildet wird, sondern auf zwei Tabellen. Wir können das wohl lösen, indem wir in SQL einen Join auf die Tabellen anwenden, wenn wir die Daten abfragen. Verwenden wir einen Join, dann müssen wir die Autorendaten in ein Autorenobjekt filtern oder alternativ doch zwei SQL-Abfragen verwenden, um den zwei Objekten zu entsprechen. In beiden Fällen haben wir ein Problem, da unser Domain-Objekt von einer Klasse erbt, die an eine einzelne Datenbanktabelle gebunden ist, obwohl für das Objekt eigentlich zwei Tabellen abgefragt werden müssen.

Das Beispiel demonstriert eine unausweichliche Tatsache der Anwendungsentwicklung - Objekte lassen sich nur in sehr simplen Domain-Models 1:1 in Datenbanken abbilden (im Sinne von "ein Objekt - eine Tabelle"). Letztendlich benötigt jedes komplexere Model komplexere Logik, um diese beiden miteinander zu verbinden. Das führt uns zu einem jener Pattern, die hauptsächlich außerhalb der simplen Szenarios verwendet werden: dem Data Mapper. Übrigens, raten Sie mal, wer es definiert hat? Martin Fowler - und ja, Sie sollten sein Buch wirklich lesen, wenn Sie eine Ausgabe davon finden können...

Worauf will ich also hinaus? Nun, wenn wir den Datenbankzugriff bis an die Oberfläche gelangen lassen (zum Beispiel, indem wir von Zend_Db_Table erweitern, das nur eine einzelne Tabelle repräsentiert), stecken wir bald in einem inflexiblen Design fest, das nur schwer mit Unterschieden zwischen der Objekt-Domain und der Datenbanktabellen-Domain umgehen kann. Wenn zum Beispiel unser Entry-Domainobjekt Zend_Db_Table erweitert, ist es direkt an die Entries-Tabelle gebunden. Woher kommt dann der Autor? Autoren können nicht geladen werden, indem wir eine Abfrage an die Entries-Tabelle stellen, also müssen wir zusätzlichen Code mit hinein ziehen oder direkt SQL-Abfragen ablaufen lassen (damit sind wir wieder bei Joins oder zwei seperaten Abfragen) etc. All das zeigt uns, dass es einfach nicht funktionieren kann, wenn wir eine Klasse erweitern, die an nur eine Tabelle gebunden ist.

Die Alternative ist, unser Domainobjekt frei von jeglichen Datenbank-bezogenen Methoden zu halten und stattdessen alle Datenbankzugriffe in einem Data-Mapper zu verstecken, der sich um die Abbildung der Objekte auf Tabellen kümmert und der mit einer beliebigen Anzahl von Tabellen umgehen kann (lassen Sie einfach drei weitere Mapper auf ihn los). Damit sind unsere Domainobjekte vom Datenbankschema entkoppelt.

Lassen Sie uns einen Blick auf Fowlers Definition eines Data-Mappers werfen.

A layer of Mappers that moves data between objects and a database while keeping them independent of each other and the mapper itself.

Die Definition bestätigt unseren Verdacht: Domainobjekte wissen nicht einmal, dass eine Datenbank existiert, wenn ein Data-Mapper verwendet wird.

Falls Sie von diesem Abschnitt irgendetwas mitnehmen müssen, dann Folgendes: Klassen für den direkten Datenbankzugriff wie Zend_Db_Table zu erweitern ist für sehr einfache Domain-Models in Ordnung. Es geht leicht von der Hand, ist leicht zu verstehen und benötigt sehr wenig Code. In komplexeren Domain-Models jedoch, in dem Domain-Objekte andere Domain-Objekte enthalten können, wird dieser Weg der Vererbung nicht gut funktionieren. An diesem Punkt benötigen wir eindeutig eine bessere Lösung wie einen Data-Mapper und ein etwas komplexeres Klassendesign.

Ich habe mich mit voller Absicht strikt an Fowlers Patterns gehalten. Je mehr sich die einfache Vererbung als Problem herausstellt, desto wahrscheinlicher ist es, dass Entwickler ihr Design anpassen, um die mangelnde Flexibilität wettzumachen. In der Community des Zend Framework entstand schon öfters die Debatte, ob Models (oder eher: Domain-Objekte) eine is-a-Beziehung (über Vererbung) oder eine has-a-Beziehung (über Komposition) mit den Zend_Db-Klassen eingehen sollten. Von Zeit zu Zeit weicht die Diskussion auch ins Reich der Daten-Container und Gateways ab. Dabei wird eigentlich immer über ein offensichtliches Konzept gesprochen oder rund um ein solches gecodet - es handelt sich um halb- oder fast vollständige Lösungen auf dem Weg hin zum Data-Mapper. Die Lösung ist so offensichtlich, dass viele Entwickler einen kompletten Data-Mapper implementieren ohne jemals zu realisieren, dass dafür ein formaler Name und sehr viel Wissen zur Implementierung in der Literatur existieren!

9.3. Erforschung der Domain-Objekte

Mit diesem Lösungsansatz im Repertoire können wir uns nun daran machen, die Anforderungen des tatsächlichen Domain-Objekts zu erheben, das unsere Blog-Einträge repräsentieren soll. Wir wissen aus dem letzten Abschnitt, dass wir zumindest ein Entry-Domainobjekt benötigen. An diesem Punkt beschäftigen wir uns noch nicht mit Datenbankschemen und wir haben auch nur eine vage Vorstellung davon, welche Eigenschaften unsere Einträge besitzen müssen, also konzentrieren wir uns nur auf die Kernanforderungen.

Es ist ebenfalls wichtig, dass wir uns an dieser Stelle die Definition eines Domain-Objekts in Erinnerung rufen. Es repräsentiert eine eindeutig identifizierbare Entität mit einem Satz an Verhaltensweisen. Das Schlüsselwort dabei ist "eindeutig", da jedes Domain-Objekt eine einzelne Entität repräsentiert. Wollen wir eine Vielzahl ähnlicher Entitäten abbilden, benötigen wir eine Klasse, die eine Sammlung von Einträgen vorhält.

Unser Domain-Objekt wird zumindest die folgenden Eigenschaften zur Verfügung stellen:

  • id

  • title

  • content

  • published_date

  • author

Das sind nicht alle Eigenschaften, die wir benötigen werden, aber genug für den Anfang. Da wir inkrementelle Entwicklung betreiben, machen wir uns über zusätzliche Funktionalitäten erst Gedanken, wenn sie Teil einer tatsächlichen Anforderung geworden sind.

Wir können diese Eigenschaften beschreiben, indem wir Randbedingungen (constraints) aufstellen. Die Eigenschaften müssen diesen Bedingungen entsprechen, damit unser Domain-Objekt einen gültigen Eintrag repräsentieren kann. Mit der Validierung werden wir uns zwar nicht unmittelbar in diesem Kapitel beschäftigen, doch wir werden sie etwas später integrieren.

  • Die Id (id) ist eine positive, ganze Zahl (Integer) größer 0 und identifiziert den aktuellen Eintrag eindeutig.

  • Der Title (title) ist eine Zeichenkette, die nicht leer ist, Klartext oder XHTML enthält und einen eindeutigen Eintragstitel darstellt.

  • Der Inhalt (content) ist ebenfalls eine nicht leere Zeichenkette, und zwar Klartext oder Code zur Auszeichnung mit Doctype XHTML 1.0 Strict.

  • Der Titel und der Inhalt dürfen nur eine Teilmenge der XHTML-Tags und -Attribute enthalten. Diese Teilmenge wird über eine Whitelist definiert.

  • Der Autor (author) ist ein valides Author-Domain-Objekt, das den Autor des Eintrags repräsentiert.

  • Die Eigenschaft published_date ist ein Datum entsprechend des Standards ISO 8601.

Mittels dieser Randbedingungen kann das Domain-Objekt bestimmen, ob die übermittelten Daten einem gültigen Eintrag entsprechen. Wir wir feststellen, wird unser Eintrag eine Referenz auf einen Autor enthalten. Da all diese Entitäten durch Domain-Objekte dargestellt werden, erstellen wir ein ähnliches Profil für ein Author-Objekt. Dieses Objekt enthält folgende Eigenschaften:

  • id

  • username

  • fullname

  • email

  • url

9.4. Erforschung des Entry-Data-Mappers

Während das Entry-Domain-Objekt sich mit einem einzelnen Eintrag, dessen Eigenschaften und Gültigkeit beschäftigt, kümmert sich der Data-Mapper um die Persistenz dieser Objekte zwischen den Anfragen. Seine Aufgabe ist es, über eine Datenbankzugriffsschicht Daten der Domain-Objekte in der Datenbank anzulegen, zu lesen, zu aktualisieren und zu löschen (zusammengefasst werden diese oft als CRUD-Operationen bezeichnet: create, read, update, delete). Natürlich umfasst diese Aufgabe auch, die Eigenschaften der Domain-Objekte auf die entsprechenden Tabellen und Spaltennamen umzulegen. Der Mapper muss dies bewerkstelligen, ohne das Datenbankschema, die Zugriffsart oder die Mapping-Logik offenzulegen.

Er ist dafür verantwortlich, die Daten der Entität aus der Datenbank zu holen, um daraus ein Domain-Objekt zu erstellen und zurückzuliefern. Daher liegt es nahe, dass er alle Hilfsmethoden zur Verfügung stellt, die CRUD-Operationen und ihre Auswahlkriterien betreffen (die z.B. dem WHERE-Teil von SQL-Anfragen entsprechen oder dem Teil der Spaltenauswahl, weil eventuell nur bestimmte Informationen für eine Entität benötigt werden). Da hier so viel passiert, sollte verständlich sein, dass Domain-Objekte eher keine Aufrufe an den Data-Mapper absetzen werden. Stattdessen werden wir in unserer Anwendung den Data-Mapper verwenden und ihm die Domain-Objekte übergeben, um damit zu arbeiten. Dieses Design ist nicht unbedingt das am leichtesten verwendbare - wir benötigen in der Controller-Schicht unserer Anwendung aus offensichtlichen Gründen mehr Code, da sich durch die Einführung des Data-Mappers die Anzahl der Objekte verdoppelt.

Eine gängige Methode, um das Problem der großen Anzahl an Objekten einzudämmen ist, das Domain-Objekt von der Existenz ihres Data-Mappers in Kenntnis zu setzen und gleichzeitig abzusichern, dass diese Kenntnis nicht über die Data-Mapper-API hinausgeht. Denken Sie dran, dass wir in der objektorientierten Programmierung Code immer nur auf das Interface hin schreiben sollten, nie auf die Implementierung. Wir werden hier aber keine Abkürzungen nehmen - unser Domain-Model für das Bloggen ist simpel genug, so dass wir mit jeder Verkomplizierung unserer Lösung mehr Zeit für die Entwicklung aufwenden müssten, als es für diesen Data-Mapper angebracht ist.

9.5. Auswahl der Werkzeuge für die Implementierung

Nachdem wir unser Model genauer ausgearbeitet haben, können wir nun ermitteln, welche Funktionalitäten des Models wir in existierende Zend-Framework-Komponenten auslagern können.

9.5.1. Domain-Objekte

Für die Implementierung von Domain-Objekten in ihrer Grundform benötigt man nur eines. PHP 5. Alle Domain-Objekte sind einfach nur gute, alte PHP-Objekte ohne Besonderheiten. Ich weiß, das ist enttäuschend! Das Model sollte komplex sein, unmöglich zu verstehen, man sollte einen Doktor-Titel dafür brauchen. Stattdessen haben wir es in ein System von Objekten eingedampft, die für sich genommen nicht besonders kompliziert sind.

9.5.2. Validierungs- und Filter-Regeln

Bevor unser Model gespeichert werden kann, muss sichergestellt sein, dass die Daten des Models die von uns definierten Randbedingungen erfüllen und Regeln befolgen, dass sie also valide sind und alle Werte gefiltert wurden, wie es eben nötig ist. Wir könnten Validatoren in Form von Zend_Validate und Filter in Form von Zend_Filter verwenden, aber unsere Formulare für das Model würden dieses Regeln duplizieren. Vervielfältigung ist schlecht, und daher werden wir in logischer Folge Zend_Form-Instanzen als Basis verwenden, um diese Regeln zu implementieren.

Der Einsatz von Zend_Form an dieser Stelle kommt nicht ohne Fragen aus. Da die Klasse ein Formular repräsentiert, werden wir sie doch eindeutig häufig in unserer View verwenden. Vermischen wir damit nicht Model und View, verwenden die Klasse also in unangebrachter Weise? Das ist die eine Sichtweise. Die andere ist, dass Zend_Form-Instanzen eine Doppelrolle als Element der Präsentation und als Container für vom Model abgeleitete Validierungs- und Filterregeln wahrnehmen können. Das mag zwar nicht immer der Fall sein, und bei komplexen Models kann es sogar vorkommen, dass Formulare nur zu einem kleinen Teil den in einem Model vorgehaltenen Daten entsprechen. Doch unser Blog ist ziemlich simpel, also brauchen wir uns darüber kaum Gedanken zu machen. Von einem technischen Standpunkt aus wäre die ideale thereotische Lösung eine Formular-Klasse, die zwei unabhängige Teile enthält: einen Daten-Container mit Validatoren und Filtern (sehr nahe an einem Domain-Objekt) und einen Satz an Renderern, welche die Container in das Formular einer View transformieren können. Doch lassen wir das Wunschdenken beiseite (selbst die theoretisch perfekten Lösungen in diesem Gebiet sind mindestens so komplex wie Zend_Form, wenn nicht schlimmer), geben wir uns mit dem zufrieden was wir haben und passen wir es an unsere Bedürfnisse an.

Wir werden uns Zend_Form in einem späteren Kapitel ansehen und uns damit beschäftigen, wie es implementiert wird.

9.5.3. Datenbankzugriff

Da es sich hier um ein Zend-Framework-Buch handelt, werden wir natürlich Zend_Db verwenden. Genau genommen Zend_Db_Table_Abstract, welches das Table-Data-Gateway-Pattern aus Martin Fowlers POEAA implementiert. Dieses Gateway-Pattern kann definiert werden als eine Klasse, die den Zugriff auf eine Tabelle in einer Datenbank ermöglicht und es uns erlaubt, Inserts, Selects, Updates und Deletes auf jede Zeile oder auf Gruppen von Zeilen dieser Tabelle auszuführen. Fowler definiert dieses Pattern wie folgt::

An object that acts as a Gateway to a database table. One instance handles all the rows in the table.

Zend_Db bietet zudem eine Implementierung des Row-Data-Gateway-Pattern in Form von Zend_Db_Table_Row an. Das Pattern ist dem Table-Data-Gateway ähnlich mit der Ausnahme, dass es sich mit einer einzelnen Zeile einer Datenbanktabelle auseinandersetzt. In beiden Fällen bietet Zend_Db über seine öffentliche API einen abstrahierten Zugriff. Sie können SQL-Abfragen konstruieren, indem Sie Objekt-Methoden hintereinander aufrufen. Das wird bei beiden Pattern-Implementierungen so gemacht.

Wir werden für unser Model Data-Mapper stellen, die auf die Datenbank mittels Zend_Db_Table zugreifen (also die Option Table-Data-Gateway). Das stimmt mit dem Konzept eines Data-Mappers überein, der von vielen Domain-Objekten verwendet werden kann, aber von ihnen unabhängig bleibt, das heißt: er ist nicht mit einem spezifischen Domain-Objekt verbunden, kann aber spezifische, konkrete Subklassen für jedes Domain-Objekt anbieten, um eine für dieses Domain-Objekt spezifische Mapping-Logik umzusetzen.

Wichtig: unsere Domain-Objekte oder Mapper werden nie Zend_Db_Table erweitern, wie es im Referenzhandbuch vorgeschlagen wird. Dadurch würde unser Model auf eine Implementierung mit Datenbankzugriff festgenagelt. Dadurch müsste unser Model auch Kenntnis vom Speicher-Backend, also dem Objekt zur Persistenzhaltung der Daten, erlangen. Dadurch würden Entwickler ermuntert, Datenbank und Nicht-Datenbank-Code überall miteinander zu vermischen. Solange es nicht unser Ziel ist, wirklich nur Datenbankabstraktion zu verwenden, handelt es sich dabei nur um schlechtes objektorientiertes Design. Die Quintessenz daraus ist, dass wir Wert auf die Regel "composition over inheritance" legen, "Komposition vor Vererbung", eine fundamentale "Best Practice" in der objektorientierten Programmierung. All unsere Domain-Objekte werden eine "has-a" oder "has-many"-Beziehung mit anderen Klassen eingehen, ausgenommen vielleicht abstrakte Elternklassen oder Interfaces, mittels derer wir sichergehen können, dass alle Domain-Objekte in ihrer API zumindest einen ähnlichen Ansatz verfolgen.

9.6. Implementierung

Wie ich Sie bereits zu Beginn des Buches gewarnt habe, entwickle ich Code (abgesehen von kurzen Artikeln in meinem Blog) immer entsprechend des Test-Driven Design. Deswegen wird der gesamte folgenden Code Schritt für Schritt mittels Unit-Tests vorgestellt. Sehen Sie es positiv: immerhin haben Sie etwas, dass Sie ins Verzeichnis /tests schreiben können! Um das Setup für das initiale Testframework vorzunehmen, werfen Sie bitte einen Blick in Appendix C: Unit-Testing und Test-Driven Design (wird bald hinzugefügt).

9.6.1. Unit-Tests zur Testsuite hinzufügen

Die Tests für unser Model werden in /tests/ZFExt/Model gespeichert. Ein Test für unser Entry-Domain-Objekt zum Beispiel wird in der Datei /tests/ZFExt/Model/EntryTest.php abgelegt. Damit der Test ausgeführt wird, müssen wir in demselben Verzeichnis eine Datei AllTests.php mit folgendem Inhalt hinzufügen:

  • <?php
  • if (!defined('PHPUnit_MAIN_METHOD')) {
  • define('PHPUnit_MAIN_METHOD', 'ZFExt_Model_AllTests::main');
  • }
  • require_once 'TestHelper.php';
  • require_once 'ZFExt/Model/EntryTest.php';
  • class ZFExt_Model_AllTests
  • {
  • public static function main()
  • {
  • PHPUnit_TextUI_TestRunner::run(self::suite());
  • }
  • public static function suite()
  • {
  • $suite = new PHPUnit_Framework_TestSuite('ZFSTDE Blog Suite: Models');
  • $suite->addTestSuite('ZFExt_Model_EntryTest');
  • return $suite;
  • }
  • }
  • if (PHPUnit_MAIN_METHOD == 'ZFExt_Model_AllTests::main') {
  • ZFExt_Model_AllTests::main();
  • }

Während wir unser Model implementieren, werden Sie weitere Tests in dieser Datei hinzufügen müssen, damit sie ausgeführt werden. Sie können hinzugefügt werden, indem Sie dem Muster für die Suite ZFExt_Model_EntryTest folgen. Da es sich hierbei nicht um die Datei AllTests.php im obersten Verzeichnis handelt, sollten Sie dies in die "root"-Testdatei /tests/AllTests.php einfügen:

  • <?php
  • if (!defined('PHPUnit_MAIN_METHOD')) {
  • define('PHPUnit_MAIN_METHOD', 'AllTests::main');
  • }
  • require_once 'TestHelper.php';
  • require_once 'ZFExt/Model/AllTests.php';
  • class AllTests
  • {
  • public static function main()
  • {
  • PHPUnit_TextUI_TestRunner::run(self::suite());
  • }
  • public static function suite()
  • {
  • $suite = new PHPUnit_Framework_TestSuite('ZFSTDE Blog Suite');
  • $suite->addTest(ZFExt_Model_AllTests::suite());
  • return $suite;
  • }
  • }
  • if (PHPUnit_MAIN_METHOD == 'AllTests::main') {
  • AllTests::main();
  • }

Wie im Anhang beschrieben werden Tests gestartet, in dem Sie in der Konsole zu /tests/ZFExt/Model navigieren (oder /tests, um jeden einzelnen Test der gesamtem Anwendung ablaufen zu lassen) und folgenden Befehl aufrufen:

phpunit AllTests.php

9.6.2. Die Domain-Objekte

Da es sich bei unserem Entry-Domainobjekt nur um ein gewöhnliches Objekt handelt, können wir es zu Beginn als einen simplen Datencontainer behandeln. Wir sollten unsere Klassen immer mit einem Namespace versehen (nach der alten Art des Namespacings vor PHP 5.3). Daher werden wir den NamespaceZFExt_Model für alle Model-bezogenen Klassen verwenden. Das gilt auch für die Test-Dateien. Vorerst speichern wir alles im Verzeichnis /library. Lassen Sie uns mit ersten Tests beginnen, die überprüfen, ob wir Eigenschaften des Domain-Objekts setzen und eine Instanz eines Entry-Domain-Objekts mit einem Array von Daten erstellen können. Dies ist zu Beginn der Inhalt von /tests/ZFExt/Model/EntryTest.php:

  • <?php
  • require_once 'ZFExt/Model/Entry.php';
  • class ZFExt_Model_EntryTest extends PHPUnit_Framework_TestCase
  • {
  • public function testSetsAllowedDomainObjectProperty()
  • {
  • $entry = new ZFExt_Model_Entry;
  • $entry->title = 'My Title';
  • $this->assertEquals('My Title', $entry->title);
  • }
  • public function testConstructorInjectionOfProperties()
  • {
  • $data = array(
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author' => new ZFExt_Model_Author
  • );
  • $entry = new ZFExt_Model_Entry($data);
  • $expected = $data;
  • $expected['id'] = null;
  • $this->assertEquals($expected, $entry->toArray());
  • }
  • }

Das können wir nun in /library/ZFExt/Model/Entry.php implementieren (die Tests schlagen auf jeden Fall fehl, wenn die Klasse nicht geschrieben wurde!):

  • <?php
  • class ZFExt_Model_Entry
  • {
  • protected $_data = array(
  • 'id' => null,
  • 'title' => '',
  • 'content' => '',
  • 'published_date' => '',
  • 'author' => null
  • );
  • public function __construct(array $data = null)
  • {
  • if (!is_null($data)) {
  • foreach ($data as $name => $value) {
  • $this->{$name} = $value;
  • }
  • }
  • }
  • public function toArray()
  • {
  • return $this->_data;
  • }
  • public function __set($name, $value)
  • {
  • $this->_data[$name] = $value;
  • }
  • public function __get($name)
  • {
  • if (array_key_exists($name, $this->_data)) {
  • return $this->_data[$name];
  • }
  • }
  • }

Sie wundern sich vielleicht, warum nicht alle Eigenschaften als public deklariert wurden. Wenn man ein Array mit der Sichtbarkeit "protected" und PHPs magische Methoden (wie __set()) für den Datenzugriff verwendet, hat man den Vorteil, dass man beim Zugriff immer ein Gateway passiert. Dadurch können wir beim Setzen eines Wertes nach Belieben Überprüfungen ablaufen lassen und Ausnahmen werfen, falls Fehler auftreten.

Unser neues Objekt ist noch sehr einfach gehalten. Lassen Sie uns den Rest der magischen Standardmethoden hinzufügen, damit wir kontrollieren können, ob die Eigenschaften im geschützten Array gesetzt werden und ob sie zurückgesetzt werden, falls wir das benötigen. Unten werden nur die neuen Tests gezeigt.

  • <?php
  • require_once 'ZFExt/Model/Entry.php';
  • class ZFExt_Model_EntryTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testReturnsIssetStatusOfProperties()
  • {
  • $entry = new ZFExt_Model_Entry;
  • $entry->title = 'My Title';
  • $this->assertTrue(isset($entry->title));
  • }
  • public function testCanUnsetAnyProperties()
  • {
  • $entry = new ZFExt_Model_Entry;
  • $entry->title = 'My Title';
  • unset($entry->title);
  • $this->assertFalse(isset($entry->title));
  • }
  • }
  • <?php
  • class ZFExt_Model_Entry
  • {
  • protected $_data = array(
  • 'id' => null,
  • 'title' => '',
  • 'content' => '',
  • 'published_date' => '',
  • 'author' => null
  • );
  • public function __construct(array $data = null)
  • {
  • if (!is_null($data)) {
  • foreach ($data as $name => $value) {
  • $this->{$name} = $value;
  • }
  • }
  • }
  • public function toArray()
  • {
  • return $this->_data;
  • }
  • public function __set($name, $value)
  • {
  • $this->_data[$name] = $value;
  • }
  • public function __get($name)
  • {
  • if (array_key_exists($name, $this->_data)) {
  • return $this->_data[$name];
  • }
  • }
  • public function __isset($name)
  • {
  • return isset($this->_data[$name]);
  • }
  • public function __unset($name)
  • {
  • if (isset($this->_data[$name])) {
  • unset($this->_data[$name]);
  • }
  • }
  • }

Unser Domain-Objekt ist nun besser definiert. Momentan kann man ohne Einschränkung Eigenschaften setzen, aber unser Domain-Objekt benötigt nur die Eigenschaften, die im anfänglichen Datenarray als Schlüssel gesetzt sind. Wir können verhindern, dass nicht benötigte Eigenschaften gesetzt werden, und eine Ausnahme werfen, falls das passiert, indem wir wie folgt eine zusätzliche Kontrolle in die Methode __set()einbauen.

  • <?php
  • require_once 'ZFExt/Model/Entry.php';
  • class ZFExt_Model_EntryTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testCannotSetNewPropertiesUnlessDefinedForDomainObject()
  • {
  • $entry = new ZFExt_Model_Entry;
  • try {
  • $entry->notdefined = 1;
  • $this->fail('Setting new property not defined in class should'
  • . ' have raised an Exception');
  • } catch (ZFExt_Model_Exception $e) {
  • }
  • }
  • }
  • <?php
  • class ZFExt_Model_Entry
  • {
  • protected $_data = array(
  • 'id' => null,
  • 'title' => '',
  • 'content' => '',
  • 'published_date' => '',
  • 'author' => null
  • );
  • public function __construct(array $data = null)
  • {
  • if (!is_null($data)) {
  • foreach ($data as $name => $value) {
  • $this->{$name} = $value;
  • }
  • }
  • }
  • public function toArray()
  • {
  • return $this->_data;
  • }
  • public function __set($name, $value)
  • {
  • if (!array_key_exists($name, $this->_data)) {
  • throw new ZFExt_Model_Exception('You cannot set new properties'
  • . 'on this object');
  • }
  • $this->_data[$name] = $value;
  • }
  • public function __get($name)
  • {
  • if (array_key_exists($name, $this->_data)) {
  • return $this->_data[$name];
  • }
  • }
  • public function __isset($name)
  • {
  • return isset($this->_data[$name]);
  • }
  • public function __unset($name)
  • {
  • if (isset($this->_data[$name])) {
  • unset($this->_data[$name]);
  • }
  • }
  • }

Als nächstes soll unser Entry-Domain-Objekt ein Autoren-Objekt enthalten. Da jedes weitere Domain-Objekt unseren bisher in ZFExt_Model_Entry geschriebenen Code vermutlich duplizieren würde, sollten wir unsere Klasse refaktorisieren, damit sie alle möglicherweise wiederverwendbaren Methoden von einer Parent-Klasse erbt. Wir fügen daher nun eine neue Parent-Klasse hinzu namens ZFExt_Model_Entity hinzu, die diese Rolle übernimmt.

  • <?php
  • class ZFExt_Model_Entity
  • {
  • public function __construct(array $data = null)
  • {
  • if (!is_null($data)) {
  • foreach ($data as $name => $value) {
  • $this->{$name} = $value;
  • }
  • }
  • }
  • public function toArray()
  • {
  • return $this->_data;
  • }
  • public function __set($name, $value)
  • {
  • if (!array_key_exists($name, $this->_data)) {
  • throw new ZFExt_Model_Exception('You cannot set new properties'
  • . ' on this object');
  • }
  • $this->_data[$name] = $value;
  • }
  • public function __get($name)
  • {
  • if (array_key_exists($name, $this->_data)) {
  • return $this->_data[$name];
  • }
  • }
  • public function __isset($name)
  • {
  • return isset($this->_data[$name]);
  • }
  • public function __unset($name)
  • {
  • if (isset($this->_data[$name])) {
  • unset($this->_data[$name]);
  • }
  • }
  • }
  • <?php
  • class ZFExt_Model_Entry extends ZFExt_Model_Entity
  • {
  • protected $_data = array(
  • 'id' => null,
  • 'title' => '',
  • 'content' => '',
  • 'published_date' => '',
  • 'author' => null
  • );
  • }

Ein weiterer Lauf unserer Tests wird bestätigen, dass die Refaktorierung erfolgreich war.

Nun wollen wir eine ähnliche Klasse für Autoren hinzufügen. Um den neuen Test anzupassen, bearbeiten Sie die Datei /tests/ZFExt/Model/AllTests.php und fügen einen neuen Test unter /tests/ZFExt/Model/AuthorTest.php hinzu. Die Tests und die Klassen, die diese enthalten, werden denen des Entry-Domain-Objekts sehr ähnlich sein. Hier sind die ersten Tests, die jenen des Entry-Objekts entsprechen, aber die Eigenschaften des Autoren-Objekts berücksichtigen.

  • <?php
  • class ZFExt_Model_AuthorTest extends PHPUnit_Framework_TestCase
  • {
  • public function testSetsAllowedDomainObjectProperty()
  • {
  • $author = new ZFExt_Model_Author;
  • $author->fullname = 'Joe';
  • $this->assertEquals('Joe', $author->fullname);
  • }
  • public function testConstructorInjectionOfProperties()
  • {
  • $data = array(
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • );
  • $author = new ZFExt_Model_Author($data);
  • $expected = $data;
  • $expected['id'] = null;
  • $this->assertEquals($expected, $author->toArray());
  • }
  • public function testReturnsIssetStatusOfProperties()
  • {
  • $author = new ZFExt_Model_Author;
  • $author->fullname = 'Joe Bloggs';
  • $this->assertTrue(isset($author->fullname));
  • }
  • public function testCanUnsetAnyProperties()
  • {
  • $author = new ZFExt_Model_Author;
  • $author->fullname = 'Joe Bloggs';
  • unset($author->fullname);
  • $this->assertFalse(isset($author->fullname));
  • }
  • public function testCannotSetNewPropertiesUnlessDefinedInClass()
  • {
  • $author = new ZFExt_Model_Author;
  • try {
  • $author->notdefinedinclass = 1;
  • $this->fail('Setting new property not defined in class should'
  • . ' have raised an Exception');
  • } catch (ZFExt_Model_Exception $e) {
  • }
  • }
  • }

Hier ist die Implementierung, die alle neuen Tests erfüllt.

  • <?php
  • class ZFExt_Model_Author extends ZFExt_Model_Entity
  • {
  • protected $_data = array(
  • 'id' => null,
  • 'username' => '',
  • 'fullname' => '',
  • 'email' => '',
  • 'url' => ''
  • );
  • }

Um uns endgültig abzusichern, dass unser Interface an die Verwendung dieser Domain-Objekte gebunden ist, kontrollieren wir, dass ZFExt_Model_Entry nur ein ZFExt_Model_Author-Objekt akzeptiert, wenn die Eigenschaft author gesetzt wird. Wie üblich schreiben wir zuerst den Test und dann den Code, mittels dem dieser Test bestanden wird.

  • <?php
  • class ZFExt_Model_EntryTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testThrowsExceptionIfAuthorNotAnAuthorEntityObject()
  • {
  • $entry = new ZFExt_Model_Entry;
  • try {
  • $entry->author = 1;
  • $this->fail('Setting author should have raised an Exception'
  • . ' since value was not an instance of ZFExt_Model_Author');
  • } catch (ZFExt_Model_Exception $e) {
  • }
  • }
  • }
  • <?php
  • class ZFExt_Model_Entry extends ZFExt_Model_Entity
  • {
  • protected $_data = array(
  • 'id' => null,
  • 'title' => '',
  • 'content' => '',
  • 'published_date' => '',
  • 'author' => null
  • );
  • public function __set($name, $value)
  • {
  • if ($name == 'author' && !$value instanceof ZFExt_Model_Author) {
  • throw new ZFExt_Model_Exception('Author can only be set using'
  • . ' an instance of ZFExt_Model_Author');
  • }
  • parent::__set($name, $value);
  • }
  • }

Alles was wir hier getan haben ist, die Methode __set() der Elternklasse zu überschreiben, um eine neue Kontrolle hinzuzufügen, die dafür sorgt, dass unser Objekt als Wert für author nur ein Objekt des Typs ZFExt_Model_Author enthält. Wird nicht der Autor gesetzt, übergeben wir der Elternklasse die Kontrolle, um die Eigenschaft zu setzen.

Wir sind fürs Erste fertig! Wenden wir unsere Aufmerksamkeit nun unserer Implementierung des Data-Mappers zu, damit wir diese Objekte in einer Datenbank abspeichern oder sie von dort beziehen können.

9.6.3. Die Data-Mapper

Unsere Data-Mapper werden im Hintergrund Zend_Db_Table verwenden, weswegen ihre Funktion in diesem Design ist, typische CRUD-Operationen auszuführen. Später werden wir sehen, dass sie ebenfalls Träger von Methoden mit speziellerem Verwendungszweck (z.B. Abfragen von Datensätzen, die bestimmte Bedingungen erfüllen) sein können. Für den Moment konzentrieren wir uns aber darauf, sie erst einmal aufzusetzen. Im ersten Test, den wir erstellen, wird eine Instanz unseres Data-Mappers erzeugt und eine konfigurierte Instanz von Zend_Db_Table_Abstract erstellt, mit der gearbeitet werden kann. Wie Sie sehen werden, verwende ich keine echte Datenbank. Obwohl man am Beginn einiges an Code schreiben muss, verwende ich statt einem echten Zend_Db_Table_Abstract-Objekt ein Mock-Object (quasi einen Doppelgänger für den Test). Dadurch kann ich genau kontrollieren, was dieses Objekt macht, welche Rückgabewerte erzeugt werden, ich kann Erwartungen festlegen, welche Methoden aufgerufen werden sollten, mit welchen Argumenten das geschehen sollte etc. Der Hauptgrund warum ich das tue ist, dass eine reale Datenbank hier nichts Neues bietet - unsere Tests brauchen keine Datenbank. Würden wir eine Datenbank verwenden, würde es auch funktionieren, aber dann würden wir zusätzlich zu allem anderen auch noch Zend_Db_Table_Abstract testen, da es ja schließlich von unserem Code in Anspruch genommen wird. Zend Framework hat aber bereits Tests für diese Komponente.

Fügen Sie wie zuvor die Datei und Klasse zu /tests/ZFExt/Model/AllTests.php hinzu, um den Test zu Ihrer Suite hinzuzufügen. Die Tests selbst werden in /tests/ZFExt/Model/EntryMapperTest.php geschrieben.

  • <?php
  • class ZFExt_Model_EntryMapperTest extends PHPUnit_Framework_TestCase
  • {
  • protected $_tableGateway = null;
  • protected $_adapter = null;
  • protected $_rowset = null;
  • protected $_mapper = null;
  • public function setup()
  • {
  • $this->_tableGateway = $this->_getCleanMock(
  • 'Zend_Db_Table_Abstract'
  • );
  • $this->_adapter = $this->_getCleanMock(
  • 'Zend_Db_Adapter_Abstract'
  • );
  • $this->_rowset = $this->_getCleanMock(
  • 'Zend_Db_Table_Rowset_Abstract'
  • );
  • $this->_tableGateway->expects($this->any())->method('getAdapter')
  • ->will($this->returnValue($this->_adapter));
  • $this->_mapper = new ZFExt_Model_EntryMapper($this->_tableGateway);
  • }
  • protected function _getCleanMock($className) {
  • $class = new ReflectionClass($className);
  • $methods = $class->getMethods();
  • $stubMethods = array();
  • foreach ($methods as $method) {
  • if ($method->isPublic() || ($method->isProtected()
  • && $method->isAbstract())) {
  • $stubMethods[] = $method->getName();
  • }
  • }
  • $mocked = $this->getMock(
  • $className,
  • $stubMethods,
  • array(),
  • $className . '_EntryMapperTestMock_' . uniqid(),
  • false
  • );
  • return $mocked;
  • }
  • }

Alle Data-Mapper-Tests verwenden ein Mock-Objekt, eine Imitation von Zend_Db_Table_Abstract - die Komponente ist bereits vom Zend-Framework-Team getestet worden, also wäre es sinnlos, ein Objekt zu verwenden, das tatsächlich mit einer Datenbank verbunden ist. Wenn wir die Klasse in der Anwendung verwenden, werden wir normalerweise nicht eine echte Instanz an den Konstruktor übergeben, sondern wir können uns stattdessen darauf verlassen, dass der Konstruktor eine passende Instanz erzeugen wird. Das obige Grundgerüst für den Test ist darauf ausgelegt, eine voll funktionsfähige Imitation von Zend_Db_Table_Abstract zu erzeugen.

Auch wenn man die Information im Handbuch zu PHPUnit nur schwer findet ohne in den Quelltext einzutauchen, ermöglicht es die geschützte Methode _getCleanMock() die ich verwende, ein komplett "jungfräuliches" Mock-Objekt zu erzeugen, bei der alle herkömmlichen Methoden durch Imitationen ersetzt wurden.Sie erzeugt für das Imitat bei jedem Aufruf einen einzigartigen Namen, um sicherzustellen, dass es zu keinem Konflikt bei den Namen der imitierten Klassen kommt. Momentan ist nur ein Schritt für uns notwendig: wir müssen darauf achten, dass alle Zend_Db_Table_Abstract-Mock-Objekte auch ein Adapter-Imitat zurückgeben. Ansich gibt es nur einen Grund, ein Mock-Objekt des Adapters zu erstellen, nämlich die häufig verwendete Methode quoteInto(), mit der Werte in einer SQL-Expression oder einer Bedingung maskier werden.

Hier ist unsere anfängliche (bis jetzt ungetestete) Implementierung, die zeigt, warum es nicht wert ist, sich die Mühe mit dem Testen der echten Instanz anzutun - die Implementierung ist extrem einfach, und um es noch einmal zu betonen, wir würden einfach nur Zend_Db_Table_Abstract testen.

  • <?php
  • class ZFExt_Model_EntryMapper
  • {
  • protected $_tableGateway = null;
  • protected $_tableName = 'entries';
  • public function __construct(Zend_Db_Table_Abstract $tableGateway)
  • {
  • if (is_null($tableGateway)) {
  • $this->_tableGateway = new Zend_Db_Table($this->_tableName);
  • } else {
  • $this->_tableGateway = $tableGateway;
  • }
  • }
  • protected function _getGateway()
  • {
  • return $this->_tableGateway;
  • }
  • }

Im obigen Code setzen wir eine Instanz von Zend_Db_Table auf, mit der wir auf die Datenbank zugreifen. Obwohl diese Klasse als Abstract bezeichnet wird, enthält sie eigentlich keine abstrakten Methoden. Als einzige Konfiguration, die wir vorerst benötigen, müssen wir dieser Instanz mitteilen, welche Datenbanktabelle verwendet werden soll. Wir müssen keine Einstellungen für eine Datenbankverbindung übergeben, da wir später einen Standarddatenbankadapter von unserer Bootstrap setzen können.

Lassen Sie uns nun einige nützliche Methoden hinzufügen. Wir beginnen mit einer Methode, um ein neues Domain-Objekt zu speichern. Da wir Zend_Db_Table_Abstract nachgeahmt haben, definieren wir fürs Erste keine Zusicherungen (assertions). Die Zusicherungen ergeben sich in Wirklichkeit daraus, das wir Erwartungen in den Mock-Objekten definieren und kontrollieren, dass unser Mapper die erwartete Methode insert() von Zend_Db_Table_Abstract mit dem korrekten Daten-Array aufruft.

  • <?php
  • class ZFExt_Model_EntryMapperTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testSavesNewEntryAndSetsEntryIdOnSave() {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 2,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • $entry = new ZFExt_Model_Entry(array(
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author' => $author
  • ));
  • // Setze Erwartungen für das Mock-Objekt beim Aufruf von Zend_Db_Table::insert()
  • $insertionData = array(
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author_id' => 2
  • );
  • $this->_tableGateway->expects($this->once())
  • ->method('insert')
  • ->with($this->equalTo($insertionData))
  • ->will($this->returnValue(123));
  • $this->_mapper->save($entry);
  • $this->assertEquals(123, $entry->id);
  • }
  • // ...
  • }

Hier ist die Implementierung, die diesen Test besteht.

  • <?php
  • class ZFExt_Model_EntryMapper
  • {
  • protected $_tableGateway = null;
  • protected $_tableName = 'entries';
  • public function __construct(Zend_Db_Table_Abstract $tableGateway)
  • {
  • if (is_null($tableGateway)) {
  • $this->_tableGateway = new Zend_Db_Table($this->_tableName);
  • } else {
  • $this->_tableGateway = $tableGateway;
  • }
  • }
  • protected function _getGateway()
  • {
  • return $this->_tableGateway;
  • }
  • public function save(ZFExt_Model_Entry $entry)
  • {
  • $data = array(
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $entry->id = $this->_getGateway()->insert($data);
  • }
  • }

Das Speichern eines neuen Eintrags in die Datenbank beinhaltet den Aufruf von Zend_Db_Table_Abstract::insert() mit einem Array der Spaltennamen und Werte, die in die Datenbanktabelle "entries" eingefügt werden sollen. Der Name der Tabelle wird im Konstruktor des Data-Mappers gesetzt. Die id wird weggelassen, da sie über den Rückgabewert von Zend_Db_Table_Abstract::insert() gesetzt wird.

Wie Sie sehen können, kennt unser Mapper das Datenbankschema - er bildet die Eigenschaft id des Autoren-Objekts auf eine Tabellenspalte namens author_id ab. Das Domainobjekt weiß nicht, dass diese Datenbankspalte existiert. Der Rest der Autorendaten wird ignoriert, da sie in einer anderen Tabelle gespeichert werden und nicht neu sind. Eigentlich ist es recht offensichtlich: Sie können einen Autor nicht so speichern, da Autoren-Objekte nur durch einen zukünftigen Autoren-Data-Mapper gespeichert werden können.

Wir werden Einträge auch aktualisieren wollen. Sie sollten leicht aufzuspüren sein, da sie bereits einen existierenden Wert für die Id haben. Lassen Sie uns einen Test und die Implementierung für das Verhalten bei einer Aktualisierung hinzufügen. Auch diesmal werden wir Erwartungen in einem Mock-Objekt anstelle von Zusicherungen verwenden. Denken Sie daran, dass Erwartungen des Mock-Objekts natürlich kontrolliert werden. Falls irgendwelche Anforderungen wie die Anzahl der Methodenaufrufe oder ähnliches von unserer Implementierung nicht erfüllt werden, wird der Test also fehlschlagen.

  • <?php
  • class ZFExt_Model_EntryMapperTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testUpdatesExistingEntry() {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 2,
  • 'name' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • $entry = new ZFExt_Model_Entry(array(
  • 'id' => 1,
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author' => $author
  • ));
  • // Setze Erwartungen für das Mock-Objekt beim Aufruf von Zend_Db_Table::update()
  • $updateData = array(
  • 'id' => 1,
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author_id' => 2
  • );
  • // quoteInto() wird aufgerufen, um die Parameter des Adapters zu maskieren
  • $this->_adapter->expects($this->once())
  • ->method('quoteInto')
  • ->will($this->returnValue('id = 1'));
  • $this->_tableGateway->expects($this->once())
  • ->method('update')
  • ->with($this->equalTo($updateData), $this->equalTo('id = 1'));
  • $this->_mapper->save($entry);
  • }
  • // ...
  • }
  • <?php
  • class ZFExt_Model_EntryMapper
  • {
  • protected $_tableGateway = null;
  • protected $_tableName = 'entries';
  • public function __construct(Zend_Db_Table_Abstract $tableGateway)
  • {
  • if (is_null($tableGateway)) {
  • $this->_tableGateway = new Zend_Db_Table($this->_tableName);
  • } else {
  • $this->_tableGateway = $tableGateway;
  • }
  • }
  • protected function _getGateway()
  • {
  • return $this->_tableGateway;
  • }
  • public function save(ZFExt_Model_Entry $entry)
  • {
  • if (!$entry->id) {
  • $data = array(
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $entry->id = $this->getGateway()->insert($data);
  • } else {
  • $data = array(
  • 'id' => $entry->id,
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $where = $this->getGateway()->getAdapter()
  • ->quoteInto('entry_id = ?', $entry->id);
  • $this->getGateway()->update($data, $where);
  • }
  • }
  • }

Fügen wir eine weitere Methode hinzu, bevor wir Schluss machen - wir werden während der Entwicklung unserer Blogging-Anwendung weitere Methoden hinzufügen. Wir werden nicht nur Einträge speichern und aktualisieren, sondern zumindest auch löschen und abfragen.

Das stellt uns vor zumindest ein Problem, denn Einträge enthalten Autoren. Damit der EntryMapper ein Autoren-Objekt beziehen kann, müssen wir zuerst einen Data-Mapper für Autoren hinzufügen. Hier ist der komplette Satz an Tests und eine Implementierung für die Klasse ZFExt_Model_AuthorMapper (die meisten Tests sind denen sehr ähnlich, die wir bisher geschrieben haben - und einige werden wir uns bald für den Entry-Data-Mapper genauer ansehen).

  • <?php
  • class ZFExt_Model_AuthorMapperTest extends PHPUnit_Framework_TestCase
  • {
  • protected $_tableGateway = null;
  • protected $_adapter = null;
  • protected $_rowset = null;
  • protected $_mapper = null;
  • public function setup()
  • {
  • $this->_tableGateway = $this->_getCleanMock(
  • 'Zend_Db_Table_Abstract'
  • );
  • $this->_adapter = $this->_getCleanMock(
  • 'Zend_Db_Adapter_Abstract'
  • );
  • $this->_rowset = $this->_getCleanMock(
  • 'Zend_Db_Table_Rowset_Abstract'
  • );
  • $this->_tableGateway->expects($this->any())->method('getAdapter')
  • ->will($this->returnValue($this->_adapter));
  • $this->_mapper = new ZFExt_Model_AuthorMapper($this->_tableGateway);
  • }
  • public function testCreatesSuitableTableDataGatewayObjectWhenInstantiated()
  • {
  • $mapper = new ZFExt_Model_EntryMapper($this->_tableGateway);
  • $this->assertTrue($mapper->getGateway()
  • instanceof Zend_Db_Table_Abstract);
  • }
  • public function testSavesNewAuthorAndSetsAuthorIdOnSave() {
  • $author = new ZFExt_Model_Author(array(
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • // Setze Erwartungen für das Mock-Objekt beim Aufruf von Zend_Db_Table::insert()
  • $insertionData = array(
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • );
  • $this->_tableGateway->expects($this->once())
  • ->method('insert')
  • ->with($this->equalTo($insertionData))
  • ->will($this->returnValue(123));
  • $this->_mapper->save($author);
  • $this->assertEquals(123, $author->id);
  • }
  • public function testUpdatesExistingAuthor() {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 2,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • // Setze Erwartungen für das Mock-Objekt beim Aufruf von Zend_Db_Table::update()
  • $updateData = array(
  • 'id' => 2,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • );
  • $this->_adapter->expects($this->once())
  • ->method('quoteInto')
  • ->will($this->returnValue('id = 2'));
  • $this->_tableGateway->expects($this->once())
  • ->method('update')
  • ->with($this->equalTo($updateData), $this->equalTo('id = 2'));
  • $this->_mapper->save($author);
  • }
  • public function testFindsRecordByIdAndReturnsDomainObject()
  • {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 1,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • // Erwartetes Rowset-Ergebnis für den gefundenen Eintrag
  • $dbData = new stdClass;
  • $dbData->id = 1;
  • $dbData->fullname = 'Joe Bloggs';
  • $dbData->username = 'joe_bloggs';
  • $dbData->email = 'joe@example.com';
  • $dbData->url = 'http://www.example.com';
  • // Setze Erwartungen für das Mock-Objekt beim Aufruf von Zend_Db_Table::find()
  • $this->_rowset->expects($this->once())
  • ->method('current')
  • ->will($this->returnValue($dbData));
  • $this->_tableGateway->expects($this->once())
  • ->method('find')
  • ->with($this->equalTo(1))
  • ->will($this->returnValue($this->_rowset));
  • $entryResult = $this->_mapper->find(1);
  • $this->assertEquals($author, $entryResult);
  • }
  • public function testDeletesAuthorUsingEntryId()
  • {
  • $this->_adapter->expects($this->once())
  • ->method('quoteInto')
  • ->with($this->equalTo('id = ?'), $this->equalTo(1))
  • ->will($this->returnValue('author_id = 1'));
  • $this->_tableGateway->expects($this->once())
  • ->method('delete')
  • ->with($this->equalTo('id = 1'));
  • $this->_mapper->delete(1);
  • }
  • public function testDeletesAuthorUsingEntryObject()
  • {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 1,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • $this->_adapter->expects($this->once())
  • ->method('quoteInto')
  • ->with($this->equalTo('id = ?'), $this->equalTo(1))
  • ->will($this->returnValue('author_id = 1'));
  • $this->_tableGateway->expects($this->once())
  • ->method('delete')
  • ->with($this->equalTo('id = 1'));
  • $this->_mapper->delete($author);
  • }
  • protected function _getCleanMock($className) {
  • $class = new ReflectionClass($className);
  • $methods = $class->getMethods();
  • $stubMethods = array();
  • foreach ($methods as $method) {
  • if ($method->isPublic() || ($method->isProtected()
  • && $method->isAbstract())) {
  • $stubMethods[] = $method->getName();
  • }
  • }
  • $mocked = $this->getMock(
  • $className,
  • $stubMethods,
  • array(),
  • $className . '_AuthorMapperTestMock_' . uniqid(),
  • false
  • );
  • return $mocked;
  • }
  • }

Die Implementierung dieser Klasse entspricht jener für den Entry-Mapper und fügt die Methoden find() und delete() hinzu.

  • <?php
  • class ZFExt_Model_AuthorMapper
  • {
  • protected $_tableGateway = null;
  • protected $_tableName = 'authors';
  • protected $_entityClass = 'ZFExt_Model_Author';
  • public function __construct(Zend_Db_Table_Abstract $tableGateway)
  • {
  • if (is_null($tableGateway)) {
  • $this->_tableGateway = new Zend_Db_Table($this->_tableName);
  • } else {
  • $this->_tableGateway = $tableGateway;
  • }
  • }
  • protected function _getGateway()
  • {
  • return $this->_tableGateway;
  • }
  • public function save(ZFExt_Model_Author $author)
  • {
  • if (!$author->id) {
  • $data = array(
  • 'fullname' => $author->fullname,
  • 'username' => $author->username,
  • 'email' => $author->email,
  • 'url' => $author->url
  • );
  • $author->id = $this->_getGateway()->insert($data);
  • } else {
  • $data = array(
  • 'id' => $author->id,
  • 'fullname' => $author->fullname,
  • 'username' => $author->username,
  • 'email' => $author->email,
  • 'url' => $author->url
  • );
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $author->id);
  • $this->_getGateway()->update($data, $where);
  • }
  • }
  • public function find($id)
  • {
  • $result = $this->_getGateway()->find($id)->current();
  • $author = new $this->_entityClass(array(
  • 'id' => $result->id,
  • 'fullname' => $result->fullname,
  • 'username' => $result->username,
  • 'email' => $result->email,
  • 'url' => $result->url
  • ));
  • return $author;
  • }
  • public function delete($author)
  • {
  • if ($author instanceof ZFExt_Model_Author) {
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $author->id);
  • } else {
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $author);
  • }
  • $this->_getGateway()->delete($where);
  • }
  • }

Diesen neuen Autoren-Data-Mapper können wir in unserem Entry-Data-Mapper verwenden, um ein Autoren-Objekt zu holen und in das Entry-Objekt einzufügen, das von der neuen Methode find() zurückgegeben wird. Wir werden auch eine ähnliche Methode delete() einfügen.

Hier sind die Tests für das Finden eines Eintrags über die Eigenschaft id und für das Löschen eines Eintrags über die Id.

  • <?php
  • class ZFExt_Model_EntryMapperTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testFindsRecordByIdAndReturnsDomainObject()
  • {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 1,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • $entry = new ZFExt_Model_Entry(array(
  • 'id' => 1,
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author' => $author
  • ));
  • // Erwartetes Rowset-Ergebnis für den gefundenen Eintrag
  • $dbData = new stdClass;
  • $dbData->id = 1;
  • $dbData->title = 'My Title';
  • $dbData->content = 'My Content';
  • $dbData->published_date = '2009-08-17T17:30:00Z';
  • $dbData->author_id = 1;
  • // Setze Erwartungen für das Mock-Objekt beim Aufruf von Zend_Db_Table::find()
  • $this->_rowset->expects($this->once())
  • ->method('current')
  • ->will($this->returnValue($dbData));
  • $this->_tableGateway->expects($this->once())
  • ->method('find')
  • ->with($this->equalTo(1))
  • ->will($this->returnValue($this->_rowset));
  • // Erstelle ein Mock-Objekt von AuthorMapper - es hat eigene Tests
  • $authorMapper = $this->_getCleanMock('ZFExt_Model_AuthorMapper');
  • $authorMapper->expects($this->once())
  • ->method('find')->with($this->equalTo(1))
  • ->will($this->returnValue($author));
  • $this->_mapper->setAuthorMapper($authorMapper);
  • $entryResult = $this->_mapper->find(1);
  • $this->assertEquals($entry, $entryResult);
  • }
  • public function testDeletesEntryUsingEntryId()
  • {
  • $this->_adapter->expects($this->once())
  • ->method('quoteInto')
  • ->with($this->equalTo('id = ?'), $this->equalTo(1))
  • ->will($this->returnValue('entry_id = 1'));
  • $this->_tableGateway->expects($this->once())
  • ->method('delete')
  • ->with($this->equalTo('id = 1'));
  • $this->_mapper->delete(1);
  • }
  • public function testDeletesEntryUsingEntryObject()
  • {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 2,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • $entry = new ZFExt_Model_Entry(array(
  • 'id' => 1,
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author' => $author
  • ));
  • $this->_adapter->expects($this->once())
  • ->method('quoteInto')
  • ->with($this->equalTo('id = ?'), $this->equalTo(1))
  • ->will($this->returnValue('entry_id = 1'));
  • $this->_tableGateway->expects($this->once())
  • ->method('delete')
  • ->with($this->equalTo('id = 1'));
  • $this->_mapper->delete($entry);
  • }
  • // ...
  • }

Hier ist unsere Implementierung für diese zwei neuen Methoden. Wie die Tests es vorschreiben, können wir Einträge löschen, indem wir die Id als Integer-Wert oder das Domain-Objekt selbst übergeben.

  • <?php
  • class ZFExt_Model_EntryMapper
  • {
  • protected $_tableGateway = null;
  • protected $_tableName = 'entries';
  • protected $_entityClass = 'ZFExt_Model_Entry';
  • protected $_authorMapperClass = 'ZFExt_Model_AuthorMapper';
  • protected $_authorMapper = null;
  • public function __construct(Zend_Db_Table_Abstract $tableGateway)
  • {
  • if (is_null($tableGateway)) {
  • $this->_tableGateway = new Zend_Db_Table($this->_tableName);
  • } else {
  • $this->_tableGateway = $tableGateway;
  • }
  • }
  • protected function _getGateway()
  • {
  • return $this->_tableGateway;
  • }
  • public function save(ZFExt_Model_Entry $entry)
  • {
  • if (!$entry->id) {
  • $data = array(
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $entry->id = $this->_getGateway()->insert($data);
  • } else {
  • $data = array(
  • 'id' => $entry->id,
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $entry->id);
  • $this->_getGateway()->update($data, $where);
  • }
  • }
  • public function find($id)
  • {
  • $result = $this->_getGateway()->find($id)->current();
  • if (!$this->_authorMapper) {
  • $this->_authorMapper = new $this->_authorMapperClass;
  • }
  • $author = $this->_authorMapper->find($result->author_id);
  • $entry = new $this->_entityClass(array(
  • 'id' => $result->id,
  • 'title' => $result->title,
  • 'content' => $result->content,
  • 'published_date' => $result->published_date,
  • 'author' => $author
  • ));
  • return $entry;
  • }
  • public function delete($entry)
  • {
  • if ($entry instanceof ZFExt_Model_Entry) {
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $entry->id);
  • } else {
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $entry);
  • }
  • $this->_getGateway()->delete($where);
  • }
  • public function setAuthorMapper(ZFExt_Model_AuthorMapper $mapper)
  • {
  • $this->_authorMapper = $mapper;
  • }
  • }

Und endlich...wir haben eine funktionierende Implementierung des Data-Mappers! So sollte die finale Ausgabe von PHPUnit zur Testreihe aussehen.

PHPUnit 3.3.17 by Sebastian Bergmann.

.......................

Time: 0 seconds

OK (23 tests, 51 assertions)

9.6.4. Lazy-Loading von Domain-Objekten

Unsere Implementierung des Data-Mappers in ZFExt_Model_EntryMapper benötigt zwei SQL-Abfragen, um ein vollständiges Domain-Objekt zu erzeugen: eine Abfrage für den Eintrag selbst und eine weitere für den referenzierten Autor. Manchmal benötigen wir die Details zum Autor gar nicht; in diesem Fall ist die zusätzliche Abfrage überflüssig. Es wäre besser, den Data-Mapper so anzupassen, dass er die Autoren-Daten nur bei Bedarf lädt und uns so ab und zu den Weg zur Datenbank erspart (auch bekannt als "lazy loading").

Wir haben bereits gesehen, wie wir die Methode __set() überschreiben können, um einen Wert vor dem Setzen der Eigenschaft zu validieren. Wir können die Methode __get() verwenden, um eine ähnliche Funktionalität zu erreichen. Wir fangen den Versuch ab, auf das Autoren-Objekt in unserem Entry-Domain-Objekt zuzugreifen und starten eine Abfrage über ZFExt_Model_AuthorMapper, um das Objekt zu erhalten.

Da dadurch offensichtlich bereits getestetes Verhalten geändert wird, müssen wir zumindest einen Test für den Entry-Data-Mapper abändern. Wir müssen irgendwie die Id des Autors im Entry-Domain-Objekt speichern, damit wir etwas haben, was wir "lazy-loaden" können, und wir müssen sicher gehen, dass das nachträgliche Laden tatsächlich funktioniert. Hier sind die neuen, überarbeiteten Tests für den Entry-Mapper und das Entry-Domain-Objekt:

  • <?php
  • class ZFExt_Model_EntryMapperTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testFindsRecordByIdAndReturnsDomainObject()
  • {
  • $entry = new ZFExt_Model_Entry(array(
  • 'id' => 1,
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z'
  • ));
  • // Erwartetes Rowset-Ergebnis für den gefundenen Eintrag
  • $dbData = new stdClass;
  • $dbData->id = 1;
  • $dbData->title = 'My Title';
  • $dbData->content = 'My Content';
  • $dbData->published_date = '2009-08-17T17:30:00Z';
  • $dbData->author_id = 1;
  • // Setze Erwartungen für das Mock-Objekt beim Aufruf von Zend_Db_Table::find()
  • $this->_rowset->expects($this->once())
  • ->method('current')
  • ->will($this->returnValue($dbData));
  • $this->_tableGateway->expects($this->once())
  • ->method('find')
  • ->with($this->equalTo(1))
  • ->will($this->returnValue($this->_rowset));
  • $entryResult = $this->_mapper->find(1);
  • $this->assertEquals('My Title', $entryResult->title);
  • }
  • public function testFoundRecordCausesAuthorReferenceIdToBeSetOnEntryObject()
  • {
  • $entry = new ZFExt_Model_Entry(array(
  • 'id' => 1,
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z'
  • ));
  • // Erwartetes Rowset-Ergebnis für den gefundenen Eintrag
  • $dbData = new stdClass;
  • $dbData->id = 1;
  • $dbData->title = 'My Title';
  • $dbData->content = 'My Content';
  • $dbData->published_date = '2009-08-17T17:30:00Z';
  • $dbData->author_id = 5;
  • // Setze Erwartungen für das Mock-Objekt beim Aufruf von Zend_Db_Table::find()
  • $this->_rowset->expects($this->once())
  • ->method('current')
  • ->will($this->returnValue($dbData));
  • $this->_tableGateway->expects($this->once())
  • ->method('find')
  • ->with($this->equalTo(1))
  • ->will($this->returnValue($this->_rowset));
  • $entryResult = $this->_mapper->find(1);
  • $this->assertEquals(5, $entryResult->getReferenceId('author'));
  • }
  • // ...
  • }
  • <?php
  • class ZFExt_Model_EntryTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testAllowsAuthorIdToBeStoredAsAReference()
  • {
  • $entry = new ZFExt_Model_Entry;
  • $entry->setReferenceId('author', 5);
  • $this->assertEquals(5, $entry->getReferenceId('author'));
  • }
  • public function testLazyLoadingAuthorsRetrievesAuthorDomainObject()
  • {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 5,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • $entry = new ZFExt_Model_Entry;
  • $entry->setReferenceId('author', 5);
  • $authorMapper = $this->_getCleanMock('ZFExt_Model_AuthorMapper');
  • $authorMapper->expects($this->once())
  • ->method('find')
  • ->with($this->equalTo(5))
  • ->will($this->returnValue($author));
  • $entry->setAuthorMapper($authorMapper);
  • $this->assertEquals('Joe Bloggs', $entry->author->fullname);
  • }
  • protected function _getCleanMock($className) {
  • $class = new ReflectionClass($className);
  • $methods = $class->getMethods();
  • $stubMethods = array();
  • foreach ($methods as $method) {
  • if ($method->isPublic() || ($method->isProtected()
  • && $method->isAbstract())) {
  • $stubMethods[] = $method->getName();
  • }
  • }
  • $mocked = $this->getMock(
  • $className,
  • $stubMethods,
  • array(),
  • $className . '_EntryTestMock_' . uniqid(),
  • false
  • );
  • return $mocked;
  • }
  • // ...
  • }

Als Ausgangslage für die Implementierung ändern wir die Klasse ZFExt_Model_Entry so ab, dass sie die Referenz-Id des Autors zur späteren Verwendung annimmt. Da das Lazy-Loading innerhalb dieses Objekts geschieht, müssen wir die Kenntnis von ZFExt_Model_AuthorMapper, die ursprünglich ZFExt_Model_EntryMapper besitzt, in das Domain-Objekt selbst transferieren. Technisch gesehen können Referenzen in jedem Domain-Objekt auftreten, das sie benötigt, weswegen wir dieses Feature der Elternklasse ZFExt_Model_Entity hinzufügen können. ZFExt_Model_Entry kann diese Methoden der Elternklasse verwenden, um Informationen zu den Referenzen zu setzen oder auszulesen.

  • <?php
  • class ZFExt_Model_Entity
  • {
  • protected $_references = array();
  • // ...
  • public function setReferenceId($name, $id)
  • {
  • $this->_references[$name] = $id;
  • }
  • public function getReferenceId($name)
  • {
  • if (isset($this->_references[$name])) {
  • return $this->_references[$name];
  • }
  • }
  • }
  • <?php
  • class ZFExt_Model_Entry extends ZFExt_Model_Entity
  • {
  • protected $_data = array(
  • 'id' => null,
  • 'title' => '',
  • 'content' => '',
  • 'published_date' => '',
  • 'author' => null
  • );
  • protected $_authorMapperClass = 'ZFExt_Model_AuthorMapper';
  • protected $_authorMapper = null;
  • public function __set($name, $value)
  • {
  • if ($name == 'author' && !$value instanceof ZFExt_Model_Author ) {
  • throw new ZFExt_Model_Exception('Author can only be set using'
  • . ' an instance of ZFExt_Model_Author');
  • }
  • parent::__set($name, $value);
  • }
  • public function __get($name)
  • {
  • if ($name == 'author' && $this->getReferenceId('author')
  • && !$this->_data['author'] instanceof ZFExt_Model_Author) {
  • if (!$this->_authorMapper) {
  • $this->_authorMapper = new $this->_authorMapperClass;
  • }
  • $this->_data['author'] = $this->_authorMapper
  • ->find($this->getReferenceId('author'));
  • }
  • return parent::__get($name);
  • }
  • public function setAuthorMapper(ZFExt_Model_AuthorMapper $mapper)
  • {
  • $this->_authorMapper = $mapper;
  • }
  • }

Achten Sie auf die neue Methode __get(). Sie fängt alle Versuche ab, auf die Eigenschaft author des Domain-Objekts zuzugreifen. Falls das Objekt nicht bereits ein Autoren-Objekt enthält, versucht es eines aus der Datenbank zu laden, aber auch nur dann, wenn eine Referenz-Id (das heißt: eine Autoren-Id) gesetzt wurde, zum Beispiel als der Eintrag selbst geladen wurde. Anderenfalls wird der Wert null retourniert, was auch passieren sollte, wenn es sich um ein neues Objekt ohne einen Autor handelt.

Hier sehen Sie die überarbeitete Klasse ZFExt_Model_EntryMapper. Als einzige Änderung wurde das automatische Laden des Autoren-Objekts entfernt und durch das Setzen des Werts von author_id als Referenz im resultierenden Eintrag-Objekt ersetzt.

  • <?php
  • class ZFExt_Model_EntryMapper
  • {
  • protected $_tableGateway = null;
  • protected $_tableName = 'entries';
  • protected $_entityClass = 'ZFExt_Model_Entry';
  • public function __construct(Zend_Db_Table_Abstract $tableGateway)
  • {
  • if (is_null($tableGateway)) {
  • $this->_tableGateway = new Zend_Db_Table($this->_tableName);
  • } else {
  • $this->_tableGateway = $tableGateway;
  • }
  • }
  • protected function _getGateway()
  • {
  • return $this->_tableGateway;
  • }
  • public function save(ZFExt_Model_Entry $entry)
  • {
  • if (!$entry->id) {
  • $data = array(
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $entry->id = $this->_getGateway()->insert($data);
  • } else {
  • $data = array(
  • 'id' => $entry->id,
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $entry->id);
  • $this->_getGateway()->update($data, $where);
  • }
  • }
  • public function find($id)
  • {
  • $result = $this->_getGateway()->find($id)->current();
  • $entry = new $this->_entityClass(array(
  • 'id' => $result->id,
  • 'title' => $result->title,
  • 'content' => $result->content,
  • 'published_date' => $result->published_date
  • ));
  • $entry->setReferenceId('author', $result->author_id);
  • return $entry;
  • }
  • public function delete($entry)
  • {
  • if ($entry instanceof ZFExt_Model_Entry) {
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $entry->id);
  • } else {
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $entry);
  • }
  • $this->_getGateway()->delete($where);
  • }
  • }

Et voilá! Wir haben unseren Data-Mapper so modifiziert, dass er an geeigneten Stellen das Lazy-Loading von Objekten unterstützt. Ich gebe dazu, dass es sich hierbei eigentlich um einen Fall von frühzeitiger Optimierung handelt - wir haben keine Ahnung, ob diese Maßnahme die Leistung unserer Anwendung in irgendeiner Hinsicht steigern wird, da wir bisher keine Verbesserung messen können. Da ich das aber bereits bei früheren Gelegenheiten getan habe, kann ich davon ausgehen, dass es der Leistung unserer Anwendung zuträglich sein wird. Datenbankoperationen sind teuer, oft sogar die teuersten Operationen.

9.6.5. Doppelten Entitäten mit einer Identity-Map vorbeugen

Eine weitere Optimierungsmöglichkeit, wenn auch nicht vollständig auf die Leistung bezogen, ist die Verwendung einer Identity-Map. Damit ich erklären kann, was ich damit meine, stellen Sie sich bitte ein Szenario vor, in dem Sie 20 Einträge ausgelesen haben. Jeder Eintrag wurde über unseren Entry-Data-Mapper eingelesen, wobei der Autor bisher nicht gesetzt wurde, damit er - wie wir es gerade implementiert haben - über Lazy-Loading nachgeladen werden kann. Wie werden die Autoren geladen? Indem der Author-Data-Mapper verwendet wird, um sie aus der Datenbank zu beziehen. Für unsere Implementierung bedeutet das, dass mit jedem Eintrag, den wir laden, auch ein Autor geladen werden kann. Das klingt vernünftig, bis Sie sich die Beziehung zwischen Eintrag und Autor ansehen. Ein Autor kann viele Einträge schreiben, weswegen viele Einträge genau denselben Autor gemeinsam haben bzw. teilen werden. Das bedeutet, dass wir denselben Autor viele viele Male aus der Datenbank auslesen werden. Und das ist ganz offensichtlich ein Problem - unsere Domain-Objekte sollten so einzigartig wie möglich sein.

Auf den ersten Blick ergeben sich daraus keine problematischen Nebeneffekte außer dass wir eine Menge unnötiger Datenbankaufrufe absetzen. Doch was passiert, wenn wir die Entität eines Autors verändern? Wir haben ja eine ganze Menge davon! Wenn wir eine Entität ändern, ändern wir dabei nicht die anderen, weswegen Einträge innerhalb desselben Prozesses (= Seitenaufrufes) veraltete Autoreninformationen verwenden werden. Diese Asynchronität müssen wir eliminieren.

Eine einleuchtende Lösung dafür ist, jede einzigartige Entität für alle gemeinsam benutzbar zu machen. Wenn wir einen Autor in einem Eintrag laden und ein anderer Eintrag denselben Autor braucht, kann er irgendwie die ZFExt_Model_Author-Instanz des ersten Eintrags lokalisieren und verwenden. Die gebräuchlichste Lösung in diesem Bereich ist als Identity-Map-Pattern bekannt. Richtig, es handelt sich um ein weiteres von Martin Fowler definiertes Entwurfsmuster...das hat Fowler dazu zu sagen:

Ensures that each object gets loaded only once by keeping every loaded object in a map. Looks up objects using the map when referring to them.

Du bist unser Held, Martin! Selbst wenn es in dieser Definition vielleicht nicht sofort ersichtlich ist, handelt es sich bei der Identity-Map auch um eine Art von Cache. Sobald ein Domain-Objekt mit einer einzigartigen Id zum ersten Mal erzeugt oder geladen wird, wird sie in der Identity-Map registriert, damit andere Domain-Objekte diese Instanz verwenden können, falls sie ein Domain-Objekt mit derselben Id laden wollen. Der Data-Mapper muss dabei nun keine zusätzlichen Datenbankaufrufe mehr absetzen.

Da unsere Data-Mapper bereits das Abrufen und die Erstellung von Domain-Objekten bewerkstelligen, erscheinen sie als der logischste Ort für die Implementierung dieser Funktion. Da es sich dabei um eine allgemeine Map handelt (es gibt keine Implementierung speziell für einen Mapper), fügt man sie optimalerweise natürlich einer gemeinsamen Elternklasse hinzu, um die Duplizierung von Code zu vermeiden. Das macht mich aus noch einem anderen Grund glücklich - es ist die perfekte Ausrede, um alle Data-Mapper von einer gemeinsamen Klasse abzuleiten und jeglichen doppelten Code aus den beiden Data-Mappern in ihre gemeinsame Elternklasse zu verlegen.

Wenn wir schon dabei sind, können wir doppelten Code aus unseren beiden Data-Mappern in diese Elternklasse verschieben. Doch zuerst kommen die neuen Tests dran!

  • <?php
  • class ZFExt_Model_EntryMapperTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testFindsRecordByIdAndReturnsMappedObjectIfExists()
  • {
  • $entry = new ZFExt_Model_Entry(array(
  • 'id' => 1,
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z'
  • ));
  • // Erwartetes Rowset-Ergebnis für den gefundenen Eintrag
  • $dbData = new stdClass;
  • $dbData->id = 1;
  • $dbData->title = 'My Title';
  • $dbData->content = 'My Content';
  • $dbData->published_date = '2009-08-17T17:30:00Z';
  • $dbData->author_id = 1;
  • // Setze Erwartungen für das Mock-Objekt beim Aufruf von Zend_Db_Table::find()
  • $this->_rowset->expects($this->once())
  • ->method('current')
  • ->will($this->returnValue($dbData));
  • $this->_tableGateway->expects($this->once())
  • ->method('find')
  • ->with($this->equalTo(1))
  • ->will($this->returnValue($this->_rowset));
  • $mapper = new ZFExt_Model_EntryMapper($this->_tableGateway);
  • $result = $mapper->find(1);
  • $result2 = $mapper->find(1);
  • $this->assertSame($result, $result2);
  • }
  • public function testSavingNewEntryAddsItToIdentityMap() {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 2,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • $entry = new ZFExt_Model_Entry(array(
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author' => $author
  • ));
  • // Setze Erwartungen für das Mock-Objekt beim Aufruf von Zend_Db_Table::insert()
  • $insertionData = array(
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author_id' => 2
  • );
  • $this->_tableGateway->expects($this->once())
  • ->method('insert')
  • ->with($this->equalTo($insertionData))
  • ->will($this->returnValue(123));
  • $mapper = new ZFExt_Model_EntryMapper($this->_tableGateway);
  • $mapper->save($entry);
  • $result = $mapper->find(123);
  • $this->assertSame($result, $entry);
  • }
  • // ...
  • }

Der neue Test ähnelt dem, mit dem wir die Funktion der Data-Mapper-Methode find() getestet haben. Im Unterschied dazu starten wir diesmal einen zweiten Aufruf (ohne die Erwartung des Mock-Objekts zu ändern, dass Zend_Db_Table_Abstract nur einmal verwendet wird) und kontrollieren, ob es sich bei den daraus resultierenden Objekten um dasselbe handelt. PHPUnit geht so weit, dass es die Objekt-Ids kontrolliert, um sicherzustellen, dass beide Ergebnisse exakt dasselbe Objekt referenzieren. Wir erstellen zudem für jeden Test ein neues Mapper-Objekt, anstatt jenes aus der Eigenschaft $_mapper der Testklasse zu verwenden. Dadurch vermeiden wir, dass Aufrufe in anderen Tests Objekte in der Identity-Map erzeugen, was zu einem falsch-positiven Ergebnis führen könnte. Hier der zusätzliche Test, diesmal für ZFExt_Model_AuthorMapper.

  • <?php
  • class ZFExt_Model_AuthorMapperTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testFindsRecordByIdAndReturnsMappedObjectIfExists()
  • {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 1,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • // Erwartetes Rowset-Ergebnis für den gefundenen Eintrag
  • $dbData = new stdClass;
  • $dbData->id = 1;
  • $dbData->fullname = 'Joe Bloggs';
  • $dbData->username = 'joe_bloggs';
  • $dbData->email = 'joe@example.com';
  • $dbData->url = 'http://www.example.com';;
  • // Setze Erwartungen für das Mock-Objekt beim Aufruf von Zend_Db_Table::find()
  • $this->_rowset->expects($this->once())
  • ->method('current')
  • ->will($this->returnValue($dbData));
  • $this->_tableGateway->expects($this->once())
  • ->method('find')
  • ->with($this->equalTo(1))
  • ->will($this->returnValue($this->_rowset));
  • $mapper = new ZFExt_Model_AuthorMapper($this->_tableGateway);
  • $result = $mapper->find(1);
  • $result2 = $mapper->find(1);
  • $this->assertSame($result, $result2);
  • }
  • public function testSavingNewAuthorAddsItToIdentityMap() {
  • $author = new ZFExt_Model_Author(array(
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • // Setze Erwartungen für das Mock-Objekt beim Aufruf von Zend_Db_Table::insert()
  • $insertionData = array(
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • );
  • $this->_tableGateway->expects($this->once())
  • ->method('insert')
  • ->with($this->equalTo($insertionData))
  • ->will($this->returnValue(123));
  • $mapper = new ZFExt_Model_AuthorMapper($this->_tableGateway);
  • $mapper->save($author);
  • $result = $mapper->find(123);
  • $this->assertSame($result, $author);
  • }
  • // ...
  • }

Wir beginnen unsere Implementierung mit der Erstellung der gemeinsamen Elternklasse ZFExt_Model_Mapper. Sowohl ZFExt_Model_EntryMapper als auch ZFExt_Model_AuthorMapper werden diese Klasse erweitern.

  • <?php
  • class ZFExt_Model_Mapper
  • {
  • protected $_tableGateway = null;
  • protected $_identityMap = array();
  • public function __construct(Zend_Db_Table_Abstract $tableGateway)
  • {
  • if (is_null($tableGateway)) {
  • $this->_tableGateway = new Zend_Db_Table($this->_tableName);
  • } else {
  • $this->_tableGateway = $tableGateway;
  • }
  • }
  • protected function _getGateway()
  • {
  • return $this->_tableGateway;
  • }
  • protected function _getIdentity($id)
  • {
  • if (array_key_exists($id, $this->_identityMap)) {
  • return $this->_identityMap[$id];
  • }
  • }
  • protected function _setIdentity($id, $entity)
  • {
  • $this->_identityMap[$id] = $entity;
  • }
  • }

Nun stehen noch die Änderungen an, die an den beiden Data-Mappern durchgeführt werden müssen, damit neu abgefragte Objekte in der Identity-Map abgelegt und dann auch bevorzugt von dort abgefragt werden, anstatt der Datenbank einen weiteren Besuch abzustatten. Achten Sie darauf, dass die obigen Methoden _getGateway und __construct() aus den Data-Mapper-Klassen entfernt werden sollten, da sie von der neuen Elternklasse übernommen werden.

  • <?php
  • class ZFExt_Model_EntryMapper extends ZFExt_Model_Mapper
  • {
  • // ...
  • public function save(ZFExt_Model_Entry $entry)
  • {
  • if (!$entry->id) {
  • $data = array(
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $entry->id = $this->_getGateway()->insert($data);
  • $this->_setIdentity($entry->id, $entry); // add new
  • } else {
  • $data = array(
  • 'id' => $entry->id,
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $entry->id);
  • $this->_getGateway()->update($data, $where);
  • }
  • }
  • public function find($id)
  • {
  • if ($this->_getIdentity($id)) {
  • return $this->_getIdentity($id);
  • }
  • $result = $this->_getGateway()->find($id)->current();
  • $entry = new $this->_entityClass(array(
  • 'id' => $result->id,
  • 'title' => $result->title,
  • 'content' => $result->content,
  • 'published_date' => $result->published_date
  • ));
  • $entry->setReferenceId('author', $result->author_id);
  • $this->_setIdentity($id, $entry); // add retrieved
  • return $entry;
  • }
  • // ...
  • }
  • <?php
  • class ZFExt_Model_AuthorMapper extends ZFExt_Model_Mapper
  • {
  • // ...
  • public function save(ZFExt_Model_Author $author)
  • {
  • if (!$author->id) {
  • $data = array(
  • 'fullname' => $author->fullname,
  • 'username' => $author->username,
  • 'email' => $author->email,
  • 'url' => $author->url
  • );
  • $author->id = $this->_getGateway()->insert($data);
  • $this->_setIdentity($author->id, $author);
  • } else {
  • $data = array(
  • 'id' => $author->id,
  • 'fullname' => $author->fullname,
  • 'username' => $author->username,
  • 'email' => $author->email,
  • 'url' => $author->url
  • );
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $author->id);
  • $this->_getGateway()->update($data, $where);
  • }
  • }
  • public function find($id)
  • {
  • if ($this->_getIdentity($id)) {
  • return $this->_getIdentity($id);
  • }
  • $result = $this->_getGateway()->find($id)->current();
  • $author = new $this->_entityClass(array(
  • 'id' => $result->id,
  • 'fullname' => $result->fullname,
  • 'username' => $result->username,
  • 'email' => $result->email,
  • 'url' => $result->url
  • ));
  • $this->_setIdentity($id, $author);
  • return $author;
  • }
  • // ...
  • }

Damit haben wir die Implementierung unsere Domain-Models für dieses Kapitel weit genug vorangetrieben. Man könnte noch weitere Methoden hinzufügen und andere Probleme lösen. Wenn wir die Anwendung erweitern, werden wir auch das hier erstellte Fundament erweitern.

9.7. Fazit

Das war das erste Kapitel in diesem Buch, in dem wir uns eingehend mit Code befasst haben. Wie Sie nun wissen, liegt der Fokus weniger darauf, Wissen über Zend_Db und seine Unterklassen zu vermitteln (das Referenzhandbuch macht das sehr gut) als darüber, wie man diese Datenbankzugriffsklassen verwendet, wenn man ein Model entwirft. Ich habe Sie auch in einen weiteren Schwerpunkt dieses Buches eingeführt - die Verwendung von Tests, um die Entwicklung voranzutreiben. Im Voraus vorbereitete Code-Beispiele funktionieren im Allgemeinen gut, aber ich hoffe, dass der etwas längere Weg, den Code in den Kapiteln mittels Unit-Tests zu entwickeln, Sie dabei unterstützt zu verstehen, warum und wie wir Designentscheidungen treffen.

Ich hoffe auch, dass Sie ein potentielles Problem erkannt haben. Warum bauen wir uns von Grund auf einen eigenen Data-Mapper?

Ich habe in der Einleitung des Buches erwähnt, dass ich selten Skrupel habe, Zeter und Mordio zu schreien, wenn es nötig ist - und hier ist es nötig. PHP bietet Bibliotheken für diese Art von Problemen an. Es gibt da draußen großartige Data-Mapper-Bibliotheken, viele ORM-Bibliotheken, und sogar im Zend-Framework-Inkubator findet sich eine vollständige Data-Mapper-Lösung in Entwicklung. Wir sollten sie verwenden, solange es keine gewichtigen Gründe gibt, die dagegen sprechen. Zend_Db implementiert das Row-Data-Gateway und das Table-Data-Gateway-Pattern, aber außer bei sehr einfach gehaltenen Anwendungen ist seine Implementierung sehr zeitaufwändig. Kurz gesagt, wenn Sie Ihren Verstand behalten, Zeit in der Entwicklung und langfristig Geld bei Projekten sparen möchten, verwenden Sie bei allem Komplexeren als einem Blog stattdessen eine externe Bibliothek (oder warten Sie auf die ZF-eigene Data-Mapper-Lösung). Ich weiß, dass das harsch klingt, und wahrscheinlich haben Sie nicht erwartet das zu hören, aber es muss gesagt werden, solange Sie sich noch im seichten Teil des Beckens befinden.

Verliert das Zend Framework dadurch gegenüber seinen Alternativen wie Symfony oder Ruby On Rails an Wert? Nein! Symfony verwendet selbst eine externe ORM-Bibliothek, die einfach direkt mit dem Framework ausgeliefert wird - niemand hält Sie davon ab, eine ähnliche Bibliothek (oder dieselbe - Doctrine ist sehr gut und ich verwende es selbst) in Ihren Anwendungen zu verwenden. Ruby On Rails verwendet eine ActiveRecord-Implementierung, die an die Datenbankschicht gebunden ist, aber das hat die Ruby-Community nicht davon abgehalten, Lösungen wie den Data-Mapper von merb zu entwickeln, damit die Objekte nicht so eng an das Datenbankschema gebunden sind. Es wird interessant sein zu beobachten, wie Rails 3.0 dadurch beeinflusst wird, da merb agnostisch gegenüber allen Lösungen bleibt, die ein System mit Plugin-Fähigkeiten bevorzugen. Wenn Sie sonst nichts hiervon mitnehmen, dann behalten Sie im Gedächtnis, dass ein Framework Ihnen eine Menge an Funktionalität anbietet, aber dass Sie nie dazu verpflichtet sein sollten, alle Funktionen davon zu verwenden, wenn es andere, passendere Bibliothek für diese Funktion existiert. In einem zukünftigen Kapitel werden wir uns noch mehr mit einer dieser Alternativen zu Zend_Db beschäftigen.

Was also ist die Essenz dieses Kapitels? Nur weil bereits Lösungen existieren, mit denen man sofort loslegen kann, heißt das nicht, dass wir ihre Funktionsweise nicht verstehen und sie nicht selbst implementieren können. Ein Projekt kann zu simpel sein (für ein simples Skript würden Sie keine ORM-Bibliothek verwenden), zu viele Altlasten mit sich herumschleppen, als dass sich etwas anderes als eine einfache Abstraktion bezahlt machen würde, vielleicht schreibt Ihnen jemand von einer oberen Stufe der Nahrungskette vor, was Sie verwenden müssen, oder Sie wollen aus anderen Gründen eine eigene Lösung erschaffen. Ein gutes Beispiel dafür sind Systeme, in denen nicht in eine relationale Datenbank gespeichert wird - etwas, was immer häufiger vorkommt, seit sich dokumenten- und objektorientierte Alternativen zu materialisieren begonnen haben. Warum auch immer Sie es tun wollen, dieses Kapitel sollte Ihnen zeigen, wie Sie eine bessere Lösung als das einfache Zend_Db erzeugen können.