Abschnitt 16 · Lektion 5
Konfiguration
Jede Anwendung hat eine Handvoll Werte, die sich zwischen deinem Rechner und dem Server unterscheiden. Wo die Datenbank liegt. Wie die Seite heißt. Ob Fehler auf der Seite stehen dürfen. Welche Zeitzone gilt. Und Zugangsdaten, die nirgends stehen sollten.
Fünf Werte, und du findest sie nicht wieder
<?php
// Hier steht der Name der Anwendung. Zweimal, weil er in den Titel
// und in die Ueberschrift gehoert.
function kopf(): string
{
return '<!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>';
} <?php
// Und hier steht, wo die Datenbank liegt.
function datenbank(): PDO
{
$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')");
return $db;
} <?php
// Und hier stehen die Zeitzone und die Betriebsart. Fuenf Werte,
// drei Dateien, und keine Datei weiss von den anderen. Zaehl beim
// Lesen mit, an wie vielen Stellen du etwas aendern muesstest, um
// diese Anwendung auf einen Server zu bringen.
date_default_timezone_set("Europe/Berlin");
if ("entwicklung" === "entwicklung") {
ini_set("display_errors", "1");
error_reporting(E_ALL);
}
require __DIR__ . "/src/kopf.php";
require __DIR__ . "/src/datenbank.php";
$db = datenbank();
$anzahl = (int) $db->query("SELECT count(*) FROM notizen")->fetchColumn();
echo kopf();
echo "\n <p>Notizen: " . $anzahl . "</p>";
echo "\n <p>Zeitzone: " . date_default_timezone_get() . "</p>";
echo "\n</body>\n</html>\n"; Sieh dir das erste Beispiel an, aber diesmal nicht die Seite, sondern die drei Reiter. Der Name der
Anwendung steht in src/kopf.php, und zwar zweimal. Der Pfad zur Datenbank steht in
src/datenbank.php. Die Zeitzone und die Betriebsart stehen in index.php. Fünf Werte, drei
Dateien, sechs Stellen.
Die Anwendung ist völlig in Ordnung, sie tut, was sie soll. Nur ist das Hochladen auf einen Server jetzt eine Suchaktion, und Suchaktionen gehen schief. Man findet vier von fünf Stellen, die Seite läuft trotzdem, und die fünfte fällt erst auf, wenn ein Besucher eine Fehlermeldung mit deinem Dateipfad sieht.
Alles an eine Stelle
<?php
// Alles, was sich zwischen deinem Rechner und dem Server
// unterscheidet, an einer Stelle. Ein gewoehnliches Array, das die
// Datei zurueckgibt.
return [
"datenbank" => __DIR__ . "/daten.db",
"titel" => "Notizbuch von Mia",
"modus" => "entwicklung",
"zeitzone" => "Europe/Berlin",
"db_passwort" => getenv("DB_PASSWORT") ?: "",
]; <?php
// Der Kopf weiss nicht mehr, wie die Anwendung heisst. Er bekommt es
// gesagt.
function kopf(array $konfig): string
{
$titel = htmlspecialchars($konfig["titel"], ENT_QUOTES, "UTF-8");
return '<!doctype html>
<html lang="de">
<head><meta charset="utf-8"><title>' . $titel . '</title>
<style>body{font-family:system-ui,sans-serif;max-width:34rem;margin:2rem auto}</style></head>
<body>
<h1>' . $titel . '</h1>';
} <?php
// Und die Datenbankdatei weiss nicht mehr, wo sie liegt.
function datenbank(array $konfig): PDO
{
$db = new PDO("sqlite:" . $konfig["datenbank"], null, $konfig["db_passwort"], [
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')");
return $db;
} <?php
// Dieselbe Anwendung, dieselbe Ausgabe. Nur steht jetzt oben eine
// Zeile, die alles holt, und darunter kein einziger fester Wert mehr.
$konfig = require __DIR__ . "/konfiguration.php";
date_default_timezone_set($konfig["zeitzone"]);
if ($konfig["modus"] === "entwicklung") {
ini_set("display_errors", "1");
error_reporting(E_ALL);
}
require __DIR__ . "/src/kopf.php";
require __DIR__ . "/src/datenbank.php";
$db = datenbank($konfig);
$anzahl = (int) $db->query("SELECT count(*) FROM notizen")->fetchColumn();
echo kopf($konfig);
echo "\n <p>Notizen: " . $anzahl . "</p>";
echo "\n <p>Zeitzone: " . date_default_timezone_get() . "</p>";
echo "\n</body>\n</html>\n"; Das zweite Beispiel gibt Zeichen für Zeichen dieselbe Seite aus. Der Unterschied ist eine Datei mehr
und in den anderen dreien kein einziger fester Wert. konfiguration.php gibt ein Array zurück, und
wer etwas davon braucht, bekommt es gesagt.
Warum ein zurückgegebenes Array und nicht ein Haufen Konstanten. Eine Konstante mit define ist
bequem, weil sie überall gilt, und genau das ist ihr Problem. Sie gilt überall, auch dort, wo du sie
nicht erwartest. Du kannst sie nicht als Parameter weiterreichen, du kannst sie nicht überschreiben,
und in einem Test kannst du sie nicht durch etwas anderes ersetzen, denn ein zweites define ändert
nichts, wie du in Lektion 2.6 gesehen hast. Ein Array ist ein gewöhnlicher Wert: Du reichst es
weiter, du kannst zwei davon nebeneinander haben, und eine Funktion, die es als Parameter nimmt, sagt
in ihrer Signatur, dass sie Konfiguration braucht.
Und warum require und kein JSON. Die Konfiguration ist PHP, also darfst du darin rechnen:
__DIR__ einsetzen, eine Umgebungsvariable lesen, einen Rückfall angeben. Eine JSON-Datei kann das
alles nicht, und dann steht der Pfad absolut darin, und beim nächsten Umzug stimmt er nicht mehr.
Der Schalter zwischen Entwicklung und Betrieb
<?php
// Derselbe Fehler, zweimal ausgeloest, einmal je Betriebsart. Sieh
// dir die Seite an und danach das Protokoll des Servers (im Terminal:
// grep Warning /tmp/lern-web.log): Der Unterschied steht an beiden
// Orten, und er ist nicht derselbe.
$daten = ["titel" => "Notizbuch"];
echo "<!doctype html>\n<html lang=\"de\"><head><meta charset=\"utf-8\">";
echo "<title>Zwei Betriebsarten</title></head><body>\n";
foreach (["entwicklung", "betrieb"] as $modus) {
if ($modus === "entwicklung") {
ini_set("display_errors", "1");
error_reporting(E_ALL);
} else {
ini_set("display_errors", "0");
ini_set("log_errors", "1");
}
echo "<h2>Modus " . $modus . "</h2>\n<p>Wert: ";
echo $daten["fehlt"];
echo "</p>\n";
}
echo "</body>\n</html>\n"; Der wichtigste Eintrag ist modus, und er hängt an einer Sache, die du schon kennst. In Lektion 7.5
und 15.5 ging es darum, dass auf einem öffentlichen Server display_errors aus und log_errors an
gehört. Beim Entwickeln willst du genau das Gegenteil.
Das dritte Beispiel löst denselben Fehler zweimal aus, einmal je Betriebsart. Im ersten Block steht
die Warnung mitten auf der Seite, im zweiten Block ist die Zeile leer. Sieh danach ins Protokoll des
Servers: Es liegt in /tmp/lern-web.log, und grep Warning /tmp/lern-web.log im Terminal zeigt
beide Warnungen. Das ist kein Widerspruch, sondern der Beweis,
dass die zwei Schalter unabhängig sind. display_errors entscheidet, ob die Meldung zusätzlich in
die Ausgabe geht, log_errors, ob sie protokolliert wird. Auf dem Server willst du nur das zweite.
Damit ist der Modus mehr als eine Beschriftung: An ihm hängt, was ein Besucher zu sehen bekommt, wenn
etwas schiefgeht. Genau deshalb steht er in der Konfiguration und nicht irgendwo in einem if in der
Mitte der Anwendung.
Was nicht in die Konfiguration gehört
Der Unterschied ist einfach: In die Konfiguration gehört, was die Anwendung beschreibt. Nicht hinein gehört, was sie geheim halten muss.
Ein Datenbankpasswort, ein API-Schlüssel, das Geheimnis, mit dem Sitzungen signiert werden: Diese
Werte stehen nicht im Quelltext, auch nicht in einer Datei, die konfiguration.php heißt. Der Grund
ist nicht Paranoia, sondern der Alltag: Der Quelltext liegt in der Versionsverwaltung, wird kopiert,
geteilt, in ein Backup gelegt und irgendwann von jemandem gelesen, an den beim Schreiben niemand
gedacht hat. Ein Passwort, das einmal in der Versionsgeschichte steht, bekommt man da auch nicht
wieder heraus.
Der übliche Weg dafür ist eine Umgebungsvariable. Der Server setzt sie, PHP liest sie mit
getenv("DB_PASSWORT"), und in der Konfiguration steht nur diese Zeile statt des Werts. Auf dem
eigenen Rechner legt man die Werte dafür in eine Datei, die nicht mit eingecheckt wird, und in fast
jedem Projekt heißt sie .env. Wichtig ist dabei nur eins: Diese Datei steht in .gitignore, und
zwar bevor sie zum ersten Mal einen echten Wert enthält.
getenv gibt false zurück, wenn es die Variable nicht gibt. Deshalb steht in der Konfiguration
getenv("DB_PASSWORT") ?: "" und nicht nur der nackte Aufruf: Der Kurzform-Operator aus Lektion 4.1
macht daraus einen leeren Text, statt ein false weiterzureichen, das an der nächsten Stelle als
Warnung auffällt.
Zum Mitnehmen
Jede Anwendung hat eine Handvoll Werte, die sich zwischen deinem Rechner und dem Server unterscheiden: ein Pfad, ein Schalter, ein Zugang. Solange sie verstreut im Code stehen, ist jedes Hochladen eine Suchaktion. Diese Lektion sammelt sie ein, und einen davon nimmt sie ganz aus dem Quelltext heraus.
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, auf dem Server geprüft
Öffnet sich mit dem Basis Konto.