Modulprogrammierung 3: Administrationsoberfläche

Aus PapayaCMS

Wechseln zu: Navigation, Suche

Tutorial

Zusammenfassung Dieses Tutorial beschreibt die Entwicklung einer Administrationsoberfläche für papaya CMS-Modulpakete.
Zielgruppe PHP-Entwickler
Schwierigkeitsgrad Fortgeschrittene
Softwarevoraussetzungen SVN-Checkout von papaya CMS; Anleitung hier
Datum 2011-01-19
Vorheriges Tutorial Modulprogrammierung 2: Datenbankunterstützung
Nächstes Tutorial Modulprogrammierung 4: Frontend-Interaktivität

In diesem dritten Teil des grundlegenden Tutorials zur Modulprogrammierung erstellen wir eine angepasste Backend-Administrationsoberfläche für unser Modulpaket. Sie stellt eine bequeme Möglichkeit zum Hinzufügen, Ändern und Löschen von Planeten bereit und verwendet dabei die Datenbankzugriffsmethoden, die wir im vorigen Teil erstellt haben.

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
|
+ edmodule_tutorial_hello.php
|
+ [Hello]
|  |
|  + Connector.php
|  |
|  + [Page]
|  |  |
|  |  + Base.php
|  |
|  + Page.php
|  |
|  + [Planet]
|  |  |
|  |  + Admin.php
|  |  |
|  |  + [Database]
|  |     |
|  |     + Access.php
|  |
|  + Planet.php
|
+ modules.xml

 Hinweis: Namen in eckigen Klammern bezeichnen Verzeichnisse

Im Verzeichnis testing/tests-unittests/papaya-lib/modules/special/myprojects/tutorial werden Sie die folgende Struktur erstellt haben (neue Dateien aus dem aktuellen Teil sind fett hervorgehoben):

+ [Hello]
   |
   + ConnectorTest.php
   |
   + [Page]
   |  |
   |  + BaseTest.php
   |
   + [Planet]
   |  |
   |  + AdminTest.php
   |  |
   |  + [Database]
   |     |
   |     + AccessTest.php
   |
   + PlanetTest.php
   |
   + BaseTest.php

 Hinweis: Namen in eckigen Klammern bezeichnen Verzeichnisse

Das Modul einrichten

Administrationsmodule bestehen aus mindestens zwei Dateien: Die Moduldatei selbst, die die Umgebung initialisiert, und eine Klassendatei, die den eigentlichen Inhalt liefert. Wir erzeugen zuerst die Moduldatei. Um den Rewrite-Rules von papaya CMS zu entsprechen, müssen Sie einer Administrationsmoduldatei das Präfix edmodule_ voranstellen, gefolgt von einem komplett klein geschriebenen Namen, der möglichst dem Paketnamen entspricht. In diesem Fall verwenden wir edmodule_tutorial_hello.php. Fügen Sie dazu folgenden Block zur Datei modules.xml hinzu:

  <module type="admin"
          guid=""
          name="Hello World Tutorial"
          class="edmodule_tutorial_hello"
          file="edmodule_tutorial_hello.php">
    The administration interface allows you to add, delete, and modify planets.
  </module>

Wie üblich müssen Sie eine GUID erstellen und als Wert des Attribute guid einfügen (Details werden im Abschnitt Die Datei modules.xml vorbereiten im ersten Teil dieser Tutorialreihe erläutert).

Die Moduldatei schreiben

Moduldateien sind ziemlich kurz und einfach, und sie sind der einzige Teil der aktuellen Modulimplementierung, der nicht durch Unit-Tests abgedeckt wird. Hier der vollständige Code von edmodule_tutorial_hello.php:

<?php
/**
* Hello World tutorial modification module
*
* @package Papaya-Modules
* @subpackage Tutorial-HelloWorld
*/
 
/**
* Basic class modification module
*/
require_once(PAPAYA_INCLUDE_PATH.'system/base_module.php');
 
/**
* Hello World modification module
*
* Planet administration
*
* @package Papaya-Modules
* @subpackage Tutorial-HelloWorld
*/
class edmodule_tutorial_hello extends base_module {
  /**
  * Permissions
  * @var array
  */
  public $permissions = array(
    1 => 'Manage'
  );
 
  /**
  * Execute module
  */
  public function execModule() {
    if ($this->hasPerm(1, TRUE)) {
      include_once(dirname(__FILE__)."/Planet/Admin.php");
      $planetAdmin = new PlanetAdmin();
      $planetAdmin->module = &$this;
      $planetAdmin->msgs = &$this->msgs;
      $planetAdmin->images = &$this->images;
      $planetAdmin->layout = &$this->layout;
      $planetAdmin->execute();
      $planetAdmin->getXml($this->layout);
      $planetAdmin->getButtons();
    }
  }
}
 
?>

Wie Sie sehen, erweitert die Modulklasse base_module. Diese Klasse definiert die gemeinsamen Features von Backend-Administrationsmodulen. Die Klasse selbst definiert nur ein Attribut und eine Methode.

Das Attribut $permissions enthält ein Array mit Berechtigungen für Backend-Benutzer. Komplexe Administrationsmodule wie die papaya-Community definieren oft bis zu zehn verschiedene Berechtigungen, um verschiedenen Benutzerrollen unterschiedliche Aktionsarten zu gestatten oder zu verwehren. Dieses einfache Modul enthält nur eine einzige Berechtigung. Wenn eine bestimmte Benutzerin dieses Recht innehat, hat sie Zugriff auf die komplette Administrationsoberfläche dieses Moduls.

Die Methode execModule() wird automatisch durch papaya/module.php aufgerufen, wenn eine Moduleditor-URL den papaya-Rewrite-Rules entspricht. Sie überprüft zunächst die grundlegende Berechtigung 1 ('Manage'). Benutzer ohne diese Berechtigung haben gar keinen Zugriff auf das Administrationsmodul. Als Nächstes wird eine Instanz der Klasse PlanetAdmin erstellt. Diese Klasse wird im nächsten Abschnitt erläutert; sie enthält die eigentliche Bedienoberfläche und Funktionalität des Moduls.

Als Nächstes müssen Sie einige öffentliche Attribute der Admin-Klasse setzen: module ist die aktuelle Datei, d.h. $this. msgs ist eine Instanz der Klasse papaya_errors; sie wird für Fehlermeldungen verwendet. Übergeben Sie aus dem Kontext der Moduldatei einfach eine Referenz auf $this->msgs, um der Admin-Klasse die aktuellen Fehlermeldungen zur Verfügung zu stellen. images ist ein Array von Standard-Icons, die für Menü- und Listenelemente eingesetzt werden können. Sie können alle verfügbaren Icons im papaya-Backend sehen: Klicken Sie in der Gruppe Administration auf Einstellungen und anschließend auf Icons ansehen. layout ist die Umgebung, in der die grafische Oberfläche (GUI) mit Hilfe spezieller, vordefinierter XML-Strukturen dargestellt wird. Die Verwendung von Icons und GUI-XML wird weiter unten in diesem Tutorial erläutert.

