Praxisbeispiel: MVC, FC-Actions, Pager

Aus APF Wiki [de]
Wechseln zu: Navigation, Suche

Inhaltsverzeichnis

Zielsetzung

Das Tutorial soll anhand eines einfachen Beispiels das Zusammenspiel mehrere APF-Komponenten aufzeigen. Hierbei soll eine Umsetzung anhand von MVC geschehen. Der Benutzer hat die Möglichkeit, Nachrichten in einer Datenbank zu speichern. Diese werden beim Besuch der Seite angezeigt.

Das Erfassen einer Nachricht, die bereits existiert, wird durch eine entsprechende Meldung angezeigt und eine Speicherung unterbleibt.

Durch das Anklicken der Nachricht wird diese, durch das Auslösen einer Frontcontroller-Action, gelöscht. Um die Übersichtlichkeit der vorhandenen Nachrichten zu wahren, wird zudem der Pager eingesetzt.

Voraussetzungen

Das vorliegende Tutorial wurde unter Verwendung des APF 1.14 erstellt.

Der Anwender sollte Grundkenntnisse im Umgang mit dem APF (Ordnerstruktur, Namespaces) haben.

Verzeichnis- und Dateistruktur

/
index.php
-/css
---default.css
-/apps
--/messenger
---/fcactions
-----deleteMessageAction.php
-----deleteMessageInput.php
---/pres
----/controller
------messengerController.php
----/model
------messengerMapper.php
----/view
------main.html
----/validator
------DoubleEntry.php
--/config
---/actions
----/messenger
------DEFAULT_actionconfig.ini
---/core
----/database
-----/messenger
-------DEFAULT_connections.ini
---/modules
----/pager
-----/messenger
-------DEFAULT_pager.ini
-------DEFAULT_load_entries_count.sql
-------DEFAULT_load_entry_ids.sql

Vorbereitung Datenbank

Anlegen der Datenbanktabelle

Die Datenbank messenger für das Anwendungsbeispiel beschränkt sich auf eine einzelne Tabelle, in der die Datensätze gespeichert werden. Diese muss im Vorfeld manuell angelegt werden.

