Modulprogrammierung 2: Datenbankunterstützung
Aus PapayaCMS
|
Tutorial |
|
| Zusammenfassung | Dieses Tutorial beschreibt, wie man Datenbankunterstützung zu papaya-CMS-Modulpaketen hinzufügt. |
| Zielgruppe | PHP-Entwickler |
| Schwierigkeitsgrad | Fortgeschrittene |
| Softwarevoraussetzungen | SVN-Checkout von papaya CMS; Anleitung hier PHPUnit >= 3.5 für Unit-Tests |
| Datum | 2010-05-11 |
| Vorheriges Tutorial | Modulprogrammierung 1: Content-Module |
| Nächstes Tutorial | Modulprogrammierung 3: Administrationsoberfläche |
In diesem zweiten Teil des grundlegenden Tutorials zur Modulprogrammierung schauen wir uns an, wie man Datenbankunterstützung zu einem Modulpaket hinzufügt. Wenn die papaya-Modulprogrammierung für Sie neu ist, lesen Sie zuerst hier den ersten Teil.
Bitte beachten Sie, dass Sie sich an die Papaya CMS Coding Standards halten sollten, besonders, wenn Sie vorhaben, Ihre Module der papaya-Community zur Verfügung zu stellen.
In diesem Tutorial werden Unit-Tests verwendet, eine sehr empfehlenswerte Methode der Softwareentwicklung. Damit Sie ein papaya CMS mit dem PHPUnit-Framework und der Klasse PapayaTestCase haben, benötigen Sie einen SVN-Checkout des Systems. Wie Sie ihn erhalten, erfahren Sie hier.
Inhaltsverzeichnis |
Übersicht über Dateien und Verzeichnisse
Nach dem Durcharbeiten dieses Tutorials werden Sie in papaya-lib/modules/special/myproject/tutorial die folgenden Dateien und Unterverzeichnisse erstellt haben (neue Dateien aus dem aktuellen Teil sind fett hervorgehoben):
+ [DATA] | | | + table_tutorial_planets.xml | + [Hello] | | | + Connector.php | | | + [Page] | | | | | + Base.php | | | + Page.php | | | + [Planet] | | | | | + [Database] | | | | | + Access.php | | | + Planet.php | + modules.xml Hinweis: Namen in eckigen Klammern bezeichnen Verzeichnisse
Im Verzeichnis testing/tests-unittests/papaya-lib/modules/special/myprojects/tutorial werden Sie die folgende Struktur erstellt haben (neue Dateien aus dem aktuellen Teil sind fett hervorgehoben):
+ [Hello] | + ConnectorTest.php | + [Page] | | | + BaseTest.php | + [Planet] | | | + [Database] | | | + AccessTest.php | + PlanetTest.php | + BaseTest.php Hinweis: Namen in eckigen Klammern bezeichnen Verzeichnisse
Die Datenbankzugriffsklasse vorbereiten und schreiben
Die meisten Webanwendungen verwenden eine Datenbank, um Inhalte daraus zu laden oder Daten darin zu speichern. papaya CMS enthält eine eigene Datenbankzugriffsschnittstelle, die wir in diesem Abschnitt des Tutorials untersuchen werden. Unser Ziel besteht darin, "World" in der Überschrift "Hello World" durch einen ausgewählten Planeten zu ersetzen, der aus einer Datenbanktabelle geladen wurde.
Die Datenbanktabelle einrichten
Der erste Schritt in Richtung Datenbankunterstützung in einem Modulpaket besteht darin, die Datenbanktabelle oder -tabellen zu definieren, die Sie verwenden möchten. Die Tabelle mit Planeten, die wir für unser Modul verwenden, besteht nur aus zwei Spalten:
Spalte | SQL-Datentyp | Bemerkungen ------------+---------------+-------------------------------------------------- planet_id | int | Primärschlüssel zur Identifikation eines Planeten planet_name | varchar(30) | Name des Planeten
Sie können Ihr übliches Datenbank-Administrationstool verwenden, um diese Tabelle zu erstellen. Wählen Sie einfach die Datenbank Ihrer papaya-CMS-Installation aus und führen Sie folgende Abfrage aus, um die Tabelle zu erzeugen (bitte beachten Sie, dass dies eine MySQL-Abfrage ist, die für andere von papaya unterstützte Datenbanken angepasst werden muss):
CREATE TABLE papaya_tutorial_planets ( planet_id int AUTO_INCREMENT, planet_name varchar(30), PRIMARY KEY (planet_id) )
Bitte beachten: Falls Sie für Ihre papaya-Installation ein anderes Tabellenpräfix als papaya gewählt haben, müssen Sie dieses im Tabellennamen verwenden.
Als Nächstes müssen Sie die Tabelleninformations-XML-Datei erstellen. Fügen Sie eine Zeile zum Bereich tables Ihrer modules.xml-Datei hinzu (oder fügen Sie diesen Bereich überhaupt hinzu, unter dem schließenden </modules>-Tag); das Ganze muss so aussehen:
<tables> <table name="tutorial_planets" /> </tables>
Navigieren Sie zum Bereich Module im papaya-Backend und klicken Sie die Schaltfläche Module suchen an. Klicken Sie nach erfolgter Suche auf das Modulpaket Hello World tutorial. Im Kasten Paketinhalt befindet sich nun der Abschnitt Tabellen. Klicken Sie auf das Plus-Zeichen, um den Bereich zu öffnen, und wählen Sie dann die Tabelle papaya_tutorial_planets aus. Sie erhalten die Fehlermeldung Konnte Tabellenstrukturdatei nicht laden! Klicken Sie in der Untersymbolleiste des Bereichs Module auf Tabelle exportieren, und die XML-Datei für die Tabelle wird erzeugt und auf Ihrem Computer gespeichert.
Der Inhalt dieser Datei sieht so aus (denken Sie daran, dass Sie diese Dateien niemals von Hand schreiben oder ändern sollten; verwenden Sie stets die oben beschriebene Export-Funktionalität):
<?xml version="1.0" encoding="ISO-8859-1" ?> <table name="tutorial_planets" prefix="yes"> <fields> <field name="planet_id" type="integer" size="4" null="no" autoinc="yes"/> <field name="planet_name" type="string" size="30" null="yes"/> </fields> <keys> <primary-key> <field>planet_id</field> </primary-key> </keys> </table>
Sie müssen das Datum aus dem Dateinamen entfernen (vorher: ein Name wie table_tutorial_planets_2010-04-30.xml; nachher: table_tutorial_planets.xml). Speichern Sie diese Datei dann in einem Unterverzeichnis von tutorial namens DATA. Klicken Sie die Tabelle erneut im Backend an, und die Tabellenstruktur sollte angezeigt werden. Sie müssen diese gesamte Prozedur jedes Mal wiederholen, wenn Sie Tabellen hinzufügen oder ändern.
Eine öffentliche Klasse für den Datenbankzugriff schreiben
Bevor wir die eigentliche Low-Level-Datenbankzugriffsklasse schreiben, implementieren wir eine Klasse, die ein Planet-Objekt modelliert. Gemäß unserem Top-Down-Ansatz ist die erste Implementierung dieser Klasse statisch. Wenn wir später die Datenbankzugriffsklasse implementieren, werden wir diese Klasse modifizieren: Die statische Implementierung wird durch Aufrufe der Datenbankklasse ersetzt. Dieser Ansatz hilft erneut, die öffentliche Schnittstelle vor der internen Logik zu implementieren.
Natürlich sollten Sie sich wieder an den Test-First-Ansatz halten, das heißt jeweils einen Test und die zugehörige Methode implementieren. Aber da Sie dieses Konzept bereits kennen, sehen Sie hier die vollständige Testklasse (die unter tutorial/Hello/PlanetTest.php in Ihrem Testverzeichnis gespeichert wird):
<?php require_once(substr(dirname(__FILE__), 0, -52).'/Framework/PapayaTestCase.php'); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet.php'); class HelloPlanetTest extends PapayaTestCase { /** * Get the HelloPlanet object to be tested * * @return HelloPlanet */ private function getPlanetObjectFixture() { return new HelloPlanet(); } /** * @covers HelloPlanet::getById */ public function testGetById() { $planetObject = $this->getPlanetObjectFixture(); $expected = array('planet_id' => 1, 'planet_name' => 'Mars'); $this->assertEquals($expected, $planetObject->getById(1)); } /** * @covers HelloPlanet::getAll */ public function testGetAll() { $planetObject = $this->getPlanetObjectFixture(); $expected = array(1 => 'Mars', 2 => 'Jupiter', 3 => 'Saturn'); $this->assertEquals($expected, $planetObject->getAll()); } /** * @covers HelloPlanet::create */ public function testCreate() { $planetObject = $this->getPlanetObjectFixture(); $data = array('planet_name' => 'Jupiter'); $this->assertTrue($planetObject->create($data)); } /** * @covers HelloPlanet::update */ public function testUpdate() { $planetObject = $this->getPlanetObjectFixture(); $data = array('planet_name' => 'Saturn'); $this->assertTrue($planetObject->update(3, $data)); } /** * @covers HelloPlanet::delete */ public function testDelete() { $planetObject = $this->getPlanetObjectFixture(); $this->assertTrue($planetObject->delete(2)); } }
Wie Sie an den Testmethoden sehen, stellt diese Klasse die öffentliche Schnittstelle zu klassischen Datenbankzugriffsmethoden zur Verfügung (sogenannte CRUD-Methoden für Create, Read, Update und Delete). Hier sehen Sie die statische Implementierung der Klasse -- speichern Sie sie unter tutorial/Hello/Planet.php in Ihrem Modulpaket-Verzeichnis:
<?php /** * Hello World tutorial, Planet class * * The Planet class provides the public interface for planets to be stored in * and loaded from database. * * @package Papaya-Modules * @subpackage tutorial */ /** * Hello World tutorial, Planet class * * @package Papaya-Modules * @subpackage tutorial */ class HelloPlanet { /** * Get a planet by id * * @param integer $planetId id of the planet to load * @return mixed array planet data if id exists, FALSE otherwise */ public function getById($planetId) { return array('planet_id' => 1, 'planet_name' => 'Mars'); } /** * Get all planets * * @return mixed array planet data if planets exist, FALSE otherwise */ public function getAll() { return array(1 => 'Mars', 2 => 'Jupiter', 3 => 'Saturn'); } /** * Add a new planet to the database * * @param array $data planet data * @return boolean TRUE on success, FALSE otherwise */ public function create($data) { return TRUE; } /** * Modify an existing planet * * @param integer $planetId id of the planet to modify * @param array $data modified planet data * @return boolean TRUE on success, FALSE otherwise */ public function update($planetId, $data) { return TRUE; } /** * Delete a specific planet by id * * @param integer $planetId * @return boolean TRUE on success, FALSE otherwise */ public function delete($planetId) { return TRUE; } }
Einführung in den papaya-Datenbankzugriff
Als Nächstes implementieren wir die Datenbankzugriffsklasse selbst. Der Datenbankzugriff wird durch die Klasse PapayaDatabaseObject (aus der Datei Object.php im Verzeichnis papaya-lib/system/Papaya/Database) bereitgestellt. Sie können diese Klasse für Ihre eigenen Datenbankzugriffsklassen erweitern; sie stellt unter anderem die folgenden Methoden bereit:
- databaseGetTableName($name, $usePrefix) ermittelt den Namen einer Datenbanktabelle. Normalerweise wird der Boolean-Wert $usePrefix auf TRUE gesetzt, so dass der Wert der Konfigurationskonstante PAPAYA_DB_TABLEPREFIX zum Basisnamen hinzugefügt wird. Da das Präfix in der papaya-Konfigurationsdatei conf.inc.php geändert werden kann, ist dies eine sichere Methode, stets die korrekten Tabellennamen zu verwenden.
- databaseQueryFmt($sql, $params, $limit = NULL, $offset = NULL) führt die im String $sql angegebene SQL-Abfrage aus. $sql ist ein Formatstring im printf-Stil, $params dagegen ein Array mit Werten, die Sie anstelle der Formatplatzhalter einsetzen möchten. Sie sollten die Datenbanktabellennamen und alle dynamischen Werte mithilfe dieser Platzhalter angeben. Die optionalen Parameter $limit und $offset begrenzen die maximale Anzahl der Ergebnisdatensätze, angefangen beim angegebenen Offset (dessen Standardwert 0 ist, das heißt der erste Datensatz in der von der Abfrage definierten Reihenfolge). Diese Methode ist schreibgeschützt, so dass sie nur für SELECT-Abfragen geeignet ist.
- databaseQueryFmtWrite($sql, $params, $limit = NULL, $offset = NULL) ist die Schreib-Lese-Version von databaseQueryFmt( ). Sie brauchen sie nur für besonders komplexe Änderungsabfragen zu benutzen; in den meisten Fällen sind die unten beschriebenen Methoden ausreichend.
- databaseInsertRecord($table, $keyField = NULL, $data) wird verwendet, um einen Datensatz in eine Tabelle einzufügen. Das erste Argument ist der Name der Tabelle; es folgt der optionale Name des Primärschlüsselfeldes (verwenden Sie es nur bei ganzzahligen Auto-Inkrement-Primärschlüsseln; im Erfolgsfall gibt die Methode dann den Index des neuen Datensatzes zurück). Das dritte Argument ist ein assoziatives Array mit den Daten, die Sie in die Tabelle einfügen möchten; die Schlüssel sind die Feldnamen. Der Rückgabewert ist im Fehlerfall FALSE, andernfalls hängt er davon ab, ob Sie einen Primärschlüssel angegeben haben oder nicht. Um auf Erfolg zu prüfen, vergleichen Sie den Wert mithilfe der strikten Identitätsprüfung (===) und nicht etwa per einfacher Gleichheit (==) mit den Wert FALSE.
- databaseInsertRecords($table, $data) ist eine Multi-Datensatz-Variante von databaseInsertRecord. Das Argument $table ist der Tabellenname, während $data ein Array mit assoziativen Arrays ist, von denen jedes einen Datensatz enthält, wie er für die vorige Methode beschrieben wurde. Der Rückgabewert ist im Fehlerfall FALSE und im Erfolgsfall die Anzahl der eingefügten Zeilen.
- databaseUpdateRecord($table, $data, $field, $value = NULL) ändert Datensätze, die einer bestimmten Bedingung entsprechen. Sie geben den Tabellennamen, ein Array mit Daten wie oben bei databaseInsertRecord( ) beschrieben und dann die Bedingung an, letztere entweder als einzelnes Feld und einzelnen Wert oder aber als assoziatives Array mit Feld-Wert-Paaren. Der Rückgabewert ist FALSE im Fehlerfall oder die Anzahl der geänderten Zeilem bei Erfolg (0 Zeilen können durchaus Erfolg bedeuten, also denken Sie daran, den Rückgabewert mit === zu überprüfen).
- databaseDeleteRecord($table, $field, $value = NULL) löscht Datensätze, die einer Bedingung entsprechen; der Parameter $field, der optionale Parameter $value und der Rückgabewert funktionieren genau wie bei databaseUpdateRecord( ).
- databaseGetSQLCondition($field, $value = NULL) erzeugt eine Bedingung, die in databaseQueryFmt( ) oder databaseQueryFmtWrite( ) verwendet werden kann. Genau wie bei databaseUpdateRecord( ) können Sie entweder ein einzelnes Feld und einen Wert (der auch ein Array von Werten sein kann, was eine Bedingung ergibt, die auf jeden der angegebenen Werte passt) angeben, oder aber ein assoziatives Array mit Feld-Wert-Paaren. Normalerweise werden mehrere Feld-Wert-Bedingungen mithilfe des Operators AND verknüpft, aber Sie können den String 'OR' als Array-Wert ohne bestimmten Schlüssel zwischen zwei Feld-Wert-Paaren einfügen, um stattdessen OR zu verwenden.
Unit-Test für die Datenbankzugriffsklasse
Um Unit-Tests für eine von PapayaDatabaseObject abgeleitete Klasse zu schreiben, können Sie ihre setDatabaseAccess( )-Methode verwenden, um papayas Datenbankimplementierung durch ein Mock-Objekt zu ersetzen. Wir haben dieses Verfahren im vorigen Tutorial eingesetzt, um unsere eigene Base-Klasse zu ersetzen. Diesmal importieren wir allerdings keine echte Klasse, nach der das Datenbankzugriffs-Mock-Objekt modelliert wird, sondern wir verwenden ein generisches Mock-Objekt und fügen die zu verwendenden Methodennamen hinzu. In der Klasse PapayaDatabaseAccess aus dem Vereichnis papaya-lib/system/Papaya/Database tragen diese Methoden dieselben Namen wir die oben beschriebenen Methoden, allerdings ohne das Präfix 'database' und mit kleinem Anfansbuchstaben, zum Beispiel queryFmt( ) oder deleteRecord( ).
Hier sehen Sie die Unit-Test-Klasse für unsere Datenbankzugriffsklasse, die in tutorial/Hello/Planet/Database/AccessTest.php in der Unit-Test-Verzeichnisstruktur gespeichert wird:
<?php require_once(substr(dirname(__FILE__), 0, -68).'/Framework/PapayaTestCase.php'); PapayaTestCase::registerPapayaAutoloader(); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet/Database/Access.php'); class HelloPlanetDatabaseAccessTest extends PapayaTestCase { /** * Get the HelloPlanetDatabaseAccess object to be tested * * @return HelloPlanetDatabaseAccess */ private function getPlanetDatabaseAccessObjectFixture() { return new HelloPlanetDatabaseAccess(); } /** * Get a PapayaDatabaseAccess mock object * * @param array $methods the methods to be modeled * @return PapayaDatabaseAccess mock object */ private function getPapayaDatabaseAccessObjectFixture($methods) { if (!in_array('getTableName', $methods)) { $methods[] = 'getTableName'; } $databaseAccessObject = $this->getMock( 'PapayaDatabaseAccess', $methods, array(), 'Mock_'.md5(__CLASS__.microtime()), FALSE ); $databaseAccessObject ->expects($this->once()) ->method('getTableName') ->with($this->equalTo('tutorial_planets'), $this->isTrue()) ->will($this->returnValue('papaya_tutorial_planets')); return $databaseAccessObject; } /** * Get a database result mock object * * @param array $methods List of methods to be mocked * @return dbresult_mysql mock object */ private function getDatabaseResultObjectFixture($methods) { if (!defined('DB_FETCHMODE_ASSOC')) { define('DB_FETCHMODE_ASSOC', 0); } return $this->getMock( 'dbresult_mysql', $methods, array(), 'Mock_'.md5(__CLASS__.microtime()), FALSE ); } /** * @covers HelloPlanetDatabaseAccess::getById */ public function testGetById() { $planetDatabaseAccessObject = $this->getPlanetDatabaseAccessObjectFixture(); $databaseAccessObject = $this->getPapayaDatabaseAccessObjectFixture( array('queryFmt') ); $databaseResultObject = $this->getDatabaseResultObjectFixture( array('fetchRow') ); $expectedData = array('planet_id' => 1, 'planet_name' => 'Mars'); $databaseResultObject ->expects($this->once()) ->method('fetchRow') ->will($this->returnValue($expectedData)); $databaseAccessObject ->expects($this->once()) ->method('queryFmt') ->will($this->returnValue($databaseResultObject)); $planetDatabaseAccessObject->setDatabaseAccess($databaseAccessObject); $this->assertEquals($expectedData, $planetDatabaseAccessObject->getById(1)); } /** * @covers HelloPlanetDatabaseAccess::getAll */ public function testGetAll() { $planetDatabaseAccessObject = $this->getPlanetDatabaseAccessObjectFixture(); $databaseAccessObject = $this->getPapayaDatabaseAccessObjectFixture( array('queryFmt') ); $databaseResultObject = $this->getDatabaseResultObjectFixture( array('fetchRow') ); $expectedData = array( array('planet_id' => 1, 'planet_name' => 'Mars'), array('planet_id' => 2, 'planet_name' => 'Jupiter'), array('planet_id' => 3, 'planet_name' => 'Saturn') ); $databaseResultObject ->expects($this->atLeastOnce()) ->method('fetchRow') ->will( $this->onConsecutiveCalls( $this->returnValue($expectedData[0]), $this->returnValue($expectedData[1]), $this->returnValue($expectedData[2]), FALSE ) ); $databaseAccessObject ->expects($this->once()) ->method('queryFmt') ->will($this->returnValue($databaseResultObject)); $planetDatabaseAccessObject->setDatabaseAccess($databaseAccessObject); $expectedResult = array(); foreach ($expectedData as $row) { $expectedResult[$row['planet_id']] = $row['planet_name']; } $this->assertEquals($expectedResult, $planetDatabaseAccessObject->getAll()); } /** * @covers HelloPlanetDatabaseAccess::create */ public function testCreate() { $planetDatabaseAccessObject = $this->getPlanetDatabaseAccessObjectFixture(); $databaseAccessObject = $this->getPapayaDatabaseAccessObjectFixture( array('insertRecord') ); $databaseAccessObject ->expects($this->once()) ->method('insertRecord') ->will($this->returnValue(1)); $planetDatabaseAccessObject->setDatabaseAccess($databaseAccessObject); $this->assertTrue($planetDatabaseAccessObject->create(array('planet_name' => 'Mars'))); } /** * @covers HelloPlanetDatabaseAccess::update */ public function testUpdate() { $planetDatabaseAccessObject = $this->getPlanetDatabaseAccessObjectFixture(); $databaseAccessObject = $this->getPapayaDatabaseAccessObjectFixture( array('updateRecord') ); $databaseAccessObject ->expects($this->once()) ->method('updateRecord') ->will($this->returnValue(1)); $planetDatabaseAccessObject->setDatabaseAccess($databaseAccessObject); $this->assertTrue($planetDatabaseAccessObject->update(2, array('planet_name' => 'Jupiter'))); } /** * @covers HelloPlanetDatabaseAccess::delete */ public function testDelete() { $planetDatabaseAccessObject = $this->getPlanetDatabaseAccessObjectFixture(); $databaseAccessObject = $this->getPapayaDatabaseAccessObjectFixture( array('deleteRecord') ); $databaseAccessObject ->expects($this->once()) ->method('deleteRecord') ->will($this->returnValue(1)); $planetDatabaseAccessObject->setDatabaseAccess($databaseAccessObject); $this->assertTrue($planetDatabaseAccessObject->delete(9)); } }
In diesem Test wird ein wenig mehr Mocking betrieben als zuvor, und er sieht komplizierter aus, aber mit ein wenig Erklärung dürfte er verständlich sein. Wir benötigen Mock-Objekte für zwei Klassen:
- PapayaDatabaseAccess wurde oben bereits erläutert. Bitte beachten Sie den ausführlichen getMock( )-Aufruf mit fünf Argumenten: Der Name der Originalklase, die ersetzt wird, ein Array der zur Verfügung zu stellenden Methoden, ein Array von Parametern für den Konstruktor (in diesem Fall ein leeres Array), ein dynamischer Name für die Mock-Klasse -- 'Mock_'.md5(__CLASS__.microtime()) ist eine gute Wahl, weil microtime( ) sich schnell genug ändert, um für jede Klasse einen eindeutigen Namen bereitzustellen -- und ein Boolean, der angibt, ob der Konstruktor der Originalklasse aufgerufen werden soll (TRUE) oder nicht (FALSE; dies ist hier der Fall).
- dbresult_mysql ist der Rückgabewert einer erfolgreichen SELECT-Abfrage, wenn Sie die MySQL-Datenbankschnittstelle verwenden (theoretisch können Sie jede der papaya-Datenbank-Result-Klassen für Ihr Mock-Objekt verwenden, aber diese funktioniert so gut wie jede andere). Lesen Sie die obige PapayaDatabaseAccess-Erläuterung für Details über die getMock( )-Parameter. Zuletzt definieren wir die Konstante DB_FETCHMODE_ASSOC mit einem beliebigen Wert, falls sie noch nicht definiert ist. Die Implementierung verwendet diese Konstante als Argument für die Methode fetchRow( ) des Ergebnis-Objekts, die zum Lesen eines Datensatzes aus der Ergebnismenge benutzt wird.
Hinter dem Import der Klasse PapayaTestCase steht ein statischer Aufruf ihrer Methode registerPapayaAutoloader( ). Dieser stellt sicher, dass der papaya-Autoloader-Mechanismus auch für die Unit-Tests funktioniert.
Der Test für getAll( ) ist bei weitem der längste und komplexeste. Da wir mehr als einen Datensatz aus der Ergebnismenge lesen werden, müssen wir sicherstellen, dass unser Mock-Ergebnis-Objekt für jeden Aufruf andere Werte zurückliefert. Dies erreichen Sie mithilfe der Erwartung atLeastOnce( ) für den Methodenaufruf und onConsecutiveCalls( ) für die Rückgabewerte, wie oben gezeigt. Der letzte Rückgabewert ist FALSE, weil wir fetchRow( ) als Bedingung einer while-Schleife aufrufen werden und so dafür sorgen, dass sie aufhört, wenn es keine weiteren Datensätze mehr zu lesen gibt.
Den Rest der Testklasse sollten Sie verstehen können, wenn Sie der Tutorial-Reihe bis hier gefolgt sind. In der Methode getPapayaDatabaseAccessObjectFixture( ) fügen wir 'getTableName' zum Array der Mock-Methoden hinzu, falls nicht bereits vorhanden, und wir erwarten, dass diese Methode den Tabellennamen papaya_tutorial_planets zurückgibt -- es ist einfacher, dies hier im Fixture zu erledigen, weil wir es für jeden Test brauchen, der das PapayaDatabaseAccess-Mock-Objekt verwendet.
Bitte beachten Sie die with-Klausel in diesem Einsatz von expects, der dafür sorgt, dass der erwartete Methodenaufruf die gewünschten Argumente verwendet (in diesem Fall den String 'tutorial_planets' und den Boolean TRUE). Dies sorgt für noch exaktere Unit-Test-Ergebnisse, weil Sie nicht nur erwarten, dass ein bestimmter Methodenaufruf stattfindet, sondern auch, dass er bestimmte Argumentwerte verwendet.
Die Datenbankzugriffsklasse implementieren
Hier die Datenbankzugriffsklasse, die alle obigen Tests bestehen sollte:
<?php /** * Hello World tutorial, Planet database access class * * The Planet database access class provides the internal functionality * to store planets in and load them from the database * * @package Papaya-Modules * @subpackage tutorial */ /** * Hello World tutorial, Planet database access class * * @package Papaya-Modules * @subpackage tutorial */ class HelloPlanetDatabaseAccess extends PapayaDatabaseObject { /** * Database table planets * @var string */ private $_tablePlanets = 'tutorial_planets'; /** * Get a planet by id * * @param integer $planetId id of the planet to load * @return mixed array planet data if id exists, FALSE otherwise */ public function getById($planetId) { $sql = "SELECT planet_id, planet_name FROM %s WHERE planet_id = %d"; $params = array($this->databaseGetTableName($this->_tablePlanets, TRUE), $planetId); $result = FALSE; if ($res = $this->databaseQueryFmt($sql, $params)) { if ($row = $res->fetchRow(DB_FETCHMODE_ASSOC)) { $result = $row; } } return $result; } /** * Get all planets * * The return value is an array in which the keys are planet ids and the values are planet names. * If no planets exist, the return value will be FALSE. * * @return mixed array planet data if planets exist, FALSE otherwise */ public function getAll() { $sql = "SELECT planet_id, planet_name FROM %s"; $params = array($this->databaseGetTableName($this->_tablePlanets, TRUE)); $result = FALSE; if ($res = $this->databaseQueryFmt($sql, $params)) { while ($row = $res->fetchRow(DB_FETCHMODE_ASSOC)) { if ($result === FALSE) { $result = array(); } $result[$row['planet_id']] = $row['planet_name']; } } return $result; } /** * Add a new planet to the database * * @param array $data planet data * @return boolean TRUE on success, FALSE otherwise */ public function create($data) { $result = FALSE; $success = $this->databaseInsertRecord( $this->databaseGetTableName($this->_tablePlanets, TRUE), 'planet_id', $data ); if ($success !== FALSE) { $result = TRUE; } return $result; } /** * Modify an existing planet * * @param integer $planetId id of the planet to modify * @param array $data modified planet data * @return boolean TRUE on success, FALSE otherwise */ public function update($planetId, $data) { $result = FALSE; $success = $this->databaseUpdateRecord( $this->databaseGetTableName($this->_tablePlanets, TRUE), $data, 'planet_id', $planetId ); if ($success !== FALSE) { $result = TRUE; } return $result; } /** * Delete a specific planet by id * * @param integer $planetId * @return boolean TRUE on success, FALSE otherwise */ public function delete($planetId) { $result = FALSE; $success = $this->databaseDeleteRecord( $this->databaseGetTableName($this->_tablePlanets, TRUE), 'planet_id', $planetId ); if ($success !== FALSE) { $result = TRUE; } return $result; } }
Alle Methoden in dieser Klasse arbeiten mit der Datenbanktabelle papaya_tutorial_planets: getById( ) versucht, einen Datensatz mit einem bestimmten Wert des Feldes planet_id zu lesen, getAll( ) liefert ein Array aller Planeten, create( ) fügt einen neuen Datensatz ein, update( ) modifiziert einen existierenden und delete( ) löscht einen Datensatz, wiederum anhand seiner ID. Das in allen Methoden verwendete Verfahren zum Setzen des Rückgabewerts bewahrt Sie vor dem Schreiben zu vieler Unit-Tests: Sobald Sie mehr als eine return-Anweisung zu einer Methode hinzufügen oder ein else für eine if-Anweisung verwenden, benötigen Sie einen zusätzlichen Test für diese Methode (mit Namen wie testDeleteExpectingSuccess( ) and testDeleteExpectingFailure( ) oder dergleichen). Sie können auch Data Provider verwenden, was hier in der PHPUnit-Dokumentation erläutert wird, um denselben Test mit mehr als einem Satz von Werten zu überprüfen und dabei unterschiedliche Ergebnisse zu erwarten.
Ein letztes Wort zum Thema Rückgabewerte: In diesem einfachen Beispiel geben wir einfach FALSE zurück, wenn eine der Methoden nicht erfolgreich ausgeführt werden kann. Für größere Projekte sollten Sie es in Erwägung ziehen, Exceptions auszulösen, abzufangen und per Unit-Test zu überprüfen, und zwar jeweils unterschiedliche für ungültige Argumente, Datenbankverbindungsfehler und andere Probleme.
Refactoring der öffentlichen Zugriffsklasse
Da die Datenbankzugriffsklasse nun funktioniert, können wir uns ans Refactoring der Klasse HelloPlanet machen, um diese zu nutzen. Dies ist recht einfach, deshalb sehen Sie hier ohne weitere Erläuterungen den geänderten Unit-Test, gefolgt von der Klasse selbst:
<?php require_once(substr(dirname(__FILE__), 0, -52).'/Framework/PapayaTestCase.php'); PapayaTestCase::registerPapayaAutoloader(); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet.php'); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet/Database/Access.php'); class HelloPlanetTest extends PapayaTestCase { /** * Get the HelloPlanet object to be tested * * @return HelloPlanet */ private function getPlanetObjectFixture() { return new HelloPlanet(); } /** * Get a HelloPlanetDatabaseAccess mock object fixture * * @return HelloPlanetDatabaseAccess mock object */ private function getDatabaseAccessObjectFixture() { return $this->getMock('HelloPlanetDatabaseAccess'); } /** * @covers HelloPlanet::setDatabaseAccessObject */ public function testSetDatabaseAccessObject() { $planetObject = $this->getPlanetObjectFixture(); $databaseAccessObject = $this->getDatabaseAccessObjectFixture(); $planetObject->setDatabaseAccessObject($databaseAccessObject); $this->assertAttributeSame($databaseAccessObject, '_databaseAccessObject', $planetObject); } /** * @covers HelloPlanet::getDatabaseAccessObject */ public function testGetDatabaseAccessObject() { $planetObject = $this->getPlanetObjectFixture(); $databaseAccessObject = $planetObject->getDatabaseAccessObject(); $this->assertAttributeInstanceOf( 'HelloPlanetDatabaseAccess', '_databaseAccessObject', $planetObject ); } /** * @covers HelloPlanet::getById */ public function testGetById() { $planetObject = $this->getPlanetObjectFixture(); $databaseAccessObject = $this->getDatabaseAccessObjectFixture(); $expected = array('planet_id' => 1, 'planet_name' => 'Mars'); $databaseAccessObject ->expects($this->once()) ->method('getById') ->will($this->returnValue($expected)); $planetObject->setDatabaseAccessObject($databaseAccessObject); $this->assertEquals($expected, $planetObject->getById(1)); } /** * @covers HelloPlanet::getAll */ public function testGetAll() { $planetObject = $this->getPlanetObjectFixture(); $databaseAccessObject = $this->getDatabaseAccessObjectFixture(); $expected = array(1 => 'Mars', 2 => 'Jupiter', 3 => 'Saturn'); $databaseAccessObject ->expects($this->once()) ->method('getAll') ->will($this->returnValue($expected)); $planetObject->setDatabaseAccessObject($databaseAccessObject); $this->assertEquals($expected, $planetObject->getAll()); } /** * @covers HelloPlanet::create */ public function testCreate() { $planetObject = $this->getPlanetObjectFixture(); $databaseAccessObject = $this->getDatabaseAccessObjectFixture(); $databaseAccessObject ->expects($this->once()) ->method('create') ->will($this->returnValue(TRUE)); $planetObject->setDatabaseAccessObject($databaseAccessObject); $data = array('planet_name' => 'Jupiter'); $this->assertTrue($planetObject->create($data)); } /** * @covers HelloPlanet::update */ public function testUpdate() { $planetObject = $this->getPlanetObjectFixture(); $databaseAccessObject = $this->getDatabaseAccessObjectFixture(); $databaseAccessObject ->expects($this->once()) ->method('update') ->will($this->returnValue(TRUE)); $planetObject->setDatabaseAccessObject($databaseAccessObject); $data = array('planet_name' => 'Saturn'); $this->assertTrue($planetObject->update(3, $data)); } /** * @covers HelloPlanet::delete */ public function testDelete() { $planetObject = $this->getPlanetObjectFixture(); $databaseAccessObject = $this->getDatabaseAccessObjectFixture(); $databaseAccessObject ->expects($this->once()) ->method('delete') ->will($this->returnValue(TRUE)); $planetObject->setDatabaseAccessObject($databaseAccessObject); $this->assertTrue($planetObject->delete(2)); } }
Und hier die geänderte Klasse HelloPlanet:
<?php /** * Hello World tutorial, Planet class * * The Planet class provides the public interface for planets to be stored in * and loaded from database. * * @package Papaya-Modules * @subpackage tutorial */ /** * Hello World tutorial, Planet class * * @package Papaya-Modules * @subpackage tutorial */ class HelloPlanet { /** * The HelloPlanetDatabaseAccess object to be used * @var HelloPlanetDatabaseAccess */ private $_databaseAccessObject = NULL; /** * Set the HelloPlanetDatabaseAccess object to be used * * @param HelloPlanetDatabaseAccess $databaseAccessObject */ public function setDatabaseAccessObject($databaseAccessObject) { $this->_databaseAccessObject = $databaseAccessObject; } /** * Get (and, if necessary, initialize) the HelloPlanetDatabaseAccess object * * @return HelloPlanetDatabaseAccess */ public function getDatabaseAccessObject() { if (!is_object($this->_databaseAccessObject)) { include_once(dirname(__FILE__).'/Planet/Database/Access.php'); $this->_databaseAccessObject = new HelloPlanetDatabaseAccess(); } return $this->_databaseAccessObject; } /** * Get a planet by id * * @param integer $planetId id of the planet to load * @return mixed array planet data if id exists, FALSE otherwise */ public function getById($planetId) { $databaseAccessObject = $this->getDatabaseAccessObject(); return $databaseAccessObject->getById($planetId); } /** * Get all planets * * @return mixed array planet data if planets exist, FALSE otherwise */ public function getAll() { $databaseAccessObject = $this->getDatabaseAccessObject(); return $databaseAccessObject->getAll(); } /** * Add a new planet to the database * * @param array $data planet data * @return boolean TRUE on success, FALSE otherwise */ public function create($data) { $databaseAccessObject = $this->getDatabaseAccessObject(); return $databaseAccessObject->create($data); } /** * Modify an existing planet * * @param integer $planetId id of the planet to modify * @param array $data modified planet data * @return boolean TRUE on success, FALSE otherwise */ public function update($planetId, $data) { $databaseAccessObject = $this->getDatabaseAccessObject(); return $databaseAccessObject->update($planetId, $data); } /** * Delete a specific planet by id * * @param integer $planetId * @return boolean TRUE on success, FALSE otherwise */ public function delete($planetId) { $databaseAccessObject = $this->getDatabaseAccessObject(); return $databaseAccessObject->delete($planetId); } }
Alle benötigten Konzepte, insbesondere Dependency-Injection wie bei HelloPlanet::setDatabaseAccessObject( ) und Lazy Initialization wie bei HelloPlanet::getDatabaseAccessObject( ), wurden bereits im ersten Tutorial erläutert.
Ein Connector-Modul erstellen
Um die Methoden der Klasse Planet für das Content-Modul verfügbar zu machen, schreiben wir ein Connector-Modul. Connectoren können ohne Rücksicht auf Verzeichnisstrukturen inkludiert werden. Dies macht sie zur besten Wahl, um Funktionalität aus Basisklassen in Content- oder Administrations-Modulen zu verwenden, sowohl im selben Paket als auch in anderen.
Den Connector registrieren
Da die Connector-Klasse ein Modul ist, muss sie in der modules.xml-Datei des Pakets registriert werden. Dies wurde detailliert im ersten Tutorial erläutert. Fügen Sie den folgenden XML-Block zum Abschnitt <modules> von modules.xml hinzu:
<module type="connector" guid="" name="Hello World Connector" class="HelloConnector" file="Hello/Connector.php"> The Hello World Connector provides basic functionality for content and administration modules </module>
Fügen Sie eine GUID als Wert des Attributs guid hinzu; das vorige Tutorial beschreibt, wie Sie eine erhalten. Wir benötigen die GUID, um den Connector zu benutzen, deshalb sehen Sie hier meine GUID: eeb42aad2491cd607c7c64bc57eae455
Den Connector testen und implementieren
Normalerweise sammelt ein Connector die Methoden aller Basisklassen in einem Paket, um sie für Module verfügbar zu machen. In unserem Paket haben wir nur eine Basisklasse, Planet, aber nichtsdestotrotz fügen wir Planet oder Planets zu den Methodennamen hinzu, beispielsweise getPlanetById( ) statt getById( ) oder getAllPlanets( ) statt getAll( ). Dies ist hilfreich, um den Connector später um die Funktionalität anderer Basisklassen zu ergänzen.
Es ist einfach, die Unit-Test-Klasse und die Connector-Klasse zu schreiben. Wie üblich schauen wir uns zuerst den Test an; er wird als ConnectorTest.php im Verzeichnis tutorial/Hello der Unit-Test-Verzeichnisstruktur gespeichert:
<?php require_once(substr(dirname(__FILE__), 0, -52).'/Framework/PapayaTestCase.php'); PapayaTestCase::registerPapayaAutoloader(); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Connector.php'); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet.php'); class HelloConnectorTest extends PapayaTestCase { /** * @covers HelloConnector::setPlanetObject */ public function testSetPlanetObject() { $helloConnectorObject = new HelloConnector_TestProxy(); $planetObject = $this->getMock('HelloPlanet'); $helloConnectorObject->setPlanetObject($planetObject); $this->assertAttributeSame($planetObject, '_planetObject', $helloConnectorObject); } /** * @covers HelloConnector::getPlanetObject */ public function testGetPlanetObject() { $helloConnectorObject = HelloConnector_TestProxy(); $planetObject = $helloConnectorObject->getPlanetObject(); $this->assertInstanceOf('HelloPlanet', $planetObject); } /** * @covers HelloConnector::getPlanetById */ public function testGetPlanetById() { $helloConnectorObject = HelloConnector_TestProxy(); $planetObject = $this->getMock('HelloPlanet'); $expected = array('planet_id' => 1, 'planet_name' => 'Mars'); $planetObject ->expects($this->once()) ->method('getById') ->will($this->returnValue($expected)); $helloConnectorObject->setPlanetObject($planetObject); $this->assertEquals($expected, $helloConnectorObject->getPlanetById(1)); } /** * @covers HelloConnector::getAllPlanets */ public function testGetAllPlanets() { $helloConnectorObject = HelloConnector_TestProxy(); $planetObject = $this->getMock('HelloPlanet'); $expected = array(1 => 'Mars', 2 => 'Jupiter', 3 => 'Saturn'); $planetObject ->expects($this->once()) ->method('getAll') ->will($this->returnValue($expected)); $helloConnectorObject->setPlanetObject($planetObject); $this->assertEquals($expected, $helloConnectorObject->getAllPlanets()); } /** * @covers HelloConnector::createPlanet */ public function testCreatePlanet() { $helloConnectorObject = HelloConnector_TestProxy(); $planetObject = $this->getMock('HelloPlanet'); $planetObject ->expects($this->once()) ->method('create') ->will($this->returnValue(TRUE)); $helloConnectorObject->setPlanetObject($planetObject); $this->assertTrue( $helloConnectorObject->createPlanet(array('planet_name' => 'Neptune')) ); } /** * @covers HelloConnector::updatePlanet */ public function testUpdatePlanet() { $helloConnectorObject = HelloConnector_TestProxy(); $planetObject = $this->getMock('HelloPlanet'); $planetObject ->expects($this->once()) ->method('update') ->will($this->returnValue(TRUE)); $helloConnectorObject->setPlanetObject($planetObject); $this->assertTrue( $helloConnectorObject->updatePlanet(1, array('planet_name' => 'Mars')) ); } /** * @covers HelloConnector::deletePlanet */ public function testDeletePlanet() { $helloConnectorObject = HelloConnector_TestProxy(); $planetObject = $this->getMock('HelloPlanet'); $planetObject ->expects($this->once()) ->method('delete') ->will($this->returnValue(TRUE)); $helloConnectorObject->setPlanetObject($planetObject); $this->assertTrue($helloConnectorObject->deletePlanet(2)); } } class HelloConnector_TestProxy extends HelloConnector { public function __construct() { // Nothing to do here; just provide a constructor without parameters } }
Die Connector-Klasse selbst (tutorial/Hello/Connector.php) sieht folgendermaßen aus:
<?php /** * Hello World tutorial, connector class * * The connector class provides the base methods to be used by content and admin modules. * * @package Papaya-Modules * @subpackage tutorial */ /** * Base class base_connector */ require_once(PAPAYA_INCLUDE_PATH.'system/base_connector.php'); /** * Hello World tutorial, connector class * * @package Papaya-Modules * @subpackage tutorial */ class HelloConnector extends base_connector { /** * The HelloPlanet object to be used * @var HelloPlanet */ protected $_planetObject = NULL; /** * Set the HelloPlanet object to be used * * @param HelloPlanet $planetObject */ public function setPlanetObject($planetObject) { $this->_planetObject = $planetObject; } /** * Get (and, if necessary, initialize) the HelloPlanet object * * @return HelloPlanet */ public function getPlanetObject() { if (!is_object($this->_planetObject)) { include_once(dirname(__FILE__).'/Planet.php'); $this->_planetObject = new HelloPlanet(); } return $this->_planetObject; } /** * Get a planet by id * * @param integer $planetId * @return mixed array if the planet exists, FALSE otherwise */ public function getPlanetById($planetId) { $planetObject = $this->getPlanetObject(); return $planetObject->getById($planetId); } /** * Get all planets * * @return mixed array if planets exist, FALSE otherwise */ public function getAllPlanets() { $planetObject = $this->getPlanetObject(); return $planetObject->getAll(); } /** * Insert a new planet * * @param array $planetData * @return boolean TRUE on success, FALSE otherwise */ public function createPlanet($data) { $planetObject = $this->getPlanetObject(); return $planetObject->create($data); } /** * Update a planet * * @param integer $planetId * @param array $data * @return boolean TRUE on success, FALSE otherwise */ public function updatePlanet($planetId, $data) { $planetObject = $this->getPlanetObject(); return $planetObject->update($planetId, $data); } /** * Delete a planet * * @param integer $planetId * @return boolean TRUE on success, FALSE otherwise */ public function deletePlanet($planetId) { $planetObject = $this->getPlanetObject(); return $planetObject->delete($planetId); } }
Bitte beachten Sie, dass die Eigenschaft $_planetObject die Sichtbarkeit protected statt private benötigt. Andernfalls könnte sie nicht mit der Proxy-Klasse getestet werden, die von der echten Connector-Klasse abgeleitet ist. Eigentlich liest PHPUnit Attribute über Reflection, so dass es kein Problem geben dürfte -- es handelt sich um einen Bug, der in einer der nächsten PHPUnit-Versionen gefixt werden wird.
Ein Connector-Objekt in einem Modul verwenden
Wenn Sie einen Connector in Ihrem Modul verwenden möchten, können Sie ihn mithilfe einer Instanz der papaya-Klasse base_pluginloader laden. Der Code, den Sie dafür brauchen, ist recht einfach:
$pluginloader = new base_pluginloader(); $connectorObject = $pluginloader->getPluginInstance($connectorGuid, $owner);
$owner ist dabei das aktuelle Content-Modul, und $connectorGuid ist ein String, der die GUID des Connectors enthält. Da wir die Connector-Klasse mocken möchten, um Unit-Tests für das Modul zu schreiben, das diese verwendet, brauchen wir Getter- und Setter-Methoden sowohl für die base_pluginloader- als auch für die Connector-Instanz, und außerdem eine setOwner( )-Methode, die von der Content-Klasse aufgerufen wird. Dies ist die modifizierte Klasse HelloPageBaseTest (wir haben nur die Methoden testSetPageData( ) und testGetPageXml( ) ausgelassen, weil sie im Moment so bleiben):
<?php require_once(substr(__FILE__, 0, -63).'/Framework/PapayaTestCase.php'); PapayaTestCase::registerPapayaAutoloader(); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Page/Base.php'); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Connector.php'); class HelloPageTest extends PapayaTestCase { /** * Instantiate the HelloPageBase object to be tested * * @return HelloPageBase */ private function getHelloPageBaseObjectFixture() { $this->defineConstantDefaults('PAPAYA_DB_TBL_MODULES'); return new HelloPageBase(); } /** * Get an owner mock object (it doesn't need a specific class) * * @return object */ private function getOwnerObjectFixture() { return $this->getMock('GenericContentClass'); } /** * Get a base_pluginloader mock object * * @return base_pluginloader mock object */ private function getPluginloaderObjectFixture() { return $this->getMock('base_pluginloader'); } /** * Get a HelloConnector mock object * * @return HelloConnector mock object */ private function getHelloConnectorObjectFixture() { return $this->getMock( 'HelloConnector', array(), array(), 'Mock_'.md5(__CLASS__.microtime()), FALSE ); } /** * @covers HelloPageBase::setOwner */ public function testSetOwner() { $helloPageBaseObject = $this->getHelloPageBaseObjectFixture(); $owner = $this->getOwnerObjectFixture(); $helloPageBaseObject->setOwner($owner); $this->assertAttributeSame($owner, '_owner', $helloPageBaseObject); } /** * @covers HelloPageBase::setPluginloaderObject */ public function testSetPluginloaderObject() { $helloPageBaseObject = $this->getHelloPageBaseObjectFixture(); $pluginloaderObject = $this->getPluginloaderObjectFixture(); $helloPageBaseObject->setPluginloaderObject($pluginloaderObject); $this->assertAttributeSame( $pluginloaderObject, '_pluginloaderObject', $helloPageBaseObject ); } /** * @covers HelloPageBase::getPluginloaderObject */ public function testGetPluginloaderObject() { $helloPageBaseObject = $this->getHelloPageBaseObjectFixture(); $pluginloaderObject = $helloPageBaseObject->getPluginloaderObject(); $this->assertInstanceOf('base_pluginloader', $pluginloaderObject); } /** * @covers HelloPageBase::setHelloConnectorObject */ public function testSetHelloConnectorObject() { $helloPageBaseObject = $this->getHelloPageBaseObjectFixture(); $helloConnectorObject = $this->getHelloConnectorObjectFixture(); $helloPageBaseObject->setHelloConnectorObject($helloConnectorObject); $this->assertAttributeSame( $helloConnectorObject, '_helloConnectorObject', $helloPageBaseObject ); } /** * @covers HelloPageBase::getHelloConnectorObject */ public function testGetHelloConnectorObject() { $helloPageBaseObject = $this->getHelloPageBaseObjectFixture(); $owner = $this->getOwnerObjectFixture(); $pluginloaderObject = $this->getPluginloaderObjectFixture(); $helloConnectorObject = $this->getHelloConnectorObjectFixture(); $pluginloaderObject ->expects($this->once()) ->method('getPluginInstance') ->will($this->returnValue($helloConnectorObject)); $helloPageBaseObject->setOwner($owner); $helloPageBaseObject->setPluginloaderObject($pluginloaderObject); $this->assertSame( $helloConnectorObject, $helloPageBaseObject->getHelloConnectorObject() ); } // testSetPageData() und testGetPageXml() bleiben, wie sie waren }
defineConstantDefaults( ) ist eine PHPUnit_Framework_TestCase-Methode, die eine benötigte Konstante mit einem Standardwert definiert. PAPAYA_DB_TBL_MODULES ist eine Konstante, die den Namen der papaya-Modul-Datenbanktabelle enthält. Diese Tabelle wird von base_pluginloader verwendet, um das Modul anhand seiner GUID zu laden. Sie sollten den Rest der neuen Tests ohne Probleme verstehen, so dass hier der neue Code für die Klasse HelloPageBase kommt (fügen Sie ihn am Beginn des Klassenrumpfs ein und denken Sie daran, dass die Deklaration der Eigenschaft $_data schon vorher vorhanden war):
/** * Owner object * @var HelloPage */ private $_owner = NULL; /** * base_pluginloader object * @var base_pluginloader */ private $_pluginloaderObject = NULL; /** * HelloConnector object * @var HelloConnector */ private $_helloConnectorObject = NULL; /** * Page configuration data * @var array */ private $_data = array(); /** * Set owner object * * @param HelloPage $owner */ public function setOwner($owner) { $this->_owner = $owner; } /** * Set the base_pluginloader object to use * * @param base_pluginloader $pluginloaderObject */ public function setPluginloaderObject($pluginloaderObject) { $this->_pluginloaderObject = $pluginloaderObject; } /** * Get (and, if necessary, initialize) the base_pluginloader object * * @return base_pluginloader */ public function getPluginloaderObject() { if (!is_object($this->_pluginloaderObject)) { $this->_pluginloaderObject = new base_pluginloader(); } return $this->_pluginloaderObject; } /** * Set the HelloConnector object to use * * @param HelloConnector $helloConnectorObject */ public function setHelloConnectorObject($helloConnectorObject) { $this->_helloConnectorObject = $helloConnectorObject; } /** * Get (and, if necessary, initialize) the HelloConnector object * * @return HelloConnector */ public function getHelloConnectorObject() { if (!is_object($this->_helloConnectorObject)) { $pluginloaderObject = $this->getPluginloaderObject(); $this->_helloConnectorObject = $pluginloaderObject->getPluginInstance( 'eeb42aad2491cd607c7c64bc57eae455', $this->_owner ); } return $this->_helloConnectorObject; }
Falls Sie eine andere GUID für Ihr Connector-Modul verwendet haben, ersetzen Sie sie. Sobald die Unit-Tests funktionieren, haben wir erfolgreich Code hinzugefügt, der das Connector-Objekt initialisiert, so dass wir im nächsten Schritt dessen Methoden verwenden können.
Die Datenbankfunktionalität im Content-Modul verwenden
In der Basisklasse unseres Content-Moduls werden wir zwei Methoden des Connectors verwenden: getAllPlanets( ), um die Liste der Planeten zu laden, und getPlanetById( ), um einen einzelnen Planeten zu laden. Die Liste wird verwendet, um während der Seitenkonfiguration einen Planeten auszuwählen. Dessen ID wird in den Seitendaten gespeichert, und wenn die Seite angezeigt wird, wird der Name wieder anhand der ID geladen.
Die Base-Klasse erweitern
Zuerst ändern wir eine Methode in der Klasse HelloBase und fügen eine weitere hinzu. Führen Sie die folgenden Änderungen an testGetPageXml( ) in der Klasse HelloPageBaseTest durch:
/** * @covers HelloPageBase::getPageXml */ public function testGetPageXml() { $helloPageBaseObject = $this->getHelloPageBaseObjectFixture(); $helloConnectorObject = $this->getHelloConnectorObjectFixture(); $helloConnectorObject ->expects($this->once()) ->method('getPlanetById') ->will($this->returnValue(array('planet_id' => 1, 'planet_name' => 'Mars'))); $helloPageBaseObject->setHelloConnectorObject($helloConnectorObject); $helloPageBaseObject->setPageData(array('planet_id' => 1, 'text' => 'Hello')); $xml = '<title>Hello Mars!</title> <text>Hello</text>'; $this->assertEquals($xml, $helloPageBaseObject->getPageXml()); }
Ersetzen Sie nun die existierende Methode getPageXml( )in HelloPageBase durch diese:
/** * Get the page's XML output * * @return string XML */ public function getPageXml() { $helloConnectorObject = $this->getHelloConnectorObject(); $planet = ''; $planetData = $helloConnectorObject->getPlanetById($this->_data['planet_id']); if (is_array($planetData) && !empty($planetData)) { $planet = $planetData['planet_name']; } $result = sprintf('<title>Hello %s!</title>'.LF, $planet); $result .= sprintf('<text>%s</text>', papaya_strings::escapeHTMLChars($this->_data['text'])); return $result; }
Wie Sie sehen, initialisieren wir den Namen des Planeten als leeren String und ersezen ihn nur dann durch das Element planet_name aus dem Ergebnis, wenn dieses ein nicht leeres Array ist. Dies beugt Fehlern vor, zum Beispiel wenn eine in den Seitendaten gespeicherte Planeten-ID nicht mehr in der Planetentabelle existiert.
Als Nächstes kommt unsere Methode an die Reihe, die die Liste der Planeten für das Auswahlfeld im Seitenkonfigurationsdialog lädt. Der Unit-Test sieht folgendermaßen aus (fügen Sie ihn am Ende des Klassenrumpfs von HelloPagBaseTest ein):
/** * @covers HelloPageBase::getPlanetSelector */ public function testGetPlanetSelector() { $helloPageBaseObject = $this->getHelloPageBaseObjectFixture(); $owner = $this->getOwnerObjectFixture(); $owner->paramName = 'tut'; $helloConnectorObject = $this->getHelloConnectorObjectFixture(); $helloConnectorObject ->expects($this->once()) ->method('getAllPlanets') ->will($this->returnValue(array(1 => 'Mars', 2 => 'Jupiter'))); $helloPageBaseObject->setOwner($owner); $helloPageBaseObject->setHelloConnectorObject($helloConnectorObject); $expectedXml = '<select name="tut[planet_id]" class="dialogSelect dialogScale"> <option value="1" selected="selected">Mars</option> <option value="2">Jupiter</option> </select>'; $this->assertEquals( $expectedXml, $helloPageBaseObject->getPlanetSelector('planet_id', 1) ); }
Nun können wir in HelloPageBase die Methode implementieren, die diesen Test besteht; fügen Sie auch diese am Ende des Klassenrumpfs ein:
/** * Get a select box for planets * * @param string $name * @param integer $data */ public function getPlanetSelector($name, $data) { $result = ''; $helloConnectorObject = $this->getHelloConnectorObject(); $planetList = $helloConnectorObject->getAllPlanets(); if (is_array($planetList) && !empty($planetList)) { $result = sprintf( '<select name="%s[%s]" class="dialogSelect dialogScale">'.LF, $this->_owner->paramName, $name ); foreach ($planetList as $planetId => $planetName) { $selected = ($planetId == $data) ? ' selected="selected"' : ''; $result .= sprintf( '<option value="%d"%s>%s</option>'.LF, $planetId, $selected, $planetName ); } $result .= '</select>'; } return $result; }
In dieser Methode gibt es nichts wirklich Neues, was besprochen werden müsste. Wieder einmal haben wir uns durch die korrekte Verarbeitungsreihenfolge die Mühe erspart, mehr als einen Test für diese Methode schreiben zu müssen: Wir verwenden keinen else-Block für eine if-Anweisung, und wir verwenden nur eine return-Anweisung. Die CSS-Klassen dialogSelect dialogScale werden vom papaya-CMS-Backend verwendet, um das Auswahlfeld und die anderen, automatisch generierten Formularelemente identisch aussehen zu lassen. Da $data die aktuell ausgewählte Planeten-ID enthält, können wir die zugehörige Option als vorausgewählt kennzeichnen.
Die Content-Klasse anpassen
Zuletzt müssen wir dafür sorgen, dass unsere Content-Klasse HelloPage an die Änderungen in der Base-Klasse angepasst wird. An der Methode getParsedData( ) braucht nichts Weltbewegendes geändert zu werden -- fügen Sie unter der Zeile $baseObject = $this->getBaseObject(); einfach folgende Zeile ein:
$baseObject->setOwner($this);
Sie brauchen dafür noch nicht einmal den Unit-Test anzupassen, weil die Methode automatisch gemockt wird und auch keinen Rückgabewert besitzt, den wir erwarten müssten.
Fügen Sie nun das Feld planet_id zur Definition von $editFields hinzu; die vollständige Definition sollte wie folgt aussehen:
/** * Edit fields for page configuration * @var array */ public $editFields = array( 'text' => array( 'Text', 'isNoHTML', TRUE, 'textarea', 5, '', 'Greetings from the new module' ), 'planet_id' => array( 'Planet', 'isNum', FALSE, 'function', 'callbackPlanetSelector' ) );
Das Feld planet_id verwendet den Überprüfungstyp isNum, der nur numerische Werte erlaubt. Das Feld ist kein Pflichtfeld (FALSE). Der Feldtyp ist function -- das bedeutet, das eine Callback-Methode aufgerufen wird, sobald das Feld vorkommt; papaya CMS übergibt ihr den Feldnamen, das komplette Felddefinitions-Array und den aktuellen Wert. Das nächste Argument muss der Name dieser Methode sein. Bevor wir sie implementieren, schauen Sie sich ihren Unit-Test an (der am Ende des Klassenrumpfs von HelloPageTest eingefügt wird):
/** * @covers HelloPage::callbackPlanetSelector */ public function testGetCallbackPlanetSelector() { $helloPageObject = $this->getHelloPageObjectFixture(); $helloPageBaseObject = $this->getMock('HelloPageBase'); $helloPageBaseObject ->expects($this->once()) ->method('getPlanetSelector') ->will($this->returnValue('<select />')); $helloPageObject->setBaseObject($helloPageBaseObject); $this->assertEquals( '<select />', $helloPageObject->callbackPlanetSelector('planet_id', array(), 1) ); }
Auf dieser Ebene interessieren wir uns nicht für Implementierungsdetails der Base-Klasse, so dass es genügt, als Rückgabewert einen leeren XML-Knoten zu erwarten. Und wir brauchen keine Informationen aus dem Felddefinitions-Array (es ist noch nicht einmal ein Parameter für HelloPageBase::getPlanetSelector( )), so dass wir hier einfach ein leeres Array verwenden. Die Implementierung der Methode am Ende der Klasse HelloPage sieht so aus:
/** * Callback method for a planet select field * * @param string $name * @param array $field * @param integer $data * @return string XML */ public function callbackPlanetSelector($name, $field, $data) { $baseObject = $this->getBaseObject(); $baseObject->setOwner($this); return $baseObject->getPlanetSelector($name, $data); }
Und das war's. Bevor Sie das Content-Modul praktisch ausprobieren, vergessen Sie nicht, im papaya-Backend nach neuen Modulen zu suchen (denn andernfalls wird das neue Connector-Modul nicht gefunden). Fügen Sie außerdem mit Hilfe Ihres Datenbank-Administrationstools einige Planeten in die Datenbanktabelle ein. Im nächsten Tutorial werden wir allerdings eine Administrations-Klasse für dieses Paket erstellen, in der wir Planeten hinzufügen, löschen und ändern können.