mitmario.dev

Ein Front Controller

PHP Sandbox 4 Min Lesezeit 3 BeispieleLektion 3 von 8

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

Zwei Dateien, zwei Adressen
<!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

Eine Datei, drei Adressen
<?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

Wenn alles im selben Ordner liegt
<?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, kostenlos

Zu 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.