Zum Schluss werden die Hauptmethoden der Admin-Klasse aufgerufen: execute() wird verwendet, um die Befehle des Moduls auszuführen; diese werden durch Buttons in Menüleisten oder Listenansichten aktiviert. getXml() fügt die XML-Repräsentation der GUI zum aktuellen Layout hinzu. getButtons() schließlich liefert das XML für das Hauptmenü.

Die Admin-Klassen-Datei schreiben

Die Admin-Klassen-Datei erhält wieder Unit-Tests, so dass wir mit dem Grundgerüst der Unit-Test-Klasse beginnen. Erstellen Sie im Unit-Test-Verzeichnis Ihres Pakets eine Datei namens tutorial/Hello/Planet/AdminTest.php. Fügen Sie folgenden Inhalt hinzu:

<?php
require_once(substr(dirname(__FILE__), 0, -59).'/Framework/PapayaTestCase.php');
PapayaTestCase::registerPapayaAutoloader();
require_once(PAPAYA_INCLUDE_PATH.'modules/special/myproject/tutorial/Hello/Planet/Admin.php');
 
class HelloPlanetAdminTest extends PapayaTestCase {
  /**
  * Get the HelloPlanetAdmin object to be tested
  *
  * @return HelloPlanetAdmin
  */
  private function getPlanetAdminObjectFixture() {
    $this->defineConstantDefaults(array('PAPAYA_DB_TBL_MODULES'));
    return new HelloPlanetAdmin();
  }
}

Die Konstante PAPAYA_DB_TBL_MODULES, die wir mit einem Standardwert definieren, wird von der aktuellen Implementierung der Klasse base_pluginloader verwendet. Diese Klasse wird später verwendet, um eine Connector-Instanz zu laden, und die Erzeugung des Mock-Objekts, das wir für den Unit-Test verwenden, würde den Test ansonsten aufgrund der fehlenden Konstantendefinition scheitern lassen.

Fügen Sie nun das zugehörige Implementierungs-Grundgerüst unter dem Namen tutorial/Planet/Admin.php zu Ihrem Tutorial-Modulverzeichnis hinzu:

<?php
/**
* Hello World tutorial, Planet administration class
*
* The Planet class provides the public interface for planets to be stored in
* and loaded from database.
*
* @package Papaya-Modules
* @subpackage tutorial
*/
 
/**
* Basic class base_object
*/
require_once(PAPAYA_INCLUDE_PATH.'system/sys_base_object.php');
 
/**
* Hello World tutorial, Planet class
*
* @package Papaya-Modules
* @subpackage tutorial
*/
class HelloPlanetAdmin extends base_object {
  /**
  * Parameter namespace
  * @var string
  */
  public $paramName = 'tut';
}

Die öffentliche Eigenschaft $this->paramName wird für Link- und Formularfeld-Parameter verwendet, genau wie bei Content-Modulen.

Eine Connector-Instanz bereitstellen

Das Erste, was wir in der Admin-Klasse benötigen, ist eine Instanz der Klasse HelloConnector. Das Konzept für das Laden einer Connector-Instanz mit Hilfe einer base_pluginloader-Instanz wurde bereits im vorherigen Tutorial erläutert. Fügen Sie gleich unter der Methode getPlanetAdminObjectFixture() den folgenden Code zu der Unit-Test-Klasse hinzu, der Tests für das Setzen und Laden der base_pluginloader- und Connector-Objekte bereitstellt:

  /**
  * @covers HelloPlanetAdmin::setPluginloaderObject
  */
  public function testSetPluginloaderObject() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $pluginloaderObject = $this->getMock('base_pluginloader');
    $adminObject->setPluginloaderObject($pluginloaderObject);
    $this->assertAttributeSame($pluginloaderObject, '_pluginloaderObject', $adminObject);
  }
 
  /**
  * @covers HelloPlanetAdmin::getPluginloaderObject
  */
  public function testGetPluginloaderObject() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $pluginloaderObject = $adminObject->getPluginloaderObject();
    $this->assertInstanceOf('base_pluginloader', $pluginloaderObject);
  }
 
  /**
  * @covers HelloPlanetAdmin::setConnectorObject
  */
  public function testSetConnectorObject() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $connectorObject = $this->getMock('HelloConnector');
    $adminObject->setConnectorObject($connectorObject);
    $this->assertAttributeSame($connectorObject, '_connectorObject', $adminObject);
  }
 
  /**
  * @covers HelloPlanetAdmin::getConnectorObject
  */
  public function testGetConnectorObject() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $connectorObject = $this->getMock('HelloConnector');
    $pluginloaderObject = $this->getMock('base_pluginloader');
    $pluginloaderObject
      ->expects($this->once())
      ->method('getPluginInstance')
      ->will($this->returnValue($connectorObject));
    $adminObject->setPluginloaderObject($pluginloaderObject);
    $this->assertSame($connectorObject, $adminObject->getConnectorObject());
  }

Nun können Sie den zugehörigen Implementierungscode zum Rumpf der Klasse HelloPlanetAdmin hinzufügen und danach die Tests ausführen:

  /**
  * Plugin loader object
  * @var base_pluginloader
  */
  protected $_pluginloaderObject = NULL;
 
  /**
  * Hello world connector object
  * @var HelloConnector
  */
  protected $_connectorObject = NULL;
 
  /**
  * 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 Hello world connector object to be used
  *
  * @param HelloConnector $connectorObject
  */
  public function setConnectorObject($connectorObject) {
    $this->_connectorObject = $connectorObject;
  }
 
  /**
  * Get (and, if necessary, initialize) the Hello world connector object
  *
  * @return HelloConnector
  */
  public function getConnectorObject() {
    if (!is_object($this->_connectorObject)) {
      $pluginloaderObject = $this->getPluginloaderObject();
      $this->_connectorObject =
        $pluginloaderObject->getPluginInstance('eeb42aad2491cd607c7c64bc57eae455', $this);
    }
    return $this->_connectorObject;
  }

Eine Liste der vorhandenen Planeten anzeigen

Die erste Aufgabe der GUI-Implementierung besteht darin, die Liste der vorhandenen Planeten anzuzeigen. Diese Liste wird immer angezeigt; die für diese Funktionalität zuständige Methode wird mit Hilfe der Methode getXml() aufgerufen. Um einen Unit-Test für die neue Methode getPlanetsList() zu schreiben, benötigt unsere Test-Klasse ein wenig Refactoring: Wir verwenden eine Proxy-Klasse, diesmal nicht wegen des Constructors, sondern zum Überschreiben einiger nicht testfreundlicher papaya-API-Methoden. Um die Proxy-Klasse bereitzustellen, fügen Sie unter der schließenden geschweiften Klammer } der Klasse HelloPlanetAdminTest folgenden Code hinzu:

