dieser FAQ-Eintrag soll zeigen, wie die dynamische Ausgabe von Datenbank-Inhalten mit redaktionell gepflegten "Tags" mit Hilfe des APF umgesetzt werden kann.
1. Aufgabenstellung
In einem CMS gibt es immer wieder die Anfordeurngen, dass ein Redakteur mit definierten Platzhaltern (regulär oder literal) dynamisch Funktionen zu einer Seite hinzufügen kann. Beispiel: auf einer Seite mit reinem Inhalt möchte der Redakteur eine Bewertungsfunktion einbinden. Diese soll dann an Stelle des Platzhalters (hier: "[rank]", also literal) ausgegeben werden.
2. Umsetzung
Um die Umsetzung beschreiben zu können, müssen noch einige Rahmen-Bedingungen definiert werden. Diese sind:
- Die Inhalte einer Seite liegen in einer Datenbank-Tabelle. Diese besitzt die Spalten "UrlName" für den Namen der Seite, "PageID" für die ID der Seite, "Title" für den Titel der Seite und "Content" für den Inhalt.
- Die Ausgabe der redaktionellen Inhalte soll über ein eigenes Template mit zugehörigem Controller abgewickelt werden, damit der Inhaltsbereich frei im Haupt-Template der Seite positioniert sein kann.
- Die "Navigation" wird bereits für uns erledigt und die Parameter können aus einer Model-Klasse abgefragt werden. Diese heißt "NavigationModel" und hat getter für die ID oder den Namen der Seite, je nach dem, was in der URL referenziert ist. Üblicherweise kann eine Seite per http://www.example.com/page/123 oder http://www.example.com/page/my-page-name angesprochen werden.
Wie oben beschrieben ist die Information über die anzuzeigende Seite in einem Model gespeichert. Das hat den Vorteil, dass in der Ausgabe-Funktion nicht erst die Information aus der URL (oder z.B. aus einer Session) gelesen werden muss. Die Model-Klasse hat dabei folgende Gestalt:
Code: Alles auswählen
class NavigationModel {
private $__PageId = null;
private $__PageName = null;
public function NavigationModel(){
}
public function setPageId($pageId){
$this->__PageId = $pageId;
}
public function getPageId(){
return $this->__PageId;
}
public function setPageName($pageName){
$this->__PageName = $pageName;
}
public function getPageName(){
return $this->__PageName;
}
}
Code: Alles auswählen
$model = &$this->__getServiceObject('sites::testwebsite::biz','NavigationModel');
2.2. Aufbau der Basis-Webseite
Um eine Basis für die Entwicklung der dynamischen Ausgabe zu haben, erstellen wir zunächst eine einfache Webseite. Um von gleichen Pfad- und Namespace-Angaben auszuheben, sollte dazu das Tutorial Webseite erstellen verwendet und bis einschließlich Kapitel 4 bearbeitet werden. Anschließend können wir die fehlende Funktion wie folgt nachrüsten:
2.3. Anlegen eines neuen Document-Controllers
Um die Ausgabe dynamisch zu gestalten, nutzen wir die Möglichkeit einen Document-Controller für ein Template definieren zu können. Dieser wird mit den Attributen des aktuellen Templates, einer Referenz auf das aktuelle Dokument im DOM-Baum und dem Inhalt desselben ausgestattet. Um z.B. die aktuelle Uhrzeit im Inhaltsbereich über den Document-Controller anzeigen zu können muss das Template content.html den folgenden Inhalt aufweisen:
Code: Alles auswählen
<@controller namespace="sites::testwebsite::pres::controller" file="content_controller" class="content_controller" @>
Hierzu legen wir zunächst die Datei content_controller.php im Ordner apps/sites/testsite/pres/controller/ an. Anschließend definieren wir die Klasse content_controller und füllen den Inhalt des Dokumentes mit der aktuellen Uhrzeit:
Code: Alles auswählen
class content_controller extends baseController {
public function transformContent(){
$this->__Content = date('d.m.Y, H:i:s');
}
}
Code: Alles auswählen
http://localhost/testwebsite/
erscheinen.20.06.2009, 16:54
2.4. Anlegen der Datenbank
Um nun dynamische Inhalte aus der Datenbank im Inhaltsbereich der Seite darzustellen können wir wie im Beispiel unter 2.3. vorgehen. Unterschied: die Daten müssen vorher aus der Datenbank gelesen werden. Hierzu stellt das APF den connectionManager zur Verfügung.
Um diesen einsetzen zu können, müssen wir zunächst eine Datenbank-Konfiguration anlegen. Hierzu einfach die Datei
Code: Alles auswählen
apps/config/core/database/sites/testwebsite/DEFAULT_connections.ini
Code: Alles auswählen
[CMSDB]
DB.Host = "..."
DB.User = "..."
DB.Pass = "..."
DB.Name = "..."
DB.Type = ""
Code: Alles auswählen
$cM = &$this->__getServiceObject('core::database','connectionManager');
$SQL = &$cM->getConnection('CMSDB');
Code: Alles auswählen
CREATE TABLE `cms_content` (
`PageId` TINYINT( 5 ) NOT NULL AUTO_INCREMENT ,
`PageName` VARCHAR( 20 ) NOT NULL ,
`Title` VARCHAR( 100 ) NOT NULL ,
`Content` TEXT NOT NULL ,
PRIMARY KEY ( `PageId` ) ,
UNIQUE (
`PageName`
)
) ENGINE = MYISAM;
2.5. Auslesen der Inhalte aus der Datenbank
Das Auslesen der Inhalte einer Seite aus der Datenbank kann nun mit dem Statement
Code: Alles auswählen
SELECT `Title`, `Content`
FROM `cms_content`
WHERE `PageId` = 1
OR `PageName` LIKE '...';
Um die Inhalte der Datenbank nun in den Inhalt der Seite zu bringen, nutzen wir den bereits angelegten Document-Controller und erweitern diesen wie folgt:
Code: Alles auswählen
import('core::database','connectionManager');
class content_controller extends baseController {
public function transformContent(){
$cM = &$this->__getServiceObject('core::database','connectionManager');
$SQL = &$cM->getConnection('CMSDB');
$model = &$this->__getServiceObject('sites::testwebsite::biz','NavigationModel');
$pageId = $model->getPageId();
$pageName = $model->getPageName();
$select = 'SELECT `Content`
FROM `cms_content`
WHERE `PageId` = '.$pageId.'
OR `PageName` LIKE \''.$pageName.'\'';
$result = $SQL->executeTextStatement($select);
$data = $SQL->fetchData($result);
$this->__Content = $data['Content'];
}
}
2.6. Parsen der Platzhalter
Für die Verarbeitung der Platzhalter gibt es mehrere Möglichkeiten:
- Direkte Verarbeitung im Controller.
- Verarbeitung von eigenen Modulen im Controller.
- Injizierung der Module in den Inhalt des aktuellen Knotens.
Hierzu denken wir uns, dass die Ausgabe des Page-Rankings über das Template main.html aus dem Namespace modules::ranking::pres::templates abgedeckt wird. Um das Modul statisch auszuführen, würde es genügen ein
Code: Alles auswählen
<core:importdesign namespace="modules::ranking::pres::templates" template="main" />
Auf Grund der Architektur des APF ist das ohne Probleme möglich, es müssen dem Modul lediglich die Umgebungsinformationen mitgegeben und die für das Timing-Modell relevanten Methoden ausgeführt werden. Hierzu kann folgende Logik genutzt werden:
Code: Alles auswählen
$rankingPlaceHolder = '[rank]';
if(substr_count($rankingPlaceHolder) > 0){
$page = new Page();
$page->set('Context',$this->__Context);
$page->set('Language',$this->__Language);
$page->loadDesign('modules::ranking::pres::templates','main');
str_replace($rankingPlaceHolder,$page->transform(),$this->__Content);
}
Der Document-Controller hat nun folgende Gestalt:
Code: Alles auswählen
import('core::database','connectionManager');
class content_controller extends baseController {
public function transformContent(){
$cM = &$this->__getServiceObject('core::database','connectionManager');
$SQL = &$cM->getConnection('CMSDB');
$model = &$this->__getServiceObject('sites::testwebsite::biz','NavigationModel');
$pageId = $model->getPageId();
$pageName = $model->getPageName();
$select = 'SELECT `Content`
FROM `cms_content`
WHERE `PageId` = '.$pageId.'
OR `PageName` LIKE \''.$pageName.'\'';
$result = $SQL->executeTextStatement($select);
$data = $SQL->fetchData($result);
$this->__Content = $data['Content'];
$rankingPlaceHolder = '[rank]';
if(substr_count($rankingPlaceHolder) > 0){
$page = new Page();
$page->set('Context',$this->__Context);
$page->set('Language',$this->__Language);
$page->loadDesign('modules::ranking::pres::templates','main');
str_replace($rankingPlaceHolder,$page->transform(),$this->__Content);
}
}
}
Ein Aufruf der URL
Code: Alles auswählen
http://localhost/testwebsite/
Um innerhalb des Ranking-Moduls auf die Informationen der Seite zugreifen zu können, kann dieses entweder das oben verwendete Model befragen oder direkt die URL-Informationen nutzen.
2.7. Parsen von konfigurierbaren Platzhaltern
Um den Code des Controllers nicht unnötig aufzublähen kann eine externe Konfiguration der Tags angelegt werden. Da die Tags dynamische Module parsen können sollen, ist es notwendig sowohl die Platzhalter als auch die auszuführenden Templates zu konfigurieren. Die Konfigurationsdatei hat damit folgende Gestalt:
Code: Alles auswählen
[Ranking]
tag = "[rank]"
namespace = "modules::ranking::pres::templates"
template = "main"
[Comment]
tag = "[comment]"
namespace = "modules::comments::pres::templates"
template = "comments"
[Guestbook]
tag = "[guestbook]"
namespace = "modules::guestbook::pres::templates"
template = "guestbook"
Um die in der Konfigurationsdatei definierten Tags nun dynamisch zu parsen muss der Quelltext wie folgt erweitert werden:
Code: Alles auswählen
$config = &$this->__getConfiguration('sites::testwebsite::pres','tags');
$tags = $config->getConfiguration();
foreach($tags as $tag){
$placeHolder = $tag['tag'];
if(substr_count($placeHolder) > 0){
$page = new Page();
$page->set('Context',$this->__Context);
$page->set('Language',$this->__Language);
$page->loadDesign($tag['namespace'],$tag['template']);
str_replace($placeHolder,$page->transform(),$this->__Content);
}
}
Code: Alles auswählen
import('core::database','connectionManager');
class content_controller extends baseController {
public function transformContent(){
$cM = &$this->__getServiceObject('core::database','connectionManager');
$SQL = &$cM->getConnection('CMSDB');
$model = &$this->__getServiceObject('sites::testwebsite::biz','NavigationModel');
$pageId = $model->getPageId();
$pageName = $model->getPageName();
$select = 'SELECT `Content`
FROM `cms_content`
WHERE `PageId` = '.$pageId.'
OR `PageName` LIKE \''.$pageName.'\'';
$result = $SQL->executeTextStatement($select);
$data = $SQL->fetchData($result);
$this->__Content = $data['Content'];
$config = &$this->__getConfiguration('sites::testwebsite::pres','tags');
$tags = $config->getConfiguration();
foreach($tags as $tag){
$placeHolder = $tag['tag'];
if(substr_count($placeHolder) > 0){
$page = new Page();
$page->set('Context',$this->__Context);
$page->set('Language',$this->__Language);
$page->loadDesign($tag['namespace'],$tag['template']);
str_replace($placeHolder,$page->transform(),$this->__Content);
}
}
}
}
Wie oben angesprochen, soll zum Abschlusse die Befüllung des Models besprochen werden. Diese Aufgabe ist im klassischen Sinne eine Aufgabe, die durch einen Front-Controller zu erledigen ist. Vorteil dieser Methode ist, dass bereits beim Aufbau des APF-DOM-Baumes die Informationen des Models genutzt werden können um die Struktur des Baumes zu beeinflussen. Dies ist zwar in diesem Anwendungsfall nicht relevant, für die Implementierung eines CMS nach diesem Muster jedoch an einigen Stellen sehr hilfreich.
Was muss nun getan werden, um das Model zu füllen? Im einfachsten Fall kann dieses in der index.php mit den Werten der URL gefüllt werden. Erweitern wir hierzu die Bootstrap-Datei der Test-Webseite hat diese folgende Gestalt:
Code: Alles auswählen
require_once('./apps/core/pagecontroller/pagecontroller.php');
import('sites::testwebsite::biz','NavigationModel');
$page = new Page();
$page->loadDesign('sites::testwebsite','pres/templates/website');
$model = &Singleton::getInstance('NavigationModel');
if(isset($_REQUEST['page'])){
$pageIndicator = $_REQUEST['page'];
if(is_numeric($pageIndicator)){
$model->setPageId($pageIndicator);
} else {
$model->setPageName($pageIndicator);
}
} else {
$model->setPageId(1);
}
echo $page->transform();
Code: Alles auswählen
class NavigateAction extends AbstractFrontcontrollerAction {
public function run(){
$model = &$this->__getServiceObject('sites::testwebsite::biz','NavigationModel');
if(isset($_REQUEST['page'])){
$pageIndicator = $_REQUEST['page'];
if(is_numeric($pageIndicator)){
$model->setPageId($pageIndicator);
} else {
$model->setPageName($pageIndicator);
}
} else {
$model->setPageId(1);
}
}
}