Modulprogrammierung 1: Content-Module

Aus PapayaCMS

Wechseln zu: Navigation, Suche

Tutorial

Zusammenfassung Dieses Tutorial beschreibt, wie man ein einfaches Modul oder Plugin für papaya CMS schreibt.
Zielgruppe PHP-Entwickler
Schwierigkeitsgrad Fortgeschrittene
Softwarevoraussetzungen SVN-Checkout von papaya CMS; Anleitung hier
PHPUnit >= 3.5 für Unit-Tests
Datum 2010-05-06
Nächstes Tutorial Modulprogrammierung 2: Datenbankunterstützung

Bevor Sie mit dem Schreiben von Modulen anfangen können, müssen Sie papaya CMS korrekt installieren und vorkonfigurieren, was detailliert in der Dokumentation beschrieben wird.

Bitte beachten Sie, dass Sie sich an die Papaya CMS Coding Standards halten sollten, insbesondere, wenn Sie planen, Module für die papaya-Community beizusteuern.

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 (siehe unten in Das passende Verzeichnis auswählen bezüglich der Auswahl von special/myproject und möglicher Alternativen):

+ [Hello]
|  |
|  + [Page]
|  |  |
|  |  + Base.php
|  |
|  + Page.php
|
+ modules.xml

 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:

+ [Hello]
   |
   + [Page]
   |  |
   |  + BaseTest.php
   |
   + BaseTest.php

 Hinweis: Namen in eckigen Klammern bezeichnen Verzeichnisse

Loslegen

Das in diesem Tutorial entwickelte Modul ist ein einfaches Seitenmodul, das die berühmte Nachricht "Hello World" ausgibt. Es benötigt keinen Datenbankzugriff und besteht nur aus einer Datei. Im weiteren Verlauf wird das Modul etwas ausgebaut; neben "Hello World" wird ein vom Redakteur pflegbarer Text ausgegeben.

Terminologie: Modul und Modulpaket

Wenn Sie das Unterverzeichnis modules im Verzeichnis papaya-lib durchsuchen, werden Sie verschiedene Unterverzeichnisse finden, die im nächsten Unterabschnitt genauer erläutert werden. Jedes dieser Unterverzeichnisse enthält entweder Dateien oder weitere Unterverzeichnisse. Ein Verzeichnis wie modules/_base/community wird als Modulpaket bezeichnet: Die meisten der darin befindlichen Dateien teilen sich dieselben Datenbanktabellen und arbeiten zusammen, um einen bestimmten Dienst bereitzustellen; das Community-Paket verarbeitet beispielsweise Frontend-Logins, Benutzerprofile, Kontakte und so weiter.

Module sind Dateien, die sich im Verzeichnis eines Modulpakets befinden und über einen Global Universal Identifier (guid) in der Datei modules.xml registriert sind. Ein Modul stellt einen Aspekt der Aufgaben des Pakets zur Verfügung. Das Community-Paket enthält beispielsweise eine Datei namens content_login.php, eine Standard-Loginseite, oder box_login.php, eine Login-Box, die in andere Seiten integriert werden kann.

Sie können alle verfügbaren Pakete und ihre Module betrachten, indem Sie in der Hauptsymbolleiste des papaya-CMS-Backends auf Module klicken. Ein Kasten auf der linken Seite zeigt die Liste aller Pakete an. Sobald Sie eines von ihnen anklicken, wird in der mittleren Spalte eine Liste der Module (und Datenbanktabellen) in diesem Paket angezeigt. Klicken Sie ein Modul an, dann erscheinen rechts Informationen über das Modul, und Sie können einige Einstellungen dafür vornehmen.

Das passende Verzeichnis auswählen

Wenn Sie sich im Verzeichnis papaya-lib/modules Ihrer papaya-Installation umschauen, werden Sie mindestens folgende Unterverzeichnisse entdecken:

  • _base -- Basismodule wie eine einfache HTML-Box oder sämtliche Community-Module
  • free -- freie Module unter der GNU GPL
  • gpl -- freie Module, die auf GPL-Software von Drittanbietern basieren
  • sample -- ein einfaches Beispielmodul; es ähnelt demjenigen, das wir in diesem Tutorial entwickeln.

Falls Sie ein oder mehrere kommerzielle papaya-Module kaufen, werden diese in einem weiteren Unterverzeichnis namens commercial gespeichert.

Wenn Sie ein Modul schreiben, das nur für den Bedarf Ihres spezifischen Projekts geeignet ist, erstellen Sie ein neues Unterverzeichnis namens special und darin wiederum ein Unterverzeichnis, dessen Name Ihrem Projekt entspricht. Module für einen weniger spezifischen Anwendungszweck werden für gewöhnlich im Verzeichnis free abgelegt (oder in einem beta-Verzeichnis, solange sie in Entwicklung sind). Tun Sie dies jedoch nicht, ohne uns zu kontaktieren, denn es könnte spätere Updates Ihres papaya CMS verkomplizieren, falls jemand anders ein gleichnamiges Modulpaket beiträgt. Bleiben wir also im Moment bei special/myproject.