CREATE TABLE IF NOT EXISTS `messages` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `Message` varchar(100) NOT NULL,
  `ModificationTimestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `message` (`message`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 ;

DEFAULT_connections.ini

Mit dieser Konfigurationsdatei wird dem APF der Zugriff auf die oben angelegte Datenbank ermöglicht. Sie wird unter /apps/config/core/database/messenger/DEFAULT_connections.ini abgelegt.

[Messenger]
DB.Host = "localhost"
DB.User = "user"
DB.Pass = "pwd"
DB.Name = "messenger"
DB.Type = "MySQLx"
DB.Charset = "utf8"
DB.Collation = "utf8_general_ci"

Umsetzung

Bootstrap erstellen

Die zentrale Datei der Anwendung ist die index.php, welche direkt in / liegt.

<?php
// Fehlermeldungen einschalten
ini_set('html_errors', 'on');
 
// Zeitzone setzen
date_default_timezone_set('Europe/Berlin');
 
// Pagecontroller starten
require_once './apps/core/pagecontroller/pagecontroller.php';
 
// Frontcontroller starten
import('core::frontcontroller', 'Frontcontroller');
 
$fC = &Singleton::getInstance('Frontcontroller');
$fC->setContext('messenger');
 
// Startseite anzeigen
echo $fC->start('messenger::pres::view', 'main');
?>

Ausgangstemplate erstellen

Das Ausgangstemplate /apps/messenger/pres/view/main.html gibt ein input-Feld aus und erlaubt es dem Benutzer dort eine Nachricht einzutragen. Diese wird nach dem Absenden zunächst auf die richtige Länge validiert und ggf. eine entsprechende Fehlermeldung ausgegeben. Zusätzlich erfolgt mittels eines Filters die Umwandlung von Sonderzeichen in HTML-Entities sowie das Entfernen von HTML- und PHP-Code.

<@controller namespace="messenger::pres::controller" file="messengerController" class="messengerController" @>
<core:addtaglib namespace="tools::form::taglib" prefix="html" class="form" />
 
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>Messages</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <link rel="stylesheet" type="text/css" href="./css/default.css" />
  </head>
  <body>
 
    <h1><a href="index_messenger.php">Messages</a></h1>
 
    <html:form name="NewMessage">
	New Message <form:text name="NewMessage" minlength="6" maxlength="100"/>
	<form:button name="AddMessage" value="Add Message"/>
	<form:error><p class="info">Error: The message must consist of 6 to 100 characters.</p></form:error>
	<form:addvalidator class="TextLengthValidator" control="NewMessage" button="AddMessage"/>
	<form:addfilter class="OnlyHTMLEntitiesFilter" control="NewMessage" button="AddMessage"/>
	<form:addfilter class="StripTagsFilter" control="NewMessage" button="AddMessage"/>
    </html:form>
    <hr/>
</body>
</html>

Im weiteren Verlauf wird dieses Template sukzessiv ergänzt, so dass unterhalb der Eingabemaske die bereits vorhandenen Nachrichten dargestellt werden.

CSS-Datei

Unter /css/default.css wird eine CSS-Datei angelegt, die sich um die farbliche Gestaltung der Darstellung kümmert.

.apf-form-error{
    border: 2px solid red;
}
 
.info{
    background: red;
    color: white;
    padding: 0.5em;
}

Controller zum Speichern einer Nachricht

Der Controller (/apps/messenger/pres/controller/messengerController.php) prüft zunächst, ob das abgesendete Formular valide ist. Trifft dies zu und die neue Nachricht ist noch nicht vorhanden, so wird diese gespeichert. Ansonsten wird das Eingabeformular erneut angezeigt.

<?php
 
class messengerController extends base_controller{
 
    public function  transformContent() {
 
	$form = &$this->getForm('NewMessage');
 
	if ($form->isSent() && $form->isValid()){
 
	    // Nachricht holen
            $Message = $form->getFormElementByName('NewMessage')->getAttribute('value');
 
	    // Mapper aufrufen
	    $mM = &$this->getMapper();
 
	    // Eintrag ist noch nicht vorhanden
	    if ($mM->writeMessage($Message)){
 
		// im weiteren Verlauf erfolgen hier noch weitere Anweisungen
	    }
	    // Eintrag ist bereits vorhanden
	    else{
		// View ausgeben
		$form->transformOnPlace();
	    }
 
	}
 
	$form->transformOnPlace();
    }
 
    // Datenbankverbindung herstellen
    private function &getMapper(){
	return $this->getServiceObject('messenger::pres::model', 'messengerMapper');
    }
 
}
?>

Model zum Speichern der Daten

Mit dem nachfolgenden Model (/apps/messenger/pres/model/messengerMapper.php) ist die Prüfung möglich, ob eine Nachricht bereits existiert sowie das Speichern einer neuen Nachricht.

<?php
import('core::database', 'ConnectionManager');
 
class messengerMapper extends APFObject{
 
    // Neue Nachricht eintragen
    public function writeMessage($Message){
 
	// Datenbankverbindung
	$SQL = &$this->getConnection();
 
	// Existiert der Eintrag bereits?
	if ($this->checkMessage($Message)){
	    // Eintrag noch nicht vorhanden
 
	    // Query formulieren
	    $insert = 'INSERT INTO messages (`message`) VALUES (\'' . $SQL->escapeValue($Message) . '\')';
 
	    // Daten schreiben
	    $result = $SQL->executeTextStatement($insert);
 
	    // ID holen
	    $lastId = $SQL->getLastID();
 
	    // Eintrag erfolgreich
	    return true;
	}
 
	// Eintrag bereits vorhanden bzw. nicht erfolgreich
	return false;
 
    }
 
 
    // Prüft ob eine Nachricht bereits vorhanden ist
    public function checkMessage($Message){
 
	// Datenbankverbindung
	$SQL = &$this->getConnection();
 
	// Query formulieren
	$select = 'SELECT `id` FROM `messages` WHERE `message` = \'' .$Message . '\'';
 
	// Query ausführen
	$result = $SQL->executeTextStatement($select);
 
	// Anzahl Datensätze holen
	$id = $SQL->getNumRows($result);
 
	if($id){
	    // Eintrag existiert bereits
	    return false;
	}
	else{
	    // Eintrag exisitiert noch nicht
	    return true;
	}
    }
 
 
    // Verbindung zur Datenbank herstellen
    protected function &getConnection(){
	$cM = &$this->getServiceObject('core::database', 'ConnectionManager');
	return $cM->getConnection('Messenger');
    }
 
}
?>

Validator zur Überprüfung existierender Nachrichten

Um dem Benutzer mitzuteilen, ob eine Nachricht bereits existiert, soll ihm dies durch einen eigenen Validator mitgeteilt werden.

Anpassung des Ausgangstemplates

Die apps/messenger/pres/view/main.html wird daher wie folgt ergänzt:

<@controller namespace="messenger::pres::controller" file="messengerController" class="messengerController" @>
<core:addtaglib namespace="tools::form::taglib" prefix="html" class="form" />
 
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>Messages</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <link rel="stylesheet" type="text/css" href="./css/default.css" />
  </head>
  <body>
 
    <h1><a href="index_messenger.php">Messages</a></h1>
 
    <html:form name="NewMessage">
	New Message <form:text name="NewMessage" minlength="6" maxlength="100"/>
	<form:button name="AddMessage" value="Add Message"/>
	<form:error><p class="info">Error: The message must consist of 6 to 100 characters.</p></form:error>
	<form:listener name="DoublEntryListener" control="NewMessage" validator="DoubleEntry">
	    <p class="info">Existing message!</p>
	</form:listener>
	<form:addvalidator class="TextLengthValidator" control="NewMessage" button="AddMessage"/>
	<form:addfilter class="OnlyHTMLEntitiesFilter" control="NewMessage" button="AddMessage"/>
	<form:addfilter class="StripTagsFilter" control="NewMessage" button="AddMessage"/>
	<form:addvalidator namespace="messenger::pres::validator" class="DoubleEntry" control="NewMessage" button="AddMessage" type="special"/>
    </html:form>
 
    <hr/>
</body>
</html>

DoubleEntry - Validator

Der Validator greift zur Überprüfung auf bereits existierende Einträge auf die Methode checkMessage() der Klasse messengerMapper (s.o.) zurück.

<?php
import('messenger::pres::model', 'messengerMapper');
 
class DoubleEntry extends TextFieldValidator{
 
    public function validate($Message) {
 
	$mM = &$this->getMapper();
 
	// Eintrag ist noch nicht vorhanden
	if ($mM->checkMessage($Message)){
	    return true;
	}
	// Eintrag ist bereits vorhanden
	return false;
    }
 
    public function  notify() {
	$this->notifyValidationListeners($this->__Control);
	$this->markControl($this->__Control);
    }
 
    private function &getMapper(){
	return $this->getServiceObject('messenger::pres::model', 'messengerMapper');
    }
 
}
?>

Ausgabe vorhandener Nachrichten

Als nächster Schritt sollen alle bereits vorhandenen Nachrichten unterhalb des Eingabefeldes angezeigt werden. Um die Übersicht zu wahren, kommt zudem der Pager zum Einsatz. Dieser soll maximal zehn Nachrichten pro Seite anzeigen.

Anpassung des Templates

Zunächst erfolgt eine Anpassung des Templates.

<@controller namespace="messenger::pres::controller" file="messengerController" class="messengerController" @>
<core:addtaglib namespace="tools::form::taglib" prefix="html" class="form" />
 
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>Messages</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <link rel="stylesheet" type="text/css" href="./css/default.css" />
  </head>
  <body>
 
    <h1><a href="index_messenger.php">Messages</a></h1>
 
    <html:form name="NewMessage">
	New Message <form:text name="NewMessage" minlength="6" maxlength="100"/>
	<form:button name="AddMessage" value="Add Message"/>
	<form:error><p class="info">Error: The message must consist of 6 to 100 characters.</p></form:error>
	<form:listener name="DoublEntryListener" control="NewMessage" validator="DoubleEntry">
	    <p class="info">Existing message!</p>
	</form:listener>
	<form:addvalidator class="TextLengthValidator" control="NewMessage" button="AddMessage"/>
	<form:addfilter class="OnlyHTMLEntitiesFilter" control="NewMessage" button="AddMessage"/>
	<form:addfilter class="StripTagsFilter" control="NewMessage" button="AddMessage"/>
	<form:addvalidator namespace="messenger::pres::validator" class="DoubleEntry" control="NewMessage" button="AddMessage" type="special"/>
    </html:form>
 
    <hr/>
 
    <h2>Messages</h2>
    <p>(Click on message to delete)</p>
    <table>
	<tr>
	    <th>Message</th>
	    <th>Timestamp</th>
	</tr>
	<html:placeholder name="list"/>
 
	<core:addtaglib namespace="tools::html::taglib" prefix="html" class="iterator"/>
	<html:iterator name="MessageList">
	    <iterator:item>
		<tr>
		    <td><a href="<item:placeholder name="link" />"><item:placeholder name="Message" /></a></td>
		    <td><item:placeholder name="ModificationTimestamp" /></td>
		</tr>
	    </iterator:item>
	</html:iterator>
    </table>
    <p><html:placeholder name="Pager" /></p>
 
</body>
</html>

Konfiguration des Pagers

DEFAULT_pager.ini

Zunächst wird die Konfigurationsdatei /apps/config/modules/pager/messenger/DEFAULT_pager.ini mit nachfolgendem Inhalt angelegt.

[messagePager]
Pager.DatabaseConnection = "Messenger"
 
Pager.EntriesPerPage = "10"
 
Pager.ParameterPageName = "pstart"
Pager.ParameterCountName = "pact"
 
Pager.StatementNamespace = "modules::pager"
Pager.CountStatement = "load_entries_count.sql"
Pager.CountStatement.Params = ""
Pager.EntriesStatement = "load_entry_ids.sql"
Pager.EntriesStatement.Params = ""
 
Pager.DesignNamespace = "modules::pager::pres::templates"
Pager.DesignTemplate = "pager_2"
 
Pager.CacheInSession = "false"
 
; seit 1.13. < 1.13 immer true
Pager.AllowDynamicEntriesPerPage = "true"
DEFAULT_load_entries_count.sql

/apps/config/modules/pager/messenger/DEFAULT_load_entries_count.sql

SELECT COUNT(*) AS EntriesCount
FROM `messages`
DEFAULT_load_entry_ids.sql

/apps/config/modules/pager/messenger/DEFAULT_load_entry_ids.sql

SELECT `messages`.`ID` AS DB_ID
FROM `messages`
ORDER BY `messages`.`ID` DESC
LIMIT [Start], [EntriesCount];

Anpassung des Controllers

Der Controller erfährt einige Änderungen, so dass er in Verbindung mit dem Pager in der Lage ist, jeweils fünf vorhandene Nachrichten auszugeben und man durch Blättern zu den weiteren Nachrichten gelangt.

Durch Einbindung des HeaderManagers erfolgt zudem nach dem Speichern einer neuen Nachricht ein Reload der Seite, so dass die neue Nachricht direkt in der List erscheint.

Die Methode createMessageList() stattet die vorhandenen Nachrichten unter Verwendung des LinkGenerators mit einem Link aus, der es im nächsten Schritt ermöglicht, diese durch Anklicken zu löschen.

<?php
import('tools::html::taglib::documentcontroller','iteratorBaseController');
import('tools::link','LinkGenerator');
import('tools::http', 'HeaderManager');
 
class messengerController extends iteratorBaseController{
 
    public function  transformContent() {
 
	$form = &$this->getForm('NewMessage');
 
	// Vorhandene Nachrichten auslesen und anzeigen
	$this->createMessageList();
 
	$pMF = &$this->getServiceObject('modules::pager::biz', 'PagerManagerFabric');
	$pM = &$pMF->getPagerManager('messagePager');
	$this->setPlaceHolder('Pager', $pM->getPager());
 
	if ($form->isSent() && $form->isValid()){
 
	    // Nachricht holen
            $Message = $form->getFormElementByName('NewMessage')->getAttribute('value');
 
	    // Mapper aufrufen
	    $mM = &$this->getMapper();
 
	    // Eintrag ist noch nicht vorhanden
	    if ($mM->writeMessage($Message)){
 
		// Seite neu laden
		HeaderManager::forward('index.php');
	    }
	    // Eintrag ist bereits vorhanden
	    else{
		// View ausgeben
		$form->transformOnPlace();
	    }
 
	}
 
	$form->transformOnPlace();
    }
 
 
    // Nachrichtenliste erzeigen
    public function createMessageList(){
 
	// Kontakt zu Mapper herstellen
	$mM = &$this->getMapper();
 
	// Referenz auf den Interator
	$iterator = &$this->getIterator('MessageList');
 
 
	// Pagereinträge verwenden
	$Messages = $mM->loadMessages();
 
	$urlBasePath = Registry::retrieve('apf::core', 'CurrentRequestURL');
 
 
	foreach ($Messages as &$Message){
	    $Message['link'] = LinkGenerator::generateActionUrl(
		Url::fromCurrent(),
		'actions',
		'deleteAction',
		array(
		    'MessageID' => $Message['ID']
		));
	}
 
	// Datencontainer befüllen
	$iterator->fillDataContainer($Messages);
 
	// Iterator ausgeben
	$iterator->transformOnPlace();
 
    }
 
    // Datenbankverbindung herstellen
    private function &getMapper(){
	return $this->getServiceObject('messenger::pres::model', 'messengerMapper');
    }
 
}
?>

Anpassung des Models

Das Model (/apps/messenger/pres/model/messengerMapper.php) wird um die Methoden loadMessages() sowie loadArticleCommentByID() ergänzt.

<?php
import('core::database', 'ConnectionManager');
 
class messengerMapper extends APFObject{
 
    // Neue Nachricht eintragen
    public function writeMessage($Message){
 
	// Datenbankverbindung
	$SQL = &$this->getConnection();
 
	// Existiert der Eintrag bereits?
	if ($this->checkMessage($Message)){
	    // Eintrag noch nicht vorhanden
 
	    // Query formulieren
	    $insert = 'INSERT INTO messages (`message`) VALUES (\'' . $SQL->escapeValue($Message) . '\')';
 
	    // Daten schreiben
	    $result = $SQL->executeTextStatement($insert);
 
	    // ID holen
	    $lastId = $SQL->getLastID();
 
	    // Eintrag erfolgreich
	    return true;
	}
 
	// Eintrag bereits vorhanden bzw. nicht erfolgreich
	return false;
 
    }
 
 
    // Prüft ob eine Nachricht bereits vorhanden ist
    public function checkMessage($Message){
 
	// Datenbankverbindung
	$SQL = &$this->getConnection();
 
	// Query formulieren
	$select = 'SELECT `id` FROM `messages` WHERE `message` = \'' .$Message . '\'';
 
	// Query ausführen
	$result = $SQL->executeTextStatement($select);
 
	// Anzahl Datensätze holen
	$id = $SQL->getNumRows($result);
 
	if($id){
	    // Eintrag existiert bereits
	    return false;
	}
	else{
	    // Eintrag exisitiert noch nicht
	    return true;
	}
    }
 
 
    // Vorhandene Nachrichten auslesen
    public function loadMessages(){
 
	// Pager einbinden
	$pMF = &$this->getServiceObject('modules::pager::biz', 'PagerManagerFabric');
	$pM = &$pMF->getPagerManager('messagePager');
	return $pM->loadEntriesByAppDataComponent($this, 'loadArticleCommentByID');
 
    }
 
    public function loadArticleCommentByID($id){
	$SQL = &$this->getConnection();
	$select = 'SELECT * FROM `messages` WHERE `messages`.`ID` = \'' . $id . '\'';
	$result = $SQL->executeTextStatement($select);
	return $SQL->fetchData($result);
    }
 
 
    // Verbindung zur Datenbank herstellen
    protected function &getConnection(){
	$cM = &$this->getServiceObject('core::database', 'ConnectionManager');
	return $cM->getConnection('Messenger');
    }
 
}
?>

Nachricht löschen

Vorhandene Nachrichten sollen nun durch Anklicken des Titels gelöscht werden. Die Vorarbeit wurde bereits unter Punkt 5.4. geleistet, indem die Titel mit entsprechenden Links versehen wurden.

Konfigurationsdatei

/apps/config/actions/messenger/DEFAULT_actionconfig.ini

[deleteAction]
FC.ActionNamespace = "messenger::fcactions"
FC.ActionFile = "deleteMessageAction"
FC.ActionClass = "deleteMessageAction"
FC.InputFile = "deleteMessageInput"
FC.InputClass = "deleteMessageInput"
FC.InputParams = ""

Action zum Löschen einer Nachricht

/apps/messenger/fcactions/deleteMessageAction.php

<?php
 
class deleteMessageAction extends AbstractFrontcontrollerAction{
 
    public function  run() {
 
	// Übergebene Id der Nachricht holen
	$id = $this->getInput()->getAttribute('MessageID');
 
	// Kontakt zu Mapper herstellen
	$mM = &$this->getMapper();
 
	// Nachricht löschen
	$mM->deleteMessage($id);
    }
 
    private function &getMapper(){
	return $this->getServiceObject('messenger::pres::model', 'messengerMapper');
    }
 
 
}
?>

/apps/messenger/fcactions/deleteMessageInput.php

<?php
 
class deleteMessageInput extends FrontcontrollerInput{
 
    public function  __construct() {
    }
 
}
?>
Meine Werkzeuge
Namensräume
Varianten
Aktionen
menü
misc
Werkzeuge