mitmario.dev

Die Anwendung zusammensetzen

PHP Sandbox 4 Min Lesezeit 3 BeispieleLektion 7 von 8

Am Ende dieses Abschnitts liegt dieselbe Anwendung da, mit der er angefangen hat. Sie kann nichts, was sie vorher nicht konnte. Sie sieht genauso aus. Nur besteht sie jetzt aus sieben Dateien statt einer, und diese Lektion sagt, warum das ein Fortschritt ist und ab wann.

Ein Ort für jede Sorte Datei

Wohin welche Datei gehört
<?php

// Die Struktur, die am Ende dieses Abschnitts steht, mit einer
// Begruendung je Ordner. Sie ist keine Vorschrift: Fast jedes
// PHP-Projekt, das du aufmachst, sieht so oder sehr aehnlich aus.

$ordner = [
    ["oeffentlich/", "index.php, stil.css, Bilder", "Das Einzige, was der Browser erreichen darf. Genau eine PHP-Datei."],
    ["src/", "Klassen, Seitenfunktionen, Routen", "Dein Code. Von aussen nicht erreichbar."],
    ["ansichten/", "Vorlagen, also Markup mit Luecken", "Ausgabe, sonst nichts. Keine Abfrage, keine Rechnung."],
    ["privat/", "die Datenbankdatei, Uploads, Protokolle", "Daten, die entstehen. Nie im oeffentlichen Ordner."],
    ["konfiguration.php", "Pfade, Titel, Betriebsart", "Was sich zwischen Rechner und Server unterscheidet."],
    [".env", "Passwoerter, Schluessel", "Geheimnisse. Steht in .gitignore und wird nie hochgeladen."],
];