Nachdem Sie das Verzeichnis ausgewählt oder erstellt haben, in dem Ihr Modul gespeichert werden soll, erstellen Sie darin ein Unterverzeichnis namens tutorial.

Die Datei modules.xml vorbereiten

Jedes Modulpaket benötigt eine Modulinformationsdatei namens modules.xml. Diese Datei listet die Module und Datenbanktabellen innerhalb des Pakets auf, und indem papaya CMS die Modulverzeichnisse rekursiv nach diesen Dateien durchsucht, werden dem System neue, geänderte oder gelöschte Module bekannt gemacht.

Hier ein kurzes beispiel für eine modules.xml-Datei aus dem Modulpaket _base/countries:

<?xml version="1.0"  encoding="ISO-8859-1" ?>
<modulegroup>
  <name>Countries</name>
  <description>
    The country package provides backend functionality to manage continents and
    countries as well as a connector with form callback functions.
  </description>
  <modules>
    <module type="page"
            guid="fd53aef2d8bb7cb4637a64dabaf7b424"
            name="State List XML"
            class="content_statelist"
            file="content_statelist.php">
      Returns an XML list of states for a specific country, to be used for Ajax
    </module>
    <module type="admin"
            guid="bf6e40b71d3cfb0e80682c64b11d33af"
            name="Countries"
            class="edmodule_countries"
            file="edmodule_countries.php"
            glyph="countries.png">
      The administration module provides the facility to manage continents,
      countries, and their localized names.
    </module>
    <module type="connector"
            guid="99db2c2898403880e1ddeeebf7ee726c"
            name="Country Connector"
            class="connector_countries"
            file="connector_countries.php">
      Country Connector
    </module>
  </modules>
  <tables>
    <table name="continents"/>
    <table name="countries"/>
    <table name="countrynames"/>
    <table name="states"/>
    <table name="countries_old"/>
    <table name="states_old"/>
  </tables>
</modulegroup>

Das Wurzelelement modulegroup umfasst den gesamten Inhalt. Die Elemente name und description enthalten nur einfachen Text -- es handelt sich um den Namen beziehungsweise die Kurzbeschreibung des Pakets. Der Abschnitt modules enthält für jedes Modul des Pakets ein module-Element, während tables für jede Datenbanktabelle einen table-Eintrag besitzt.