class HelloPlanetAdminProxy extends HelloPlanetAdmin {
  public function getLink() {
    return 'module_edmodule_tutorial_hello.php';
  }
 
  public function _gt($str) {
    return $str;
  }
}

Die Methode getLink() kann Weblinks für Backend-Admin-Klassen erstellen; als einziges Argument benötigt sie dafür ein Array von Parametern (wobei es noch einige optionale weitere Argumente gibt). Die Methode _gt() wird verwendet, um Übersetzungen der GUI-Elemente für die Mehrsprachenunterstützung im papaya-Backend zur Verfügung zu stellen.

Um sicherzustellen, dass statt der eigentlichen PlanetAdmin-Klasse die Proxy-Klasse verwendet wird, ändern Sie die Methode getPlanetAdminObjectFixture() wie folgt (d.h. verwenden Sie sowohl für die return-Anweisung als auch für die @return-PHPDoc-Annotation HelloPlanetAdminProxy):

  /**
  * Get the HelloPlanetAdmin object to be tested
  *
  * @return HelloPlanetAdminProxy
  */
  private function getPlanetAdminObjectFixture() {
    $this->defineConstantDefaults(array('PAPAYA_DB_TBL_MODULES'));
    return new HelloPlanetAdminProxy();
  }

Zuletzt müssen Sie noch eine Include-Anweisung für die eigentliche HelloConnector-Klasse zur Unit-Test-Datei hinzufügen. Diese sorgt dafür, dass Mock-Objekte für diese Klasse mit den korrekten Methoden erstellt werden. Fügen Sie die folgende Zeile über der class-Anweisung und unter den anderen require_once()-Anweisungen ein:

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

Nun können Sie die Testmethode für die Methode getPlanetsList() hinzufügen:

  /**
  * @covers HelloPlanetAdmin::getPlanetsList
  */
  public function testGetPlanetsList() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $connectorObject = $this->getMock('HelloConnector');
    $connectorObject
      ->expects($this->once())
      ->method('getAllPlanets')
      ->will($this->returnValue(array(1 => 'Mars', 2 => 'Jupiter')));
    $adminObject->setConnectorObject($connectorObject);
    $expectedXml = '<listview title="Planets">
<cols>
<col>Planet</col>
</cols>
<items>
<listitem href="module_edmodule_tutorial_hello.php" title="Mars" />
<listitem href="module_edmodule_tutorial_hello.php" title="Jupiter" selected="selected" />
</items>
</listview>
';
    $adminObject->params = array('id' => 2);
    $this->assertEquals($expectedXml, $adminObject->getPlanetsList());
  }

Sie können diese Methode gleich unter der Methode getPlanetAdminObjectFixture() einfügen, da die Methode getPlanetsList() sich an derselben Position in der Klasse HelloPlanetAdmin befinden wird -- über den Servicemethoden für die Pluginloader- und Connector-Objekte.

Fügen Sie jetzt die Methode getPlanetsList() zur Admin-Datei hinzu, über der Methode setPluginloaderObject():

  /**
  * Get the list of existing planets
  *
  * @return string XML
  */
  public function getPlanetsList() {
    $result = '';
    $connectorObject = $this->getConnectorObject();
    $planets = $connectorObject->getAllPlanets();
    if (!empty($planets)) {
      $result = sprintf('<listview title="%s">'.LF, $this->_gt('Planets'));
      $result .= '<cols>'.LF;
      $result .= sprintf('<col>%s</col>'.LF, $this->_gt('Planet'));
      $result .= '</cols>'.LF;
      $result .= '<items>'.LF;
      foreach ($planets as $id => $name) {
        $selected = '';
        if (isset($this->params['id']) && $id == $this->params['id']) {
          $selected = ' selected="selected"';
        }
        $link = $this->getLink(array('cmd' => 'edit_planet', 'id' => $id));
        $result .= sprintf(
          '<listitem href="%s" title="%s"%s />'.LF,
          $link,
          papaya_strings::escapeHTMLChars($name),
          $selected
        );
      }
      $result .= '</items>'.LF;
      $result .= '</listview>'.LF;
    }
    return $result;
  }

Führen Sie nun die Unit-Tests aus; sie sollten problemlos funktionieren.

Die XML-Struktur <listview>...</listview> ist ein Standardausgabeelement für Backend-Klassen. Die Struktur sieht wie folgt aus:

  <listview title="{Titel}">
    <cols>  <!-- Spalten und ihre Überschriften definieren -->
      <col>{Überschrift}</col>
      <!-- Weitere col-Elemente für mehrere Spalten -->
    </cols>
    <items>  <!-- Die Zeilen der Liste oder Listitems beginnen hier -->
      <listitem href="{Link}" title="{Inhalt}"[ selected="selected"]>
        <!-- Das optionale Attribut selected="selected" markiert das aktuelle Element -->
        <subitem>{Text}</subitem>
        <!-- Für mehr als zwei Spalten weitere subitem-Elemente einfügen
             oder subitems weglassen, wenn es nur eine Spalte gibt -->
      </listitem>
    </items>
  </listview>

Als Nächstes fügen wir die erste Implementierung der Methode getXml() hinzu. Im Moment ruft sie lediglich die Methode getPlanetsList() auf und fügt deren Ergebnis zum Layout-Objekt des Moduls hinzu.

Ändern Sie in der Methode getPlanetAdminObjectFixture() den Aufruf von defineConstantDefaults() so, dass PAPAYA_XSLT_EXTENSION als weitere Konstante hinzukommnt:

    $this->defineConstantDefaults(
      array('PAPAYA_DB_TBL_MODULES', 'PAPAYA_XSLT_EXTENSION')
    );

Diese Konstante wird von der Klasse papaya_xsl verwendet, von der wir ein Mock-Objekt als Layout-Objekt des Moduls erstellen.

Fügen Sie nun über der Methode testGetPlanetsList() folgende Unit-Test-Methode ein:

  /**
  * @covers HelloPlanetAdmin::getXml
  */
  public function testGetXml() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $layout = $this->getMock('papaya_xsl', array('addLeft'));
    $layout
      ->expects($this->once())
      ->method('addLeft');
    $connectorObject = $this->getMock('HelloConnector');
    $connectorObject
      ->expects($this->once())
      ->method('getAllPlanets')
      ->will($this->returnValue(array()));
    $adminObject->setConnectorObject($connectorObject);
    $adminObject->getXml($layout);
  }

