Dynamische Ausgabe von Datenbank-Inhalten

Das Forum soll der Ablage von Lösungen für immer wieder auftauchende Problemstellungen dienen. // This forum contains solutions to problems that frequently occur.
Antworten
Benutzeravatar
dr.e.
Administrator
Beiträge: 4525
Registriert: 04.11.2007, 16:13:53

Dynamische Ausgabe von Datenbank-Inhalten

Beitrag von dr.e. » 20.06.2009, 18:00:16

Hallo zusammen,

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.

2.1. Das Model
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;
   }

}

Bei der Ausgabe kann das Model dann wie folgt aufgerufen werden:

Code: Alles auswählen

$model = &$this->__getServiceObject('sites::testwebsite::biz','NavigationModel');

Zunächst gehen wir bei der Entwicklung der Ausgabe-Funktion davon aus, dass immer die richtigen Informationen im Model vorhanden sind, wie diese dort hinein gelagen, wird am Schluss des Eintrags beschrieben.

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" @>

Die eigentliche Funktion steckt nun im Controller selbst.

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');
   }

}

Beim Aufruf der URL

Code: Alles auswählen

http://localhost/testwebsite/

sollte nun die aktuelle Uhrzeit in der Form

20.06.2009, 16:54

erscheinen.

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

anlegen und mit folgendem Inhalt füllen:

Code: Alles auswählen

[CMSDB]  
DB.Host = "..." 
DB.User = "..." 
DB.Pass = "..." 
DB.Name = "..." 
DB.Type = ""

Die Parameter "Host", "User", "Pass" und "Name" müssen dabei mit den lokalen Zugangsdaten der verwendeten Datenbank gefüllt werden. Anschließend können wir per

Code: Alles auswählen

$cM = &$this->__getServiceObject('core::database','connectionManager');  
$SQL = &$cM->getConnection('CMSDB');

auf unsere Datenbank zugreifen. Wie oben besprochen, erzeugen wir nun per

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;

Die Inhaltstabelle für unsere dynamische Ausgabe.

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 '...';

bewerkstellit werden.

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'];

   }

}

Nun wird der Inhalt der Datenbank direkt im Inhaltsbereich der Seite angezeigt. Nachteil: vom Redakteur hinzugefügte Platzhalter werden noch nicht verarbeitet. Um dies im Controller vorzunehmen, muss dieser wir im nächsten Kapitel gezeigt erweitert werden.

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.
In diesem FAQ-Eintrag wollen wir Möglichkeit zwei besprechen. Diese sieht vor, dass das zur Ausgabe verwendeten Moduls bereits als "fertiges" APF-Modul vorliegt und im Controller lediglich eine Brücke zwischen der aktuellen Seite und dem Modul geschaffen werden muss, damit dieses dynamisch ausgeführt wird.

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" />

in das Template content.html einzufügen. Das bedeutet, dass für das Modul lediglich ein Rahmen geschaffen werden muss, der dem aktuellen Rahmen gleicht, in der auch die Test-Seite erzeugt wird.

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);

}

Im Beispiel nutzen wir aus, dass der Page-Controller jedem DOM-Knoten eine definierte Umgebung bereitstellt, in der er ausgeführt werden kann. Dabei ist es (zumeist) nicht relevant, ob der Vater-Knoten das Template content.html ist oder nicht. Dies gewinnt erst dann an Relevant, wenn Informationen des Vater-Knotens benötigt werden.

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);

      }

   }

}

Um den Code testen zu können, sollte zunächst die Datei main.html im Ordner apps/modules/ranking/pres/templates/ angelegt und mit einem beliebigen Inhalt gefüllt und in der Datenbank eine neue Seite erzeugt werden. Anschließend sollten das Attribut $__PageId in der Klasse NavigationModel mit der angelegten PageId aus der Datenbank gefüllt werden, damit das Select-Statement einen Rückgabe-Wert liefert.

Ein Aufruf der URL

Code: Alles auswählen

http://localhost/testwebsite/

sollte nun den dynamischen Inhalt der Datenbank-Spalte Content enthalten. Sofern in dieser der Platzhalter "[rank]" enthalten ist, sollte an dessen Stelle nun der Inhalt der Datei main.html (Template des Moduls ranking) angezeigt werden.

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"

Da der Document-Controller unter dem Namespace sites::testwebsite::pres::controller angesiedelt ist und die Funktion zur Test-Seite gehört, soll die Konfiguration (Name: tags) auch dort (Namespace: sites::testwebsite::pres) abgelegt werden. Hierzu muss nun (wie unter http://adventure-php-framework.org/Seit ... nd-Dateien beschrieben) der Ordner apps/config/sites/testwebsite/pres/sites/testwebsite/ angelegt, dort die Datei DEFAULT_tags.ini erstellt und mit dem oben gezeigten Inhalt gefüllt werden.

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);

   }

}

Mit dem nun erstellten Document-Controller können nun dynamisch konfigurierbare Tags durch die Ausgabe von konfigurierbaren Modulen ersetzt werden. Die Controller-Klasse hat damit folgenden finalen Quelltext:

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);

         }

      }

   }

}


3. Befüllen des Models
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();

