Skip to content

Naives MVC

Seit 2014 setze ich sowohl bei neuen Websites als auch bei der “Renovierung” alter auf static site generators, also Generatoren für statische Webseiten, die aus Templates und Inhalten in verschiedenen Markups, aus Helper-Klassen und ggf. auch Datenbankinhalten HTML-Seiten generieren, die dann auf den Server hochgeladen werden. Das hat verschiedene Vorteile ggü. CMS oder selbstgeschriebenem Code: weil nur statisches HTML ausgeliefert wird, muss auf dem Server keine Scriptsprache wie PHP laufen, was die Last vermindert und vor allem auch aus Sicherheitsgründen angenehm ist, denn wo kein Code läuft, gibt es auch keine Sicherheitslücken. Außerdem bringen Generatoren wie Nanoc, mein bevorzugtes Werkzeug, ein ganzes Toolset wie Helper-Klassen für häufige Aufgaben und Filter für die verschiedensten Markup-Formate mit. So habe ich mich daran gewöhnt, dass ich das Grundgerüst einer Website in Form von Templates, meistens in HAML, erstelle, für die optische Gestaltung CSS-Frameworks wie Bootstrap nutzen kann, wobei ein LESS-Compiler automatisch mitkommt, und dass ich die eigentlichen Inhalte in Markdown (oder in einer Kombination aus HAML und Markdown) erstellen kann. Auf diese Weise habe ich mir einen gut funktionierenden Workflow geschaffen.

Das alles erfüllt seinen Zweck im Grundsatz sehr gut, auch für in begrenztem Umfang dynamische Elemtente: es genügt ja, die Webseitengenerierung in regelmäßigen Abständen neu anzustoßen, bspw. nachts, oder wenn man will auch stündlich. Nanoc kann man zudem aus Datenbanken speisen, und auch beim bloßen Umgang mit Textdateien lassen sich deren Metadaten auswerten, um bspw. die letzten Änderungen zu verlinken, Termine anzukündigen (aber nur, solange sie noch bevorstehen), Zeitangaben automatisch zu ersetzen (“Diese Seiten stehen schon seit x Jahren online”) und anderes mehr. Man kann auch mal ein PHP-Script dazwischenmixen, beispielsweise für ein Kontaktformular. Ihre Grenzen erreicht die Technik erst, wenn es um tatsächlich dynamische Seiten geht, also bspw. die Suche in Datenbanken (bei der man die Einträge nicht schlicht insgesamt vorab als statische Seiten erzeugen kann) - oder wenn ein Passwortschutz (jenseits von Basic Auth) oder ein Session-Management implementiert werden soll. Dann braucht es etwas anderes. Nur was?

Bestimmt fünf Jahre lang habe ich die Überarbeitung verschiedener Webseiten vor mir hergeschoben, deren Anforderungen über die Möglichkeit von static site generators hinausgehen. Der ursprüngliche Ansatz aus meinen ersten (und zweiten) Schritten mit PHP - jede Seite ein Mix von HTML und PHP, letzteres vor allem Funktionen zum Generieren der Seitenstruktur (Header, Footer) und Navigation als “Templating für Arme” - kam natürlich nicht mehr in Betracht; natürlich machen allein Templates in diesem Zusammenhang Sinn. Aber ich wollte die Inhalte auch nicht mehr in HTML verfassen, sondern zumindest in Markdown; und ein CSS-Baukasten wie Bootstrap sollte sich auch einbinden lassen, was einen Preprocessor wie LESS oder SASS zumindest wünschenswert macht. Über die Jahre habe ich mit vielen Gedanken gespielt, bspw. an Frameworks wie Laravel oder Symfony; früher hatte ich über viele Jahre die CakePHP-Mailingliste abonniert. Das erschien mir dann aber doch vergleichsweise aufwendig, um letztlich einen Passwortschutz und ein paar Formulare bzw. Datenbankausgaben zu implementieren.