Beachten Sie, dass dieser Test keinen assert...()-Aufruf enthält. Das ist vollkommen in Ordnung, weil die Methode getXml() keinen Wert zurückgibt. Dennoch gibt es in diesem test zwei Assertions, denn jede expects()-Anweisung ist ebenfalls eine Assertion. Die Assertion für das Layout-Objekt enthält keine will()-Klausel, weil die hier erwartete Methode addLeft() ebenfalls keinen Wert zurückgibt.

Die zugehörige Implementierung, die oberhalb der Methode getPlanetsList() eingefügt wird, sie wie folgt aus:

  /**
  * Get the module's GUI XML
  *
  * @param papaya_xsl $layout
  */
  public function getXml($layout) {
    $layout->addLeft($this->getPlanetsList());
  }

Die Methode addLeft() fügt den Inhalt zur linken Spalte des Inhaltsbereichs hinzu, die eine feste Breite besitzt.

At this stage, you can even perform your first browser test of the admin module. To do this, you just need to add stubs for the execute() and getButtons() methods that are invoked by the edmodule_... class. Put execute() above the getXml() method, and getButtons() above setPluginloaderObject(): Zu diesem Zeitpunkt können Sie sogar bereits den ersten Browsertest des Admin-Moduls durchführen. Dazu brauchen Sie nur noch Grundgerüste für die Methoden execute() und getButtons() hinzuzufügen, die von der edmodule_...-Klasse aufgerufen werden. Platzieren Sie execute() über die Methode getXml() und getButtons() über setPluginloaderObject():

  /**
  * Execute the module's commands
  */
  public function execute() {
    $this->initializeParams();
    // TO DO: Add implementation
  }
 
  /**
  * Get the main toolbar
  */
  public function getButtons() {
    // TO DO: Add implementation
  }

Und stellen Sie sicher, dass Sie mit Hilfe Ihres bevorzugten Datenbank-Administrations-Tools einige Planeten in die Datenbanktabelle einfügen, falls Sie dies nicht bereits im Lauf des vorigen Tutorials getan haben.

Danach können Sie im papaya-Backend die Modulliste aktualisieren und die Anwendung starten, in dem Sie Anwendungen > Hello World Planet Administration wählen. Sie werden Ihre Planetenliste sehen, und wenn Sie einen von ihnen anklicken, wird die Seite neu geladen und der ausgewählte Planet wird hervorgehoben bleiben. Dies geschieht mit Hilfe des initalizeParams()-Aufrufs in execute(), der die Anfrageparameter lädt, und des Attributs selected="selected" für den entsprechenden Eintrag in der Listenansicht.

Ein Formular zum Erstellen und Ändern von Planeten hinzufügen

Als Nächstes benötigen wir einen Dialog beziehungsweise ein Webformular, um neue Planeten hinzuzufügen oder vorhandene zu bearbeiten. Das einzige interaktive Element in diesem Formular ist ein einfaches Textfeld zur Eingabe oder Änderung eines Planetennamen. Webformulare werden in papaya CMS mit Hilfe der Klasse base_dialog erstellt, obwohl neue, verbesserte und vollständig Unit-getestete Dialogklassen in Entwicklung sind und demnächst in einer aktualisierten Fassung dieses Tutorials vorgestellt werden.

Ein base_dialog-Objekt wird mit Hilfe dreier Arrays erzeugt: interaktive Felder, Vorgabedaten für die Feldinhalte und versteckte Felder. Die Struktur für die Felder entspricht der im vorigen Tutorial beschriebenen Struktur der Edit-Fields eines Content-Moduls. Vorgabedaten und Hidden-Felder sind dagegen einfache assoziative Arrays, in denen die Schlüssel für die Feldnamen und die Werte für die Feldwerte stehen. Der Konstruktor von base_dialog benötigt noch zwei weitere Argumente: Eine Referenz auf die aufrufende Klasse ($this) und den aktuellen Parameter-Namespace (normalerweise $this->paramName oder $this->_owner->paramName für Content-Helferklassen wie HelloPageBase).

Wir verwenden ein weiteres Paar von Setter/Getter-Methoden für das Dialogobjekt. Fügen Sie den folgenden Code für diese Methoden zur Unit-Test-Klasse hinzu (unter allen anderen Testmethoden):

  /**
  * @covers HelloPlanetAdmin::setDialogObject
  */
  public function testSetDialogObject() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $dialogObject = $this->getMock(
      'base_dialog',
      array(),
      array(),
      'Mock_'.md5(__CLASS__.microtime()),
      FALSE
    );
    $adminObject->setDialogObject($dialogObject, TRUE);
    $this->assertAttributeSame($dialogObject, '_dialogObject', $adminObject);
    $this->assertAttributeEquals(TRUE, '_dialogInitialized', $adminObject);
  }
 
  /**
  * @covers HelloPlanetAdmin::getDialogObject
  */
  public function testGetDialogObject() {
    $this->markTestSkipped();
  }

Ja, genau: Wir schreiben einen Test für getDialogObject(), in dem wir den Test offiziell als übersprungen kennzeichnen. Damit machen wir vollkommen klar, dass wir diese Methode nicht aus Versehen ungetestet lassen, sondern weil sie mit der aktuellen Implementierung von base_dialog nicht getestet werden kann.

In der Admin-Klasse müssen wir zwei weitere private Attribute deklarieren (fügen Sie diesen Code unter der Deklaration des Connector-Objekts ein):

  /**
  * Dialog object
  * @var base_dialog
  */
  protected $_dialogObject = NULL;
 
  /**
  * Has the dialog already been initialized?
  * @var boolean
  */
  protected $_dialogInitialized = FALSE;

Wieder einmal müssen die Attribute protected statt private sein, um sicherzustellen, dass sie in der Proxy-Unterklasse verfügbar sind.

Das Attribut $this->_dialogInitialized wird verwendet, um sicherzustellen, dass der Dialog nicht versehentlich zweimal initialisiert wird (einmal beim Speichern von Daten und einmal beim Anzeigen des Dialogs). Es wäre auch möglich, einfach zu überprüfen, ob das Attribut $this->_dialogObject bereits ein Objekt ist, aber da wir es für den Test von außen setzen müssen, würde diese Überprüfung verhindern, dass ein Großteil der Methode durch den Test ausgeführt wird.

Fügen Sie nun die folgenden Methoden unter allen anderen Methoden zur Admin-Klasse hinzu:

  /**
  * Set the dialog object to be used
  *
  * @param base_dialog $dialogObject
  * @param boolean $initialized optional, default FALSE
  */
  public function setDialogObject($dialogObject, $initialized = FALSE) {
    $this->_dialogObject = $dialogObject;
    $this->_dialogInitialized = $initialized;
  }
 
  /**
  * Get (and, if necessary, initialize) the dialog object
  *
  * @param array $fields interactive form fields
  * @param array $data default data for the form fields
  * @param array $hidden hidden fields
  * @return base_dialog
  */
  public function getDialogObject($fields, $data, $hidden) {
    if (!is_object($this->_dialogObject)) {
      include_once(PAPAYA_INCLUDE_PATH.'system/base_dialog.php');
      $this->_dialogObject = new base_dialog($this, $this->paramName, $fields, $data, $hidden);
    }
    return $this->_dialogObject;
  }

