Abschnitt 16 · Lektion 3
Ein Front Controller
In Lektion 10.5 hast du mehrere Seiten gebaut, und jede war eine eigene Datei: index.php,
impressum.php, kontakt.php. Der Server hat entschieden, welche läuft, und zwar am Dateinamen. Das
funktioniert, und für eine Handvoll Seiten ist es genau richtig.
Wo diese Bauweise an ihre Grenze kommt
<!doctype html>
<html lang="de">
<head><meta charset="utf-8"><title>Impressum</title>
<style>body{font-family:system-ui,sans-serif;max-width:34rem;margin:2rem auto}</style></head>
<body>
<h1>Impressum</h1>
<p>Angaben nach Paragraf 5 DDG.</p>
<p><a href="/">Zurueck zur Startseite</a></p>
</body>
</html> <!doctype html>
<html lang="de">
<head><meta charset="utf-8"><title>Startseite</title>
<style>body{font-family:system-ui,sans-serif;max-width:34rem;margin:2rem auto}</style></head>
<body>
<h1>Startseite</h1>
<p>Zwei Dateien, zwei Adressen. Klick auf das
<a href="/impressum.php">Impressum</a> und sieh dir die Adresszeile an:
Dort steht der Dateiname, und er steht in jedem Link.</p>
<p>Ruf danach <code>/gibtesnicht.php</code> auf. Du bekommst diese
Startseite zurueck, mit Status 200, obwohl es die Seite nicht gibt.
Dein Code merkt davon nichts.</p>
</body>
</html> Sieh dir das erste Beispiel an und klick auf den Link. In der Adresszeile steht /impressum.php, und
derselbe Dateiname steht in jedem Link, der dorthin führt. Willst du die Seite umbenennen, änderst du
die Datei und alle Links.
Der zweite Punkt ist unsichtbarer und wichtiger. Angenommen, jede Seite soll die Sitzung starten, Fehler abfangen und prüfen, ob jemand angemeldet ist. Dann steht dieser Vorspann in jeder einzelnen Datei, und beim Anlegen der nächsten vergisst man ihn. Genau so entstehen Seiten, die man ohne Anmeldung aufrufen kann, obwohl alle anderen geschützt sind.
Und drittens die falschen Adressen. Ruf im ersten Beispiel /gibtesnicht.php auf. Du bekommst die
Startseite, und im Reiter Netzwerk steht Status 200, obwohl es diese Seite nicht gibt. Im
Terminal sagt es curl -si localhost:3000/gibtesnicht.php | head -1 in einer Zeile: 200 OK. Der eingebaute Server nimmt bei einem
unbekannten Pfad einfach die Indexdatei, und index.php merkt davon nichts: Sie weiß nicht, dass sie
gerade als Vertretung läuft. Auf einem Apache ohne weitere Einstellung bekämst du stattdessen dessen
eigene Fehlerseite, die du nicht gestalten und nicht protokollieren kannst, weil dein Code gar nicht
erst startet. Zwei verschiedene Antworten auf dieselbe Frage, und keine davon ist deine.
Alles durch eine Tür
<?php
// Links der Pfad, rechts der Name der Funktion, die ihn beantwortet.
// Wer eine Seite umbenennen will, aendert diese Zeile und sonst
// nichts.
return [
"/" => "seiteStart",
"/impressum" => "seiteImpressum",
"/datenschutz" => "seiteDatenschutz",
]; <?php
// Ein Front Controller in seiner kleinsten Form. Alle drei Adressen
// landen in dieser Datei, und erst der Code hier entscheidet, welche
// Funktion antwortet.
$routen = require __DIR__ . "/routen.php";
function seiteStart(): string
{
return "<h1>Startseite</h1>
<p>Drei Adressen, eine Datei. Klick auf das
<a href=\"/impressum\">Impressum</a> und auf den
<a href=\"/datenschutz\">Datenschutz</a>: In der Adresszeile steht
kein .php mehr.</p>
<p>Ruf danach <code>/gibtesnicht</code> auf. Diesmal antwortet nicht
der Server, sondern dein Code.</p>";
}
function seiteImpressum(): string
{
return "<h1>Impressum</h1><p>Angaben nach Paragraf 5 DDG.</p>
<p><a href=\"/\">Zurueck</a></p>";
}
function seiteDatenschutz(): string
{
return "<h1>Datenschutz</h1><p>Diese Seite setzt keine Cookies.</p>
<p><a href=\"/\">Zurueck</a></p>";
}
$pfad = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
if (isset($routen[$pfad])) {
$inhalt = $routen[$pfad]();
} else {
http_response_code(404);
$inhalt = "<h1>Nicht gefunden</h1><p>Die Adresse " . htmlspecialchars($pfad)
. " kennt diese Anwendung nicht.</p><p><a href=\"/\">Zurueck</a></p>";
}
?>
<!doctype html>
<html lang="de">
<head><meta charset="utf-8"><title>Front Controller</title>
<style>body{font-family:system-ui,sans-serif;max-width:34rem;margin:2rem auto}</style></head>
<body>
<?= $inhalt ?>
</body>
</html> Ein Front Controller dreht das um: Jede Anfrage landet in derselben Datei, und dort entscheidet
dein Code, was passiert. Das zweite Beispiel zeigt die kleinste Form davon. Drei Adressen, eine
Datei, und in der Adresszeile steht kein .php mehr. Ruf auch hier /gibtesnicht auf: Diesmal
antwortet dein Code, mit deiner Seite und einem 404, das du selbst gesetzt hast; der Reiter
Netzwerk zeigt es, und curl -si localhost:3000/gibtesnicht | head -1 im Terminal auch.
Zwei Teile stecken darin. Die Routentabelle ist ein gewöhnliches Array: links der Pfad, rechts
der Name der Funktion, die ihn beantwortet. Und der Verteiler ist ein if mit else: nachsehen,
ob es einen Eintrag gibt, die Funktion aufrufen, sonst 404.
Was du dadurch gewinnst, ist eine Stelle, an der alles vorbeikommt. Ein session_start() steht dort
einmal statt zwanzigmal. Eine Anmeldeprüfung ebenso. Und wenn eine Seite umziehen soll, ist das eine
Zeile in der Tabelle.
Wie das auf einem echten Server funktioniert
Damit wirklich jede Adresse in index.php landet, muss der Webserver mitspielen. Bei Apache ist das
eine Regel in der .htaccess („wenn es zu diesem Pfad keine Datei gibt, nimm index.php”), bei
nginx eine try_files-Zeile. Beides steht in der Anleitung jedes Frameworks, und beides sagt
dasselbe.
In diesem Kurs brauchst du dafür nichts zu tun, und der Grund ist dieselbe Eigenheit, die eben noch
gestört hat: php -S liefert eine Datei aus, wenn es sie gibt, und schickt alles andere an die
Indexdatei. Für /impressum gibt es keine Datei, also landet der Aufruf in index.php, mit dem
echten Pfad in $_SERVER["REQUEST_URI"]. Was im ersten Beispiel eine stille Falschantwort war, ist
hier genau das gewünschte Verhalten, denn jetzt sieht dein Code nach, ob es die Adresse gibt. Lektion
12.5 und 15.3 haben denselben Rückfall schon benutzt, ohne ihn zu benennen.
Und warum nur ein Ordner öffentlich ist
; Die Konfiguration dieser Anwendung. Sie liegt im selben Ordner wie
; index.php, also im oeffentlichen Verzeichnis. Genau das ist der
; Fehler, den dieses Beispiel vorfuehrt.
datenbank = privat/daten.db
db_passwort = geheim123
modus = betrieb <?php
// Diese Anwendung hat keinen eigenen oeffentlichen Ordner: Der Server
// liefert alles aus, was neben index.php liegt. Was das heisst, siehst
// du mit einem einzigen Aufruf.
$konfig = parse_ini_file(__DIR__ . "/konfiguration.ini");
?>
<!doctype html>
<html lang="de">
<head><meta charset="utf-8"><title>Alles im selben Ordner</title>
<style>body{font-family:system-ui,sans-serif;max-width:34rem;margin:2rem auto}</style></head>
<body>
<h1>Alles im selben Ordner</h1>
<p>Diese Seite laeuft im Modus <strong><?= $konfig["modus"] ?></strong>.
Woher sie das weiss, steht in <code>konfiguration.ini</code>, und die
Datei liegt gleich nebenan.</p>
<p>Ruf jetzt <a href="/konfiguration.ini">/konfiguration.ini</a> auf.
Der Server liefert sie aus, im Klartext, samt Passwort.</p>
</body>
</html> Wenn ohnehin alles durch eine Datei geht, braucht der Browser auch nur noch diese eine zu erreichen.
Daraus folgt die Verzeichnisstruktur, die du in jedem PHP-Projekt sehen wirst: ein öffentlicher
Ordner mit index.php und den Stylesheets, und daneben, außerhalb, alles andere.
Warum das kein Ordnungsfimmel ist, zeigt das dritte Beispiel. Dort liegt die Konfiguration neben
index.php, und der Server liefert sie auf Anfrage aus, im Klartext, mit dem Passwort darin: Tipp
/konfiguration.ini in die Adresszeile, oder curl -s localhost:3000/konfiguration.ini im
Terminal. Er tut
nichts Falsches: Es ist eine Datei im öffentlichen Verzeichnis, also gibt er sie heraus. Dieselbe
Sache wie die Datenbankdatei aus Lektion 15.6, nur mit einer anderen Endung.
Liegt die Datei dagegen außerhalb der Dokumentwurzel, gibt es sie für den Server schlicht nicht. Der Aufruf landet dann auf dem Front Controller, und der antwortet mit 404. Das ist der Unterschied zwischen „ich habe es hoffentlich niemandem gesagt” und „es ist nicht erreichbar”, und nur der zweite Satz ist eine Sicherheitsmaßnahme.
Zum Mitnehmen
Bis jetzt hat der Dateiname entschieden, welcher Code läuft: /impressum.php führte zu impressum.php. Ab hier entscheidet dein Code. Das klingt nach einem Umweg und ist der Grund, warum jede größere Anwendung genau eine Eingangstür hat.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.