Soll eine "echte" Front-Controller-Action nach der APF-Implementierung eingesetzt werden, muss diese gemäß http://adventure-php-framework.org/Seit ... figuration konfiguriert werden und die Action-Klasse besitzt (wie unter http://dev.adventure-php-framework.org/ ... ut-Klassen beschrieben) den Inhalt:

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);
      }

   }

}
Viele Grüße,
Christian

Benutzeravatar
Tom
Beiträge: 25
Registriert: 06.08.2009, 15:54:29
Kontaktdaten:

Re: Dynamische Ausgabe von Datenbank-Inhalten

Beitrag von Tom » 07.08.2009, 07:31:59

Hallo,

ich hoffe ich bin hier im FAQ Bereich nicht falsch mit Fragen, aber nach meinem Verständnis gehört doch die Datenbankabfrage in eine extra Schicht oder irre ich mich da?
Ich bin gerade dabei mich mit APF einzuarbeiten und bisher gefällt mir das Framework sehr gut (mit ein paar Startschwierigkeiten - da gibt es noch vieles was mir nicht ganz klar ist).
Gruß,
Tom

Benutzeravatar
dr.e.
Administrator
Beiträge: 4525
Registriert: 04.11.2007, 16:13:53

Re: Dynamische Ausgabe von Datenbank-Inhalten

Beitrag von dr.e. » 07.08.2009, 08:02:37

Hallo Tom,

Herzlich Willkommen im APF-Forum! :geek:

ich hoffe ich bin hier im FAQ Bereich nicht falsch mit Fragen, aber nach meinem Verständnis gehört doch die Datenbankabfrage in eine extra Schicht oder irre ich mich da?

In einem (sauberen) Softwaredesign sollte man jeder Komponente nur eine Aufgabe zuweisen. Dazu gehört, dass das Handling der Daten (sprich Laden + Speichern) in eine eigene Komponente ausgelagert werden sollte. Im Sinne des Schichten-Modells - oder genauer der 3-Schicht-Architektur - gehört dies in eine Datenschicht. Diese kannst du dann z.B. DataCarrier oder DataMapper nennen. Sofern du keine wirkliche Business-Logik hast - und das ist bei reinen Webseiten so - kannst du auch auf eine Business-Schicht verzichten und die Daten direkt in der Präsentationsschicht (sprich einem Document-Controller oder einer TagLib) vom DataMapper beziehen.

Im oben beschriebenen Beispiel geht es mir jedoch um das Handling von dynamischen Datenbank-Inhalten und die Interaktion mit der Datenbank selbst ist sehr übersichlich. Aus diesem Grund habe ich mich entschieden, die Inhalte direkt aus der Datenbank zu ziehen. Da du mit dem APF-Templates, -Taglibs und -Controllern ohnehin schon ohne es zu merken nach dem MVC-Pattern implementierst, ist es aber völlig legal die Logik der Applikation in den Controller zu schreiben. Es geht zwar auch noch "schöner" oder "ästhetischer" (wie im letzten Absatz beschrieben) aber das ist in vielen Fällen völlig ausreichend.

Ich bin gerade dabei mich mit APF einzuarbeiten und bisher gefällt mir das Framework sehr gut (mit ein paar Startschwierigkeiten - da gibt es noch vieles was mir nicht ganz klar ist).

Freut mich, dass dir das APF gefällt. Mit der Zeit wirst du sehen, dass die Konzepte, die anfänglich kompliziert erscheinen, für den täglichen Einsatz und vor allem für komplexe Applikationen sehr gut geeignet sind.

Wenn dir etwas unklar ist, dann stelle einfach alle deine Fragen. Vieles kannst du sicher aus der Doku und aus dem Forum entnehmen, aber manchmal ist direkt Fragen stellen einfacher. :) Ich verschiebe das dann einfach nach FAQ, dann wird die FAQ-Datenbank immer besser.

Viele Grüße,
Christian
Viele Grüße,
Christian

Benutzeravatar
Tom
Beiträge: 25
Registriert: 06.08.2009, 15:54:29
Kontaktdaten:

Re: Dynamische Ausgabe von Datenbank-Inhalten

Beitrag von Tom » 07.08.2009, 09:40:16

Wow, umfangreiche Antwort, Danke!

Ok, gut zu Wissen.

Irre ich mich oder muss NavigationModel nicht von coreObject erben?
Gruß,
Tom

Benutzeravatar
dr.e.
Administrator
Beiträge: 4525
Registriert: 04.11.2007, 16:13:53

Re: Dynamische Ausgabe von Datenbank-Inhalten

Beitrag von dr.e. » 07.08.2009, 12:09:17

Hallo Tom,

Wow, umfangreiche Antwort, Danke!
Ok, gut zu Wissen.

Gerne! :)

Irre ich mich oder muss NavigationModel nicht von coreObject erben?

Es muss nicht von coreObject erben, kann aber. Für dieses Thema gelten folgende Regeln:

  • Alle Document-Controller müssen von baseController (und damit implizit von coreObject) erben.
  • Alle Taglibs müssen von Document (und damit implizit von coreObject) erben.
  • Alle Klassen, die Konfigurationen oder Service-Objekte nutzen müssen von coreObject erben.

Alle anderen Klassen kannst du frei definieren und wenn du Funktionen aus der Klasse nutzen möchtest, davon ableiten.

Viele Grüße,
Christian
Viele Grüße,
Christian

Antworten

Wer ist online?

Mitglieder in diesem Forum: 0 Mitglieder und 1 Gast