Praxisbeispiel: MVC, FC-Actions, Pager
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() { } } ?>