Nach langer Pause und Stagnation wollte ich das Thema dieses Jahr über die Osterzeit aber noch einmal angehen. Dazu gehörten mehrere Schritte: Zunächst waren einige eng verbundene Webseiten zu entkoppeln, die gemeinsame Ressourcen nutzten, also bspw. alle dieselbe CSS-Datei eingebunden hatten. Danach brauchte es ein Design, das optisch den bisherigen Webseiten ähnlich sein sollte, nur schicker und möglichst unter Verwendung von Bootstrap - und dafür wiederum musste ich mich mit Bootstrap 5 auseinandersetzen (meine bisherigen Webseiten setzen alle noch auf die Version 3 auf), und dafür wiederum von LESS auf SASS wechseln. Der wiederum nächste Schritt wäre dann ein PHP-basiertes Templating-System, und dann musste all das zusammengesetzt und um die notwendige Funktionalität erweitert werden.

Bootstrap 5 und SASS

Bootstrap 5 erwies sich als gar nicht so anders; klar, das Grid ist neu und setzt auf Flexbox, es gibt einen Haufen Utility-Klassen für Abstände und bei den Komponenten hat das neue “Card”-Element einige bisherige Elemente ersetzt, aber die einzige Neuerung mit Umstellungs- und Änderungsbedarf war der Wechsel von LESS als CSS-Preporzessor zu SASS.

Ich habe also zunächst die Quell-Dateien heruntergeladen und mich damit vertraut gemacht und mir dann eines der Beispiele ausgesucht, um davon ausgehend einen groben Design-Entwurf zu puzzeln. Das war dann auch der Zeitpunkt, wo ich einen SASS-Compiler brauchte. Das erwies sich als überraschend einfach: Dart Sass bietet Pakete für Windows an. Herunterladen, dart.exe und sass.snapshot in ein bin/-Verzeichnis packen, eine Batch-Datei mit bin/dart.exe bin/sass.snapshot mein.scss mein.css ins Hauptverzeichnis des Projekts, und schon kann ich aus mein.scss eine CSS-Datei zaubern. In der .scss-Datei importiere ich dann die notwendigen Bootstrap-Komponenten (oder auch einfach alles), ggf. weitere Elemente (wie Font Awesome), passe Variablen an (insbesondere das Farbschema), ergänze eigenes CSS - et voilá. Und all das kann ich dann direkt lokal auf meinem unter Windows laufenden Entwicklungsrechner testen: Änderungen vornehmen, Build-Script starten, HTML-Datei im Browser neu laden - und ggf. von vorne.

Eine solche .scss-Datei kann dann beispielsweise so anfangen:

/* --------------------------------------------------------
 *  SCSS styles and Bootstrap import
 */

// custom variables
$primary:   #314e8c;
$secondary: #bad1e1;
$info:      #ced4da; /* gray-400 */
$body-bg:   $secondary;

// import from Bootstrap
@import "bootstrap/scss/bootstrap.scss";

// import from FontAwesome (Solid)
@import "fontawesome/scss/fontawesome.scss";
@import "fontawesome/scss/solid.scss";

// custom SCSS
/* --------------------------------------------------------
 * Text elements
 */

h1, h2, h3, h4, h5, h6 {
  color: $primary;
}

Damit werden die gewünschten Primär- und Sekundärfarben gesetzt, die ich aus dem bisherigen Design übernommen habe, die Farbe für “Info”-Boxen auf einen Grauton umgestellt und der Hintergrund in der Sekundärfarbe eingefärbt. Dann ziehe ich das komplette Bootstrap-Framework und Font Awesome als Webfont ein (hier kann man sich natürlich auf die Elemente beschränken, die man nutzt; das macht alles schlanker), und danach folgen die Anpassungen und eigenen Elemente. So färbe ich bspw. die Überschriften gerne in der Primärfarbe der jeweiligen Website ein.

Das war, ehrlich gesagt, im Ergebnis dann sehr viel einfacher als ich befürchtet habe, und es gelang mir überraschend schnell, ein brauchbares Mockup zu erstellen. Feinheiten folgen dann, wie immer, sobald sie für bestimmte Elemente oder Situationen benötigt werden.

Den ganzen Kram - Bootstrap-Quellcode, Font-Awesome-Quellcode, die ausführbaren Dateien von Dart Sass und meine .scss-Datei - habe ich natürlich in ein Git-Repository gepackt. Dabei haben sowohl Bootstrap als auch Font Awesome als auch Dart Sass jeweils nach dem initialen Einkopieren der Dateien einen eigenen Branch erhalten. Wenn ich nun bspw. eine neue Bootstrap-Version einspielen will, genügt es, den Bootstrap-Branch auszuchecken, die neuen Dateien einzukopieren und ihn in den Master-Branch zu mergen. So sehe ich sofort alle Änderungen und kann ggf. Anpassungen vornehmen.