Der Inhalt des Dialogs wird mit Hilfe einer Methode namens initializeDialog() festgelegt. Diese Methode wird aufgerufen, um den Dialog vorzubereiten -- sowohl für Inhaltsprüfungen vor dem Speichern von Daten als auch für die Anzeige des Dialogs. Fügen Sie unter testGetPlanetsList() folgenden Unit-Test hinzu:

  /**
  * @covers HelloPlanetAdmin::initializeDialog
  */
  public function testInitializeDialog() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $dialogObject = $this->getMock(
      'base_dialog',
      array('loadParams'),
      array(),
      'Mock_'.md5(__CLASS__.microtime()),
      FALSE
    );
    $adminObject->setDialogObject($dialogObject);
    $connectorObject = $this->getMock('HelloConnector');
    $connectorObject
      ->expects($this->once())
      ->method('getPlanetById')
      ->will($this->returnValue(array('planet_id' => 2, 'planet_name' => 'Mars')));
    $adminObject->setConnectorObject($connectorObject);
    $adminObject->params = array('id' => 2);
    $adminObject->initializeDialog(FALSE);
    $this->assertAttributeEquals(TRUE, '_dialogInitialized', $adminObject);
  }

Hier die Methode initializeDialog(), die Sie zur Admin-Klasse hinzufügen müssen:

  /**
  * Initialze the dialog
  *
  * @param boolean $new TRUE => new planet, FALSE => edit existing planet
  */
  public function initializeDialog($new) {
    if (!$this->_dialogInitialized) {
      $fields = array('planet_name' => array('Name', 'isNoHTML', TRUE, 'input', 100));
      $data = array();
      $hidden = array('cmd' => 'save_planet');
      if (!$new && isset($this->params['id'])) {
        $connectorObject = $this->getConnectorObject();
        $planetData = $connectorObject->getPlanetById($this->params['id']);
        if (!empty($planetData)) {
          $hidden['id'] = $this->params['id'];
          $data = array('planet_name' => $planetData['planet_name']);
        }
      }
      $dialogObject = $this->getDialogObject($fields, $data, $hidden);
      if (is_object($dialogObject)) {
        $this->_dialogInitialized = TRUE;
        $dialogObject->buttonTitle = $this->_gt('Save');
        $dialogObject->dialogTitle = $this->_gt('Add planet');
        if (!$new && isset($this->params['id'])) {
          $dialogObject->dialogTitle = $this->_gt('Edit planet');
        }
        $dialogObject->loadParams();
      }
    }
  }

Der Parameter $new bestimmt, ob wir den Dialog zum Erstellen eines neuen oder zum Bearbeiten eines vorhandenen Planeten verwenden. Indem wir ihn im Unit-Test auf FALSE setzen, sorgen wir dafür, dass alle Codezeilen der Methode von diesem Test abgedeckt werden.

Als Nächstes kommt die Methode, die die XML-Ausgabe des Dialogs ausgibt. Fügen Sie die folgende Testmethode zur Unit-Test-Klasse hinzu:

  /**
  * @covers HelloPlanetAdmin::getDialog
  */
  public function testGetDialog() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $dialogObject = $this->getMock(
      'base_dialog',
      array('getDialogXML'),
      array(),
      'Mock_'.md5(__CLASS__.  microtime()),
      FALSE
    );
    $dialogObject
      ->expects($this->once())
      ->method('getDialogXML')
      ->will($this->returnValue('<dialog />'));
    $adminObject->setDialogObject($dialogObject, TRUE);
    $this->assertEquals('<dialog />', $adminObject->getDialog(FALSE));
  }

Beachten Sie, wie das Setzen des Attributs $this->_dialogInitialized auf TRUE (indem wir den Wert mittels setDialogObject() übergeben) uns davor bewahrt, alle notwendigen Objekte und Methoden für initializeDialog() mocken zu müssen -- der Hauptteil dieser Methode wird gar nicht erst ausgeführt.

Die Implementierung von getDialog() ist schlicht und einfach:

  /**
  * Get the dialog's XML output
  *
  * @param boolean $new TRUE => new planet, FALSE => edit existing planet
  */
  public function getDialog($new) {
    $result = '';
    $this->initializeDialog($new);
    if (is_object($this->_dialogObject)) {
      $result = $this->_dialogObject->getDialogXML();
    }
    return $result;
  }

Unser Dialog ist einsatzbereit, also können wir den Aufruf zu getXml() hinzufügen. Wie üblich sollten Sie zuerst den Unit-Test erweitern. Ersetzen Sie die vorhandene Implementierung von testGetXml() durch diese:

  /**
  * @covers HelloPlanetAdmin::getXml
  * @dataProvider cmdProvider
  */
  public function testGetXml($cmd) {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $layout = $this->getMock('papaya_xsl', array('addLeft', 'add'));
    $layout
      ->expects($this->once())
      ->method('addLeft');
    $layout
      ->expects($this->once())
      ->method('add');
    $connectorObject = $this->getMock('HelloConnector');
    $connectorObject
      ->expects($this->once())
      ->method('getAllPlanets')
      ->will($this->returnValue(array()));
    $adminObject->setConnectorObject($connectorObject);
    $dialogObject = $this->getMock(
      'base_dialog',
      array('getDialogXML'),
      array(),
      'Mock_'.md5(__CLASS__.microtime()),
      FALSE
    );
    $dialogObject
      ->expects($this->once())
      ->method('getDialogXML')
      ->will($this->returnValue('<dialog />'));
    $adminObject->setDialogObject($dialogObject, TRUE);
    $adminObject->params = array('cmd' => $cmd);
    $adminObject->getXml($layout);
  }

Die Annotation @dataProvider legt den Namen einer Datenprovider-Methode fest. Diese Methode kann verwendet werden, um verschiedene Werte für das Argument (oder die Argumente) der Testklasse zu setzen, wodurch der Test je einmal für die verschiedenen Sätze von Werten ausgeführt wird. Der Datenprovider selbst ist eine öffentliche, statische Methode, die ein Array mit Werte-Arrays zurückgibt; diese werden nacheinander verwendet. Fügen Sie ihn am Ende des Klassenrumpfs ein:

  /**
  * Data provider for commands
  */
  public static function cmdProvider() {
    return array(
      array('add_planet'),
      array('edit_planet')
    );
  }

Die Methode testGetXml() wird zweimal ausgeführt, mit den Befehlen add_planet beziehungsweise edit_planet.

