Modulprogrammierung 4: Frontend-Interaktivität
Aus PapayaCMS
|
Tutorial |
|
| Zusammenfassung | Dieses Tutorial beschreibt, wie Sie Webformulare für Frontend-Module hinzufügen und Community-Features verwenden. |
| Zielgruppe | PHP-Entwickler |
| Schwierigkeitsgrad | Fortgschrittene |
| Softwarevoraussetzungen | SVN-Checkout von papaya CMS; Anleitung hier PHPUnit >= 3.5 für Unit-Tests |
| Datum | 2011-04-15 |
| Vorheriges Tutorial | Modulprogrammierung 3: Administrationsoberfläche |
| Nächstes Tutorial | Modulprogrammierung 5: Refactoring und mehr |
Im vierten Teil des grundlegenden papaya CMS-Modulentwickler-Tutorials implementieren wir ein Webformular für ein Frontend-Modul. Das Formular ist nur für eingeloggte Frontend-User (oder Surfer, wie sie in der papaya CMS-Terminologie genannt werden) verfügbar. Um ein wenig Abwechslung zu bieten, handelt es sich bei dem Modul, das wir in diesem Tutorial entwickeln, um ein Boxmodul, das in eine Seite eingebettet werden kann. Zum Schluss erstellen wir ein eigenes XSLT-Template, um die Inhalte des Moduls anzuzeigen.
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 | | | + table_tutorial_planet_ratings.xml | + edmodule_tutorial_hello.php | + [Hello] | | | + Connector.php | | | + [Page] | | | | | + Base.php | | | + Page.php | | | + [Planet] | | | | | + Admin.php | | | | | + [Database] | | | | | | | + Access.php | | | | | + [Rating] | | | | | | | + [Box] | | | | | | | | | + Base.php | | | | | | | + Box.php | | | | | | | + [Database] | | | | | | | + Access.php | | | | | + Rating.php | | | + Planet.php | + modules.xml --------------------------------------- papaya-data/templates/default-xhtml/html/box_planet_rating.xsl Hinweis: Namen in eckigen Klammern bezeichnen Verzeichnisse
Im Verzeichnis testing/tests-unittests/papaya-lib/modules/special/myproject/tutorial werden Sie die folgende Struktur erstellt haben (neue Dateien aus dem aktuellen Teil sind fett hervorgehoben):
+ [Hello] | + ConnectorTest.php | + [Page] | | | + BaseTest.php | + [Planet] | | | + AdminTest.php | | | + [Database] | | | | | + AccessTest.php | | | + [Rating] | | | | | + [Box] | | | | | | | + BaseTest.php | | | | | + BoxTest.php | | | | | + [Database] | | | | | + AccessTest.php | | | + RatingTest.php | + PlanetTest.php | + BaseTest.php Hinweis: Namen in eckigen Klammern bezeichnen Verzeichnisse
Das Modul einrichten
Die Idee dieser Erweiterung besteht darin, eingeloggten Surfern die Bewertung von Planeten zu ermöglichen. Benutzer, die nicht eingeloggt sind, können nur die Ergebnisse sehen. Für die Bewertungen benötigen wir eine weitere Datenbanktabelle. Fügen Sie die folgende Zeile zur Datei modules.xml hinzu, unter der bestehenden <table>-Zeile:
<table name="tutorial_planet_ratings" />Verwenden Sie nun Ihr Datenbank-Administrations-Tool, um eine Datenbanktabelle namens papaya_tutorial_planet_ratings mit folgenden Spalten zu erstellen:
- surfer_id -- VARCHAR(32)
- planet_id -- INT
- rating_points -- INT
Setzen Sie einen zusammengesetzten Primärschlüssel auf die Felder surfer_id und planet_id.
Sie können auch die folgende Abfrage verwenden, um die Tabelle zu erzeugen:
CREATE TABLE papaya_tutorial_planet_ratings ( surfer_id VARCHAR(32), planet_id INT, rating_points INT, PRIMARY KEY(surfer_id, planet_id) )
Um die Tabellendefinition für die Benutzer des Moduls verfügbar zu machen, führen Sie die folgenden Schritte durch:
- Wählen Sie Module im Bereich Administration des papaya-Backends.
- Klicken Sie in der Liste Packages links auf Hello World tutorial module.
- Öfnnen Sie im Bereich Package content die Liste Tabellen, indem Sie auf das Plus-Zeichen klicken.
- Klicken Sie die Tabelle papaya_tutorial_planet_ratings an. Die Fehlermeldung Konnte Tabellenstrukturdatei nicht laden! wird angezeigt.
- Klicken Sie in der Hauptsymbolleiste auf den Button Tabelle exportieren. Ihr Browser wird Ihnen anbieten, die Datei zu speichern; folgen Sie den Anweisungen.
- Kopieren Sie die neue Datei in das Verzeichnis DATA innerhalb des Hauptverzeichnisses Ihres Modulpakets, wobei Sie den Zeitstempel aus dem Dateinamen entfernen müssen. Der korrekte Dateiname ist table_tutorial_planet_ratings.xml.
- Klicken Sie im Abschnitt Tabellen erneut auf papaya_tutorial_planet_ratings, um sicherzustellen, dass die Tabellendefinition existiert und mit der eigentlichen Tabelle übereinstimmt.
Fügen Sie nun das folgende XML zum Abschnitt <modules> der Datei modules.xml hinzu, um das neue Box-Modul einzuführen (erstellen Sie dabei wie üblich eine GUID und fügen Sie sie hinzu):
<module type="box" guid="" name="Planet Rating Box" class="HelloPlanetRatingBox" file="Hello/Planet/Rating/Box.php"> This module allows logged-in surfers to rate planets; all others can see the results. </module>
Ein statisches Boxmodul erstellen
Genau wie bei dem Seitenmodul, das wir im ersten und zweiten Teil dieser Tutorial-Serie erstellt haben, beginnen wir mit einem statischen Boxmodul. Dynamische Inhalte und Interaktivität werden später über eine zusätzliche Klasse hinzugefügt. Erstellen Sie zwei neue Verzeichnisse, die jeweils Rating heißen, in den Hello/Planet-Unterverzeichnissen des Modul- beziehungsweise Unit-Test-Verzeichnisses. Erstellen Sie im Verzeichnis Rating des Testbereichs eine PHP-Datei namens BoxTest.php. Erstellen Sie dann die zugehörige Klassendatei im Modulpaket, Hello/Planet/Rating/Box.php.
Boxmodule funktionieren fast genau wie Seitenmodule: Auch sie besitzen die Eigenschaft $this->editFields für ihre Inhaltseinstellungen und die Methode getParsedData(), um die XML-Ausgabe zurückzugeben. Einer der wichtigsten Unterschiede ist, dass Boxmodule base_actionbox statt base_content erweitern.
Hier die Testdatei für die erste, statische Implementierung:
<?php require_once(substr(dirname(__FILE__), 0, -66).'/Framework/PapayaTestCase.php'); PapayaTestCase::registerPapayaAutoloader(); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet/Rating/Box.php'); class HelloPlanetRatingBoxTest extends PapayaTestCase { /** * @covers HelloPlanetRatingBox::getParsedData */ public function testGetParsedData() { $box = new PlanetRatingBox_TestProxy(); $this->assertEquals('<ratings />', $box->getParsedData()); } } class HelloPlanetRatingBox_TestProxy extends PlanetRatingBox { public function __construct() { // Nothing to do here, just override the constructor } }
Genau wie bei den Seitenmodulen verwenden wir eine Proxy-Klasse, um den eigentlichen Konstruktor des Boxmoduls zu überschreiben. Die erste, statische Implementierung des Boxmoduls ist genauso einfach:
<?php /** * Hello World tutorial, Planet rating box * * The Planet rating box allows for logged-in users to rate planets * while all others can view the results. * * @package Papaya-Modules * @subpackage tutorial */ /** * Basic class base_object */ require_once(PAPAYA_INCLUDE_PATH.'system/sys_base_object.php'); /** * Hello World tutorial, Planet rating box * * @package Papaya-Modules * @subpackage tutorial */ class HelloPlanetRatingBox extends base_actionbox { /** * Parameter namespace * @var string */ public $paramName = 'tut'; /** * Edit fields for box configuration * @var array */ public $editFields = array(); /** * Get the box module's XML output * * @return string XML */ public function getParsedData() { return '<ratings />'; } }
Die Rating-Box-Basisklasse erstellen
Die eigentliche Rating-Interaktivität und -ausgabe wird nicht von der Boxklasse, sondern von einer separaten Klasse namens HelloPlanetRatingBoxBase vorgenommen. Die erste Version dieser Klasse ist statisch, da wir den agilen Top-Down-Ansatz befolgen. Hier ist die Unit-Test-Klasse, Hello/Planet/Rating/Box/BaseTest.php, in der Sie eine Methode nach der anderen schreiben sollten:
<?php require_once(substr(dirname(__FILE__), 0, -70).'/Framework/PapayaTestCase.php'); PapayaTestCase::registerPapayaAutoloader(); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet/Rating/Box/Base.php'); class HelloPlanetRatingBoxBaseTest extends PapayaTestCase { /** * @covers HelloPlanetRating::setData */ public function testSetData() { $baseObject = new HelloPlanetRatingBoxBase(); $expectedData = array('title' => 'Planet ratings'); $baseObject->setData($expectedData); $this->assertAttributeEquals($expectedData, '_data', $baseObject); } /** * @covers HelloPlanetRating::setParams */ public function testSetParams() { $baseObject = new HelloPlanetRatingBoxBase(); $expectedParams = array('planet_id' => 1, 'rating_points' => 5); $baseObject->setParams($expectedParams); $this->assertAttributeEquals($expectedParams, '_params', $baseObject); } /** * @covers HelloPlanetRating::getRatingsXml */ public function testGetRatingsXml() { $ratingsObject = new HelloPlanetRatingBoxBase(); $expectedXml = '<ratings> <planet name="Mars" rating="5" /> <planet name="Jupiter" rating="4.5" /> </ratings> '; $this->assertEquals($expectedXml, $ratingsObject->getRatingsXml()); } /** * @covers HelloPlanetRating::getBoxXml */ public function testGetBoxXml() { $ratingsObject = new HelloPlanetRatingBoxBase(); $ratingsObject->setData(array('title' => 'Planet ratings')); $expectedXml = '<ratingbox> <title>Planet ratings</title> <ratings> <planet name="Mars" rating="5" /> <planet name="Jupiter" rating="4.5" /> </ratings> </ratingbox> '; $this->assertEquals($expectedXml, $ratingsObject->getBoxXml()); } }
Und hier die zugehörige Implementierung, Hello/Planet/Rating/Box/Base.php:
<?php /** * Hello World tutorial, Planet rating box base class * * Provides basic functionality for the rating box. * * @package Papaya-Modules * @subpackage tutorial */ /** * Hello World tutorial, Planet rating box base class * * @package Papaya-Modules * @subpackage tutorial */ class HelloPlanetRatingBoxBase { /** * Box configuration data * @var array */ private $_data = array(); /** * Box request parameters * @var array */ private $_params = array(); /** * Set configuration data * * @param array $data */ public function setData($data) { $this->_data = $data; } /** * Set box request parameters * @param array $params */ public function setParams($params) { $this->_params = $params; } /** * Get XML for the average ratings */ public function getRatingsXml() { $result = '<ratings>'.LF; $result .= '<planet name="Mars" rating="5" />'.LF; $result .= '<planet name="Jupiter" rating="4.5" />'.LF; $result .= '</ratings>'.LF; return $result; } /** * Get box XML output * * @return string XML */ public function getBoxXml() { $result = '<ratingbox>'.LF; $result .= sprintf( '<title>%s</title>'.LF, papaya_strings::escapeHTMLChars($this->_data['title']) ); $result .= $this->getRatingsXml(); $result .= '</ratingbox>'.LF; return $result; } }
Für diese Klasse sind noch keine Erläuterungen nötig. Fügen Sie einfach eine Methode nach der anderen hinzu und führen Sie jeweils die Unit-Tests aus. Die Klasse wird später erweitert und refactored, wenn die Rating-Funktionalität verfügbar ist.
Refactoring der Boxklasse
Wir sind nun bereit für das Refactoring der Boxklasse, so dass sie die Base-Klasse verwendet. Modifizieren Sie die Unit-Test-Klasse HelloPlanetRatingBoxTest wie folgt, um Tests für die neuen Methoden setBaseObject() und getBaseObject() bereitzustellen und die Methode testGetParsedData() zu erweitern:
<?php require_once(substr(dirname(__FILE__), 0, -66).'/Framework/PapayaTestCase.php'); PapayaTestCase::registerPapayaAutoloader(); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet/Rating/Box.php'); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet/Rating/Box/Base.php'); class HelloPlanetRatingBoxTest extends PapayaTestCase { /** * @covers HelloPlanetRatingBox::setBaseObject */ public function testSetBaseObject() { $box = new HelloPlanetRatingBox_TestProxy(); $baseObject = $this->getMock('HelloPlanetRatingBoxBase'); $box->setBaseObject($baseObject); $this->assertAttributeSame($baseObject, '_baseObject', $box); } /** * @covers HelloPlanetRatingBox::getBaseObject */ public function testGetBaseObject() { $box = new HelloPlanetRatingBox_TestProxy(); $baseObject = $box->getBaseObject(); $this->assertInstanceOf('HelloPlanetRatingBoxBase', $baseObject); } /** * @covers HelloPlanetRatingBox::getParsedData */ public function testGetParsedData() { $box = new HelloPlanetRatingBox_TestProxy(); $baseObject = $this->getMock('HelloPlanetRatingBoxBase'); $expectedXml = '<ratingbox> <title>Planet ratings</title> <ratings> <planet name="Mars" rating="5" /> <planet name="Jupiter" rating="4.5" /> </ratings> </ratingbox>'; $baseObject ->expects($this->once()) ->method('getBoxXml') ->will($this->returnValue($expectedXml)); $box->setBaseObject($baseObject); $box->data = array('title' => 'Planet ratings'); $this->assertEquals($expectedXml, $box->getParsedData()); } } class HelloPlanetRatingBox_TestProxy extends HelloPlanetRatingBox { public function __construct() { // Nothing to do here, just override the constructor } public function initializeParams() { // Nothing here either, just override the original method } }
Die Notwendigkeit, initializeParams() in der Proxy-Klasse zu überschreiben, wurde bereits in einem früheren Teil dieser Serie besprochen.
Die Implementierung der Klasse HelloPlanetRatingBox sieht nach dem Refactoring so aus:
<?php /** * Hello World tutorial, Planet rating box * * The Planet rating box allows for logged-in users to rate planets * while all others can view the results. * * @package Papaya-Modules * @subpackage tutorial */ /** * Basic class base_actionbox */ require_once(PAPAYA_INCLUDE_PATH.'system/base_actionbox.php'); /** * Hello World tutorial, Planet rating box * * @package Papaya-Modules * @subpackage tutorial */ class HelloPlanetRatingBox extends base_actionbox { /** * The base object to be used * @var PlanetRatingBoxBase */ protected $_baseObject = NULL; /** * Parameter namespace * @var string */ public $paramName = 'tut'; /** * Edit fields for box configuration * @var array */ public $editFields = array( 'title' => array('Title', 'isNoHTML', TRUE, 'input', 100, '', 'Planet Ratings') ); /** * Set the base object to be used * * @param HelloPlanetRatingBoxBase $baseObject */ public function setBaseObject($baseObject) { $this->_baseObject = $baseObject; } /** * Get (and, if necessary, initialize) the base object * * @return HelloPlanetRatingBoxBase */ public function getBaseObject() { if (!is_object($this->_baseObject)) { include_once(dirname(__FILE__).'/Box/Base.php'); $this->_baseObject = new HelloPlanetRatingBoxBase(); } return $this->_baseObject; } /** * Get the box module's XML output * * @return string XML */ public function getParsedData() { $this->setDefaultData(); $this->initializeParams(); $baseObject = $this->getBaseObject(); $baseObject->setBoxData($this->data); return $baseObject->getBoxXml(); } }
Implementierung der Rating-Funktionalität
Um den Top-Down-Ansatz weiter zu verfolgen, sollten Sie folgende Schritte durchführen:
- Statische Implementierungen der Rating-Methoden im Connector und Refactoring der Klasse HelloPlanetRatingBoxBase
- Statische Implementierungen der Rating-Methoden in der Klasse HelloPlanetRatng und Refactoring des Connectors, so dass er diese verwendet
- Implementierung der eigentlichen Rating-Methoden in der Klasse HelloPlanetRatingDatabaseAccess und Refactoring der Klasse HelloPlanetRating
Da Sie dieses Prinzip bereits kennen, lassen wir einige Schritte aus, um das Ganze zu beschleunigen. Die nachfolgenden Unterabschnitte stellen den Zustand dar, nachdem alle drei Schritte bereits ausgeführt wurden. Dennoch sollten Sie diese Schritte zur Übung befolgen.
Implementierung der Rating-Datenbankzugriffsklasse
Das Rating verwendet seine eigene Datenbankzugriffsklasse. Implementieren Sie wie üblich zuerst die Unit-Test-Klasse. Sie heißt HelloPlanetRatingDatabaseAccessTest und liegt in der Datei Hello/Planet/Rating/Database/AccessTest.php in Ihrem Test-Verzeichnis. Hier der vollständige Quellcode der Test-Klasse:
<?php require_once(substr(dirname(__FILE__), 0, -75).'/Framework/PapayaTestCase.php'); PapayaTestCase::registerPapayaAutoloader(); require_once( PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet/Rating/Database/Access.php' ); class HelloPlanetRatingDatabaseAccessTest extends PapayaTestCase { /** * 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 ); 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 HelloPlanetRatingDatabaseAccess::getAverageRatings */ public function testGetAverageRatings() { $ratingDatabaseAccess = new HelloPlanetRatingDatabaseAccess(); $papayaDatabaseAccess = $this->getPapayaDatabaseAccessObjectFixture( array('queryFmt') ); $resultObject = $this->getDatabaseResultObjectFixture(array('fetchRow')); $data = array( array('planet_id' => 1, 'planet_name' => 'Mars', 'points' => 5), array('planet_id' => 2, 'planet_name' => 'Jupiter', 'points' => 4.5) ); $resultObject ->expects($this->atLeastOnce()) ->method('fetchRow') ->will( $this->onConsecutiveCalls($data[0], $data[1], NULL) ); $papayaDatabaseAccess ->expects($this->exactly(2)) ->method('getTableName') ->will( $this->onConsecutiveCalls( 'papaya_table_tutorial_planets', 'papaya_table_tutorial_planet_ratings' ) ); $papayaDatabaseAccess ->expects($this->once()) ->method('queryFmt') ->will($this->returnValue($resultObject)); $ratingDatabaseAccess->setDatabaseAccess($papayaDatabaseAccess); $this->assertEquals( array(1 => $data[0], 2 => $data[1]), $ratingDatabaseAccess->getAverageRatings() ); } /** * @covers HelloPlanetRatingDatabaseAccess::getUserRatings */ public function testGetUserRatings() { $ratingDatabaseAccess = new HelloPlanetRatingDatabaseAccess(); $papayaDatabaseAccess = $this->getPapayaDatabaseAccessObjectFixture( array('queryFmt') ); $resultObject = $this->getDatabaseResultObjectFixture(array('fetchRow')); $ratings = array( array('surfer_id' => 'abc123', 'planet_id' => 1, 'rating_points' => 4), array('surfer_id' => 'abc123', 'planet_id' => 2, 'rating_points' => 5) ); $resultObject ->expects($this->atLeastOnce()) ->method('fetchRow') ->will($this->onConsecutiveCalls($ratings[0], $ratings[1], NULL)); $papayaDatabaseAccess ->expects($this->once()) ->method('getTableName') ->will($this->returnValue('papaya_tutorial_ratings')); $papayaDatabaseAccess ->expects($this->once()) ->method('queryFmt') ->will($this->returnValue($resultObject)); $ratingDatabaseAccess->setDatabaseAccess($papayaDatabaseAccess); $expected = array(1 => 4, 2 => 5); $this->assertEquals($expected, $ratingDatabaseAccess->getUserRatings('abc123')); } /** * @covers HelloPlanetRatingDatabaseAccess::deleteRatings */ public function testDeleteRatings() { $ratingDatabaseAccess = new HelloPlanetRatingDatabaseAccess(); $papayaDatabaseAccess = $this->getPapayaDatabaseAccessObjectFixture( array('deleteRecord') ); $papayaDatabaseAccess ->expects($this->once()) ->method('getTableName') ->will($this->returnValue('papaya_tutorial_planet_ratings')); $papayaDatabaseAccess ->expects($this->once()) ->method('deleteRecord') ->will($this->returnValue(2)); $ratingDatabaseAccess->setDatabaseAccess($papayaDatabaseAccess); $this->assertTrue( $ratingDatabaseAccess->deleteRatings('1234567890abcdef1234567890abcdef', array(1, 2)) ); } /** * @covers HelloPlanetRatingDatabaseAccess::modifyRatings */ public function testModifyRatings() { $ratingDatabaseAccess = new HelloPlanetRatingDatabaseAccess(); $papayaDatabaseAccess = $this->getPapayaDatabaseAccessObjectFixture( array('deleteRecord', 'insertRecords') ); $papayaDatabaseAccess ->expects($this->exactly(2)) ->method('getTableName') ->will($this->returnValue('papaya_tutorial_planet_ratings')); $papayaDatabaseAccess ->expects($this->once()) ->method('deleteRecord') ->will($this->returnValue(2)); $papayaDatabaseAccess ->expects($this->once()) ->method('insertRecords') ->will($this->returnValue(2)); $ratingDatabaseAccess->setDatabaseAccess($papayaDatabaseAccess); $this->assertTrue( $ratingDatabaseAccess->modifyRatings( '1234567890abcdef1234567890abcdef', array(1 => 4, 2 => 5) ) ); } }
Nach dem Schreiben der Tests können wir die Klasse selbst implementieren, HelloPlanetRatingDatabaseAccess in der Datei Hello/Planet/Rating/Database/Access.php:
<?php /** * Hello World tutorial, Planet rating database access * * Provides the database access functionality for planet rating. * * @package Papaya-Modules * @subpackage tutorial */ /** * Hello World tutorial, Planet rating database access class * * @package Papaya-Modules * @subpackage tutorial */ class HelloPlanetRatingDatabaseAccess extends PapayaDatabaseObject { /** * Base name of database table planets * @var string */ private $_tablePlanets = 'tutorial_planets'; /** * Base name of database table planet ratings * @var string */ private $_tableRatings = 'tutorial_planet_ratings'; /** * Get average ratings * * @return array */ public function getAverageRatings() { $result = array(); $sql = "SELECT r.planet_id, AVG(r.rating_points) points, p.planet_name FROM %s r RIGHT JOIN %s p ON r.planet_id = p.planet_id GROUP BY r.planet_id"; $sqlParams = array( $this->databaseGetTableName($this->_tableRatings), $this->databaseGetTableName($this->_tablePlanets) ); if ($res = $this->databaseQueryFmt($sql, $sqlParams)) { while ($row = $res->fetchRow(DB_FETCHMODE_ASSOC)) { $result[$row['planet_id']] = $row; } } return $result; } /** * Get ratings for a specific surfer * * @param string $surferId * @return array */ public function getUserRatings($surferId) { $sql = "SELECT surfer_id, planet_id, rating_points FROM %s WHERE surfer_id = '%s'"; $sqlParams = array($this->databaseGetTableName($this->_tableRatings), $surferId); $result = array(); if ($res = $this->databaseQueryFmt($sql, $sqlParams)) { while ($row = $res->fetchRow()) { $result[$row['planet_id']] = $row['rating_points']; } } return $result; } /** * Delete ratings * * @param string $surferId optional, default empty string * @param array $planetIds optional, default empty array * @return boolean TRUE on success, FALSE otherwise */ public function deleteRatings($surferId = '', $planetIds = array()) { $result = FALSE; $conditions = array(); if (!empty($surferId)) { $conditions['surfer_id'] = $surferId; } if (!empty($planetIds)) { $conditions['planet_id'] = $planetIds; } $success = $this->databaseDeleteRecord( $this->databaseGetTableName($this->_tableRatings), $conditions ); if (FALSE !== $success) { $result = TRUE; } return $result; } /** * Add/replace ratings * * @param string $surferId * @param array $ratings * @return boolean TRUE on success, FALSE otherwise */ public function modifyRatings($surferId, $ratings) { $result = FALSE; $this->deleteRatings($surferId, array_keys($ratings)); $data = array(); foreach ($ratings as $planetId => $points) { $data[] = array( 'surfer_id' => $surferId, 'planet_id' => $planetId, 'rating_points' => $points ); } $success = $this->databaseInsertRecords( $this->databaseGetTableName($this->_tableRatings), $data ); if (FALSE !== $success) { $result = TRUE; } return $result; } }
Diese Datenbankzugriffsklasse enthält vier Methoden mit folgenden Aufgaben:
- getAverageRatings( ) gibt ein Array mit den durchschnittlichen Bewertungen jedes Planeten zurück. Die Methode wird verwendet, um die Ergebnisse in der Box anzuzeigen.
- getUserRatings( ) gibt ein Array mit den Bewertungen eines einzelnen Benutzers zurück, der durch die eindeutige Surfer-ID identifiziert wird. Diese Methode wird verwendet, um die Optionen bei einem Benutzer vorauszuwählen, der die Box erneut lädt.
- deleteRatings( ) wird nur verwendet, um die Bewertungen eines Benutzers zu löschen, bevor dessen neue Bewertungen gespeichert werden. Die Implementierung der Methode ermöglicht es jedoch, alle Bewertungen eines bestimmten Benutzers, alle Bewertungen für einen bestimmten Planeten oder die Gesamtheit aller Bewertungen zu löschen. Wenn Sie eine weitere Übungsaufgabe möchten, können Sie einen Aufruf dieser Methode in die Admin-Klasse einbauen, um die Bewertungen eines Planeten zu löschen, wenn dieser gelöscht wird. Dabei werden Sie allerdings die entsprechende Methode im Connector aufrufen.
- modifyRatings( ) speichert alle Planetenbewertungen für einen bestimmten Benutzer. Bevor die neuen Bewertungen eingefügt werden, wird deleteRatings( ) aufgerufen, um potentiell existierende Bewertungen desselben Benutzers loszuwerden. Dies ist oft einfacher, als zu bestimmen, ob Daten existieren und dann entsprechend Update oder Insert aufzurufen.
Die Rating-Datenbankzugriffsmethoden funktionieren ähnlich wie die in Teil 2 dieses Tutorials behandelte Klasse HelloPlanetDatabaseAccess. Die einzige neue Methode, die hier verwendet wird, ist databaseInsertRecords( ). Sie benötigt zwei Argumente, den Tabellennamen und ein verschachteltes Array mit Daten, in dem jeder Datensatz ein Array von Feld => Wert-Paaren ist.
Implementierung der öffentlichen Zugriffsklasse für die Rating-Methoden
Genau wie für die Planeten implementieren wir auch für die Rating-Methoden, die die Datenbankzugriffsklasse bereitstellt, eine öffentliche Zugriffsklasse. Für diese Klasse werden keine besonderen Erläuterungen benötigt. Der Unit-Test, Hello/Planet/RatingTest.php, sieht wie folgt aus:
<?php require_once(substr(dirname(__FILE__), 0, -59).'/Framework/PapayaTestCase.php'); PapayaTestCase::registerPapayaAutoloader(); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet/Rating.php'); require_once( PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet/Rating/Database/Access.php' ); class HelloPlanetRatingTest extends PapayaTestCase { /** * @covers HelloPlanetRating::setDatabaseAccessObject */ public function testSetDatabaseAccessObject() { $rating = new HelloPlanetRating(); $databaseAccessObject = $this->getMock('HelloPlanetRatingDatabaseAccess'); $rating->setDatabaseAccessObject($databaseAccessObject); $this->assertAttributeSame($databaseAccessObject, '_databaseAccessObject', $rating); } /** * @covers HelloPlanetRating::getDatabaseAccessObject */ public function testGetDatabaseAccessObject() { $rating = new HelloPlanetRating(); $databaseAccessObject = $rating->getDatabaseAccessObject(); $this->assertInstanceOf('HelloPlanetRatingDatabaseAccess', $databaseAccessObject); } /** * @covers HelloPlanetRating::getRatings */ public function testGetRatings() { $rating = new HelloPlanetRating(); $expected = array(1 => array('planet_id' => 1, 'planet_name' => 'Mars', 'points' => 5)); $databaseAccessObject = $this->getMock('HelloPlanetRatingDatabaseAccess'); $databaseAccessObject ->expects($this->once()) ->method('getAverageRatings') ->will($this->returnValue($expected)); $rating->setDatabaseAccessObject($databaseAccessObject); $this->assertEquals($expected, $rating->getRatings()); } /** * @covers HelloPlanetRating::getUserRatings */ public function testGetUserRatings() { $rating = new HelloPlanetRating(); $expected = array(1 => 4, 2 => 5); $databaseAccessObject = $this->getMock('HelloPlanetRatingDatabaseAccess'); $databaseAccessObject ->expects($this->once()) ->method('getUserRatings') ->will($this->returnValue($expected)); $rating->setDatabaseAccessObject($databaseAccessObject); $this->assertEquals($expected, $rating->getUserRatings('abc123')); } /** * @covers HelloPlanetRating::deleteRatings */ public function testDeleteRatings() { $rating = new HelloPlanetRating(); $databaseAccessObject = $this->getMock('HelloPlanetRatingDatabaseAccess'); $databaseAccessObject ->expects($this->once()) ->method('deleteRatings') ->will($this->returnValue(TRUE)); $rating->setDatabaseAccessObject($databaseAccessObject); $this->assertTrue( $rating->deleteRatings('1234567890abcdef1234567890abcdef', array(1, 2)) ); } /** * @covers HelloPlanetRating::modifyRatings */ public function testModifyRatings() { $rating = new HelloPlanetRating(); $databaseAccessObject = $this->getMock('HelloPlanetRatingDatabaseAccess'); $databaseAccessObject ->expects($this->once()) ->method('modifyRatings') ->will($this->returnValue(TRUE)); $rating->setDatabaseAccessObject($databaseAccessObject); $this->assertTrue( $rating->modifyRatings('1234567890abcdef1234567890abcdef', array(1 => 4, 2 => 5)) ); } }
Als Nächstes können Sie die Rating-Klasse selbst implementieren und sie in Hello/Planet/Rating.php speichern:
<?php /** * Hello World tutorial, Planet rating base functionality * * Public interface methods for planet rating * * @package Papaya-Modules * @subpackage tutorial */ /** * Hello World tutorial, Planet rating base functionality class * * @package Papaya-Modules * @subpackage tutorial */ class HelloPlanetRating { /** * The planet ratings database access object to be used * @var HelloPlanetRatingDatabaseAccess */ private $_databaseAccessObject = NULL; /** * Set the rating database access object to be used * * @param HelloPlanetRatingDatabaseAccess $databaseAccessObject */ public function setDatabaseAccessObject($databaseAccessObject) { $this->_databaseAccessObject = $databaseAccessObject; } /** * Get (and, if necessary, initialize) the rating database access object * * @return HelloPlanetRatingDatabaseAccess */ public function getDatabaseAccessObject() { if (!is_object($this->_databaseAccessObject)) { include_once(dirname(__FILE__).'/Rating/Database/Access.php'); $this->_databaseAccessObject = new HelloPlanetRatingDatabaseAccess(); } return $this->_databaseAccessObject; } /** * Get all average planet ratings * * @return array */ public function getRatings() { $databaseAccessObject = $this->getDatabaseAccessObject(); return $databaseAccessObject->getAverageRatings(); } /** * Get ratings for a specific user * * @param string $surferId * @return array */ public function getUserRatings($surferId) { $databaseAccessObject = $this->getDatabaseAccessObject(); return $databaseAccessObject->getUserRatings($surferId); } /** * Delete ratings * * @param string $surferId optional, default empty string * @param array $planetIds optional, default empty array * @return boolean TRUE on success, FALSE otherwise */ public function deleteRatings($surferId = '', $planetIds = array()) { $databaseAccessObject = $this->getDatabaseAccessObject(); return $databaseAccessObject->deleteRatings($surferId, $planetIds); } /** * Modify ratings * * @param string $surferId * @param array $ratings * @return boolean TRUE on success, FALSE otherwise */ public function modifyRatings($surferId, $ratings) { $databaseAccessObject = $this->getDatabaseAccessObject(); return $databaseAccessObject->modifyRatings($surferId, $ratings); } }
Die Rating-Methoden zur Connector-Klasse hinzufügen
Da der Connector sämtliche Basisfunktionalität bündelt, die die Content-Module benutzen, machen wir die Rating-Methoden darin verfügbar. Fügen Sie zunächst die folgenden Testmethoden zu Hello/ConnectorTest.php hinzu:
// Fügen Sie die folgenden Methoden unter testGetPlanetObject() ein: /** * @covers HelloConnector::setRatingObject */ public function testSetRatingObject() { $helloConnectorObject = new HelloConnector_TestProxy(); $ratingObject = $this->getMock('HelloPlanetRating'); $helloConnectorObject->setRatingObject($ratingObject); $this->assertAttributeSame($ratingObject, '_ratingObject', $helloConnectorObject); } /** * @covers HelloConnector::getRatingObject */ public function testGetRatingObject() { $helloConnectorObject = new HelloConnector_TestProxy(); $ratingObject = $helloConnectorObject->getRatingObject(); $this->assertInstanceOf('HelloPlanetRating', $ratingObject); } // ... weitere Testmethoden ... // Fügen Sie diese vier am Ende der Klasse HelloConnectorTest ein: /** * @covers HelloConnector::getPlanetRatings */ public function testGetPlanetRatings() { $helloConnectorObject = new HelloConnector_TestProxy(); $expected = array( 1 => array('planet_id' => 1, 'planet_name' => 'Mars', 'points' => 5), 2 => array('planet_id' => 2, 'planet_name' => 'Jupiter', 'points' => 4.5) ); $ratingObject = $this->getMock('HelloPlanetRating'); $ratingObject ->expects($this->once()) ->method('getRatings') ->will($this->returnValue($expected)); $helloConnectorObject->setRatingObject($ratingObject); $this->assertEquals($expected, $helloConnectorObject->getPlanetRatings()); } /** * @covers HelloConnector::getUserPlanetRatings */ public function testGetUserPlanetRatings() { $helloConnectorObject = new HelloConnector_TestProxy(); $expected = array(1 => 5, 2 => 4); $ratingObject = $this->getMock('HelloPlanetRating'); $ratingObject ->expects($this->once()) ->method('getUserRatings') ->will($this->returnValue($expected)); $helloConnectorObject->setRatingObject($ratingObject); $this->assertEquals($expected, $helloConnectorObject->getUserPlanetRatings('abc123')); } /** * @covers HelloConnector::deletePlanetRatings */ public function testDeletePlanetRatings() { $helloConnectorObject = new HelloConnector_TestProxy(); $ratingObject = $this->getMock('HelloPlanetRating'); $ratingObject ->expects($this->once()) ->method('deleteRatings') ->will($this->returnValue(TRUE)); $helloConnectorObject->setRatingObject($ratingObject); $this->assertTrue( $helloConnectorObject->deletePlanetRatings( '1234567890abcdef1234567890abcdef', array(1, 2) ) ); } /** * @covers HelloConnector::modifyPlanetRatings */ public function testModifyPlanetRatings() { $helloConnectorObject = new HelloConnector_TestProxy(); $ratingObject = $this->getMock('HelloPlanetRating'); $ratingObject ->expects($this->once()) ->method('modifyRatings') ->will($this->returnValue(TRUE)); $helloConnectorObject->setRatingObject($ratingObject); $this->assertTrue( $helloConnectorObject->modifyPlanetRatings( '1234567890abcdef1234567890abcdef', array(1 => 4, 2 => 5, 3 => 4) ) ); }
Nachdem der Test fertig ist, fügen Sie die Implementierung zu Hello/Connector.php hinzu:
// Deklaration des Attributs $_ratingObject unter der Deklaration von $_planetObject einfügen: /** * The HelloPlanetRating object to be used * @var HelloPlanetRating */ protected $_ratingObject = NULL; // Fügen Sie diese beiden Methoden nach getPlanetObject() ein: /** * Set the HelloPlanetRating object to be used * * @param HelloPlanetRating $ratingObject */ public function setRatingObject($ratingObject) { $this->_ratingObject = $ratingObject; } /** * Get (and, if necessary, initialize) the HelloPlanetRating object * * @return HelloPlanetRating */ public function getRatingObject() { if (!is_object($this->_ratingObject)) { include_once(dirname(__FILE__).'/Planet/Rating.php'); $this->_ratingObject = new HelloPlanetRating(); } return $this->_ratingObject; } // ... weitere Methoden ... // Fügen Sie diese vier am Ende der Klasse HelloConnector ein: /** * Get planet ratings * * @return array */ public function getPlanetRatings() { $ratingObject = $this->getRatingObject(); return $ratingObject->getRatings(); } /** * Get planet ratings for a specific surfer * * @param string $surferId * @return array */ public function getUserPlanetRatings($surferId) { $ratingObject = $this->getRatingObject(); return $ratingObject->getUserRatings($surferId); } /** * Delete planet ratings * * @param string $surferId optional, default '' * @param array $ratings optional, default array() * @return boolean TRUE on success, FALSE otherwise */ public function deletePlanetRatings($surferId = '', $planetIds = array()) { $ratingObject = $this->getRatingObject(); return $ratingObject->deleteRatings($surferId, $planetIds); } /** * Modify planet ratings * * @param string $surferId * @param array $ratings * @return boolean TRUE on success, FALSE otherwise */ public function modifyPlanetRatings($surferId, $ratings) { $ratingObject = $this->getRatingObject(); return $ratingObject->modifyRatings($surferId, $ratings); }
Implementierung der Rating-Funktionalität im Boxmodul
Die Ausgabe-Klasse unseres Boxmoduls muss modifiziert und erweitert werden, um die eigentliche Funktionalität für die Bewertung und die Anzeige der Ergebnisse hinzuzufügen. Weiter unten gibt es einige Erläuterungen, aber zuvor sehen Sie hier die vollständig geänderte Unit-Test-Klasse für die Klasse HelloPlanetRatingBoxBase, d.h HelloPlanetRatingBoxBaseTest:
<?php require_once(substr(dirname(__FILE__), 0, -70).'/Framework/PapayaTestCase.php'); PapayaTestCase::registerPapayaAutoloader(); PapayaTestCase::defineConstantDefaults( array( 'PAPAYA_DB_TBL_SURFER', 'PAPAYA_DB_TBL_SURFERGROUPS', 'PAPAYA_DB_TBL_SURFERPERM', 'PAPAYA_DB_TBL_SURFERACTIVITY', 'PAPAYA_DB_TBL_SURFERPERMLINK', 'PAPAYA_DB_TBL_SURFERCHANGEREQUESTS', 'PAPAYA_DB_TBL_TOPICS' ) ); require_once( PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet/Rating/Box/Base.php' ); require_once( PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet/Rating/Box.php' ); require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Connector.php'); class HelloPlanetRatingBoxBaseTest extends PapayaTestCase { /** * @covers HelloPlanetRatingBoxBase::setPluginloaderObject */ public function testSetPluginloaderObject() { $baseObject = new HelloPlanetRatingBoxBase(); $pluginloaderObject = $this->getMock('base_pluginloader'); $baseObject->setPluginloaderObject($pluginloaderObject); $this->assertAttributeSame($pluginloaderObject, '_pluginloaderObject', $baseObject); } /** * @covers HelloPlanetRatingBoxBase::getPluginloaderObject */ public function testGetPluginloaderObject() { $baseObject = new HelloPlanetRatingBoxBase(); $pluginloaderObject = $baseObject->getPluginloaderObject(); $this->assertInstanceOf('base_pluginloader', $pluginloaderObject); } /** * @covers HelloPlanetRatingBoxBase::setConnectorObject */ public function testSetConnectorObject() { $baseObject = new HelloPlanetRatingBoxBase(); $connectorObject = $this->getMock('HelloConnector'); $baseObject->setConnectorObject($connectorObject); $this->assertAttributeSame($connectorObject, '_connectorObject', $baseObject); } /** * @covers HelloPlanetRatingBoxBase::getConnectorObject */ public function testGetConnectorObject() { $baseObject = new HelloPlanetRatingBoxBase(); $pluginloaderObject = $this->getMock('base_pluginloader'); $connectorObject = $this->getMock('HelloConnector'); $pluginloaderObject ->expects($this->once()) ->method('getPluginInstance') ->will($this->returnValue($connectorObject)); $baseObject->setPluginloaderObject($pluginloaderObject); $this->assertSame($connectorObject, $baseObject->getConnectorObject()); } /** * @covers HelloPlanetRatingBoxBase::setOwner */ public function testSetOwner() { $baseObject = new HelloPlanetRatingBoxBase(); $owner = $this->getMock( 'HelloPlanetRatingBox', array(), array(), 'Mock_'.md5(__CLASS__. microtime()), FALSE ); $baseObject->setOwner($owner); $this->assertAttributeSame($owner, '_owner', $baseObject); } /** * @covers HelloPlanetRatingBoxBase::setDialogObject */ public function testSetDialogObject() { $baseObject = new HelloPlanetRatingBoxBase(); $dialogObject = $this->getMock( 'base_dialog', array(), array(), 'Mock_'.md5(__CLASS__. microtime()), FALSE ); $baseObject->setDialogObject($dialogObject); $this->assertAttributeSame($dialogObject, '_dialogObject', $baseObject); } /** * @covers HelloPlanetRatingBoxBase::getDialogObject */ public function testGetDialogObject() { $this->markTestSkipped(); } /** * @covers HelloPlanetRatingBoxBase::setData */ public function testSetData() { $baseObject = new HelloPlanetRatingBoxBase(); $expectedData = array('title' => 'Planet ratings'); $baseObject->setData($expectedData); $this->assertAttributeEquals($expectedData, '_data', $baseObject); } /** * @covers HelloPlanetRatingBoxBase::setParams */ public function testSetParams() { $baseObject = new HelloPlanetRatingBoxBase(); $expectedParams = array('planet_id' => 1, 'rating_points' => 5); $baseObject->setParams($expectedParams); $this->assertAttributeEquals($expectedParams, '_params', $baseObject); } /** * @covers HelloPlanetRatingBoxBase::saveRatings */ public function testSaveRatingsSuccess() { $baseObject = new HelloPlanetRatingBoxBase(); $helloConnectorObject = $this->getMock('HelloConnector'); $helloConnectorObject ->expects($this->once()) ->method('modifyPlanetRatings') ->will($this->returnValue(TRUE)); $baseObject->setConnectorObject($helloConnectorObject); $baseObject->setData(array('message_success' => 'Success')); $baseObject->setParams(array('planet_1' => 4, 'planet_2' => 5)); $expectedXml = '<message type="success">Success</message> '; $this->assertEquals($expectedXml, $baseObject->saveRatings('abc123')); } /** * @covers HelloPlanetRatingBoxBase::saveRatings */ public function testSaveRatingsFailure() { $baseObject = new HelloPlanetRatingBoxBase(); $helloConnectorObject = $this->getMock('HelloConnector'); $helloConnectorObject ->expects($this->once()) ->method('modifyPlanetRatings') ->will($this->returnValue(FALSE)); $baseObject->setConnectorObject($helloConnectorObject); $baseObject->setData(array('message_error' => 'Error')); $baseObject->setParams(array('planet_1' => 4, 'planet_2' => 5)); $expectedXml = '<message type="error">Error</message> '; $this->assertEquals($expectedXml, $baseObject->saveRatings('abc123')); } /** * @covers HelloPlanetRatingBoxBase::getRatingsForm */ public function testGetRatingsForm() { $baseObject = new HelloPlanetRatingBoxBase(); $helloConnectorObject = $this->getMock('HelloConnector'); $helloConnectorObject ->expects($this->once()) ->method('getAllPlanets') ->will($this->returnValue(array(1 => 'Mars', 2 => 'Jupiter'))); $helloConnectorObject ->expects($this->once()) ->method('getUserPlanetRatings') ->will($this->returnValue(array(1 => 4, 2 => 5))); $baseObject->setConnectorObject($helloConnectorObject); $dialogObject = $this->getMock( 'base_dialog', array(), array(), 'Mock_'.md5(__CLASS__. microtime()), FALSE ); $dialogObject ->expects($this->once()) ->method('getDialogXML') ->will($this->returnValue('<dialog />')); $baseObject->setDialogObject($dialogObject); $baseObject->setData(array('caption_dialog' => 'Rate planets', 'caption_submit' => 'Rate')); $this->assertEquals('<dialog />', $baseObject->getRatingsForm('abc123')); } /** * @covers HelloPlanetRatingBoxBase::getRatingsXml */ public function testGetRatingsXml() { $baseObject = new HelloPlanetRatingBoxBase(); $expectedXml = '<ratings> <planet id="1" name="Mars" rating="5.0" /> <planet id="2" name="Jupiter" rating="4.5" /> </ratings> '; $connectorObject = $this->getMock('HelloConnector'); $connectorObject ->expects($this->once()) ->method('getPlanetRatings') ->will( $this->returnValue( array( 1 => array('planet_id' => 1, 'planet_name' => 'Mars', 'points' => 5), 2 => array('planet_id' => 2, 'planet_name' => 'Jupiter', 'points' => 4.5) ) ) ); $baseObject->setConnectorObject($connectorObject); $this->assertEquals($expectedXml, $baseObject->getRatingsXml()); } /** * @covers HelloPlanetRatingBoxBase::getBoxXml */ public function testGetBoxXmlLoggedInSave() { $baseObject = new HelloPlanetRatingBoxBase(); $connectorObject = $this->getMock('HelloConnector'); $connectorObject ->expects($this->once()) ->method('modifyPlanetRatings') ->will($this->returnValue(TRUE)); $connectorObject ->expects($this->once()) ->method('getPlanetRatings') ->will($this->returnValue(array())); $baseObject->setConnectorObject($connectorObject); $surfer = $this->getMock('base_surfer'); $surfer->isValid = TRUE; $surfer->surferId = 'abc123'; $application = $this->getMockApplicationObject(array('surfer' => $surfer)); $baseObject->setApplication($application); $baseObject->setData(array('title' => 'Planet ratings', 'message_success' => 'Success')); $baseObject->setParams(array('save' => 1, 'planet_1' => 4, 'planet_2' => 5)); $expectedXml = '<ratingbox> <title>Planet ratings</title> <message type="success">Success</message> </ratingbox> '; $this->assertEquals($expectedXml, $baseObject->getBoxXml()); } /** * @covers HelloPlanetRatingBoxBase::getBoxXml */ public function testGetBoxXmlLoggedInDialog() { $baseObject = new HelloPlanetRatingBoxBase(); $connectorObject = $this->getMock('HelloConnector'); $connectorObject ->expects($this->once()) ->method('getAllPlanets') ->will($this->returnValue(array())); $baseObject->setConnectorObject($connectorObject); $surfer = $this->getMock('base_surfer'); $surfer->isValid = TRUE; $surfer->surferId = 'abc123'; $application = $this->getMockApplicationObject(array('surfer' => $surfer)); $baseObject->setApplication($application); $baseObject->setData(array('title' => 'Planet ratings')); $expectedXml = '<ratingbox> <title>Planet ratings</title> </ratingbox> '; $this->assertEquals($expectedXml, $baseObject->getBoxXml()); } /** * @covers HelloPlanetRatingBoxBase::getBoxXml */ public function testGetBoxXmlNotLoggedIn() { $baseObject = new HelloPlanetRatingBoxBase(); $connectorObject = $this->getMock('HelloConnector'); $connectorObject ->expects($this->once()) ->method('getPlanetRatings') ->will($this->returnValue(array())); $baseObject->setConnectorObject($connectorObject); $surfer = $this->getMock('base_surfer'); $surfer->isValid = FALSE; $application = $this->getMockApplicationObject(array('surfer' => $surfer)); $baseObject->setApplication($application); $baseObject->setData(array('title' => 'Planet ratings')); $expectedXml = '<ratingbox> <title>Planet ratings</title> </ratingbox> '; $this->assertEquals($expectedXml, $baseObject->getBoxXml()); } }
Wie Sie sehen, definieren wir eine Reihe von Konstanten, indem wir die statische Methode defineConstantDefaults() aus PapayaTestCase verwenden. Dies ist notwendig, um ein Mock-Objekt der Klasse base_surfer zu erzeugen, eine ältere papaya-Basisklasse, die den aktuellen Frontend-Benutzer repräsentiert.
Und hier ist die Klasse, die die überarbeiteten Unit-Tests bestehen sollte, HelloPlanetRatingBoxBase:
<?php /** * Hello World tutorial, Planet rating box base class * * Provides basic functionality for the rating box. * * @package Papaya-Modules * @subpackage tutorial */ /** * Hello World tutorial, Planet rating box base class * * @package Papaya-Modules * @subpackage tutorial */ class HelloPlanetRatingBoxBase extends PapayaObject { /** * The plugin loader object to be used * @var base_pluginloader */ private $_pluginloaderObject = NULL; /** * The HelloConnector object to be used * @var HelloConnector */ private $_connectorObject = NULL; /** * The dialog object to be used * @var base_dialog */ private $_dialogObject = NULL; /** * Owner object * @var HelloPlanetRatingBox */ private $_owner = NULL; /** * Box configuration data * @var array */ private $_data = array(); /** * Box request parameters * @var array */ private $_params = array(); /** * Set the plugin loader object to be used * * @param base_pluginloader $pluginloaderObject */ public function setPluginloaderObject($pluginloaderObject) { $this->_pluginloaderObject = $pluginloaderObject; } /** * Get (and, if necessary, initialize) the plugin loader object * * @return base_pluginloader */ public function getPluginloaderObject() { if (!is_object($this->_pluginloaderObject)) { include_once(PAPAYA_INCLUDE_PATH.'system/base_pluginloader.php'); $this->_pluginloaderObject = new base_pluginloader(); } return $this->_pluginloaderObject; } /** * Set the HelloConnector object to be used * * @param HelloConnector $connectorObject */ public function setConnectorObject($connectorObject) { $this->_connectorObject = $connectorObject; } /** * Get (and, if necessary, initialize) the HelloConnector object * * @return HelloConnector */ public function getConnectorObject() { if (!is_object($this->_connectorObject)) { $pluginloaderObject = $this->getPluginloaderObject(); $this->_connectorObject = $pluginloaderObject->getPluginInstance('eeb42aad2491cd607c7c64bc57eae455', $this); } return $this->_connectorObject; } /** * Set the owner object * * @param HelloPlanetRatingBox $owner */ public function setOwner($owner) { $this->_owner = $owner; } /** * Set the dialog object to be used * * @param base_dialog $dialogObject */ public function setDialogObject($dialogObject) { $this->_dialogObject = $dialogObject; } /** * Get (and, if necessary, initialize) the dialog object * * @param array $fields * @param array $data * @param array $hidden * @return base_dialog */ public function getDialogObject($fields, $data, $hidden) { if (!is_object($this->_dialogObject)) { $this->_dialogObject = new base_dialog( $this, $this->_owner->paramName, $fields, $data, $hidden ); } return $this->_dialogObject; } /** * Set configuration data * * @param array $data */ public function setData($data) { $this->_data = $data; } /** * Set box request parameters * @param array $params */ public function setParams($params) { $this->_params = $params; } /** * Save the ratings * * @param string $surferId * @return string XML */ public function saveRatings($surferId) { $ratings = array(); $result = ''; foreach ($this->_params as $field => $value) { if (strpos($field, 'planet_') === 0) { $id = substr($field, 7); $ratings[$id] = $value; } } if (!empty($ratings)) { $connectorObject = $this->getConnectorObject(); $success = $connectorObject->modifyPlanetRatings($surferId, $ratings); if ($success) { $result = sprintf( '<message type="success">%s</message>'.LF, papaya_strings::escapeHTMLChars($this->_data['message_success']) ); } else { $result = sprintf( '<message type="error">%s</message>'.LF, papaya_strings::escapeHTMLChars($this->_data['message_error']) ); } } return $result; } /** * Get the ratings form * * @param string $surferId * @return string XML */ public function getRatingsForm($surferId) { $result = ''; $connectorObject = $this->getConnectorObject(); $planets = $connectorObject->getAllPlanets(); if (!empty($planets)) { $ratingCombo = array(1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5); $fields = array(); foreach ($planets as $id => $name) { $fields['planet_'.$id] = array($name, 'isNum', FALSE, 'combo', $ratingCombo); } $ratings = $connectorObject->getUserPlanetRatings($surferId); $data = array(); if (!empty($ratings)) { foreach ($ratings as $id => $points) { $data['planet_'.$id] = $points; } } $hidden = array('save' => 1); $dialog = $this->getDialogObject($fields, $data, $hidden); if (is_object($dialog)) { $dialog->dialogTitle = $this->_data['caption_dialog']; $dialog->buttonTitle = $this->_data['caption_submit']; $result = $dialog->getDialogXML(); } } return $result; } /** * Get XML for the average ratings */ public function getRatingsXml() { $result = ''; $connectorObject = $this->getConnectorObject(); $ratings = $connectorObject->getPlanetRatings(); if (!empty($ratings)) { $result = '<ratings>'.LF; foreach ($ratings as $id => $data) { $result .= sprintf( '<planet id="%d" name="%s" rating="%.1f" />'.LF, $id, papaya_strings::escapeHTMLChars($data['planet_name']), $data['points'] ); } $result .= '</ratings>'.LF; } return $result; } /** * Get box XML output * * @return string XML */ public function getBoxXml() { $result = '<ratingbox>'.LF; $result .= sprintf( '<title>%s</title>'.LF, papaya_strings::escapeHTMLChars($this->_data['title']) ); $surfer = $this->getApplication()->surfer; if ($surfer->isValid) { if (isset($this->_params['save']) && $this->_params['save'] == 1) { $result .= $this->saveRatings($surfer->surferId); $result .= $this->getRatingsXml(); } else { $result .= $this->getRatingsForm($surfer->surferId); } } else { $result .= $this->getRatingsXml(); } $result .= '</ratingbox>'.LF; return $result; } }
Die Klasse HelloPlanetRatingBoxBase erbt nun von PapayaObject. Dies ist notwendig, um die papaya-Application einsetzen zu können, eine Implementierung des Registry-Entwurfsmusters, die es ermöglicht, Objekte zu speichern und in jeder Klasse Ihres Projekts verfügbar zu machen, die von PapayaObject oder einer ihrer zahlreichen Nachkommen abgeleitet ist. Dieser Ansatz ist eine testfreundliche Alternative zur Verwendung von Singletons.
Die Eigenschaft surfer im Application-Objekt ist die Instanz der Klasse base_surfer. base_surfer repräsentiert den aktuellen Frontend-User. Wenn ein User eingeloggt ist, ist die Eigenschaft isValid TRUE. Die Eigenschaft surferId enthält die eindeutige ID des eingeloggten Surfers. Wir verwenden sie, um die Planeten-Bewertungen eines Surfers zu speichern.
Für das Rating-Formular verwenden wir die Klasse base_dialog, die wir bereits in der Admin-Klasse in Teil 3 dieses Tutorials eingesetzt haben. Wenn Sie base_dialog in einer Frontend-Klasse verwenden, besteht der einzige unterschied darin, dass Sie Ihren eigenen XSLT-Template-Code für die Ausgabe schreiben müssen, genau wie für das gesamte XML, das Ihre Module erzeugen. Beachten Sie den übersprungenen Test für getDialogObject( ); den Grund haben wir bereits in Teil 3 erläutert. Neue und verbesserte tesfreundliche Implementierungen der papaya-Benutzeroberflächen-Klassen sind zurzeit in Entwicklung, und sobald sie fertig sind, werden wir dieses Tutorial aktualisieren.
Die Haupt-Aktionsmethode in der Rating-Box-Basisklasse ist getBoxXml( ), wie gehabt. Nach dem Hinzufügen des XML-Wurzelemenets <ratingbox> und des Titel-Knotens zur Ausgabe prüft die Methode, ob ein Surfer eingeloggt ist ($surfer->isValid). Wenn dies der Fall ist, prüfen wir auf den Parameter save, der als Hidden-Formularfeld bereitgestellt wird. Wenn dieser Parameter gesetzt ist und den erwarteten Wert 1 hat, rufen wir die Methode saveRatings( ) auf, die die Formulardaten ausliest und sie als aktuelle Bewertungen des Benutzers in der Datenbank speichert. Danach rufen wir die Methode getRatingsXml() auf, die die durchschnittlichen Planetenbewertungen einschließlich der Meinung des aktuellen Benutzers anzeigt. Wenn der Parameter save nicht gesetzt ist, rufen wir stattdessen getRatingsForm() auf, um den Rating-Dialog zur Ausgabe hinzuzufügen. Wenn kein Surfer eingeloggt ist, rufen wir einfach getRatingsXml() auf, um die Ergebnisse anzuzeigen. Es gibt drei Ausführungspfade in dieser Methode, die jeweils durch einen eigenen Unit-Test abgebildet werden:
- Eingeloggt - save-Parameter auf 1 gesetzt => testGetBoxXmlLoggedInSave( )
- Eingeloggt - kein save-Parameter => testGetBoxXmlLoggedInDialog( )
- Nicht eingeloggt => testGetBoxXmlNotLoggedIn( )
Ein XSLT-Template für die Rating-Box schreiben
Da sie Box ihr eigenes XML ausgibt, benötigen wir ein spezielles Template, um es in korrektes HTML zu transformieren. Hier ist der vollständige Quellcode der Template-Datei, box_planet_rating.xsl:
<?xml version="1.0"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <!-- @papaya:modules HelloPlanetRatingBox --> <xsl:import href="./base/boxes.xsl" /> <xsl:template match="/ratingbox"> <h2><xsl:value-of select="title/text()" /></h2> <xsl:if test="dialog"> <xsl:call-template name="showDialog"> <xsl:with-param name="dialog" select="dialog" /> </xsl:call-template> </xsl:if> <xsl:if test="message"> <div> <xsl:attribute name="class"> <xsl:choose> <xsl:when test="message/@type = 'error'">message error</xsl:when> <xsl:otherwise>message info</xsl:otherwise> </xsl:choose> </xsl:attribute> <xsl:value-of select="message/text()" /> </div> </xsl:if> <xsl:if test="ratings"> <xsl:call-template name="showRatings"> <xsl:with-param name="ratings" select="ratings" /> </xsl:call-template> </xsl:if> </xsl:template> <xsl:template name="showDialog"> <xsl:param name="dialog" /> <h3><xsl:value-of select="$dialog/@title" /></h3> <form action="{$dialog/@action}" method="{$dialog/@method}"> <xsl:copy-of select="$dialog/input[@type = 'hidden']" /> <xsl:for-each select="$dialog/lines/line"> <div class="dialogSelect"> <caption for="{@fid}"><xsl:value-of select="@caption" /></caption> <select id="{@fid}" name="{select/@name}"> <xsl:copy-of select="select/option" /> </select> </div> </xsl:for-each> <input type="submit" value="{$dialog/dlgbutton/@value}" /> </form> </xsl:template> <xsl:template name="showRatings"> <xsl:param name="ratings" /> <table class="planetRatings"> <xsl:for-each select="$ratings/planet"> <tr> <td><xsl:value-of select="@name" /></td> <td><xsl:value-of select="@rating" /></td> </tr> </xsl:for-each> </table> </xsl:template> </xsl:stylesheet>
Das Template wurde für das Standard-Template-Set geschrieben, das mit papaya CMS geliefert wird. Speichern Sie es in papaya-data/templates/default-xhtml/html. Navigieren Sie dann im papaya-Backend in den Bereich Ansichten. Wenn Sie noch keine Ansicht für die Planet Rating Box aus dem Package Hello World tutorial module erstellt haben, tun Sie dies jetzt, anderfalls wählen Sie diese Ansicht aus. Kreuzen Sie in der Ausgabefilter-Leiste auf der rechten Seite die Checkbox neben der Erweiterung html an. Wählen Sie box_planet_rating.xsl aus dem Pulldown-Menü XSL-Stylesheet und klicken Sie auf Speichern. Danach können Sie in den Bereich Boxen des Backends wechseln und eine Rating-Box erzeugen. Anschließend kann diese im Unterbereich Boxen im Bereich Seiten mit einer Seite Ihrer Wahl verknüpft werden.
Der @papaya:modules-Kommentar im XSL-Stylesheet gibt das Modul oder die Module an, für die das Template verwendet werden kann. Wenn die XSLT-Datei für mehr als ein Modul geeignet ist, können Sie eine durch Komma getrennte Liste von Modulen angeben. Als Boxmodul importiert das Stylesheet die Template-Datei base/boxes.xsl, die nützliche Ressourcen für Box-Templates bereitstellt. Box-Templates enthalten üblicherweise ein template-Element mit einem match-Attribut für das Wurzelelement des Boxmodul-XML, im vorliegenden Fall /ratingbox. Seiten-Templates beginnen dagegen normalerweise mit einem benannten Template für den Inhaltsbereich der Seite, das vom Haupt-Seitentemplate aufgerufen wird.
Die XSLT-Datei enthält auch zwei benannte Templates, um das Bewertungsformular beziehungsweise die Bewertungsergebnisse anzuzeigen. Dieser Ansatz macht Ihre Templates lesbarer.
Um Ihre eigenen Templates zu erstellen, können Sie die XML-Vorschau der Seite oder Box betrachten, an der Sie arbeiten, und dann XSLT schreiben, das zu diesem XML passt. Die Rating-Box stellt Sie jedoch vor ein zusätzliches Problem, wenn Sie dies tun möchten: Das Formular wird nur angezeigt, wenn ein Surfer eingeloggt ist. Deshalb müssen Sie zunächst eine Login-Seite oder -Box zu Ihrem Content hinzufügen. Beide finden Sie im Package Community. Navigieren Sie danach zur Anwendung Community im Backend und erzeugen Sie einen Surfer. Bitte beachten Sie, dass Sie Gültig aus dem Dropdown-Menü Status auswählen müssen, damit der Surfer sich tatsächlich einloggen kann.
Hinweis: Der Modus, der veröffentlichte Seiten anzeigt, und die Vorschau verwenden verschiedene Sessions -- da die Vorschau nur funktioniert, wenn Sie im Backend eingeloggt sind, wird für sie die Admin-Session verwendet. Das bedeutet, dass Sie für die XML-Vorschau des Rating-Formulars den URL-Pfad index.<id-der-loginseite>.html.preview statt index.<id-der-loginseite>.html verwenden müssen.