Ein module-Element besteht aus fünf oder mehr Attributen und einer umschlossenen Beschreibung in Form von einfachem Text. Die Attribute werden wie folgt definiert:

  • type -- der Typ des Moduls, zum Beispiel "page" für ein Seitenmodul oder "box" für ein Boxmodul
  • guid -- eine eindeutige Identifikationsnummer für das Modul: ein String, der eine hexadezimale, 128 Bit lange Zahl enthält
  • name -- der Modulname, wie er im papaya-Backend angezeigt werden soll
  • class -- der eigentliche Name der PHP-Klasse, die das Modul definiert
  • file -- die Datei, in der sich das Modul befindet (optional können Sie für Dateien in Unterverzeichnissen relative Pfade angeben)
  • glyph -- der Name einer Icon-Datei für ein Modul (nur für Module vom Typ admin empfohlen, da diese verwendet werden, um das gesamte Paket zu konfigurieren)
  • outputfilter -- ein optionales Attribut für Content-Module (die Typen page oder box): Wenn Sie dieses Attribut auf den Wert "no" setzen, wird kein Ausgabefilter verwendet, um ihre finale Ausgabe zu erzeugen (das Konzept des Ausgabefilters wird detaillierter in ##papaya-Architektur## erläutert)

Die table-Elemente im Abschnitt tables enthalten lediglich name-Attribute. Die Tabellenstrukturen selbst werden im Unterverzeichnis DATA eines jeden Modulpaket-Verzeichnisses abgelegt. Diese Dateien sollten niemals von Hand geschrieben werden; im nächsten Tutorial erfahren Sie, wie Sie sie aus dem papaya-Backend erzeugen können.

Wir benötigen nun eine neue modules.xml-Datei für das gewünschte Paket; sie wird nur ein einzelnes Modul und keine Datenbanktabellen enthalten. Öffnen Sie Ihren bevorzugten Text- oder XML-Editor und tippen (oder kopieren) Sie Folgendes:

<?xml version="1.0" encoding="utf-8" ?>
<modulegroup>
  <name>Hello World tutorial module</name>
  <description>A tutorial to learn papaya CMS module development.</description>
  <modules>
    <module type="page"
            guid=""
            name="Hello World Page"
            class="HelloPage"
            file="Hello/Page.php">
      This simple page module displays a Hello World message
    </module>
  </modules>
</modulegroup>

Bitte beachten Sie, dass wir das Attribut guid zunächst leer gelassen haben. Aber wir brauchen natürlich einen Wert, weil das Modul anhand dieser Identifikationsnummer registriert wird. Sie können entweder den GUID-Generator verwenden oder aber einfachen PHP-Code (die meisten anderen Sprachen funktionieren auch) wie diesen hier schreiben:

<?php
 
echo md5(rand());

Fügen Sie nun Ihren neuen Hashwert zwischen den Anführungszeichen des Attributs guid ein und speichern Sie die Datei.

Das Modul schreiben

Bei den empfohlenen Verzeichnis- und Dateinamensstrukturen für papaya-Module besteht ein Unterschied zwischen älteren, PHP-4-kompatiblen Entwicklungen und neuen Nur-PHP-5-Paketen. Das liegt daran, dass für neue Module Unit Tests verwendet werden sollten. Wenn Sie noch nie etwas von Unit Tests im Allgemeinen und PHPUnit (dem Standard-Test-Framework für PHP) gehört haben: keine Sorge; alles Nötige wird im Lauf des Tutorials erläutert. Eine der besten Ressourcen, um mit Unit Tests anzufangen, ist der Artikel Test Infected: Programmers Love Writing Tests von Erich Gamma und Kent Beck. Detaillierte Informationen über PHPUnit erhalten Sie dagegen auf der offiziellen PHPUnit-Site.

Für dieses Projekt verwenden wir sogar den Test-First-Ansatz: Schreiben Sie einen Unit Test für jeden Teil Ihres Codes, bevor Sie den eigentlichen Code schreiben.

Bevor wir beginnen, den eigentlichen Code zu schreiben, erstellen wir die grundlegende Verzeichnisstruktur.

Erstellen Sie die folgenden Verzeichnisse im weiter oben ausgewählten Bereich:

+ tutorial [wurde bereits erstellt und enthält die Datei modules.xml]
   |
   + Hello
      |
      + Page

Suchen Sie jetzt das Verzeichnis testing/test-unittests Ihrer papaya-Installation. Es sollte bereits den Unterpfad papaya-lib/modules enthalten (falls nicht, erzeugen Sie ihn einfach). Erstellen Sie als Nächstes dieselbe verschachtelte Struktur tutorial/Hello/Page in denjenigen Unterverzeichnissen der Testumgebung, die dem Ort dieser Struktur in der Modulverzeichnishierarchie entspricht.

Top-down und testgetrieben: ein statisches Content-Modul

Testgetriebener, objektorientierter Code sollte nach einem Top-down-Ansatz geschrieben werden: Sie beginnen mit einer statischen Implementierung dessen, was der Benutzer auf dem Bildschirm zu sehen bekommt, und implementieren erst danach die zugrundeliegende Logik. Dieser Ansatz garantiert, dass jeder Teil des Codes unabhängig ist und zu jeder Zeit flexibel durch eine andere Implementierung ersetzt werden kann.

Der erste Teil, der implementiert wird, ist das Seitenmodul selbst. Erstellen Sie im Verzeichnis tutorial/Hello eine leere PHP-Datei namens Page.php und im Verzeichnis tutorial/Hello des Unit-Test-Verzeichnisbaums eine weitere leere PHP-Datei namens PageTest.php.

Gemäß dem Test-first-Ansatz sollten Sie die Unit-Test-Klassendatei vorbereiten und den ersten Test schreiben, bevor Sie irgendwelchen Implementierungscode schreiben. Ein PHPUnit-Test-Case erweitert die Basisklasse PHPUnit_Framework_TestCase. Für die spezifischen Bedürfnisse von papaya-CMS-Unit-Tests gibt es jedoch bereits die Klasse PapayaTestCase, die Sie erweitern können; sie befindet sich um Unterverzeichnis Framework des Pfads testing/tests-unittests.

Der Rahmen der Klassendatei PageTest.php sieht folgendermaßen aus:

<?php
 
require_once(substr(dirname(__FILE__), 0, -52).'/Framework/PapayaTestCase.php');
require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Page.php');
 
class HelloPageTest extends PapayaTestCase {
}

Um den korrekten Importpfad für die Datei PapayaTestCase.php zu ermitteln, müssen Sie die Zeichen des Unterpfads Ihres Testverzeichnisses unterhalb von testing/tests-unittests zählen. Im obigen Beispiel werden 53 Zeichen verwendet, weil wir daon ausgehen, dass der Unterpfad /papaya-lib/modules/special/myproject/tutorial/Hello ist. Sie müssen dies anpassen, falls Sie eine andere Verzeichnisstruktur verwenden. Ähnliches gilt auch für die nächste Zeile, die die zu testende Klasse selbst importiert.

Um eine papaya-Content-Modulklasse zu testen, muss ein weiteres Problem gelöst werden: Der Konstruktor von base_plugin, dem gemeinsamen Vorfahren der Basisklassen sowohl für Seiten- als auch für Boxmodule (base_content beziehungsweise base_actionbox), erwartet eine Referenz auf das Eigentümer-Objekt. Da eine Testklasse kein passender Eigentümer für ein Objekt wie dieses ist, stellen wir eine sogenannte Proxy-Klasse bereit, die unsere Modulklasse erweitert und den Konstruktur durch einen ersetzt, der keine Eigentümerreferenz benötigt. Fügen Sie hinter der schließenden geschweiften Klammer der Klassendefinition von HelloPageTest einfach folgenden Code hinzu:

class HelloPageProxy extends HelloPage {
  function __construct() {
    // Nothing to do here, just override parent's constructor
    // to get rid of the mandatory parameter
  }
}

Sie können in der Testklasse eine private Methode schreiben, um die zu testende Klasse (oder, in diesem Fall, die Proxy-Klasse) zu instantiieren. Fügen Sie dazu folgende Methode zur Klasse HelloPageTest hinzu:

  /**
  * Instantiate the HelloPage object to be tested
  *
  * @return HelloPage
  */
  private function getHelloPageObjectFixture() {
    return new HelloPageProxy();
  }

Die Methode, die wir testen und anschließend implementieren möchten, heißt getParsedData( ). Es handelt sich um die einzige Pflichtmethode in einem papaya-Content-Modul, die verwendet wird, um wohlgeformtes XML zu erzeugen, das mit Hilfe von XSLT-Templates in das endgültige Ausgabeformat umgewandelt wird. Da wir einen Top-down-Ansatz gewählt haben, ist die erste Implementierung dieser Methode statisch (im Sinne von festgelegt; nicht static im Sinne einer Klassenmethode): Wir schreiben den Test so, dass er die Rückgabe von festgelegtem XML erwartet, und implementieren die Methode dann so, dass genau dieses XML zurückgegeben wird.

Fügen Sie die folgende Methode zur Testklasse hinzu:

  /**
  * @covers HelloPage::getParsedData
  */
  public function testGetParsedData() {
    $helloPageObject = $this->getHelloPageObjectFixture();
    $expectedXml = '<title>Hello world!</title>
<text>Greetings from the new module</text>';
    $this->assertEquals(
      $expectedXml,
      $helloPageObject->getParsedData()
    );
  }

Die Namen der Testmethoden in Unit Tests müssen stets mit test beginnen, damit PHPUnit sie ausführt, und die Methoden müssen public sein. Die PHPDoc-Annotation @covers wird verwendet, um die Code Coverage Ihrer Unit Tests zu ermitteln -- einfach gesagt den Prozentsatz des Codes, der durch Tests abgedeckt ist. Mit Hilfe dieser Annotationen können Sie unter anderem verhindern, dass Ihre Tests andere Methoden abdecken, die von den getesteten Methoden implizit aufgerufen werden.

Der wichtgiste Bestandteil von Unit Tests sind die diversen assert...( )-Methoden, die verwendet werden können, um die Rückgabewerte der getesteten Methoden gegen beinahe beliebige Bedingungen zu prüfen. assertEquals( ) testet auf Gleichheit und ist eine der am häufigsten verwendeten Methoden aus der Gruppe. Bitte achten Sie darauf, stets die oben gezeigte Reihenfolge einzuhalten: der erwartete Wert ist das erste Argument und der eigentliche, zu testende Methodenaufruf das zweite.

Als Nächstes können Sie beginnen, Ihre HelloPage-Klasse zu schreiben und den Kopf der Methode getParsedData( ) hinzufügen:

<?php
 
/**
* Hello World tutorial page module
*
* @package Papaya-Modules
* @subpackage tutorial
*/
 
/**
* Base class base_content
*/
require_once(PAPAYA_INCLUDE_PATH.'system/base_content.php');
 
/**
* Hello World tutorial page module class
*
* @package Papaya-Modules
* @subpackage tutorial
*/
class HelloPage extends base_content {
 
  /**
  * Get the page output XML
  *
  * @return string XML
  */
  public function getParsedData() {
  }
 
}

Genau wie in diesem Beispiel sollten Sie stets Gebrauch von PHPDoc machen, um Ihren Code zu dokumentieren. Details über PHPDoc finden Sie auf der offiziellen Website.

Es mag ein wenig absurd klingen, aber Sie sollten jetzt Ihren Unit Test ausführen, bevor Sie den eigentlichen Code implementieren, obwohl Sie wissen, dass der Test scheitern wird. Die Arbeitsreihenfolge der testgetriebenen Entwicklung ist:

  1. Schreiben Sie einen Test, der fehlschlägt
  2. Implementieren Sie den Code, der notwendig ist, um den Test zu bestehen
  3. Führen Sie ein Code-Refactoring durch, damit die Implementierung dem Gesamtziel Ihres Projekts entspricht

Noch kürzer gesagt: "red, green, refactor" -- in den meisten grafischen Darstellungen von Unit Tests werden fehlgeschlagene Tests rot und bestandene grün dargestellt.

Um Ihren PHPUnit-Test von der Konsole aus auszuführen, geben Sie Folgendes ein:

$ phpunit [Pfad/zu/]HelloPageTest.php

Da der Test scheitert, sieht die Ausgabe so aus:

PHPUnit 3.4.2 by Sebastian Bergmann.

F

Time: 0 seconds

There was 1 failure:

1) HelloPageTest::testGetParsedData
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-<title>Hello world!</title>
-<text>Greetings from the new module</text>
+