Die am Ende dieser Operationen erzeugte mein.css-Datei kann ich dann jeweils in das Repository (oder die Repositories) für den Quellcode der einzelnen Webseiten einkopieren.

Templates

So weit, so gut. Im nächsten Schritt sollte ich dann aus dem Mockup ein Template machen, in das sich Inhalte einbinden lassen. Davor bedarf es aber eines Template-Systems, das diesen Schritt (Template laden, mit Inhalten füllen) technisch umsetzt.

Für eine simple interne Seite - Erzeugen von Mailboxen und Mailadressen über ein Webinterface - hatte ich 2017 bei der Renovierung des ursprünglich aus dem Jahr 2009 stammenden Codes auf Plates, ein einfaches PHP-Templating-System, gesetzt. Zusammen mit Bootstrap sah das hübsch aus und war funktional, wenn auch vielleicht von der Funktionalität her eher überschaubar. So richtig wollte das aber nichts werden; schon das Einbinden machte mir Schwierigkeiten, und mit dem Templating-System wurde ich ebenfalls nicht warm.

Also dachte ich mir, warum nicht etwas verwenden, von dem ich weiß, dass es sich anderswo bereits bewährt hat? So habe ich mich dann für Smarty entschieden, denn was für Serendipity gut ist, kann für ein simples Projekt nicht schlecht sein. Außerdem kann ich dann das, was ich lerne, direkt noch anderswo zum Einsatz bringen. Das funktionierte dann auch direkt sehr schön, zumal Smarty das Einbinden von Teil-Templates (Partials) in Templates unterstützt; man kann sich also das endgültige Template aus wiederverwertbaren Bausteinen zusammenstückeln. Einen Wermutstropfen gibt es freilich: die Templates sind kein HAML, sondern HTML und Smarty, aber das passt - so viele Templates braucht man ja in der Regel nicht.

Wichtiger war mir, die Inhalte nicht komplett als HTML verfassen zu müssen; Markdown ist da meine Standardlösung. Und wenn ich mich schon an Serendipity bediene, warum dann nicht auch hier? Das Markdown-Plugin dort nutzt PHP Markdown, und das lässt sich natürlich auch sehr schön in meinem Projekt verwenden.

Model-View-Controller

Jetzt mussten alle diese Einzelteile “nur noch” sinnvoll zusammengeführt werden. In meinem schon angesprochenen Plates-Projekt hatte ich für jede Webseite weiterhin jeweils eine gesonderte PHP-Datei, die im Prinzip aber nichts anderes machte, als das Template aufzurufen und mit Inhalten zu füllen. Außerdem gab es für jede Seite dann ein gesondertes Template, dass auf ein Standard-Template aufsetzte. Das kann aber natürlich nicht die Lösung sein. Nicht nur Serendipity macht das anders: im Prinzip gibt es primär eine PHP-Datei, nämlich eine index.php im Webroot, die als Parameter die jeweils anzuzeigende Webseite übergeben bekommt und sich dann um den Rest kümmert.

Im Grundsatz ist das ein Model-View-Controller-Pattern, kurz MVC. Das Model liefert die Daten, sorgt also in einer Applikation üblicherweise für die Abbildung in einer Datenbank. Views sind die Präsentation, die Templates, die mit Inhalten gefüllt werden. Und der Controller führt beides zusammen. Natürlich kann man das mehr oder weniger ausgefeilt betreiben; mir genügte eine weniger ausgefeilte Lösung, die bspw. einzelne Aktionen im Controller nicht in Funktionen auslagert, sondern im Kern auf ein langes switch-Statement setzt. Immerhin enthält der Großteil der Webseiten primär einmal Text, der angezeigt werden muss; ich will ja keine Webapp schreiben.

Am Anfang steht die Frage, wie man die anzuzeigende Webseite als Parameter übergeben möchte. Eine URL wie https://meine-website.example/index.php?page=about/contact sieht ja wirklich hässlich aus und ist auch unter SEO-Gesichtspunkten keine gute Idee. Stattdessen möchte ich gerne URLS wie https://meine-website.example/about/contact/ haben. Hier kommt mod_rewrite zu Hilfe:

