Einstieg in den GenericORMapper (GORM)
Zielsetzung
Im vorliegenden Tutorial soll anhand eines einfachen Beispiels die Verwendung des APF-eigenen GenericORMappers verdeutlicht werden.
Es wird eine Beispielanwendung erzeugt, die es ermöglicht, zunächst Autoren zu erfassen. In einer weiteren Maske stehen diese Autoren zur Verfügung und es wird das Hinzufügen eines Buchtitels ermöglicht. Des weiteren soll die Möglichkeit bestehen, die bereits erfassten Autoren sowie die Bücher in Listenform darzustellen. Auch eine Darstellung der Zuordnung Autor => Bücher soll möglich sein. Weiterhin soll eine Löschfunktion das Löschen eines einzelnen Buches sowie das Löschen eines Autors und aller ihm zugeordneten Bücher möglich sein.
Funktionsumfang:
- Hinzufügen eines Autors
- Anzeigen aller erfassten Autoren
- Hinzufügen eines Buches mit der Auswahlmöglichkeit eines zuvor erfassten Autors
- Anzeigen aller Bücher
- Anzeigen aller Bücher, geordnet nach dem Autor
- Löschen eines Buches
- Löschen eines Autors und aller ihm zugeordneten Bücher
Voraussetzungen
Das vorliegende Tutorial wurde unter Verwendung des APF 1.14 erstellt.
Der Anwender sollte Grundkenntnisse im Umgang mit dem APF (Ordnerstruktur, Namespaces) haben. Da dass Tutorial einfach gehalten ist, sollte es jedoch auch für APF-Einsteiger nachvollziehbar sein.
Eingesetzte APF-Komponenten
Verzeichnis- und Dateistruktur
/ index.php -/css --default.css -/apps --/books ---/pres ----/controller ------AddAuthorController.php ------AddBookController.php ------AuthorBooksListController.php ------AuthorsListController.php ------BooksListController.php ------DeleteAuthorController.php ------DeleteBookController.php ----/model ----/view ------author.html ------authorbookslist.html ------authorlist.html ------book.html ------booklist.html ------deleteauthor.html ------deletebook.html ------main.html ------start.html --/config ---/core ----/database -----/books -------DEFAULT_connections.ini ----/modules -----/gorm ------/books --------DEFAULT_books_objects.ini --------DEFAULT_books_relations.ini
Vorbereitung Datenbank
DEFAULT_connections.ini
apps/config/core/database/books/DEFAULT_connections.ini
[books] DB.Host = "localhost" DB.User = "user" DB.Pass = "pwd" DB.Name = "books" DB.Type = "MySQLx" DB.Charset = "utf8" DB.Collation = "utf8_general_ci"
DEFAULT_books_objects.ini
apps/config/modules/gorm/books/DEFAULT_books_objects.ini
[Authors] Author = "VARCHAR(30)" [Books] Title = "VARCHAR(30)" Price = "DECIMAL(6,2)"
DEFAULT_books_relations.ini
apps/config/modules/gorm/books/DEFAULT_books_relations.ini
[Author2Book] Type = "COMPOSITION" SourceObject = "Authors" TargetObject = "Books"
Automatisiertes GORM Datenbank-Setup
apps/modules/genericormapper/data/tools/setup.php
<?php require('../../../../../apps/core/pagecontroller/pagecontroller.php'); $langProv = new IniConfigurationProvider(); $langProv->setOmitContext(false); ConfigurationManager::registerProvider('ini', $langProv); import('modules::genericormapper::data::tools','GenericORMapperSetup'); $setup = new GenericORMapperSetup(); $setup->setContext('books'); $setup->setupDatabase('modules::gorm','books','books'); $setup->setupDatabase('modules::gorm','books'); ?>
Ergebnis des GORM Datenbank-Setup
Das Ausführen der setup.php erzeugt folgende Ausgabe:
CREATE TABLE IF NOT EXISTS `ent_authors` ( `AuthorsID` INT(5) UNSIGNED NOT NULL AUTO_INCREMENT, `Author` VARCHAR(30) character SET utf8 NOT NULL DEFAULT '', `CreationTimestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `ModificationTimestamp` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`AuthorsID`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `ent_books` ( `BooksID` INT(5) UNSIGNED NOT NULL AUTO_INCREMENT, `Title` VARCHAR(30) character SET utf8 NOT NULL DEFAULT '', `Price` DECIMAL(6,2), `CreationTimestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `ModificationTimestamp` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`BooksID`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `cmp_author2book` ( `Source_AuthorsID` INT(5) UNSIGNED NOT NULL DEFAULT '0', `Target_BooksID` INT(5) UNSIGNED NOT NULL DEFAULT '0', KEY `JOIN` (`Source_AuthorsID`, `Target_BooksID`), KEY `REVERSEJOIN` (`Target_BooksID`, `Source_AuthorsID`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Inhalt der Datenbank
In der entsprechenden Datenbank sollten automatisch folgende Tabellen erzeugt worden sein:
- cmp_author2book
- ent_authors
- ent_books
HIermit sind die Vorbereitungen zum Einsatz des GORM abgeschlossen und es kann die Umsetzung des Projekts beginnen.
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('books'); // Startseite anzeigen echo $fC->start('books::pres::view', 'main'); ?>
Hauptseite
Die Hauptseite (apps/books/pres/view/main.html) ist das zentrale Element der Anwendung. Über sie werden später die einzelnen Unterseiten angesteuert.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>Books</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>Navigation</h1> <ul> <li><a href="?page=start" title="Home">Home</a></li> <li><a href="?page=author" title="Add author">Add author</a></li> <li><a href="?page=authorlist" title="List authors">List authors</a></li> <li><a href="?page=book" title="Add book">Add book</a></li> <li><a href="?page=booklist" title="List books">List books</a></li> <li><a href="?page=authorbookslist" title="List authors and books">List authors and books</a></li> <li><a href="?page=deletebook" title="Delete a book">Delete a book</a></li> <li><a href="?page=deleteauthor" title="Delete an author and his books">Delete an author and his books</a></li> </ul> <core:importdesign namespace="books::pres::view" template="[page=start]" incparam="page"/> </body> </html>
Dazu noch eine Startseite apps/books/pres/view/start.html:
<h2>Welcome to GenericORMapper-Demopage</h2> <p>This example shows the use of the APF GenericORMapper.</p> <ol> <li>First, you should create a new author.</li> <li>After that, you are redirected to an list of existing authors.</li> <li>Finally you can create a new book and assign an author.</li> <li>At last you are redirected to an list of existing books.</li> <li>Further a book can be deleted or an author and all assigned books.</li> </ol>
CSS-Datei
Um bei fehlerhaften Eingaben (z.B. Autorenname zu kurz) die entsprechenden Felder farblich zu markieren wird noch unter /css/default.css eine entsprechende CSS-Definition abgelegt.
.apf-form-error{ border: 2px solid red; }
View zur Erfassung eines Autors
Unter apps/books/pres/views/author.html wird das Template zur Erfassung eines Autors abgelegt. In diesem kann wie im folgenden Beispiel umgesetzt auch eine Validierung stattfinden (Textlänge des Autorennamens von mindestens 3 und maximal 30 Zeichen, sowie eine Filterung der Eingabe hinsichtlich bestimmter Zeichen.
<@controller namespace="books::pres::controller" file="AddAuthorController" class="AddAuthorController" @> <core:addtaglib namespace="tools::form::taglib" prefix="html" class="form" /> <h2>Add a new author</h2> <html:form name="AddAuthor"> <label for="Author">Name : </label><form:text name="Author" id="Author" minlength="3" maxlength="30"/><br/> <form:reset name="Reset" value="Clear form"/> <form:button name="SubmitAuthor" value="Add Author"/> <form:addvalidator class="TextLengthValidator" control="Author" button="SubmitAuthor"/> <form:addfilter class="SpecialCharacterFilter" control="Author" button="SubmitAuthor"/> </html:form>
Controller zur Erfassung eines Autors
Der entsprechende Controller wird unter apps/books/pres/controller/AddAuthorController.php abgelegt. Nach dem Speichern des neuen Autors erfolge eine automatische Weiterleitung zur Auflistung aller vorhandenen Autoren mittels dem HeaderManager
<?php import('modules::genericormapper::data','GenericDomainObject'); import('tools::http', 'HeaderManager'); class AddAuthorController extends base_controller{ public function transformContent() { $form = &$this->getForm('AddAuthor'); if ($form->isSent() && $form->isValid()){ $Author = new GenericDomainObject('Authors'); $Author->setProperty('Author', $form->getFormElementByName('Author')->getAttribute('value')); $orm = $this->getMapper(); $orm->saveObject($Author); // Redirect to the authors list HeaderManager::forward('?page=authorlist'); } else{ $form->transformOnPlace(); } } private function getMapper(){ $ormFact = &$this->getServiceObject('modules::genericormapper::data', 'GenericORMapperFactory'); return $ormFact->getGenericORMapper('modules::gorm', 'books', 'books'); } } ?>
View zur Ausgabe aller Autoren
Mit diesem View werden alle vorhandenen Autoren aus der Datenbank ausgelesen und in einer Tabelle dargestellt.
Der View zur Ausgabe aller Bücher ist identisch aufgebaut. Hier sind nur entsprechende Änderungen hinsichtlich der Feldnamen sowie des Controllers vorzunehmen.
<@controller namespace="books::pres::controller" file="AuthorsListController" class="AuthorsListController" @> <h2>Available authors</h2> <table> <tr> <th>Author</th> <th>Date</th> </tr> <html:placeholder name="AuthorsList" /> </table> <html:template name="AuthorsList"> <tr> <td><template:placeholder name="AuthorName" /></td> <td><template:placeholder name="CreateDate" /></td> </tr> </html:template>
Controller zur Ausgabe aller Autoren
Der Controller holt unter Verwendung des GORM sämtliche vorhandenen Datensätze, weist sie den Platzhaltern im Template zu und gibt dieses anschließend aus.
Der Controller zur Ausgabe aller Bücher ist identisch aufgebaut. Hier sind lediglich die Bezeichnungen usw. anzupassen.
<?php import('modules::genericormapper::data','GenericDomainObject'); import('tools::http', 'HeaderManager'); class AuthorsListController extends base_controller{ public function transformContent() { $orm = $this->getMapper(); $Crit = new GenericCriterionObject(); $Crit->addPropertyIndicator('Author','%'); $AuthorsList = $orm->loadObjectListByCriterion('Authors',$Crit); $template = &$this->getTemplate('AuthorsList'); $buffer = ''; foreach ($AuthorsList as $Author){ $template->setPlaceHolder('AuthorName', $Author->getProperty('Author')); $template->setPlaceHolder('CreateDate', $Author->getProperty('CreationTimestamp')); $buffer .= $template->transformTemplate(); } $this->setPlaceHolder('AuthorsList', $buffer); } private function getMapper(){ $ormFact = &$this->getServiceObject('modules::genericormapper::data', 'GenericORMapperFactory'); return $ormFact->getGenericORMapper('modules::gorm', 'books', 'books'); } } ?>
View zur Erfassung eines Buches
Hier wird die Möglichkeit geboten, ein neues Buch zu erfassen. Hierzu erhält der Benutzer eine Auswahlmenü, in dem alle erfassten Autoren erscheinen.
<@controller namespace="books::pres::controller" file="AddBookController" class="AddBookController" @> <core:addtaglib namespace="tools::form::taglib" prefix="html" class="form" /> <h2>Add a new book</h2> <html:form name="AddBook"> <label for="Author">Author : </label> <form:select name="Author"/><br/> <label for="Title">Title : </label><form:text name="Title" id="Title"/><br/> <label for="Price">Price : </label><form:text name="Price" id="Price"/><br/> <form:reset name="Reset" value="Clear forms"/> <form:button name="SubmitBook" value="Add book"/> </html:form> <html:template name="AuthorsList"> <select:option value="<template:placeholder name="ID"/>"><template:placeholder name="AuthorName"/></select:option> </html:template>
Controller zur Erfassung eines Buches
Wichtiger Hinweis
Die Befüllung von select-Feldern muss immer stattfinden, da sonst dem Controller die Möglichkeit genommen wird, den übermittelten Inhalt zu überprüfen. Wird die Befüllung z.B. erst im else-Teil der if-Bedingung durchgeführt, erhält der Controller keine Daten für das Feld AuthorID. Das wiederum hat zur Folge, dass ein neuer, namenloser Autor angelegt wird und mit diesem das neu erfasste Buch verknüpft wird.
<?php import('modules::genericormapper::data','GenericDomainObject'); import('tools::http', 'HeaderManager'); class AddBookController extends base_controller{ public function transformContent() { $form = &$this->getForm('AddBook'); $orm = $this->getMapper(); $Crit = new GenericCriterionObject(); $Crit->addPropertyIndicator('Author','%'); $AuthorsList = $orm->loadObjectListByCriterion('Authors',$Crit); $selectOption = &$form->getFormElementByName('Author'); foreach ($AuthorsList as $Author){ $selectOption->addOption($Author->getProperty('Author'), $Author->getProperty('AuthorsID')); } if ($form->isSent() && $form->isValid()){ $Book = new GenericDomainObject('Books'); $Book->setProperty('Title', $form->getFormElementByName('Title')->getAttribute('value')); $Book->setProperty('Price', $form->getFormElementByName('Price')->getAttribute('value')); $Author = new GenericDomainObject('Authors'); $Author->setObjectId($form->getFormElementByName('Author')->getSelectedOption()->getValue()); $Book->addRelatedObject('Author2Book', $Author); $orm = $this->getMapper(); $orm->saveObject($Book); HeaderManager::forward('?page=booklist'); } else{ $form->transformOnPlace(); } } private function getMapper(){ $ormFact = &$this->getServiceObject('modules::genericormapper::data', 'GenericORMapperFactory'); return $ormFact->getGenericORMapper('modules::gorm', 'books', 'books'); } } ?>
View zur Ausgabe einer Auflistung der Autoren und ihrer Bücher
Die Datei apps/books/pres/view/authorbookslist.html enthält das Grundgerüst, welches durch den Controller befüllt wird.
<@controller namespace="books::pres::controller" file="AuthorBooksListController" class="AuthorBooksListController" @> <h2>List of all authors and their books</h2> <table> <tr> <th>Author</th> <th>Title</th> <th>Price</th> <th>Date</th> </tr> <html:placeholder name="DisplayAll" /> </table> <html:template name="DisplayAll"> <tr> <td><template:placeholder name="AuthorName" /></td> <td><template:placeholder name="Title" /></td> <td><template:placeholder name="Price" /></td> <td><template:placeholder name="CreateDate" /></td> </tr> </html:template>
Controller zur Ausgabe der Bücherliste nach Autoren
Der Controller apps/books/pres/controller/AuthorBooksListController.php:
<?php import('modules::genericormapper::data','GenericDomainObject'); import('tools::http', 'HeaderManager'); class AuthorBooksListController extends base_controller{ public function transformContent() { $orm = $this->getMapper(); $Crit = new GenericCriterionObject(); $Crit->addPropertyIndicator('Author','%'); $Crit->addOrderIndicator('Author'); $AuthorsList = $orm->loadObjectListByCriterion('Authors',$Crit); $template = &$this->getTemplate('DisplayAll'); $buffer = ''; foreach ($AuthorsList as $Author){ $BooksList= $orm->loadRelatedObjects($Author, 'Author2Book'); foreach ($BooksList as $Book){ $template->setPlaceHolder('AuthorName', $Author->getProperty('Author')); $template->setPlaceHolder('Title', $Book->getProperty('Title')); $template->setPlaceHolder('Price', number_format($Book->getProperty('Price'), 2, ',', '.')); $template->setPlaceHolder('CreateDate', $Book->getProperty('CreationTimestamp')); $buffer .= $template->transformTemplate(); } } $this->setPlaceHolder('DisplayAll', $buffer); } private function getMapper(){ $ormFact = &$this->getServiceObject('modules::genericormapper::data', 'GenericORMapperFactory'); return $ormFact->getGenericORMapper('modules::gorm', 'books', 'books'); } } ?>
View zum Löschen eines Buches
<@controller namespace="books::pres::controller" file="DeleteBookController" class="DeleteBookController" @> <core:addtaglib namespace="tools::form::taglib" prefix="html" class="form" /> <h2>Delete a book</h2> <html:form name="DeleteBook"> <label for="Book">Book : </label> <form:select name="Books"/><br/> <form:button name="SubmitBook" value="Delete book"/> </html:form> <html:template name="BooksList"> <select:option value="<template:placeholder name="ID"/>"><template:placeholder name="Title"/></select:option> </html:template>
Controller zum Löschen eines Buches
Zum Löschen eines Objekts (in diesem Fall eines Buches), stellt das APF die Methode deleteObject() zur Verfügung.
<?php import('modules::genericormapper::data','GenericDomainObject'); import('tools::http', 'HeaderManager'); class DeleteBookController extends base_controller{ public function transformContent() { $form = &$this->getForm('DeleteBook'); $orm = $this->getMapper(); $Crit = new GenericCriterionObject(); $Crit->addPropertyIndicator('Title','%'); $BooksList = $orm->loadObjectListByCriterion('Books',$Crit); $selectOption = &$form->getFormElementByName('Books'); foreach ($BooksList as $Book){ $selectOption->addOption($Book->getProperty('Title'), $Book->getProperty('BooksID')); } if ($form->isSent() && $form->isValid()){ $Book = new GenericDomainObject('Books'); $Book->setProperty('BooksID',$form->getFormElementByName('Books')->getSelectedOption()->getValue()); $orm = $this->getMapper(); $orm->deleteObject($Book); HeaderManager::forward('?page=booklist'); } else{ $form->transformOnPlace(); } } private function getMapper(){ $ormFact = &$this->getServiceObject('modules::genericormapper::data', 'GenericORMapperFactory'); return $ormFact->getGenericORMapper('modules::gorm', 'books', 'books'); } } ?>
View zum Löschen eines Autors und ihm zugeordneter Bücher
<@controller namespace="books::pres::controller" file="DeleteAuthorController" class="DeleteAuthorController" @> <core:addtaglib namespace="tools::form::taglib" prefix="html" class="form" /> <h2>Delete an autor and assigned books</h2> <html:form name="DeleteAuthor"> <label for="Author">Author : </label> <form:select name="Author"/><br/> <form:button name="SubmitBook" value="Delete author and assigned books"/> </html:form> <html:template name="AuthorList"> <select:option value="<template:placeholder name="ID"/>"><template:placeholder name="AuthorName"/></select:option> </html:template>
Controller zum Löschen eines Autors und ihm zugeordneter Bücher
<?php import('modules::genericormapper::data','GenericDomainObject'); import('tools::http', 'HeaderManager'); class DeleteAuthorController extends base_controller{ public function transformContent() { $form = &$this->getForm('DeleteAuthor'); $orm = $this->getMapper(); $Crit = new GenericCriterionObject(); $Crit->addPropertyIndicator('Author','%'); $AuthorsList = $orm->loadObjectListByCriterion('Authors',$Crit); $selectOption = &$form->getFormElementByName('Author'); foreach ($AuthorsList as $Author){ $selectOption->addOption($Author->getProperty('Author'), $Author->getProperty('AuthorsID')); } if ($form->isSent() && $form->isValid()){ $Author = new GenericDomainObject('Authors'); $Author->setProperty('AuthorsID',$form->getFormElementByName('Author')->getSelectedOption()->getValue()); $BooksList= $orm->loadRelatedObjects($Author, 'Author2Book'); foreach ($BooksList as $Book){ $orm = $this->getMapper(); $orm->deleteObject($Book); } $orm = $this->getMapper(); $orm->deleteObject($Author); HeaderManager::forward('?page=authorbookslist'); } else{ $form->transformOnPlace(); } } private function getMapper(){ $ormFact = &$this->getServiceObject('modules::genericormapper::data', 'GenericORMapperFactory'); return $ormFact->getGenericORMapper('modules::gorm', 'books', 'books'); } } ?>