getXml() selbst muss ebenfalls erweitert werden und sieht nun so aus:

  /**
  * Get the module's GUI XML
  *
  * @param papaya_xsl $layout
  */
  public function getXml($layout) {
    $layout->addLeft($this->getPlanetsList());
    if (isset($this->params['cmd'])) {
      switch ($this->params['cmd']) {
      case 'add_planet':
        $layout->add($this->getDialog(TRUE));
        break;
      case 'edit_planet':
        $layout->add($this->getDialog(FALSE));
        break;
      }
    }
  }

Klicken Sie in der Anwendung nun einen Planeten in der Planetenliste an. Sie sehen ein Formular mit der Beschriftung Edit Planet, und der Name des Planeten steht im Textfeld. Wenn Sie jedoch den Namen des Planeten ändern und auf Save/Speichern klicken, passiert nichts. Die Speicherfunktionalität fügen wir im nächsten Unterabschnitt hinzu.

Den Speicherbefehl ausführen

Wenn das Formular abgesendet wird, hat der Parameter cms den Wert 'save_planet'. Die Methode execute() muss den Parameter cms überprüfen und die Methode savePlanet() aufrufen, wenn dieser Wert gesetzt ist. In diesem Unterabschnitt implementieren wir savePlanet() und dessen Aufrufe, sowohl für neue als auch für existierende Planeten.

Es gibt drei verschiedene Testmethoden für savePlanet(), um die verschiedenen Verhaltensweisen der Methode abzudecken. Bevor wir diese Tests hinzufügen, müssen wir die Testumgebung modifizieren. Fügen Sie zunächst unter den anderen require_once()-Aufrufen die folgende Anweisung ein:

require_once(PAPAYA_INCLUDE_PATH.'system/sys_error.php');

Diese Datei definiert unter anderem einige Fehler-Level-Konstanten. defineConstantDefaults() hilft in diesem Fall nichts, weil die Klasse von anderen Abhängigkeiten inkludiert wird, was beim Ausführen der Testsuite zu lästigen Constant already defined-Fehlermeldungen führt.

Fügen Sie danach den folgenden Code am Anfang der Klassendefinition von PlanetAdminProxy ein:

  public $msgs = array();
 
  public function addMsg($level, $msg) {
    $this->msgs[] = $msg;
  }

Die Methode addMsg() wird von Backend-Admin-Klassen verwendet, um Erfolgs-, Warn- und Fehlermeldungen für den Redakteur oder Administrator anzuzeigen. In der Proxy-Klasse verwenden wir ein öffentliches Array, um diese Meldungen zu sammeln. So können unsere Assertions leicht darauf zugreifen.

Als Nächstes können Sie über testGetPlanetsList() die folgenden drei Testmethoden einfügen:

  /**
  * @covers HelloPlanetAdmin::savePlanet
  */
  public function testSavePlanetWithDialogError() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $dialogObject = $this->getMock(
      'base_dialog',
      array('checkDialogInput', 'getDialogXML'),
      array(),
      'Mock_'.md5(__CLASS__.microtime()),
      FALSE
    );
    $dialogObject
      ->expects($this->once())
      ->method('checkDialogInput')
      ->will($this->returnValue(FALSE));
    $dialogObject
      ->expects($this->once())
      ->method('getDialogXML')
      ->will($this->returnValue('<dialog />'));
    $adminObject->setDialogObject($dialogObject, TRUE);
    $layout = $this->getMock('papaya_xsl');
    $adminObject->layout = $layout;
    $adminObject->params = array('invalid_field' => 'invalid data');
    $adminObject->savePlanet();
    $this->assertEquals(
      'Input errors.',
      $adminObject->msgs[0]
    );
  }
 
  /**
  * @covers HelloPlanetAdmin::savePlanet
  */
  public function testSavePlanetNewSuccess() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $dialogObject = $this->getMock(
      'base_dialog',
      array('checkDialogInput'),
      array(),
      'Mock_'.md5(__CLASS__.microtime()),
      FALSE
    );
    $dialogObject
      ->expects($this->once())
      ->method('checkDialogInput')
      ->will($this->returnValue(TRUE));
    $adminObject->setDialogObject($dialogObject, TRUE);
    $connectorObject = $this->getMock('HelloConnector');
    $connectorObject
      ->expects($this->once())
      ->method('createPlanet')
      ->will($this->returnValue(TRUE));
    $adminObject->setConnectorObject($connectorObject);
    $adminObject->params = array('planet_name' => 'Neptune');
    $adminObject->savePlanet();
    $this->assertEquals(
      'Planet successfully saved.',
      $adminObject->msgs[0]
    );
  }
 
  /**
  * @covers HelloPlanetAdmin::savePlanet
  */
  public function testSavePlanetExistingFailure() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $dialogObject = $this->getMock(
      'base_dialog',
      array('checkDialogInput'),
      array(),
      'Mock_'.md5(__CLASS__.microtime()),
      FALSE
    );
    $dialogObject
      ->expects($this->once())
      ->method('checkDialogInput')
      ->will($this->returnValue(TRUE));
    $adminObject->setDialogObject($dialogObject, TRUE);
    $connectorObject = $this->getMock('HelloConnector');
    $connectorObject
      ->expects($this->once())
      ->method('updatePlanet')
      ->will($this->returnValue(FALSE));
    $adminObject->setConnectorObject($connectorObject);
    $adminObject->params = array(
      'cmd' => 'save_planet',
      'id' => 1,
      'planet_name' => 'Earth'
    );
    $adminObject->savePlanet();
 
    $this->assertEquals(
      'Planet could not be saved: database error.',
      $adminObject->msgs[0]
    );
  }

Die entsprechende Implementierung der Methode savePlanet() (die oberhalb von getPlanetsList() eingefügt werden sollte) sieht so aus:

  /**
  * Save a planet
  */
  public function savePlanet() {
    $new = TRUE;
    if (isset($this->params['id'])) {
      $new = FALSE;
    }
    $this->initializeDialog($new);
    if ($this->_dialogObject->checkDialogInput()) {
      $connectorObject = $this->getConnectorObject();
      if ($new) {
        $success = $connectorObject->createPlanet(
          array('planet_name' => $this->params['planet_name'])
        );
      } else {
        $success = $connectorObject->updatePlanet(
          $this->params['id'],
          array('planet_name' => $this->params['planet_name'])
        );
      }
      if ($success) {
        $this->addMsg(MSG_INFO, $this->_gt('Planet successfully saved.'));
      } else {
        $this->addMsg(MSG_ERROR, $this->_gt('Planet could not be saved: database error.'));
      }
    } else {
      $this->addMsg(MSG_ERROR, $this->_gt('Input errors.'));
      $this->layout->add($this->getDialog($new));
    }
  }