?>
<!doctype html>
<html lang="de">
<head><meta charset="utf-8"><title>Struktur</title>
<style>body{font-family:system-ui,sans-serif;max-width:44rem;margin:2rem auto}
td,th{border:1px solid #ccc;padding:.35rem .6rem;text-align:left;vertical-align:top}
code{white-space:nowrap}</style></head>
<body>
    <h1>Wohin welche Datei gehoert</h1>
    <table>
        <tr><th>Ort</th><th>Was dort liegt</th><th>Warum</th></tr>
<?php foreach ($ordner as $zeile): ?>
        <tr>
            <td><code><?= $zeile[0] ?></code></td>
            <td><?= $zeile[1] ?></td>
            <td><?= $zeile[2] ?></td>
        </tr>
<?php endforeach; ?>
    </table>
</body>
</html>

Die Struktur im ersten Beispiel ist keine Erfindung dieses Kurses. Fast jedes PHP-Projekt, das du aufmachst, sieht so oder sehr ähnlich aus, und die Ordner heißen dort meist public, src, templates und var. Was sie ordnet, ist eine einzige Frage: Wer darf das sehen, und wer muss es sehen?

oeffentlich/ ist der einzige Ordner, den der Browser erreicht, und darin liegt genau eine PHP-Datei. Alles andere liegt daneben und ist von außen nicht erreichbar, nicht weil du es niemandem gesagt hast, sondern weil der Server nicht hinkommt. privat/ ist die Ecke für alles, was entsteht: die Datenbankdatei, hochgeladene Dateien, Protokolle. Und .env ist der einzige Ort für Werte, die nirgendwo sonst stehen dürfen.

Was ausdrücklich nicht hineingehört: Eine Datenbankabfrage gehört nicht nach ansichten/. Ein Stück Markup gehört nicht nach src/. Ein Passwort gehört nicht in konfiguration.php. Diese drei Sätze sind die Zusammenfassung des ganzen Abschnitts.

Der Weg einer Anfrage

Der Weg einer Anfrage
<?php

// Eine einzelne Anfrage, von der ersten bis zur letzten Zeile. Jeder
// Schritt schreibt sich selbst in die Liste, und zwar genau dort, wo
// er wirklich passiert. Die Reihenfolge unten ist also nicht
// behauptet, sondern gelaufen.

$weg = [];
$weg[] = "Der Server nimmt GET / entgegen und startet oeffentlich/index.php.";

$konfig = [
    "titel" => "Notizbuch von Mia",
    "modus" => "entwicklung",
];
$weg[] = "Die Konfiguration wird geladen. Modus: " . $konfig["modus"] . ".";

$weg[] = "Der Speicher wird gebaut. Ab hier gibt es eine Verbindung zur Datenbank.";

$routen = [
    ["GET", "/", "seiteStart"],
    ["POST", "/notiz", "aktionAnlegen"],
];
$pfad = "/";
$methode = "GET";

$ziel = null;
foreach ($routen as $route) {
    if ($route[0] === $methode && $route[1] === $pfad) {
        $ziel = $route[2];
        break;
    }
}
$weg[] = "Der Front Controller sucht eine Route fuer " . $methode . " " . $pfad . " und findet " . $ziel . ".";

$notizen = ["Milch", "Brot"];
$weg[] = "Die Seitenfunktion holt ihre Daten: " . count($notizen) . " Notizen.";

ob_start();
echo "<ul>";
foreach ($notizen as $notiz) {
    echo "<li>" . htmlspecialchars($notiz, ENT_QUOTES, "UTF-8") . "</li>";
}
echo "</ul>";
$inhalt = ob_get_clean();
$weg[] = "Die Vorlage wird gerendert und liefert " . strlen($inhalt) . " Zeichen zurueck.";

$weg[] = "Ganz unten steht das einzige echo der Anwendung. Erst hier geht etwas hinaus.";

?>
<!doctype html>
<html lang="de">
<head><meta charset="utf-8"><title>Der Weg einer Anfrage</title>
<style>body{font-family:system-ui,sans-serif;max-width:40rem;margin:2rem auto}
li{margin:.3rem 0}</style></head>
<body>
    <h1>Der Weg einer Anfrage</h1>
    <ol>
<?php foreach ($weg as $schritt): ?>
        <li><?= $schritt ?></li>
<?php endforeach; ?>
    </ol>

    <h2>Und das kommt dabei heraus</h2>
    <?= $inhalt ?>
</body>
</html>

Das zweite Beispiel läuft die Anfrage einmal ab und schreibt jeden Schritt genau dort auf, wo er passiert. Die Liste ist also nicht gemalt, sondern gelaufen:

Der Server nimmt die Anfrage entgegen und startet oeffentlich/index.php, weil es zu diesem Pfad keine Datei gibt. Die Konfiguration wird geladen, danach steht fest, wo die Datenbank liegt und in welcher Betriebsart die Anwendung läuft. Der Speicher wird gebaut. Der Front Controller sucht in der Routentabelle eine Zeile, die zu Methode und Pfad passt, und ruft die zugehörige Funktion auf. Die holt ihre Daten und lässt eine Vorlage rendern, deren Ergebnis sie als Zeichenkette zurückgibt. Und ganz unten steht das einzige echo der ganzen Anwendung.

Der letzte Punkt ist der, an dem sich am meisten aufhängt. Solange bis zum Schluss nichts ausgegeben wurde, kannst du an jeder Stelle davor noch einen Statuscode setzen, eine Kopfzeile schicken oder weiterleiten. Das ist der Grund, warum das http_response_code(404) in deinen Aufgaben funktioniert hat und warum die Weiterleitung nach dem Absenden aus Lektion 11.3 überhaupt möglich ist. Eine Anwendung, in der irgendwo in der Mitte ein echo steht, verliert diese Möglichkeit, und zwar genau dann, wenn sie gebraucht wird.

Und war das jetzt nötig?

Dieselbe Anwendung in einer Datei
<?php

// Dieselbe Notizliste, alles in einer Datei. Sie ist kuerzer als die
// sieben Dateien nebenan, sie funktioniert genauso, und fuer ein
// Wochenendprojekt waere sie die richtige Wahl. Sie laeuft im Reiter
// Browser wirklich, du kannst also Notizen anlegen. Lies sie einmal und frag
// dich dabei, wo du suchen wuerdest, wenn die Liste in der falschen
// Reihenfolge steht.

$db = new PDO("sqlite:" . __DIR__ . "/daten.db", null, null, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$db->exec("CREATE TABLE IF NOT EXISTS notizen (id INTEGER PRIMARY KEY, titel TEXT NOT NULL)");
$db->exec("INSERT OR IGNORE INTO notizen (id, titel) VALUES (1, 'Milch'), (2, 'Brot')");

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $text = trim($_POST["text"] ?? "");
    if ($text !== "") {
        $db->prepare("INSERT INTO notizen (titel) VALUES (:t)")->execute([":t" => $text]);
    }
    header("Location: /", true, 302);
    exit;
}

$notizen = $db->query("SELECT id, titel FROM notizen ORDER BY id DESC")->fetchAll();

?>
<!doctype html>
<html lang="de">
<head><meta charset="utf-8"><title>Notizbuch von Mia</title>
<style>body{font-family:system-ui,sans-serif;max-width:34rem;margin:2rem auto}</style></head>
<body>
    <h1>Notizbuch von Mia</h1>
    <form method="post" action="/">
        <p><input type="text" name="text" size="30"> <button type="submit">Anlegen</button></p>
    </form>
    <ul>
<?php foreach ($notizen as $notiz) { ?>
        <li><?= htmlspecialchars($notiz["titel"], ENT_QUOTES, "UTF-8") ?></li>
<?php } ?>
    </ul>
    <footer><p>Alle Notizen an einem Ort.</p></footer>
</body>
</html>

Ehrliche Gegenrechnung. Das dritte Beispiel ist dieselbe Notizliste in einer einzigen Datei, vierzig Zeilen, und sie tut dasselbe. Für ein Wochenendprojekt wäre sie die richtige Wahl, und niemand sollte sich schämen, so etwas zu schreiben.

Der Unterschied zeigt sich nicht heute. Er zeigt sich, wenn eine zweite Seite dazukommt, wenn zwei Leute gleichzeitig daran arbeiten, wenn jemand nur das Aussehen ändern soll, oder wenn du in einem halben Jahr eine Zahl suchst, die nicht stimmt. Dann ist die Frage nicht mehr „wie lang ist die Datei”, sondern „wo muss ich hinsehen”, und darauf gibt die Struktur eine Antwort und die eine Datei nicht.

Die brauchbare Faustregel: Solange du beim Suchen nie scrollst, ist eine Datei richtig. Sobald du anfängst, in derselben Datei zwischen Rechnung und Markup hin und her zu springen, hat sich das erledigt.

Was ein Framework hier noch dazutut

Du hast in diesem Abschnitt einen Router, eine Vorlagenfunktion, eine Konfiguration und einen Front Controller gebaut. Genau diese vier Teile bringt jedes Framework mit, und es bringt mehr:

Ein Autoloader, der Klassen anhand ihres Namens findet, statt sie mit require einzusammeln. Das kennst du aus Lektion 9.5, und in dieser Anwendung fehlt es bewusst, damit du die require-Zeilen noch siehst. Ein Request- und Response-Objekt, statt $_GET, $_POST und header() direkt zu benutzen, was das Testen erst möglich macht. Middleware, also eine Kette von Schritten vor und nach der eigentlichen Funktion, für Anmeldung, Protokoll und Sitzung. Und fertige Antworten auf Fragen, die du noch gar nicht gestellt hast: Formularvalidierung, Übersetzungen, Migrationen für die Datenbank, ein Konsolenbefehl.

Wann es sich lohnt: sobald du merkst, dass du diese Dinge selbst nachbaust. Genau dann ist das Framework nicht mehr eine Abkürzung um das Verständnis herum, sondern eine Abkürzung mit Verständnis, und du erkennst in seiner Ordnerstruktur wieder, was du hier von Hand gemacht hast.

Zum Mitnehmen

Fünf Lektionen lang wurde einzeln aufgeräumt. Diese hier legt die Teile nebeneinander und beantwortet die Frage, die dabei offen geblieben ist: Wohin gehört was, und in welcher Reihenfolge läuft das eigentlich ab?

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.