.../testing/tests-unittests/papaya-lib/modules/special/myproject/tutorial/Hello/PageTest.php:23

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

Wie Sie sehen, unterscheidet sich der erwartete Rückgabewert (der XML-Code) vom tatsächlichen Rückgabewert (gar nichts). Das heißt, dass es definitiv Zeit ist, eine statische Version von getParsedData( ) zu implementieren, die den Test erfüllt:

  /**
  * Get the page output XML
  *
  * @return string XML
  */
  public function getParsedData() {
    $result = '<title>Hello world!</title>'.LF;
    $result .= '<text>Greetings from the new module</text>';
    return $result;
  }

Führen Sie Ihren Test erneut aus; diesmal sollte er Erfolg melden (ein einzelner '.' für einen einzelnen bestandenen Test statt eines 'F' wie 'failed' für einen gescheiterten):

PHPUnit 3.4.2 by Sebastian Bergmann.

.

Time: 0 seconds

OK (1 test, 1 assertion)

... und das war's! Sie haben erfolgreich ein -- wenn auch statisches -- papaya-Seitenmodul erstellt. Wenn Sie mit SVN oder einem anderen Versionsverwaltungssystem arbeiten, ist es jetzt Zeit, Ihren Code in das Repository zu committen. Diese Arbeitsweise wird als Continuous Integration bezeichnet, ein grundlegender Bestandteil agiler Softwareentwicklungsmethoden: Committen Sie so oft, wie Sie können, wobei durch das Bestehen zuvor geschriebener Unit Tests sichergestellt ist, dass jederzeit "clean code that works" bereitgestellt wird.