Nachdem die Speichermethode implementiert ist, wollen wir, dass execute() sie jedes Mal aufruft, wenn der Befehl save_planet gesetzt ist. Fügen Sie zunächst die folgenden Zeilen zur PlanetAdminProxy-Klassendefinition hinzu, um für die Unit-Tests Nebenwirkungen der eigentlichen initializeParams()-Methode zu umgehen:

  public function initializeParams() {
    // Nothing to do here; just override the original
  }

execute() erhält mehrere Unit-Test-Methoden, je eine für jeden Befehl. Hier die erste, die den Befehl save_planet abdeckt:

  /**
  * @covers HelloPlanetAdmin::execute
  */
  public function testExecuteSave() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $dialogObject = $this->getMock(
      'base_dialog',
      array('checkDialogInput', 'getDialogXML'),
      array(),
      'Mock_'.md5(__CLASS__.microtime()),
      FALSE
    );
    $dialogObject
      ->expects($this->once())
      ->method('checkDialogInput')
      ->will($this->returnValue(FALSE));
    $dialogObject
      ->expects($this->once())
      ->method('getDialogXML')
      ->will($this->returnValue('<dialog />'));
    $adminObject->setDialogObject($dialogObject, TRUE);
    $layout = $this->getMock('papaya_xsl');
    $adminObject->layout = $layout;
    $adminObject->params = array(
      'cmd' => 'save_planet',
      'invalid_field' => 'invalid data'
    );
    $adminObject->execute();
    $this->assertEquals('Input errors.', $adminObject->msgs[0]);
  }

Wie Sie sehen, stellt der größte Teil der Methode die Umgebung für den SavePlanet()-Aufruf bereit. Da der Fall mit ungültigen Dialogeingaben der kürzeste ist, habe ich diesen ausgewählt, aber jeder von den testSavePlanet...()-Methoden abgedeckte Fall würde funktionieren.

Die Implementierung von execute() selbst ist zu diesem Zeitpunkt sehr kurz:

  /**
  * Execute the module's commands
  */
  public function execute() {
    $this->initializeParams();
    if (isset($this->params['cmd'])) {
      switch ($this->params['cmd']) {
      case 'save_planet':
        $this->savePlanet();
        break;
      }
    }
  }

Wenn Sie in der Administrationsoberfläche einen Planeten anklicken, können Sie nun erfolgreich seinen Namen ändern. Aber wir haben noch keinen Button, um einen neuen Planeten zu erstellen. Deshalb kommt als Nächstes die Menüleiste an die Reihe.

Das Hauptmenü erstellen

Die Methode getButtons() wird verwendet, um die Hauptmenüleiste bereitzustellen, die über dem Hauptinhaltsbereich der Anwendung angezeigt wird. Sie verwendet eine Instanz der Klasse base_btnbuilder aus papaya-lib/system. Diese Klasse stellt die Methoden addButton() und addSeparator() zur Verfügung, um Buttons beziehungsweise vertikale Trennlinien bereitzustellen. addButton() besitzt die folgende Syntax:

$btnbuilderObject->addButton($caption, $href, $img, $hint, $down, $noTranslation);

Von den bis zu sechs Argumenten sind die vier letzten optional:

  • string $caption -- die Beschriftung des Buttons
  • string $href -- der Link auf die Seite, die bei einem Klick auf den Button angezeigt wird; üblicherweise mit Hilfe der Methode getLink() erstellt
  • string $img -- das Icon oder Symbol, das auf dem Button angezeigt wird; ein Index für das Attribut $this->image. Sie können alle verfügbaren Bilder im Backend sehen, wenn Sie Einstellungen > Icons ansehen anklicken.
  • string $hint -- der Tooltipp, der bei Mausberührung angezeigt wird
  • boolean $down -- bestimmt, ob der Button als angeklickt (TRUE) angezeigt wird oder nicht (FALSE; dies ist der Standardfall). Normalerweise hängt dies davon ab, ob der zum jeweiligen Button gehörende Befehl gerade aktiv ist.
  • boolean $noTranslation -- whether the caption should be automatically translated (FALSE, default) or not (TRUE)

addSeparator() erwartet keine Argumente. Die Methode sollte verwendet werden, um logische Gruppen innerhalb der Menüleiste voneinander zu trennen.

Nachdem alle erforderlichen Buttons und Trennlinien hinzugefügt wurden, können Sie die Methode getXML() des base_btnbuilder-Objekts aufrufen, um die XML-Darstellung der Menüleisten-Inhalte zu erhalten. Nachdem Sie das XML erhalten haben, können Sie es mit Hilfe der Methode addMenu() zum Layout-Objekt hinzufügen. Hier sehen Sie die Unit-Test-Methode für getButtons():

  /**
  * @covers HelloPlanetAdmin::getButtons
  */
  public function testGetButtons() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $layout = $this->getMock('papaya_xsl', array('addMenu'));
    $layout
      ->expects($this->once())
      ->method('addMenu');
    $adminObject->layout = $layout;
    $adminObject->params = array('cmd' => 'del_planet', 'id' => 4);
    $adminObject->images = array(
      'actions-generic-add' => 'actions/generic-add.png',
      'actions-generic-delete' => 'actions/generic-delete.png'
    );
    $adminObject->getButtons();
  }

Und hier die eigentliche Implementierung:

  /**
  * Get the main toolbar
  */
  public function getButtons() {
    include_once(PAPAYA_INCLUDE_PATH.'system/base_btnbuilder.php');
    $toolbar = new base_btnbuilder();
    $toolbar->images = &$this->images;
    $cmd = (isset($this->params['cmd'])) ? $this->params['cmd'] : '';
    $pushed = ($cmd == 'add_planet') ? TRUE : FALSE;
    $toolbar->addButton(
      'Add planet',
      $this->getLink(array('cmd' => 'add_planet')),
      'actions-generic-add',
      'Add a new planet',
      $pushed
    );
    if (isset($this->params['id'])) {
      $toolbar->addSeparator();
      $pushed = ($cmd == 'del_planet') ? TRUE : FALSE;
      $toolbar->addButton(
        'Delete planet',
        $this->getLink(array('cmd' => 'del_planet', 'id' => $this->params['id'])),
        'actions-generic-delete',
        'Delete current planet',
        $pushed
      );
    }
    if ($str = $toolbar->getXML()) {
      $this->layout->addMenu(sprintf('<menu ident="%s">%s</menu>'.LF, 'edit', $str));
    }
  }

Wie Sie sehen, wird der Delete planet-Button nur angezeigt, wenn ein id-Parameter vorhanden ist, der für einen aktuellen Planeten steht.

Den Löschbefehl ausführen

Das letzte Feature, das wir implementieren müssen, ist das Löschen eines Planeten. Wir wollen nicht, dass Benutzer Daten mit einem einzelnen Klick löschen können. Deshalb fügen wir eine Sicherheitsfrage hinzu. Diese wird von der Klasse base_msgdialog aus papaya-lib/system bereitgestellt. Genau wie base_dialog kann auch diese Klasse nicht durch Unit-Tests abgedeckt werden; sie wird demnächst durch eine bessere, testfreundliche Implementierung ersetzt.