RewriteEngine On
# rewrite URLs to controller
RewriteCond %{REQUEST_URI} !^/.well-known
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /index.php?page=$1 [QSA,L]

Die erste der beiden Bedingungen sorgt dafür, dass alle Aufrufe in das Verzeichnis /.well-known/ nicht umgesetzt werden; das ist bspw. erforderlich, damit certbot von Let’s encrypt seine Aufgaben erfüllen kann. Die zweite sorgt dafür, dass tatsächlich physisch vorhandene Dateien angezeigt werden. So lassen sich bspw. Textdateien oder Downloads unterbringen, die “as is” ausgeliefert werden sollen. Die darauffolgende Regel schnappt sich den Pfad - im Beispiel also about/contact/ - und ruft die index.php mit diesem Pfad als Parameter auf. Damit lässt sich arbeiten.

Im zentralen PHP-Script wird dieser Pfad dann zunächst normalisiert (damit er mit oder ohne abschließenden Slash “/” zur selben Seite führt) und auf unerwünschte Zeichen geprüft, damit nicht böse Menschen versuchen, unerwünschte Pfade (../../../../etc/passwd) zu übergeben. Der Rest des Scripts arbeitet dann mit diesem Pfad weiter. Dazu wird eine weitgehend aus Smarty abgeleitete Controller-Klasse benutzt, die ein Template-Objekt erzeugt und mit Variablen befüllt; wo erforderlich, steht eine spezialisierte Datenbank-Klasse bereit, die spezifische Funktionen für alle erforderlichen Lese- und Schreib-Operationen bereithält. Für das Session-Management konnte ich auf eine bestehende PHP-Klasse aus dem Jahre 2011 zurückgreifen, die ich damals für ein anderes Projekt geschrieben und bewusst modular aufgebaut hatte; tatsächlich war sie 10 Jahre später mit kleinen Anpassungen - vor allem in bisher nicht oder kaum genutzten Codepfaden - sofort verwendbar. Auth- und Controller-Klasse lassen sich modular wiederverwenden, die Datenbank-Klasse ist weitgehend projektspezifisch.

Den Hauptteil der index.php bildet dann tatsächlich ein langes switch-Statement, das für bestimmte Seiten spezifischen (Datenbank-)Code enthält, in den meisten Fällen aber auf default zurückfallen und schlicht eine Markdown-Datei in HTML konvertieren, in ein Template einsetzen und dann anzeigen kann.

Verzeichnisstruktur

Die Verzeichnisstruktur des Projekts sieht im Grundsatz so aus:

\___ source/
 \
  \__ data/
   \
    \__ web/

Das web-Verzeichnis ist das Webroot; dort finden sich die index.php, die Unterverzeichniss mit CSS, Javascript, Webfonts, Bildern und Icons und die Downloadverzeichnisse, also alles, was “as is” ausgeliefert werden soll.

In source liegen die Module und externen Elemente: Smarty, PHP Markdown Extra, meine eigenen Module und eine Konfigurationsdatei, die etliche Variablen setzt (bspw. für die Datenbankanbindung) und zugleich alle notwendigen Elemente einbindet.

data schließlich enthält zum einen die Templates (also die typischen Smarty-Verzeichnisse wie templates, templates_c, cache und Co.) und zum anderen im Verzeichnis content die eigentlichen Inhalte in Form von Markdown-Dateien in einer Verzeichnisstruktur, die der der Webseite entspricht. Der Inhalt, der nach Aufruf von https://meine-website.example/about/contact/ angezeigt werden soll, liegt also in data/content/about/contact.md.

Umsetzungsbeispiele

$controller = new Controller($config);
$controller->assign('page',$page);

Auf diese Weise wird ein neues Controller-Element erzeugt, dass den Inhalt des Arrays $config (aus der zentralen Konfigurationsdatei) und die anzuzeigende Seite ($page) in Smarty verfügbar macht. Die assign-Methode akzeptiert entweder ein Array oder - wie im Beispiel - ein Tupel aus Schlüssel und Wert.

# create pages from Markdown/YAML
$yaml = $controller->create_from_md($page);

Diese Funktion liest eine Datei ein, konvertiert das enthaltene Markdown in HTML und kann YAML front matter verarbeiten, also YAML, das als Vorspann (eingefasst in ---) in einer anderen Datei enthalten ist. Diese Inhalt werden in Smarty als Variablen bereitgestellt.