Zeit für einen Live-Test im Frontend

Sie sollten sich jedoch nicht allein auf Unit Tests verlassen, sondern Ihr Modul auch in Ihrem Browser testen. Da wir für das Ergebnis die XML-Elemente title und text gewählt haben, können wir das bestehende XSLT-Template für Standardseiten verwenden, so dass wir jetzt kein Template zu schreiben brauchen. Um Ihr Modul in papaya CMS zu testen, führen Sie folgende Schritte durch:

  1. Melden Sie sich im papaya-Backend als Administrator an.
  2. Wählen Sie in der Hauptsymbolleiste Module und klicken Sie dann auf Module suchen. Ihr Modul sollte nach einer gewissen Suchdauer, die durch einen Fortschrittsbalken dargestellt wird, hinzugefügt werden.
  3. Klicken Sie nun in der Hauptsymbolleiste auf Ansichten. Eine Ansicht ist die Verknüpfung zwischen einem Modul und einem Template, woraus sich eine bestimmte Art der Frontend-Ausgabe und -Funktionalität ergibt.
  4. Im Bereich Ansichten sollten Sie ein Formular mit den Feldern Titel und Modul sehen, mit dem eine neue Ansicht erstellt werden kann; falls nicht, klicken Sie die Schaltfläche Ansichten in der Untersymbolleiste an.
  5. Geben Sie als Modultitel Hello World Page ein, wählen Sie im Modulselektor [page] Hello World Page aus dem Bereich Hello World tutorial module aus und klicken Sie auf Hinzufügen.
  6. Rechts sehen Sie eine Leiste mit dem Titel Ausgabefilter (wenn nicht, haben Sie Ihr papaya CMS noch nicht konfiguriert). Kreuzen Sie das Kontrollkästchen Verknüpft neben der Erweiterung html an. Die Warnung Keine XSLT-Datei festgelegt. wird ausgegeben.
  7. Wählen Sie page_main.xsl aus dem XSL-Stylesheet-Auswahlfeld, ignorieren Sie die restlichen Einstellungen und klicken Sie auf Speichern. Die Meldung Änderungen gespeichert. und die Warnung XSLT-Datei "page_main.xsl" unterstützt das Modul "HelloPage" möglicherweise nicht. werden angezeigt; Letztere können Sie im Moment getrost ignorieren.
  8. Wählen Sie Seiten aus der Hauptsymbolleiste.
  9. Navigieren Sie mit Hilfe der Leiste am linken Bildschirmrand duch den Seitenbaum. Wenn Sie einen Platz für Ihre Seite gefunden haben, klicken Sie auf Seite hinzufügen, um eine Seite auf der Ebene der aktuell ausgewählten Seite hinzuzufügen, oder auf Unterseite hinzufügen, um eine Seite auf einer Ebene unterhalb der aktuellen zu erstellen.
  10. Geben Sie auf dem Tab Eigenschaften der neuen Seite den Titel Hello World Page ein und klicken Sie auf Speichern.
  11. Klicken Sie danach den Tab Ansicht an und wählen Sie die Ansicht Hello World Page aus dem Abschnitt Hello World tutorial module aus, indem Sie ihren Titel anklicken.
  12. Sie brauchen keinen Inhalt für die neue Seite festzulegen, da dieser statisch ist; der Tab Inhalt meldet lediglich Kein Einstellungsdialog für dieses Modul. Deshalb können Sie gleich den Tab Vorschau aktivieren und die Frontend-Ausgabe Ihres neuen Moduls betrachten: Hello world! als Überschrift und Greetings from the new module als Seiteninhalt.
  13. Der Bereich Vorschau ermöglicht es Ihnen, zwischen den Ansichtsmodi HTML und XML zu wechseln, so dass Sie sehen können, wie das Seiten-XML vom Standard-Template in HTML umgewandelt wird.

Refactoring: ein dynamisches Modul erstellen

Unser statisches Modul ist fertig, aber nun wollen wir es dynamisch machen: Der Text unter der Überschrift Hello World soll durch einen Redakteur bearbeitet werden können. Dazu ist ein wenig Refactoring nötig. Der übliche Ansatz für moderne, testgetrieben entwickelte papaya-Module umfasst eine Base-Klasse unterhalb der Frontend-Klasse, die sich um die dynamische Ausgabe und andere Aspekte der Logik kümmert. In diesem Schritt erstellen wir die Base-Klasse und verbinden sie mit der Frontend-Klasse.