Sie brauchen also nur einen sehr kurzen Unit-Test (oder eher einen Nicht-Test) für die Methode getDeleteDialog() hinzuzufügen:

  /**
  * @covers HelloPlanetAdmin::getDeleteDialog
  */
  public function testGetDeleteDialog() {
    $this->markTestSkipped();
  }

Hier die Methode getDeleteDialog(); platzieren Sie sie über der Methode getPlanetsList():

  /**
  * Get the delete confirmation dialog
  */
  public function getDeleteDialog() {
    if (isset($this->params['id']) && !isset($this->params['confirm_delete'])) {
      $dialog = new base_msgdialog(
        $this,
        $this->paramName,
        array('cmd' => 'del_planet', 'id' => $this->params['id'], 'confirm_delete' => 1),
        'Really delete current planet?',
        'question'
      );
      $this->layout->add($dialog->getMsgDialog());
    }
  }

Der Konstruktor von base_msgdialog erwartet ähnliche Argumente wie derjenige von base_dialog: Eine Referenz auf die aufrufende Klasse, den Parameter-Namespace und die versteckten Felder, die den Link bilden. Statt interaktiver Felder und ihrer Daten sind die letzten beiden Argumente die anzuzeigende Nachricht und die Art des Dialogs (in diesem Fall 'question').

Die Methode getDeleteDialog() wird von getXml() aufgerufen, wenn der Befehl del_planet gesetzt ist. Sie zeigt den Dialog nur dann an, wenn der Parameter confirm_delete nicht gesetzt ist, für den sie den Link bereitstellt.

Die Löschmethode selbst verwendet den Connector, um den aktuellen Planeten zu löschen. Sie benötigt zwei Unit-Tests, um beide möglichen Ergebnisse abzudecken:

  /**
  * @covers HelloPlanetAdmin::deletePlanet
  */
  public function testDeletePlanetSuccess() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $connectorObject = $this->getMock('HelloConnector');
    $connectorObject
      ->expects($this->once())
      ->method('deletePlanet')
      ->will($this->returnValue(TRUE));
    $adminObject->setConnectorObject($connectorObject);
    $adminObject->params = array('id' => 4, 'confirm_delete' => 1);
    $adminObject->deletePlanet();
    $this->assertEquals(
      'Planet successfully deleted.',
      $adminObject->msgs[0]
    );
  }
 
  /**
  * @covers HelloPlanetAdmin::deletePlanet
  */
  public function testDeletePlanetError() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $connectorObject = $this->getMock('HelloConnector');
    $connectorObject
      ->expects($this->once())
      ->method('deletePlanet')
      ->will($this->returnValue(FALSE));
    $adminObject->setConnectorObject($connectorObject);
    $adminObject->params = array('id' => 7, 'confirm_delete' => 1);
    $adminObject->deletePlanet();
    $this->assertEquals(
      'Could not delete planet.',
      $adminObject->msgs[0]
    );
  }

Fügen Sie als Nächstes die Methode deletePlanet() oberhalb von getDeleteDialog() ein:

  /**
  * Delete the current planet
  */
  public function deletePlanet() {
    if (isset($this->params['id']) && isset($this->params['confirm_delete']) &&
        $this->params['confirm_delete'] == 1) {
      $connectorObject = $this->getConnectorObject();
      $success = $connectorObject->deletePlanet($this->params['id']);
      if ($success) {
        $this->addMsg(MSG_INFO, $this->_gt('Planet successfully deleted.'));
      } else {
        $this->addMsg(MSG_ERROR, $this->_gt('Could not delete planet.'));
      }
    }
  }

Nun müssen wir noch dafür sorgen, dass deletePlanet() von execute() aufgerufen wird und getDeleteDialog() von getXml(). Fügen Sie für Ersteres einen weiteren execute()-Unit-Test hinzu:

  /**
  * @covers HelloPlanetAdmin::execute
  */
  public function testExecuteDelete() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $connectorObject = $this->getMock('HelloConnector');
    $connectorObject
      ->expects($this->once())
      ->method('deletePlanet')
      ->will($this->returnValue(TRUE));
    $adminObject->setConnectorObject($connectorObject);
    $adminObject->params = array('cmd' => 'del_planet', 'id' => 5, 'confirm_delete' => 1);
    $adminObject->execute();
    $this->assertEquals('Planet successfully deleted.', $adminObject->msgs[0]);
  }

Erweitern Sie die Methode execute() so, dass sie folgendermaßen aussieht:

  /**
  * Execute the module's commands
  */
  public function execute() {
    $this->initializeParams();
    if (isset($this->params['cmd'])) {
      switch ($this->params['cmd']) {
      case 'save_planet':
        $this->savePlanet();
        break;
      case 'del_planet':
        $this->deletePlanet();
        break;
      }
    }
  }

Um die Erweiterung von getXml() zu testen, ohne auf das untestbare base_msgdialog-Objekt zu stoßen, setzen wir einfach den Parameter confirm_delete. Da del_planet sich anders verhält als die beiden anderen Befehle, die getXml() beeinflussen, erweitern wir nicht den bestehenden Test, der den Datenprovider verwendet, sondern wir schreiben einen zusätzlichen Test:


  /**
  * @covers HelloPlanetAdmin::getXml
  */
  public function testGetXmlDelete() {
    $adminObject = $this->getPlanetAdminObjectFixture();
    $layout = $this->getMock('papaya_xsl', array('addLeft'));
    $layout
      ->expects($this->once())
      ->method('addLeft');
    $connectorObject = $this->getMock('HelloConnector');
    $connectorObject
      ->expects($this->once())
      ->method('getAllPlanets')
      ->will($this->returnValue(array()));
    $adminObject->setConnectorObject($connectorObject);
    $adminObject->params = array('cmd' => 'del_planet', 'id' => 5, 'confirm_delete' => 1);
    $adminObject->getXml($layout);
  }

Der letzte Schritt besteht darin, getXml() zu erweitern:

  /**
  * Get the module's GUI XML
  *
  * @param papaya_xsl $layout
  */
  public function getXml($layout) {
    $layout->addLeft($this->getPlanetsList());
    if (isset($this->params['cmd'])) {
      switch ($this->params['cmd']) {
      case 'add_planet':
        $layout->add($this->getDialog(TRUE));
        break;
      case 'edit_planet':
        $layout->add($this->getDialog(FALSE));
        break;
      case 'del_planet':
        $this->getDeleteDialog();
        break;
      }
    }
  }

Damit ist die Administrationsoberfläche fertig, und Sie können Planeten für das Tutorialpaket hinzufügen, ändern und löschen.

Persönliche Werkzeuge
In anderen Sprachen