Die technische Umsetzung der Funktion create_from_md() ist im Kern wirklich einfach:

# parse YAML and copy to Smarty
$yaml = yaml_parse_file($content_file);
$this->template->assign($yaml);

# parse Markdown, ignoring YAML front matter, and assign to content
$this->template->assign('content',Michelf\MarkdownExtra::defaultTransform($this->read_ignore_frontmatter($content_file)));

# return YAML metadate
return $yaml;

Die PHP-Funktion yaml_parse_file() kann YAML front matter verarbeiten und ignoriert den Rest. Die selbstgeschriebene Funktion read_ignore_frontmatter() ist das Gegenstück dazu; sie sorgt dafür, dass beim erneuten Einlesen der Datei diesmal das YAML front matter übersprungen und nur der Markdown-Teil verarbeitet wird. Danach werden die eingelesenen YAML-Daten zurückgegeben und stehen daher nicht nur im Smarty-Template, sondern auch für den PHP-Code zur Verfügung.

Auf diese Weise können beispielsweise Titel und Beschreibung der Seite, der Autor usw. übergeben werden. Für die (wenigen) Seiten, die nicht nur aus Markdown bestehen, sondern weitergehende Funktionalität enthalten, werden diese Daten als Array in der index.php definiert. Jede Webseite besteht also entweder aus einem speziellen Smarty-Template mit zusätzlichem Code in der index.php oder aus dem Inhalt einer Markdown-Datei, die in das Default-Template eingesetzt wird.

# check for protected pages
if(isset($yaml['login'])) $auth->check_session();

Wenn im YAML-Vorspann login gesetzt ist, wird geprüft, ob ein berechtigter Benutzer eingeloggt ist; ansonsten erfolgt die Umleitung zur Anmeldeseite.

# display template
$controller->display('index.tpl');

Am Schluss wird das mit Inhalt gefüllte Template angezeigt.

Ein paar Worte zum Schluss

Das Konzept stand natürlich nicht von Anfang an in der hier präsentierten Form, sondern ist dynamisch gewachsen - so gab es zunächst nur Smarty-Templates, dann kam die Möglichkeit von Markdown-Dateien hinzu, und noch später das Parsing von YAML front matter. Und natürlich waren viele Tests und etliche Iterationen über einige Abende und ein langes Wochenende notwendig - aber am Ende war ich überrascht, erfreut und beeindruckt, mit wie vergleichsweise geringem Aufwand sich ein solches doch (für meine Verhältnisse) einigermaßen komplexes Projekt umsetzen lässt. Frei verfügbare externe Projekte und Frameworks - Smarty, PHP Markdown, Bootstrap, SASS und natürlich am Ende auch PHP selbst -, eigene kleine Module und ein bißchen Klebstoff, um das alles miteinander zu verbinden, dazu ein altes Design zum Aufhübschen und die bestehenden Inhalte, die ergänzt und übernommen werden, sind eine ebenso mächtige wie vielseitige Kombination.

[Nachträglich veröffentlicht im August 2021.]

Titelbild © pripir - stock.adobe.com

Trackbacks

Netz - Rettung - Recht am : Relaunch der Webseiten von news.szaf.org

Vorschau anzeigen
Seit 2005 betreibe ich einen “richtigen” Newsserver, also einen solchen, der die großen (und ein paar kleine) Hierarchien mit dem Rest der Welt peered und auch für andere Nutzer aus meinem Umkreis zur Verfügung steht: news.szaf.org. Die zugeh

Netz - Rettung - Recht am : Relaunch der Webseiten von szaf.org

Vorschau anzeigen
Wie ich bereits Ende März in meinem Beitrag über die Vorbereitungen zum Relaunch der Webseiten von news.szaf.org angedeutet hatte, ging es dabei nicht nur um eine Präsenz, sondern mehrere miteinander zusammenhängende Angebote. Und so habe ich die letzten

Kommentare

Ansicht der Kommentare: Linear | Verschachtelt

Noch keine Kommentare

Kommentar schreiben

HTML-Tags werden in ihre Entities umgewandelt.
Markdown-Formatierung erlaubt
Standard-Text Smilies wie :-) und ;-) werden zu Bildern konvertiert.
BBCode-Formatierung erlaubt
Gravatar, Identicon/Ycon Autoren-Bilder werden unterstützt.
Formular-Optionen