Um eine Frontend-Klasse konfigurierbar zu machen, fügen Sie einfach ein öffentliches Attribut namens $editFields vom Typ Array hinzu. Wenn es viel zu editieren gibt, können Sie als Alternativ $editGroups verwenden, ein verschachteltes Array, in dem Sätze von Eingabefeldern auf mehreren Seiten angezeigt werden. Bleiben wir vorerst bei den klassischen $editFields, da unser Modul nur ein einziges Feld erhalten soll.

Fügen Sie folgenden Code zu Ihrer HelloPage-Klasse hinzu, direkt über dem Docblock und der Deklaration der Methode getParsedData( ):

  /**
  * Parameter namespace
  * @var string
  */
  public $paramName = 'tut';
 
  /**
  * Edit fields for page configuration
  * @var array
  */
  public $editFields = array(
    'text' => array(
      'Text',
      'isNoHTML',
      TRUE,
      'textarea',
      5,
      '',
      'Greetings from the new module'
    )
  );

Nachdem Sie diese Änderung gespeichert haben, können Sie sie sofort ausprobieren: Wählen Sie im Bereich Seiten des papaya-CMS-Backends den Tab Inhalt Ihrer neuen Seite; statt der Warnung von vorhin sehen Sie dort jetzt einen Textbereich, um den Seitentext zu bearbeiten, sowie eine Speichern-Schaltfläche.

Der String $paramName stellt den gemeinsamen Namensraum für Parameter des Moduls dar, sowohl für das Frontend als auch für das Backend (also die Inhaltskonfiguration). Der String wird dem jeweiligen als Präfix vorangestellt, durch ein konfigurierbares Trennzeichen getrennt.

Im Array $editFields ist der Schlüssel für jedes Feld dessen interner name, während der Wert ein Array mit folgenden Elementen ist:

  • Die Beschriftung, die im Inhaltsformular der Seite erscheint
  • Überprüfungstyp (wie in der Klasse sys_checkit im papaya-Verzeichnis system definiert). isNoHTML wird verwendet, um auf beliebigen Text zu überprüfen, der keine HTML-Auszeichnungen enthalten darf.
  • Ein Boolean-Wert, der bestimmt, ob es sich um ein Pflichtfeld handelt (TRUE) oder nicht (FALSE)
  • Der Feldtyp (zum Beispiel 'input' für ein einzeiliges Textfeld, 'textarea', 'checkbox' und so weiter)
  • Die Feldeinstellungen (hängen vom Feldtyp ab; bei 'input' wird beispielsweise die maximale Anzahl eingebbarer Zeichen angegeben, bei 'textarea' die Anzahl sichtbarer Zeilen)
  • Optionaler Tooltipp, der durch Mausberührung eines kleinen Lampensymbols neben der Beschriftung angezeigt wird, falls vorhanden
  • Vorgabeinhalt -- ebenfalls optional, aber Sie sollten sicherstellen, dass jedes Feld mit der Pflichtfeld-Einstellung TRUE einen Vorgabeinhalt besitzt

Sie können auch einfache Strings ohne bestimmten Index zum Array $editFields hinzufügen. Diese werden im Bearbeitungsformular als Zwischenüberschriften angezeigt.

Jedes Feld, das Sie im Array $editFields definieren, wird später in einem Attribut namens $data bereitgestellt, entweder mit seinem Vorgabewert oder mit demjenigen, den der Redakteur einstellt.

Als Nächstes werden die Klassendatei Base.php im Unterverzeichnis Page sowie ihr Unit Test erzeugt. Der Rahmen der Base-Klasse sieht folgendermaßen aus:

<?php
 
/**
* Hello World tutorial page module, base class
*
* @package Papaya-Modules
* @subpackage tutorial
*/
 
/**
* Hello World tutorial page module class, base class
*
* @package Papaya-Modules
* @subpackage tutorial
*/
class HelloPageBase {
}

Und hier der grundlegende Inhalt des Unit Tests, Page/BaseTest.php:

<?php
 
require_once(substr(dirname(__FILE__), 0, -57).'/Framework/PapayaTestCase.php');
require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Page/Base.php');
 
class HelloPageBaseTest extends PapayaTestCase {
}

Die Base-Klasse besitzt keinen dedizierten Konstruktor und erweitert auch keine andere Klasse, so dass der Code zum Laden des zu testenden Objekts ganz einfach so aussieht:

  /**
  * Instantiate the HelloPageBase object to be tested
  *
  * @return HelloPageBase
  */
  private function getHelloPageBaseObjectFixture() {
    return new HelloPageBase();
  }

