Abschnitt 16 · Lektion 4
Routen und Parameter
Eine Routentabelle mit festen Pfaden reicht für ein Impressum. Für Notizen reicht sie nicht, denn du
kannst keine Zeile für /notiz/1, /notiz/2 und alles bis /notiz/8391 schreiben. Was du brauchst,
ist eine Zeile für alle: ein Muster.
Erst einmal von Hand
<?php
// Diese Seite zeigt, was aus der Adresse geworden ist. Tipp in die
// Adresszeile verschiedene Pfade ein, zum Beispiel /notiz/12,
// /notiz/12/erledigt oder /notiz oder nur einen Schraegstrich, und
// sieh dir die Tabelle an.
$pfad = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
// trim schneidet die Schraegstriche an den Enden ab, sonst waere das
// erste Stueck immer leer. Und ein leerer Pfad ergibt sonst ein Array
// mit einer leeren Zeichenkette statt gar keinem Stueck.
$stuecke = $pfad === "/" ? [] : explode("/", trim($pfad, "/"));
function e(?string $wert): string
{
return htmlspecialchars($wert ?? "", ENT_QUOTES, "UTF-8");
}
?>
<!doctype html>
<html lang="de">
<head><meta charset="utf-8"><title>Pfad zerlegen</title>
<style>body{font-family:system-ui,sans-serif;max-width:34rem;margin:2rem auto}
td,th{border:1px solid #ccc;padding:.3rem .6rem;text-align:left}</style></head>
<body>
<h1>Pfad zerlegen</h1>
<p>Angefragt: <code><?= e($pfad) ?></code>.
Stuecke: <strong><?= count($stuecke) ?></strong></p>
<table>
<tr><th>Nummer</th><th>Stueck</th></tr>
<?php foreach ($stuecke as $nummer => $stueck): ?>
<tr><td><?= $nummer ?></td><td><?= e($stueck) ?></td></tr>
<?php endforeach; ?>
</table>
<p>Eine Notiz waere also: erstes Stueck gleich <code>notiz</code>,
zweites Stueck die Nummer. Probier
<a href="/notiz/12">/notiz/12</a> und
<a href="/notiz/12/erledigt">/notiz/12/erledigt</a>.</p>
</body>
</html> Der naheliegende Weg ist der aus Lektion 5.6: Den Pfad an den Schrägstrichen zerschneiden und die Stücke ansehen. Das erste Beispiel zeigt genau das, und du kannst in der Adresszeile über der Seite verschiedene Adressen eintippen und zusehen, was dabei herauskommt.
Zwei Kleinigkeiten sind daran schon unangenehm. Ein Pfad fängt mit einem Schrägstrich an, also wäre
das erste Stück leer, wenn man nicht vorher trim benutzt. Und / ergibt nach dem Abschneiden eine
leere Zeichenkette, aus der explode ein Array mit genau einem leeren Eintrag macht statt eines
leeren Arrays. Das ist der Grund für die Sonderbehandlung in der ersten Zeile.
Mit dieser Zerlegung könnte man weiterarbeiten: Ist das erste Stück notiz und gibt es ein zweites,
dann zeig die Notiz mit dieser Nummer. Für zwei oder drei Adressen geht das gut. Bei zehn wird daraus
eine Kette aus if, in der die Zählung der Stücke jedes Mal neu durchdacht werden muss, und
irgendwann steht die Adressstruktur der ganzen Anwendung als verschachteltes if in einer Datei.
Ein Muster beschreibt den ganzen Pfad
<?php
// Dieselbe Aufgabe, nur beschreibt jetzt ein Muster den ganzen Pfad
// auf einmal. Tipp wieder verschiedene Adressen ein und sieh dir an,
// welche Zeile trifft.
$muster = [
"/" => "Startseite",
"/notiz/{id}" => "eine Notiz ansehen",
"/notiz/{id}/erledigt" => "eine Notiz abhaken",
];
function passtRoute(string $muster, string $pfad): ?array
{
// Aus {id} wird eine Klammer, die genau eine Zahl erlaubt. Alles
// andere im Muster bleibt woertlich, dafuer sorgt preg_quote.
$regex = "#^" . str_replace('\{id\}', '([0-9]+)', preg_quote($muster, "#")) . "$#";
if (preg_match($regex, $pfad, $treffer) !== 1) {
return null;
}
return array_slice($treffer, 1);
}
$pfad = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
function e(?string $wert): string
{
return htmlspecialchars($wert ?? "", ENT_QUOTES, "UTF-8");
}
?>
<!doctype html>
<html lang="de">
<head><meta charset="utf-8"><title>Muster</title>
<style>body{font-family:system-ui,sans-serif;max-width:34rem;margin:2rem auto}
td,th{border:1px solid #ccc;padding:.3rem .6rem;text-align:left}</style></head>
<body>
<h1>Welche Route passt</h1>
<p>Angefragt: <code><?= e($pfad) ?></code></p>
<table>
<tr><th>Muster</th><th>Ergebnis</th></tr>
<?php foreach ($muster as $eintrag => $bedeutung): ?>
<?php $werte = passtRoute($eintrag, $pfad); ?>
<tr>
<td><code><?= e($eintrag) ?></code></td>
<td><?= $werte === null ? "passt nicht" : $bedeutung . ", Wert: " . e($werte[0] ?? "keiner") ?></td>
</tr>
<?php endforeach; ?>
</table>
<p>Probier <a href="/notiz/12">/notiz/12</a>,
<a href="/notiz/12/erledigt">/notiz/12/erledigt</a> und
<a href="/notiz/abc">/notiz/abc</a>.</p>
</body>
</html> Bequemer ist es, den Pfad in einem Stück zu beschreiben: /notiz/{id}. Das liest sich wie die
Adresse selbst, und aus dem {id} wird beim Prüfen ein regulärer Ausdruck, der genau eine Zahl
erlaubt.
Die Funktion dafür hat drei Anweisungen, und zwei Details darin sind wichtig. preg_quote sorgt
dafür, dass alles außer dem Platzhalter wörtlich genommen wird, ein Punkt in der Adresse also
wirklich ein Punkt ist und nicht „irgendein Zeichen”. Und die runden Klammern um [0-9]+ sind es,
die den gefundenen Wert festhalten: preg_match legt ihn in $treffer, und array_slice schneidet
den ersten Eintrag weg, denn der ist immer der ganze Treffer.
Probier im zweiten Beispiel /notiz/abc. Kein Muster passt, und das ist Absicht: Eine Notiznummer
ist eine Zahl. Wer stattdessen [^/]+ schreibt, erlaubt alles bis zum nächsten Schrägstrich und
handelt sich das Problem aus dem nächsten Abschnitt ein.
Warum die Reihenfolge zählt
<?php
// Hier steht im Muster absichtlich [^/]+ statt [0-9]+, das Muster
// erlaubt also jedes Stueck ohne Schraegstrich. Damit schluckt
// /notiz/{id} auch die Adresse /notiz/neu, und dann entscheidet die
// Reihenfolge, wer sie bekommt.
function passtRoute(string $muster, string $pfad): ?array
{
$regex = "#^" . str_replace('\{id\}', '([^/]+)', preg_quote($muster, "#")) . "$#";
if (preg_match($regex, $pfad, $treffer) !== 1) {
return null;
}
return array_slice($treffer, 1);
}
function ersteRoute(array $tabelle, string $pfad): string
{
foreach ($tabelle as $route) {
if (passtRoute($route[0], $pfad) !== null) {
return $route[1];
}
}
return "nichts gefunden";
}
$falsch = [
["/notiz/{id}", "seiteNotiz"],
["/notiz/neu", "seiteNeu"],
];
$richtig = [
["/notiz/neu", "seiteNeu"],
["/notiz/{id}", "seiteNotiz"],
];
?>
<!doctype html>
<html lang="de">
<head><meta charset="utf-8"><title>Reihenfolge</title>
<style>body{font-family:system-ui,sans-serif;max-width:36rem;margin:2rem auto}
td,th{border:1px solid #ccc;padding:.3rem .6rem;text-align:left}</style></head>
<body>
<h1>Wer bekommt die Adresse</h1>
<table>
<tr><th>Adresse</th><th>allgemeine Route zuerst</th><th>besondere Route zuerst</th></tr>
<?php foreach (["/notiz/12", "/notiz/neu"] as $pfad): ?>
<tr>
<td><code><?= $pfad ?></code></td>
<td><?= ersteRoute($falsch, $pfad) ?></td>
<td><?= ersteRoute($richtig, $pfad) ?></td>
</tr>
<?php endforeach; ?>
</table>
<p>In der linken Spalte landet <code>/notiz/neu</code> bei der falschen
Funktion, und zwar ohne jede Fehlermeldung: Sie sucht dann eine Notiz
mit der Nummer <code>neu</code>.</p>
</body>
</html> Sobald ein Muster mehr als eine Adresse trifft, kann es einer anderen Route in die Quere kommen.
Angenommen, es gibt /notiz/neu für ein leeres Formular und /notiz/{id} zum Ansehen, und im Muster
steht [^/]+. Dann passt /notiz/{id} auch auf /notiz/neu, und wer zuerst in der Tabelle steht,
gewinnt.
Das dritte Beispiel stellt beide Reihenfolgen nebeneinander. Steht die allgemeine Route oben, landet
/notiz/neu bei seiteNotiz, und die sucht dann eine Notiz mit der Nummer neu. Es gibt keine
Fehlermeldung, es kommt nur die falsche Seite. Die Regel dagegen ist einfach: Das Besondere steht
über dem Allgemeinen.
Im Kurs steht deshalb [0-9]+ und nicht [^/]+, und damit ist der Konflikt gar nicht erst da. Das
ist die zweite Regel, und sie ist die bessere: Ein Muster, das nur trifft, was es treffen soll, macht
die Reihenfolge unwichtig.
Was ein richtiger Router mehr kann
Der Router aus dieser Lektion ist etwa zwanzig Zeilen lang und deckt ab, was eine kleine Anwendung
braucht. Fertige Router wie nikic/fast-route aus Lektion 9.4 können mehr, und drei Dinge davon
merkt man schnell.
Sie kennen Typen im Muster, also {id:\d+} gegen {name}, statt die Entscheidung wie hier fest
einzubauen. Sie bauen aus allen Routen ein einziges großes Muster, statt für jede Anfrage die
ganze Tabelle durchzugehen, was bei fünf Routen egal ist und bei fünfhundert nicht mehr. Und sie
unterscheiden „kein Pfad passt” von „der Pfad passt, aber nicht mit dieser Methode”, was den
Unterschied zwischen 404 und 405 ausmacht.
Wann sich das lohnt: sobald du anfängst, an deinem eigenen Router zu bauen, statt ihn zu benutzen. Bis dahin ist der hier ehrlicher, weil du weißt, was er tut.
Zum Mitnehmen
Adressen wie /notiz/12 sind der Normalfall, und sie passen in keine Tabelle mit festen Pfaden: Es gibt keine Zeile für jede Nummer. Was fehlt, ist ein Muster und ein Weg, den Wert daraus wieder herauszubekommen.
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.