Erstellen eines CMS mit ZendFramework – Teil 1
Vorüberlegungen und Strategien
1. Routing
Das A und O von Webseiten sind ihre URLs. ZF bietet weitreichende Möglichkeiten mit dynamischen Routing, URLs an die gewünschten Anforderungen anzupassen. Jedoch sind die Möglichkeiten für ein professionelles CMS begrenzt.
Beispiel:
In unserem CMS möchten wir 5 verschiedene Seiten erstellen, die alle bis auf ‘News’ zunächst mit einfachen Texten und Bildern gefüllt werden sollen. Die Seitentitel lauten:
+ Home
+ Unser Unternehmen (über uns)
+ Kontakt
+ Abteilungen
+ Impressum
+ News
Da wir von Anfang an eine mehrsprachige Seite aufbauen wollen, möchten wir jede Seite in jeder verfügbaren Sprache ansprechen können. Sollte eine Seite noch nicht übersetzt worden sein, soll sie in der Standardsprache (im CMS konfigurieren wir hierfür deutsch) angezeigt werden. Auf Grund der Mehrsprachlichkeit, möchten wir, dass die URIs im Gegensatz zu den Seitentiteln auf englisch sind:
- index
- about
- contact
- – departments
- imprint
- news
Wir möchten im CMS auch konfigurieren können, ob für die Standardsprache in den URLs auch der Ländercode angezeigt werden soll.
Wenn wir zunächst zwei Sprachen (deutsch und englisch) im CMS definieren, möchten wir folgende URLs erhalten:
Mit Ländercode in der URL für die Standardsprache:
- http://example.com (mit Redirect auf:)
- http://example.com/de
- http://example.com/de/about
- http://example.com/de/contact
- http://example.com/de/contact/departments
- http://example.com/de/imprint
- http://example.com/de/news
- http://example.com/en
- http://example.com/en/about
- http://example.com/en/contact
- http://example.com/en/contact/departments
- http://example.com/en/imprint
- http://example.com/en/news
Ohne Ländercode in der URL für die Standardsprache:
- http://example.com
- http://example.com/about
- http://example.com/contact
- http://example.com/contact/departments
- http://example.com/imprint
- http://example.com/news
- http://example.com/en
- http://example.com/en/about
- http://example.com/en/contact
- http://example.com/en/contact/departments
- http://example.com/en/imprint
- http://example.com/en/news
Da es sich bei allen Seiten, bis auf ‘News’, die einen eigenen Controller bekommen sollen, um einfache Seiten mit Bildern und Texten handelt, erstellen wir für die Funktionalität einen Controller im ZF, den wir z.B. Article nennen und dessen indexAction die entsprechenden Inhalte aus einer Persistenzschicht ausliest und über die View ausgibt.
Problem:
Auch wenn mit den Routern von ZF viel möglich ist, wären die URLs in ZF so nicht darstellbar. Standardmäßig hätten wir URLs etwa in der Form: http://example.com/article/index/page/about/language/en
Lösung für das CMS:
Wir erstellen einen URL-Mapper bzw. einen Module-Controller-Action-Mapper, der aus dem Request anhand der Daten aus der Persistenzschicht die entsprechenden Segmente auf die Klassen von ZF mappt. Der Mapper bietet einerseits die gewünschte Flexibilität URLs syntaktisch und semantisch den Wünschen entsprechend aufzubauen und andererseits ermöglicht er volle Kompatibilität zum ZF Routing.
Für ein CMS hat das weitere Vorteile: Wir können noch weitere Daten zu der jeweiligen Seite bzw. URL speichern (z.B. ob die Seite überhaupt online ist, Angaben zur Resource für ACL, Seitenname etc.). Vor allem aber erhalten wir so, schon eine Liste aus der sich direkt eine Navigation erzeugt lässt. Das Datenobjekt der Persistenzschicht, wir nennen sie mal Page, hat also etwa folgende Eigenschaften:
- id
- parent_id
- name
- req_modul
- req_controller
- req_action
- zf_modul
- zf_controller
- zf_action
Unsere Seiten würden dann wie folgt gespeichert:
============================================================================================================================ | id | parent_id | name | req_modul | req_controller | req_action | zf_modul | zf_controller | zf_action | ============================================================================================================================ | 1 | 0 | Home | default | index | index | default | article | index | ---------------------------------------------------------------------------------------------------------------------------- | 2 | 0 | Unser Unternehmen | default | about | index | default | article | index | ---------------------------------------------------------------------------------------------------------------------------- | 3 | 0 | Kontakt | default | contact | index | default | article | index | ---------------------------------------------------------------------------------------------------------------------------- | 5 | 3 | Abteilungen | default | contact | departments | default | article | index | ---------------------------------------------------------------------------------------------------------------------------- | 4 | 0 | Impressum | default | imprint | index | default | article | index | ---------------------------------------------------------------------------------------------------------------------------- | 6 | 0 | News | default | news | index | default | news | index | ============================================================================================================================
Der URL-Mapper ruft also bis auf News immer den ArticleController mit der indexAction auf. Im Mapper wird auch sichergestellt, dass die ermittelte id später in der indexAction verfügbar ist, damit diese die Inhalte, die zu der Seite gehören in der Persistenzschicht suchen und dann dem View übergeben kann.
Was ist mit der Mehrsprachlichkeit?
Die Länderkennungen sind weder Module, noch Controller, noch Actions. Vielmehr sind sie Parameter für den Controller. Deswegen entfernen wir Sie aus der URL und schreiben sie in die Registry bevor ZF den Routingprocess startet. Schnell anskizziert könnte eine Funktion, die wir in der Bootstrap vor dem Routing aufrufen etwa wie folgt aussehen:
public static function setLanguage()
{
$oLanguageConfig = Zend_Registry::get('config')->languages;
$sCurrentLang = $oLanguageConfig->default;
if ($oLanguageConfig->enable == 1) {
preg_match('@/[^/]*@', $_SERVER['REQUEST_URI'], $aUrlMatches);
if (!empty($aUrlMatches)) {
$sUrlMatch = substr($aUrlMatches[0], 1);
$aLangs = $oLanguageConfig->lang;
if (is_string($aLangs)) {
$aLangs = array($aLangs);
}
foreach($aLangs as $sLang) {
if ($sLang == $sUrlMatch) {
$sCurrentLang = $sLang;
$_SERVER['REQUEST_URI'] = substr($_SERVER['REQUEST_URI'], (strlen($sLang) + 1));
break;
}
}
}
}
Zend_Registry::set('currentLang', $sCurrentLang);
}
Was ist mit der Mehrsprachlichkeit in der Persistenzschicht? Wir realisieren diese mit einem “page_language_overlay” Datenobjekt. Es hat folgende Eingeschaften:
- fk_page_id
- language
- name
Bevor es weiter geht, ein kleiner Exkurs zum Aufbau von URLs:
Zunächst muss man sich vor Augen halten, dass man durch den URL-Mapper sehr weitgehende Freiheiten in Bezug auf den Aufbau von URLs gewonnen hat und kaum noch an das Korsett der ZF Logik gebunden ist, dass man damit aber auch in der Verantwortung steht, den logischen Aufbau selbst zu vollziehen. Warum sollte man sich das überhaupt aufbürden? Ganz einfach:
Programmierer (und alle Technokraten dieser Welt), hätten sicher kein Problem damit, wenn URLs immer nach Schema ZF aufgebaut wären. Auftraggeber, für die letztlich das CMS gemacht wird und die unseren Lebensunterhalt finanzieren, akzeptieren aber kein:
http://example.com/article/index/page/about
sie bestehen (teilweise zu recht) auf:
http://example.com/about
Warum auf solchen URLs bestehen? Ganz einfach: Man kann sie sich besser merken (Es ist erstaunlich wie wenige Auftraggeber Bookmarks nutzen). Ach ja, auch für Suchmaschinen sind Angaben wie ‘article/index/page’ ziemlicher Blödsinn.
Schauen wir uns das Problem genauer an (unabhängig von einer möglichen Abneigung gegen Technokraten – Jenseits von Gut und Böse sozusagen) und stellen uns als Resource das klassische Offlinemedium vor: ein Buch. Schlagen wir es auf und schauen ins Inhaltsverzeichnis. Jetzt überlegen wir, wie die einzelnen Punkte im Inhaltsverzeichnis als URL mit ZF standardmäßig dargestellt werden würden. Anstatt eines Inhaltsverzeichnisses wie wir es alle kennen, würde dort etwas stehen wie:
Buch / Inhalt / zeige / Kapitel / “Götzen-Dämmerung” / Seite / 279
(module / controller / action / key / value / key / value)
Tatsächlich findet sich im Buch aber nur Folgendes im Inhaltsverzeichnis:
Götzen-Dämmerung 279
Zugegeben, weiß man ja im Internet noch nicht, dass man ein Buch aufgeschlagen hat und man weiß auch noch nicht unbedingt, dass es sich um das Buch “Jenseits von Gut und Böse” handelt – schließlich gibt es vielleicht auch andere Bücher mit gleichnamigem Kapitel und vielleicht gibt es auch Räucherkerzen die Götzen-Dämmerung heißen. Was dagegen im Ggs. zum Druckerzeugnis im Internet nicht interessiert, ist die Seite auf der das Kapitel anfängt. Nach der ZF Logik würde das zu einer Syntax etwa wie folgt führen:
http://example.com/buch/inhalt/zeige/titel/Jenseits-von-Gut-und-Boese/kapitel/Goetzen-Daemmerung
Die kürzere Syntax – die dem Auftraggeber gefällt – hingegen ist aber:
http://example.com/buch/Jenseits-von-Gut-und-Boese/Goetzen-Daemmerung
Im Grunde haben wir bei der kurzen URL aber nicht viel mehr getan, als das Verb (die Action) und die Kennzeichnung der Adjektive (oder anders gesagt, die Typen der Attribute des Buches, die keys ‘titel’ und ‘kapitel’) wegzulassen. Der Sachverhalt bezüglich der Adjektive erscheint recht klar, schließlich sagen wir ja auch nicht: “Dieses Buch hat die Farbe blau.” Vielmehr würden wir sagen “Dieses Buch ist blau.” Darüber hinaus ist in der natürlichen Sprache das Verb “sein” (in manchen Sprachen leider auch das Verb “haben”) ebenso selbstverständlich, wie im Internet die Action “zeige” oder “liste” oder “gib aus” oder “stell dar”. Deswegen können wir ja auch einfach sagen: “Dieses blaue Buch” (Nach dem Motto: “Dieser Satz kein Verb”, aber mit einem hic-deiktischem Pronomen). Erst wenn wir mit dem Buch etwas anstellen wollen, benötigen wir ein Verb: “Ich kaufe dieses Buch von Nietzsche”. Das Buch wird dabei zum Objekt des Satzes. Das bedeutet, dass eine Information aus Subjekt + eindeutiges Attribut immer dann ausreichend ist, wenn wir das Subjekt lediglich darstellen wollen und wenn wir immer mit den gleichen Typen von Attributen unsere Auswahl treffen. Wenn wir im Internet ein Buch kaufen möchten, ist das Subjekt nicht mehr das Buch, sondern der Shop der Website, wir erhalten dann eine URL wie:
http://example.com/shop/kaufe/ISBN/3-89508-038-1
oder, falls wir annehmen, dass der Buchtitel immer eindeutig ist:
http://example.com/shop/kaufe/buch/Jenseits-von-Gut-und-Boese