Die erste Methode, die wir testen und implementieren, heißt setPageData( ). Sie wird verwendet, um die Konfigurationsdaten aus dem Einstellungsdialog der Seitenklasse an die Base-Klasse zu übergeben und um vom Unit Test aus alternative Daten zu übergeben. Dies ist der Test für die geplante Methode:

  /**
  * @covers HelloPageBase::setPageData
  */
  public function testSetPageData() {
    $helloPageBaseObject = $this->getHelloPageBaseObjectFixture();
    $data = array('text' => 'Hello');
    $helloPageBaseObject->setPageData($data);
    $this->assertAttributeEquals($data, '_data', $helloPageBaseObject);
  }

Da die Konfigurationsdaten in einem privaten Attribut namens $_data gespeichert werden, muss die Überprüfung mit Hilfe der Methode assertAttributeEquals( ) durchgeführt werden. Diese Methode benötigt drei Parameter: Den Wert, mit dem Sie das Attribut vergleichen möchten, den Attributnamen als String ohne führendes $-Zeichen sowie das Objekt, dessen Attribut gelesen wird.

Fügen Sie nun folgenden Code zur Base-Klasse hinzu, gleich unterhalb der öffnenden geschweiften Klammer der Klassendefinition:

   /**
   * Page configuration data
   * @var array
   */
   private $_data = array();
 
   /**
   * Set page configuration data
   *
   * @param array $data
   */
   public function setPageData($data) {
   }

Führen Sie den Test aus, schauen Sie ihm beim Fehlschlagen zu und fügen Sie dann die eigentliche Implementierung der Method setPageData( ) in einer Zeile zwischen ihren geschweiften Klammern hinzu:

    $this->_data = $data;

Dieser Test sollte nun erfolgreich verlaufen, so dass Sie Ihren Code wieder committen können. Mit Hilfe derselben Test-Fehlschlag-Implementierung-Arbeitsreihenfolge können Sie nun eine Methode namens getPageXml( ) implementieren, die von der Methode getParsedData( ) des Seitenmoduls aufgerufen wird, um die dynamische Ausgabe der Seite zu erzeugen. Hier zunächst der Test:

  /**
  * @covers HelloPageBase::getPageXml
  */
  public function testGetPageXml() {
    $helloPageBaseObject = $this->getHelloPageBaseObjectFixture();
    $helloPageBaseObject->setPageData(array('text' => 'Hello'));
    $expectedXml = '<title>Hello world!</title>
<text>Hello</text>';
    $this->assertEquals(
      $expectedXml,
      $helloPageBaseObject->getPageXml()
    );
  }

Und dies ist die Methodenimplementierung selbst (aber vergessen Sie nicht, den Test zuerst ohne den eigentlichen Code auszuführen):

  /**
  * Get the page's XML output
  *
  * @return string XML
  */
  public function getPageXml() {
    $result = '<title>Hello world!</title>'.LF;
    $result .= sprintf('<text>%s</text>', papaya_strings::escapeHTMLChars($this->_data['text']));
    return $result;
  }

Die Implementierung ist recht einfach. Beachten Sie jedoch bitte den statischen Methodenaufruf papaya_strings::escapeHTMLChars( ). Diese Methode stellt sicher, dass HTML-Sonderzeichen in Strings, in denen einfacher Text stehen soll, escaped werden. Sie sollte stets verwendet werden, wenn Text-Benutzereingaben oder -Konfigurationsdaten zur Ausgabe zurückgegeben werden, sowohl im Frontend als auch im Backend. LF ist eine systemweite papaya-Konstante, die einen Zeilenumbruch erzeugt.

Nun ist unsere Base-Klasse fertig, und wir können uns ans Refactoring der Content-Klasse begeben, um davon Gebrauch zu machen. Als Erstes implementieren wir eine Methode namens setBaseObject( ), um die zu verwendende Instanz der Base-Klasse zu setzen. Dieses Verfahren heißt Dependency Injection. Es kann sowohl für Unit Tests verwendet werden (um das echte Objekt durch ein sogenanntes Mock-Objekt zu ersetzen, das sich genau so verhält, wie wir es wünschen), als auch für den einfachen späteren Austausch von Implementierungsdetails durch Einfügen eines anderen Objekts.

Wie üblich schreiben wir zuerst den Test (beachten Sie, dass wir nun wieder mit der Klasse HelloPageTest arbeiten). Fügen Sie zuerst eine weitere require_once( )-Anweisung unter den anderen hinzu:

require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Page/Base.php');

Wir verwenden im Test zwar keine echte Instanz dieser Klasse, aber wenn die Definition vorhanden ist, wird das Mock-Objekt, das wir stattdessen verwenden, automatisch anhand der echten Klasse modelliert, mit allen öffentlichen Attributen und Methoden. Nun zur Testmethode:

  /**
  * @covers HelloPage::setBaseObject
  */
  public function testSetBaseObject() {
    $helloPageObject = $this->getHelloPageObjectFixture();
    $helloPageBaseObject = $this->getMock('HelloPageBase');
    $helloPageObject->setBaseObject($helloPageBaseObject);
    $this->assertAttributeSame($helloPageBaseObject, '_baseObject', $helloPageObject);
  }

Bitte beachten Sie, dass wir diesmal assertAttributeSame( ) statt assertAttributeEquals( ) verwenden. Die Parametersyntax ist dieselbe, aber da wir mit einer Objektreferenz statt mit einem einfachen Wert arbeiten, ist es passender, auf strikte Objektidentität zu prüfen. Bevor Sie die Methode implementieren, fügen Sie am Anfang des Klassenrumpfs folgende Attributdeklaration hinzu:

  /**
  * Instance of the HelloPageBase class
  * @var HelloPageBase
  */
  private $_baseObject = NULL;

Und hier die Implementierung der Methode:

  /**
  * Set the HelloPageBase object to be used
  *
  * @param HelloPageBase $baseObject
  */
  public function setBaseObject($baseObject) {
    $this->_baseObject = $baseObject;
  }

Das Gegenstück zur Methode setBaseObject( ) ist getBaseObject( ), die die Klasse HelloPageBase nur bei Bedarf instantiiert, das heißt wenn das Objekt nicht zuvor mittels setBaseObject( ) gesetzt wurde. Dies ist ein weiteres wichtiges Verfahren namens Lazy Initialization. Neben der Dependency Injection ist sie eins der wichtigsten Muster des testgetriebenen, objektorientierten Softwaredesigns.

Schreiben Sie folgenden Test für die Methode getBaseObject( ):

  /**
  * @covers HelloPage::getBaseObject
  */
  public function testGetBaseObject() {
    $helloPageObject = $this->getHelloPageObjectFixture();
    $baseObject = $helloPageObject->getBaseObject();
    $this->assertTrue($baseObject instanceof HelloPageBase);
  }

Die Implementierung der Methode sieht wie folgt aus:

  /**
  * Get (and, if necessary, initialize) the HelloPageBase object
  *
  * @return HelloPageBase
  */
  public function getBaseObject() {
    if (!is_object($this->_baseObject)) {
      include_once(dirname(__FILE__).'/Page/Base.php');
      $this->_baseObject = new HelloPageBase();
    }
    return $this->_baseObject;
  }

Da nun alles bereit ist, brauchen wir nur noch die Methode getParsedData( ) neu zu implementieren, um die Konfigurationsdaten an das Base-Objekt zu übergeben und den Rückgabewert der Methode getPageXML( ) des Base-Objekts zurückzugeben. Hier der überarbeitete Test:

  /**
  * @covers HelloPage::getParsedData
  */
  public function testGetParsedData() {
    $helloPageObject = $this->getHelloPageObjectFixture();
    $helloPageBaseObject = $this->getMock('HelloPageBase');
    $expectedXml = '<title>Hello world!</title>
<text>Greetings from the new module</text>';
    $helloPageBaseObject
      ->expects($this->once())
      ->method('getPageXML')
      ->will($this->returnValue($expectedXml));
    $helloPageObject->setBaseObject($helloPageBaseObject);
    $this->assertEqualsString(
      $expectedXml,
      $helloPageObject->getParsedData()
    );
  }

Hier sehen Sie einen der Vorteile der Dependency Injection in Aktion: Wir erzeugen unser eigenes Base-Objekt und modellieren es so, dass es sich so verhält, wie wir es für den Test brauchen, und setzen es dann mit Hilfe der Methode setBaseObject( ). Um dieses benutzerdefinierte Objekt zu erstellen, rufen wir die PHPUnit-Methode getMock( ) mit dem Klassennamen auf (es gibt noch weitere, optionale Parameter für getMock( ), etwa ein Array von Methodennamen, aber wir brauchen sie nicht, weil wir die Originalklasse importiert haben). Die expects( )-Struktur definiert den gewünschten Rückgabewert für eine bestimmte Methode -- in diesem Fall erwarten wir, dass die Methode getPageXML( ) genau einmal aufgerufen wird und einen XML-String-Wert zurückgibt. Innerhalb der gestesteten Methode gibt das Mock-Objekt den erwarteten Wert zurück, und der Test schlägt fehl, wenn die Methode nicht genau einmal aufgerufen wird.

Nachdem wir den Test geschrieben haben, können wir die Methode getParsedData( ) unter Verwendung des Base-Objekts neu implementieren:

  /**
  * Get the page output XML
  *
  * @return string XML
  */
  public function getParsedData() {
    $this->setDefaultData();
    $baseObject = $this->getBaseObject();
    $baseObject->setPageData($this->data);
    return $baseObject->getPageXML();
  }

Eine letzte Erklärung betrifft die Methode setDefaultData( ) in Content-Modulen: Wenn Konfigurationsformularfelder Standardwerte besitzen (das optionale letzte Element in ihren Arrays), setzt dieser Methodenaufruf alle Elemente im Attribut $this->data auf diese Standardwerte, solange in der Seitenkonfiguration kein spezieller Wert angegeben wurde.

Führen Sie den Test erfolgreich aus. Danach können Sie den Tab Inhalt der Seite verwenden, um eine beliebige Nachricht einzugeben. Überprüfen Sie die Ausgabe mit Hilfe des Bereichs Vorschau; Ihr benutzerdefinierter Inhalt sollte angezeigt werden. Herzlichen Glückwunsch -- Sie haben Ihr erstes papaya-CMS-Modul mit dynamischen Daten geschrieben und dabei zwei oder drei Dinge über testgetriebene Entwicklung gelernt, falls Sie sich nicht bereits damit auskennen.

Persönliche Werkzeuge
In anderen Sprachen