Nette Documentation Preview

syntax
Routing
*******

<div class=perex>

Der Router kümmert sich um alles rund um URL-Adressen, damit Sie nicht mehr über sie nachdenken müssen. Wir zeigen Ihnen:

- wie man den Router einstellt, damit die URLs so aussehen, wie Sie es möchten
- wir sprechen über SEO und Weiterleitungen
- und zeigen Ihnen, wie Sie einen eigenen Router schreiben

</div>


Benutzerfreundlichere URLs (auch cool oder pretty URLs genannt) sind besser nutzbar, leichter zu merken und tragen positiv zum SEO bei. Nette denkt daran und kommt Entwicklern voll entgegen. Sie können für Ihre Anwendung genau die Struktur der URL-Adressen entwerfen, die Sie möchten. Sie können sie sogar erst dann entwerfen, wenn die Anwendung bereits fertig ist, denn es sind dafür keine Eingriffe in den Code oder die Templates nötig. Sie wird nämlich auf elegante Weise an [einer einzigen Stelle |#Integration] definiert, im Router, und ist nicht in Form von Annotationen über alle Presenter verstreut.

Der Router in Nette ist außergewöhnlich, weil er **bidirektional** ist. Er kann sowohl URLs aus HTTP-Requests dekodieren als auch Links erstellen. Er spielt daher eine entscheidende Rolle in [Nette Application |how-it-works#Nette Application], denn er entscheidet nicht nur, welcher Presenter und welche Aktion den aktuellen Request ausführen, sondern wird auch zum [Erzeugen von URLs |creating-links] im Template usw. verwendet.

Der Router ist jedoch nicht auf diese Verwendung beschränkt; Sie können ihn auch in Anwendungen einsetzen, die überhaupt keine Presenter verwenden, für REST-APIs usw. Mehr dazu im Abschnitt [#Eigenständige Verwendung].


Routensammlung
==============

Den angenehmsten Weg, die Form der URL-Adressen in der Anwendung zu definieren, bietet die Klasse [api:Nette\Application\Routers\RouteList]. Die Definition besteht aus einer Liste sogenannter Routes, also Masken von URL-Adressen und den ihnen über eine einfache API zugeordneten Presentern und Aktionen. Die Routes müssen wir in keiner Weise benennen.

```php
$router = new Nette\Application\Routers\RouteList;
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('article/<id>', 'Article:view');
// ...
```

Das Beispiel besagt: Öffnen wir im Browser `https://domain.com/rss.xml`, wird der Presenter `Feed` mit der Aktion `rss` angezeigt; bei `https://domain.com/article/12` der Presenter `Article` mit der Aktion `view` usw. Wird keine passende Route gefunden, reagiert Nette Application mit dem Werfen einer [BadRequestException |api:Nette\Application\BadRequestException], die dem Benutzer als Fehlerseite 404 Not Found angezeigt wird.


Reihenfolge der Routes
----------------------

Ganz **entscheidend ist die Reihenfolge**, in der die einzelnen Routes aufgeführt sind, denn sie werden der Reihe nach von oben nach unten ausgewertet. Es gilt die Regel, dass wir Routes **von speziell nach allgemein** deklarieren:

```php
// FALSCH: 'rss.xml' wird von der ersten Route abgefangen, die diesen String als <slug> versteht
$router->addRoute('<slug>', 'Article:view');
$router->addRoute('rss.xml', 'Feed:rss');

// GUT
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('<slug>', 'Article:view');
```

Auch beim Erzeugen von Links werden die Routes von oben nach unten ausgewertet:

```php
// FALSCH: Ein Link auf 'Feed:rss' wird als 'admin/feed/rss' erzeugt
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');
$router->addRoute('rss.xml', 'Feed:rss');

// GUT
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');
```

Wir werden Ihnen nicht verheimlichen, dass das korrekte Zusammenstellen der Routes eine gewisse Fertigkeit erfordert. Bis Sie diese beherrschen, wird Ihnen das [Routing-Panel |#Debugging des Routers] ein nützlicher Helfer sein.


Maske und Parameter
-------------------

Die Maske beschreibt den relativen Pfad vom Wurzelverzeichnis der Website. Die einfachste Maske ist eine statische URL:

```php
$router->addRoute('products', 'Products:default');
```

Häufig enthalten Masken sogenannte **Parameter**. Diese werden in spitzen Klammern angegeben (z. B. `<year>`) und an den Ziel-Presenter übergeben, zum Beispiel an die Methode `renderShow(int $year)` oder an den persistenten Parameter `$year`:

```php
$router->addRoute('chronicle/<year>', 'History:show');
```

Das Beispiel besagt: Öffnen wir im Browser `https://example.com/chronicle/2020`, wird der Presenter `History` mit der Aktion `show` und dem Parameter `year: 2020` angezeigt.

Parametern können wir direkt in der Maske einen Standardwert zuweisen, wodurch sie optional werden:

```php
$router->addRoute('chronicle/<year=2020>', 'History:show');
```

Die Route akzeptiert nun auch die URL `https://example.com/chronicle/`, die wiederum `History:show` mit dem Parameter `year: 2020` anzeigt.

Parameter können natürlich auch der Name des Presenters und der Aktion sein. Zum Beispiel so:

```php
$router->addRoute('<presenter>/<action>', 'Home:default');
```

Die angegebene Route akzeptiert z. B. URLs in der Form `/article/edit` oder auch `/catalog/list` und versteht sie als die Presenter und Aktionen `Article:edit` bzw. `Catalog:list`.

Zugleich gibt sie den Parametern `presenter` und `action` die Standardwerte `Home` und `default`, wodurch auch sie optional sind. Die Route akzeptiert also auch eine URL in der Form `/article` und versteht sie als `Article:default`. Oder umgekehrt: Ein Link auf `Product:default` erzeugt den Pfad `/product`, ein Link auf den Standard `Home:default` den Pfad `/`.

Die Maske kann nicht nur den relativen Pfad vom Wurzelverzeichnis der Website beschreiben, sondern auch einen absoluten Pfad, wenn sie mit einem Schrägstrich beginnt, oder sogar eine ganze absolute URL, wenn sie mit zwei Schrägstrichen beginnt:

```php
// relativ zum Document-Root
$router->addRoute('<presenter>/<action>', /* ... */);

// absoluter Pfad (relativ zur Domain)
$router->addRoute('/<presenter>/<action>', /* ... */);

// absolute URL inklusive Domain (relativ zum Schema)
$router->addRoute('//<lang>.example.com/<presenter>/<action>', /* ... */);

// absolute URL inklusive Schema
$router->addRoute('https://<lang>.example.com/<presenter>/<action>', /* ... */);
```


Validierungsausdrücke
---------------------

Für jeden Parameter lässt sich eine Validierungsbedingung mit einem [regulären Ausdruck|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php] angeben. Für den Parameter `id` legen wir zum Beispiel mit dem Regex `\d+` fest, dass er nur Ziffern enthalten darf:

```php
$router->addRoute('<presenter>/<action>[/<id \d+>]', /* ... */);
```

Der Standard-Regex für alle Parameter ist `[^/]+`, also alles außer einem Schrägstrich. Soll ein Parameter auch Schrägstriche akzeptieren, setzen wir den Ausdruck auf `.+`:

```php
// akzeptiert https://example.com/a/b/c, path ist dann 'a/b/c'
$router->addRoute('<path .+>', /* ... */);
```


Optionale Sequenzen
-------------------

In der Maske lassen sich optionale Teile mit eckigen Klammern kennzeichnen. Jeder Teil der Maske kann optional sein und Parameter enthalten:

```php
$router->addRoute('[<lang [a-z]{2}>/]<name>', /* ... */);

// Akzeptiert die Pfade:
//    /en/download  => lang => en, name => download
//    /download     => lang => null, name => download
```

Ist ein Parameter Teil einer optionalen Sequenz, wird natürlich auch er optional. Hat er keinen angegebenen Standardwert, ist er null.

Optionale Teile können auch in der Domain vorkommen:

```php
$router->addRoute('//[<lang=en>.]example.com/<presenter>/<action>', /* ... */);
```

Sequenzen lassen sich beliebig verschachteln und kombinieren:

```php
$router->addRoute(
	'[<lang [a-z]{2}>[-<sublang>]/]<name>[/page-<page=0>]',
	'Home:default',
);

// Akzeptiert die Pfade:
// 	/en/hello
// 	/en-us/hello
// 	/hello
// 	/hello/page-12
```

Beim Erzeugen von URLs wird die kürzeste Variante bevorzugt, es wird also alles weggelassen, was weggelassen werden kann. Deshalb erzeugt zum Beispiel die Route `index[.html]` den Pfad `/index`. Dieses Verhalten lässt sich umkehren, indem man hinter die linke eckige Klammer ein Ausrufezeichen setzt:

```php
// akzeptiert /hello und /hello.html, erzeugt /hello
$router->addRoute('<name>[.html]', /* ... */);

// akzeptiert /hello und /hello.html, erzeugt /hello.html
$router->addRoute('<name>[!.html]', /* ... */);
```

Optionale Parameter (also Parameter mit einem Standardwert) ohne eckige Klammern verhalten sich im Grunde so, als wären sie folgendermaßen eingeklammert:

```php
$router->addRoute('<presenter=Home>/<action=default>/<id=>', /* ... */);

// entspricht diesem:
$router->addRoute('[<presenter=Home>/[<action=default>/[<id>]]]', /* ... */);
```

Wollen wir das Verhalten des abschließenden Schrägstrichs beeinflussen, damit zum Beispiel `/home` statt `/home/` erzeugt wird, erreichen wir das so:

```php
$router->addRoute('[<presenter=Home>[/<action=default>[/<id>]]]', /* ... */);
```


Wildcards
---------

In der Maske einer absoluten URL können wir die folgenden Wildcards verwenden, um zum Beispiel die Domain nicht in die Maske schreiben zu müssen, die sich zwischen Entwicklungs- und Produktionsumgebung unterscheiden kann:

- `%tld%` = Top-Level-Domain, z. B. `com` oder `org`
- `%sld%` = Second-Level-Domain, z. B. `example`
- `%domain%` = Domain ohne Subdomains, z. B. `example.com`
- `%host%` = gesamter Host, z. B. `www.example.com`
- `%basePath%` = Pfad zum Wurzelverzeichnis

```php
$router->addRoute('//www.%domain%/%basePath%/<presenter>/<action>', /* ... */);
$router->addRoute('//www.%sld%.%tld%/%basePath%/<presenter>/<action>', /* ... */);
```


Erweiterte Schreibweise
-----------------------

Das Ziel der Route, üblicherweise im Format `Presenter:action` geschrieben, lässt sich auch mit einem Array angeben, das die einzelnen Parameter und ihre Standardwerte definiert:

```php
$router->addRoute('<presenter>/<action>[/<id \d+>]', [
	'presenter' => 'Home',
	'action' => 'default',
]);
```

Für eine genauere Angabe lässt sich eine noch ausführlichere Form verwenden, in der wir neben Standardwerten auch weitere Eigenschaften der Parameter setzen können, etwa einen regulären Ausdruck zur Validierung (siehe den Parameter `id`):

```php
use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>[/<id>]', [
	'presenter' => [
		Route::Value => 'Home',
	],
	'action' => [
		Route::Value => 'default',
	],
	'id' => [
		Route::Pattern => '\d+',
	],
]);
```

Wichtig ist: Sind im Array definierte Parameter nicht in der Pfadmaske aufgeführt, lassen sich ihre Werte nicht ändern, auch nicht über Query-Parameter, die in der URL hinter dem Fragezeichen angegeben werden.

Das ist nützlich für **feste Parameter** - um einer bestimmten Seite eine kurze, einprägsame URL zu geben. Damit zum Beispiel `/tos` immer `Article:view` mit `id: 123` öffnet:

```php
$router->addRoute('tos', [
	'presenter' => 'Article',
	'action' => 'view',
	'id' => 123,
]);
```


Filter und Übersetzungen
------------------------

Den Quellcode der Anwendung schreiben wir auf Englisch, wenn die Website aber tschechische URLs haben soll, dann erzeugt ein einfaches Routing wie:

```php
$router->addRoute('<presenter>/<action>', 'Home:default');
```

englische URLs, etwa `/product/123` oder `/cart`. Wollen wir, dass Presenter und Aktionen in der URL durch tschechische Wörter dargestellt werden (z. B. `/produkt/123` oder `/kosik`), können wir ein Übersetzungswörterbuch verwenden. Um es zu schreiben, brauchen wir bereits die "gesprächigere" Variante des zweiten Parameters:

```php
use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>', [
	'presenter' => [
		Route::Value => 'Home',
		Route::FilterTable => [
			// String in der URL => Presenter
			'produkt' => 'Product',
			'kosik' => 'Cart',
			'katalog' => 'Catalog',
		],
	],
	'action' => [
		Route::Value => 'default',
		Route::FilterTable => [
			'seznam' => 'list',
		],
	],
]);
```

Mehrere Schlüssel im Übersetzungswörterbuch können auf denselben Presenter führen. So entstehen für ihn verschiedene Aliase. Der letzte Schlüssel gilt als die kanonische Variante (also die, die in der erzeugten URL steht).

Die Übersetzungstabelle lässt sich auf diese Weise für jeden Parameter verwenden. Existiert keine Übersetzung, wird der ursprüngliche Wert genommen. Dieses Verhalten können wir mit `Route::FilterStrict => true` ändern, die Route weist die URL dann zurück, wenn der Wert nicht im Wörterbuch steht.

Neben dem Übersetzungswörterbuch in Form eines Arrays lassen sich eigene Übersetzungsfunktionen einsetzen.

```php
use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>/<id>', [
	'presenter' => [
		Route::Value => 'Home',
		Route::FilterIn => function (string $s): string { /* ... */ },
		Route::FilterOut => function (string $s): string { /* ... */ },
	],
	'action' => 'default',
	'id' => null,
]);
```

Die Funktion `Route::FilterIn` wandelt zwischen dem Parameter in der URL und dem String um, der dann an den Presenter übergeben wird; die Funktion `FilterOut` sorgt für die Umwandlung in die Gegenrichtung.

Die Parameter `presenter`, `action` und `module` haben bereits vordefinierte Filter, die zwischen dem PascalCase- bzw. camelCase-Stil und dem in URLs verwendeten kebab-case umwandeln. Der Standardwert der Parameter wird in der Form geschrieben, in der er an die Anwendung übergeben wird (PascalCase bei Presenter und Modul, camelCase bei der Aktion), im Fall eines Presenters schreiben wir also `<presenter=ProductEdit>`, nicht `<presenter=product-edit>`.


Allgemeine Filter
-----------------

Neben Filtern für konkrete Parameter können wir auch allgemeine Filter definieren, die ein assoziatives Array aller Parameter erhalten, das sie beliebig verändern und dann zurückgeben können. Allgemeine Filter werden unter dem leeren Schlüssel definiert.

```php
use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>', [
	'presenter' => 'Home',
	'action' => 'default',
	'' => [
		Route::FilterIn => function (array $params): array { /* ... */ },
		Route::FilterOut => function (array $params): array { /* ... */ },
	],
]);
```

Allgemeine Filter bieten die Möglichkeit, das Verhalten der Route auf absolut beliebige Weise zu verändern. Wir können sie zum Beispiel nutzen, um Parameter anhand anderer Parameter zu verändern. Etwa `<presenter>` und `<action>` anhand des aktuellen Werts des Parameters `<lang>` zu übersetzen.

Hat ein Parameter einen eigenen Filter definiert und existiert zugleich ein allgemeiner Filter, wird das eigene `FilterIn` vor dem allgemeinen ausgeführt und umgekehrt das allgemeine `FilterOut` vor dem eigenen. Innerhalb des allgemeinen Filters sind die Werte der Parameter `presenter` und `action` also im PascalCase- bzw. camelCase-Stil geschrieben.

Eine praktische Anwendung dieser Filter - das Erzeugen SEO-freundlicher URLs wie `/article/123-how-to-bake-bread` ohne jede Änderung an den Templates - finden Sie unter [Schöne URLs mit Slugs |best-practices:pretty-urls].


Flag OneWay
-----------

Einwegrouten dienen dazu, die Funktionsfähigkeit alter URLs zu erhalten, die die Anwendung nicht mehr erzeugt, aber weiterhin akzeptiert. Wir kennzeichnen sie mit dem Flag `OneWay`:

```php
// alte URL /product-info?id=123
$router->addRoute('product-info', 'Product:detail', oneWay: true);
// neue URL /product/123
$router->addRoute('product/<id>', 'Product:detail');
```

Beim Aufruf der alten URL leitet der Presenter automatisch auf die neue URL weiter, damit Suchmaschinen diese Seiten nicht doppelt indexieren (siehe [#SEO und Kanonisierung]).


Dynamisches Routing mit Callbacks
---------------------------------

Dynamisches Routing mit Callbacks erlaubt es, Routes direkt Funktionen (Callbacks) zuzuweisen, die beim Aufruf des jeweiligen Pfads ausgeführt werden. Diese flexible Funktionalität ermöglicht es, schnell und effizient verschiedene Endpunkte für Ihre Anwendung zu erstellen:

```php
$router->addRoute('test', function () {
	echo 'Sie befinden sich auf der Adresse /test';
});
```

In der Maske können Sie auch Parameter definieren, die automatisch an Ihren Callback übergeben werden:

```php
$router->addRoute('<lang cs|en>', function (string $lang) {
	echo match ($lang) {
		'cs' => 'Willkommen in der tschechischen Version unserer Website!',
		'en' => 'Willkommen in der englischen Version unserer Website!',
	};
});
```

Neben den Parametern aus der Maske kann der Callback auch Services aus dem DI-Container erhalten. Sie werden anhand des Parametertyps übergeben. Zusätzlich erhält der Parameter `$presenter` eine Instanz von [MicroPresenter |api:NetteModule\MicroPresenter], die die Route verarbeitet:

```php
$router->addRoute('<lang cs|en>', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) {
	// ...
});
```


Module
------

Haben wir mehrere Routes, die zu einem gemeinsamen [Modul |directory-structure#Presenter und Templates] gehören, verwenden wir `withModule()`. Das angegebene Modul wird dem Presenter jeder Route in der Gruppe automatisch vorangestellt und verschwindet vollständig aus der URL:

```php
$router = new RouteList;
$router->withModule('Forum') // die folgenden Routes sind Teil des Moduls Forum
	->addRoute('rss', 'Feed:rss') // Presenter ist Forum:Feed
	->addRoute('<presenter>/<action>')

	->withModule('Admin') // die folgenden Routes sind Teil des Moduls Forum:Admin
		->addRoute('sign:in', 'Sign:in');
```

Eine Alternative ist der Parameter `module`, der ebenfalls ein festes Modul setzt und es aus der URL heraushält:

```php
// URL manage/dashboard/default wird auf den Presenter Admin:Dashboard abgebildet
$router->addRoute('manage/<presenter>/<action>', [
	'module' => 'Admin',
]);
```

Der Name eines Presenters ist erst zusammen mit seinem Modul vollständig, z. B. `Front:Admin:ProductList`. Immer wenn ein solcher vollständiger Name in einem URL-Parameter landet, kodiert ihn der Router nach zwei einfachen Regeln: Jeder Doppelpunkt `:` (der Modultrenner) wird zu einem **Punkt**, und jede Wortgrenze in einem PascalCase-Namen wird zu einem **Bindestrich**. `Front:Admin:ProductList` erscheint in der URL also als `front.admin.product-list` und wird genauso wieder dekodiert. Genau deshalb erzeugt eine modulare Anwendung ohne eines der obigen Werkzeuge URLs voller Punkte.

Sowohl `withModule()` als auch der Parameter `module` vermeiden das gerade dadurch, dass sie den bekannten Modulpräfix vom Presenter-Namen abschneiden, bevor er in die URL gelangt: Da das Modul eine Konstante ist, muss es überhaupt nicht kodiert werden.

Manchmal wollen wir, dass sich das Modul selbst ändert und in der URL erscheint, und greifen daher direkt in der Maske zu `<module>`. Achten Sie dabei auf eine entscheidende Kleinigkeit: **`<module>` verschlingt den gesamten Modulpfad** - alles bis zum letzten Doppelpunkt im Presenter-Namen. Beim Presenter `Shop:Admin:Product` ist das also das Modul `Shop:Admin` und der Presenter `Product`, und weil Doppelpunkte zu Punkten werden, erhalten wir:


Subdomains
----------

Routensammlungen lassen sich nach Subdomains aufteilen:

```php
$router = new RouteList;
$router->withDomain('example.com')
	->addRoute('rss', 'Feed:rss')
	->addRoute('<presenter>/<action>');
```

Im Domainnamen lassen sich auch [#Wildcards] verwenden:

```php
$router = new RouteList;
$router->withDomain('example.%tld%')
	// ...
```


Pfadpräfix
----------

Routensammlungen lassen sich nach dem Pfad in der URL aufteilen:

```php
$router = new RouteList;
$router->withPath('eshop')
	->addRoute('rss', 'Feed:rss') // passt auf die URL /eshop/rss
	->addRoute('<presenter>/<action>'); // passt auf die URL /eshop/<presenter>/<action>
```


Kombinationen
-------------

Die obigen Gruppierungen lassen sich miteinander kombinieren:

```php
$router = (new RouteList)
	->withDomain('admin.example.com')
		->withModule('Admin')
			->addRoute(/* ... */)
			->addRoute(/* ... */)
		->end()
		->withModule('Images')
			->addRoute(/* ... */)
		->end()
	->end()
	->withDomain('example.com')
		->withPath('export')
			->addRoute(/* ... */)
			// ...
```


Query-Parameter
---------------

Masken können auch Query-Parameter enthalten (Parameter hinter dem Fragezeichen in der URL). Für sie lässt sich kein Validierungsausdruck definieren, wohl aber der Name ändern, unter dem sie an den Presenter übergeben werden:

```php
// wir wollen den Query-Parameter 'cat' in der Anwendung unter dem Namen 'categoryId' verwenden
$router->addRoute('product ? id=<productId> & cat=<categoryId>', /* ... */);
```


Foo-Parameter
-------------

Jetzt gehen wir tiefer. Foo-Parameter sind im Grunde unbenannte Parameter, die es erlauben, einen regulären Ausdruck abzugleichen. Ein Beispiel ist eine Route, die `/index`, `/index.html`, `/index.htm` und `/index.php` akzeptiert:

```php
$router->addRoute('index<? \.html?|\.php|>', /* ... */);
```

Es lässt sich auch ausdrücklich der String festlegen, der beim Erzeugen der URL verwendet wird. Der String muss direkt hinter dem Fragezeichen stehen. Die folgende Route ähnelt der vorherigen, erzeugt aber `/index.html` statt `/index`, weil der String `.html` als Wert für die Erzeugung gesetzt ist:

```php
$router->addRoute('index<?.html \.html?|\.php|>', /* ... */);
```


Integration
===========

Um den erstellten Router in die Anwendung einzubinden, müssen wir dem DI-Container von ihm erzählen. Am einfachsten ist es, eine Factory vorzubereiten, die das Router-Objekt erzeugt, und dem Container in der Konfiguration zu sagen, dass er sie verwenden soll. Nehmen wir an, wir schreiben dafür die Methode `App\Core\RouterFactory::createRouter()`:

```php
namespace App\Core;

use Nette\Application\Routers\RouteList;

class RouterFactory
{
	public static function createRouter(): RouteList
	{
		$router = new RouteList;
		$router->addRoute(/* ... */);
		return $router;
	}
}
```

Dann schreiben wir in die [Konfiguration |dependency-injection:services]:

```neon
services:
	- App\Core\RouterFactory::createRouter
```

Eventuelle Abhängigkeiten, etwa von einer Datenbank usw., werden der Factory-Methode über [Autowiring|dependency-injection:autowiring] als ihre Parameter übergeben:

```php
public static function createRouter(Nette\Database\Connection $db): RouteList
{
	// ...
}
```


SimpleRouter
============

Ein weitaus einfacherer Router als die Routensammlung ist der [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Wir verwenden ihn, wenn wir keine besonderen Anforderungen an die Form der URL haben, wenn `mod_rewrite` (oder seine Alternativen) nicht verfügbar ist oder wenn wir uns mit pretty URLs noch nicht befassen wollen.

Er erzeugt Adressen ungefähr in dieser Form:

```
http://example.com/?presenter=Product&action=detail&id=123
```

Der Parameter des Konstruktors von `SimpleRouter` ist der Standard-Presenter & die Standardaktion, also die Aktion, die ausgeführt wird, wenn wir z. B. `http://example.com/` ohne weitere Parameter öffnen.

```php
// der Standard-Presenter wird 'Home' und die Aktion 'default' sein
$router = new Nette\Application\Routers\SimpleRouter('Home:default');
```

Wir empfehlen, den SimpleRouter direkt in der [Konfiguration |dependency-injection:services] zu definieren:

```neon
services:
	- Nette\Application\Routers\SimpleRouter('Home:default')
```


SEO und Kanonisierung
=====================

Das Framework trägt zum SEO (Search Engine Optimization) bei, indem es verhindert, dass derselbe Inhalt unter verschiedenen URLs existiert. Führen mehrere Adressen zu einem bestimmten Ziel, z. B. `/index` und `/index.html`, bestimmt das Framework die erste als primär (kanonisch) und leitet die übrigen mit dem HTTP-Code 301 dorthin weiter. Dadurch indexieren Suchmaschinen die Seiten nicht doppelt und verwässern deren Page Rank nicht.

Dieser Vorgang heißt Kanonisierung. Die kanonische URL ist diejenige, die der Router erzeugt, also die erste passende Route in der Collection ohne das Flag OneWay. Deshalb führen wir in der Collection die **primären Routes zuerst** auf.

Die Kanonisierung führt der Presenter durch, mehr im Kapitel [Kanonisierung |presenters#Kanonisierung].


HTTPS
=====

Um das Protokoll HTTPS zu verwenden, muss es beim Hosting aktiviert und der Server korrekt konfiguriert sein.

Die Weiterleitung der gesamten Website auf HTTPS muss auf Serverebene eingerichtet werden, zum Beispiel über die Datei `.htaccess` im Wurzelverzeichnis unserer Anwendung, mit dem HTTP-Code 301. Die Einstellung kann sich je nach Hosting unterscheiden und sieht ungefähr so aus:

```
<IfModule mod_rewrite.c>
	RewriteEngine On
	...
	RewriteCond %{HTTPS} off
	RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
	...
</IfModule>
```

Der Router erzeugt URLs mit demselben Protokoll, mit dem die Seite geladen wurde, es muss also nichts weiter eingestellt werden.

Brauchen wir jedoch ausnahmsweise, dass verschiedene Routes unter verschiedenen Protokollen laufen, geben wir das in der Maske der Route an:

```php
// Erzeugt eine HTTP-Adresse
$router->addRoute('http://%host%/<presenter>/<action>', /* ... */);

// Erzeugt eine HTTPS-Adresse
$router->addRoute('https://%host%/<presenter>/<action>', /* ... */);
```


Debugging des Routers
=====================

Das in der [Tracy Bar |tracy:] angezeigte Routing-Panel ist ein nützlicher Helfer, der eine Liste der Routes anzeigt und außerdem die Parameter, die der Router aus der URL gewonnen hat.

Der grüne Balken mit dem Symbol ✓ stellt die Route dar, die die aktuelle URL verarbeitet hat; blaue Farbe und das Symbol ≈ kennzeichnen Routes, die die URL ebenfalls verarbeitet hätten, wenn ihnen die grüne nicht zuvorgekommen wäre. Weiter sehen wir den aktuellen Presenter & die aktuelle Aktion.

[* routing-debugger.webp *]

Kommt es zugleich wegen der [Kanonisierung |#SEO und Kanonisierung] zu einer unerwarteten Weiterleitung, lohnt sich ein Blick in den Balken *redirect* im Panel, wo Sie herausfinden, wie der Router die URL ursprünglich verstanden hat und warum er weitergeleitet hat.

.[note]
Beim Debuggen des Routers empfehlen wir, im Browser die Developer Tools zu öffnen (Strg+Umschalt+I oder Cmd+Option+I) und im Panel Network den Cache abzuschalten, damit die Weiterleitungen nicht darin gespeichert werden.


Leistung
========

Die Anzahl der Routes beeinflusst die Geschwindigkeit des Routers. Ihre Anzahl sollte auf keinen Fall einige Dutzend überschreiten. Hat Ihre Website eine zu komplizierte URL-Struktur, können Sie einen [#Eigener Router] schreiben.

Hat der Router keine Abhängigkeiten, etwa von einer Datenbank, und nimmt seine Factory keine Argumente entgegen, können wir seine kompilierte Form direkt in den DI-Container serialisieren und die Anwendung damit etwas beschleunigen.

```neon
routing:
	cache: true
```


Eigener Router
==============

Die folgenden Zeilen sind für sehr fortgeschrittene Benutzer gedacht. Sie können einen eigenen Router erstellen und ihn ganz natürlich in die Routensammlung einbinden. Der Router ist eine Implementierung des Interfaces [api:Nette\Routing\Router] mit zwei Methoden:

```php
use Nette\Http\IRequest as HttpRequest;
use Nette\Http\UrlScript;

class MyRouter implements Nette\Routing\Router
{
	public function match(HttpRequest $httpRequest): ?array
	{
		// ...
	}

	public function constructUrl(array $params, UrlScript $refUrl): ?string
	{
		// ...
	}
}
```

Die Methode `match` verarbeitet den aktuellen Request [$httpRequest |http:request], aus dem sich nicht nur die URL, sondern auch Header usw. gewinnen lassen, in ein Array mit dem Namen des Presenters und seinen Parametern. Kann sie den Request nicht verarbeiten, gibt sie null zurück. Bei der Verarbeitung des Requests müssen wir mindestens den Presenter zurückgeben; die Aktion ist optional und ist standardmäßig `default`, wenn sie nicht angegeben wird. Der Name des Presenters ist vollständig und enthält eventuelle Module:

```php
[
	'presenter' => 'Front:Home',
	'action' => 'default',
]
```

Die Methode `constructUrl` konstruiert umgekehrt aus dem Array der Parameter die resultierende absolute URL. Sie kann dabei Informationen aus dem Parameter [`$refUrl`|api:Nette\Http\UrlScript] nutzen, der die aktuelle URL ist.

Zur Routensammlung fügen Sie ihn mit `add()` hinzu:

```php
$router = new Nette\Application\Routers\RouteList;
$router->add($myRouter);
$router->addRoute(/* ... */);
// ...
```


Eigenständige Verwendung
========================

Mit eigenständiger Verwendung meinen wir den Einsatz der Fähigkeiten des Routers in einer Anwendung, die Nette Application und Presenter nicht verwendet. Fast alles, was wir in diesem Kapitel gezeigt haben, gilt auch dafür, mit diesen Unterschieden:

- für Routensammlungen verwenden wir die Klasse [api:Nette\Routing\RouteList]
- als einfachen Router die Klasse [api:Nette\Routing\SimpleRouter]
- weil das Paar `Presenter:action` nicht existiert, verwenden wir die [#Erweiterte Schreibweise]

Wir erstellen also wieder eine Methode, die uns den Router zusammenbaut, z. B.:

```php
namespace App\Core;

use Nette\Routing\RouteList;

class RouterFactory
{
	public static function createRouter(): RouteList
	{
		$router = new RouteList;
		$router->addRoute('rss.xml', [
			'controller' => 'RssFeedController',
		]);
		$router->addRoute('article/<id \d+>', [
			'controller' => 'ArticleController',
		]);
		// ...
		return $router;
	}
}
```

Wenn Sie einen DI-Container verwenden, was wir empfehlen, fügen Sie die Methode wieder in die Konfiguration ein und holen sich dann den Router samt HTTP-Request aus dem Container:

```php
$router = $container->getByType(Nette\Routing\Router::class);
$httpRequest = $container->getByType(Nette\Http\IRequest::class);
```

Oder erzeugen Sie die Objekte direkt:

```php
$router = App\Core\RouterFactory::createRouter();
$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals();
```

Nun bleibt nur noch, den Router seine Arbeit tun zu lassen:

```php
$params = $router->match($httpRequest);
if ($params === null) {
	// keine passende Route gefunden, Fehler 404 senden
	exit;
}

// die gewonnenen Parameter verarbeiten
$controller = $params['controller'];
// ...
```

Und umgekehrt den Router zum Konstruieren eines Links verwenden:

```php
$params = ['controller' => 'ArticleController', 'id' => 123];
$url = $router->constructUrl($params, $httpRequest->getUrl());
```


{{composer: nette/routing}}

Routing

Der Router kümmert sich um alles rund um URL-Adressen, damit Sie nicht mehr über sie nachdenken müssen. Wir zeigen Ihnen:

  • wie man den Router einstellt, damit die URLs so aussehen, wie Sie es möchten
  • wir sprechen über SEO und Weiterleitungen
  • und zeigen Ihnen, wie Sie einen eigenen Router schreiben

Benutzerfreundlichere URLs (auch cool oder pretty URLs genannt) sind besser nutzbar, leichter zu merken und tragen positiv zum SEO bei. Nette denkt daran und kommt Entwicklern voll entgegen. Sie können für Ihre Anwendung genau die Struktur der URL-Adressen entwerfen, die Sie möchten. Sie können sie sogar erst dann entwerfen, wenn die Anwendung bereits fertig ist, denn es sind dafür keine Eingriffe in den Code oder die Templates nötig. Sie wird nämlich auf elegante Weise an einer einzigen Stelle definiert, im Router, und ist nicht in Form von Annotationen über alle Presenter verstreut.

Der Router in Nette ist außergewöhnlich, weil er bidirektional ist. Er kann sowohl URLs aus HTTP-Requests dekodieren als auch Links erstellen. Er spielt daher eine entscheidende Rolle in Nette Application, denn er entscheidet nicht nur, welcher Presenter und welche Aktion den aktuellen Request ausführen, sondern wird auch zum Erzeugen von URLs im Template usw. verwendet.

Der Router ist jedoch nicht auf diese Verwendung beschränkt; Sie können ihn auch in Anwendungen einsetzen, die überhaupt keine Presenter verwenden, für REST-APIs usw. Mehr dazu im Abschnitt Eigenständige Verwendung.

Routensammlung

Den angenehmsten Weg, die Form der URL-Adressen in der Anwendung zu definieren, bietet die Klasse Nette\Application\Routers\RouteList. Die Definition besteht aus einer Liste sogenannter Routes, also Masken von URL-Adressen und den ihnen über eine einfache API zugeordneten Presentern und Aktionen. Die Routes müssen wir in keiner Weise benennen.

$router = new Nette\Application\Routers\RouteList;
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('article/<id>', 'Article:view');
// ...

Das Beispiel besagt: Öffnen wir im Browser https://domain.com/rss.xml, wird der Presenter Feed mit der Aktion rss angezeigt; bei https://domain.com/article/12 der Presenter Article mit der Aktion view usw. Wird keine passende Route gefunden, reagiert Nette Application mit dem Werfen einer BadRequestException, die dem Benutzer als Fehlerseite 404 Not Found angezeigt wird.

Reihenfolge der Routes

Ganz entscheidend ist die Reihenfolge, in der die einzelnen Routes aufgeführt sind, denn sie werden der Reihe nach von oben nach unten ausgewertet. Es gilt die Regel, dass wir Routes von speziell nach allgemein deklarieren:

// FALSCH: 'rss.xml' wird von der ersten Route abgefangen, die diesen String als <slug> versteht
$router->addRoute('<slug>', 'Article:view');
$router->addRoute('rss.xml', 'Feed:rss');

// GUT
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('<slug>', 'Article:view');

Auch beim Erzeugen von Links werden die Routes von oben nach unten ausgewertet:

// FALSCH: Ein Link auf 'Feed:rss' wird als 'admin/feed/rss' erzeugt
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');
$router->addRoute('rss.xml', 'Feed:rss');

// GUT
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');

Wir werden Ihnen nicht verheimlichen, dass das korrekte Zusammenstellen der Routes eine gewisse Fertigkeit erfordert. Bis Sie diese beherrschen, wird Ihnen das Routing-Panel ein nützlicher Helfer sein.

Maske und Parameter

Die Maske beschreibt den relativen Pfad vom Wurzelverzeichnis der Website. Die einfachste Maske ist eine statische URL:

$router->addRoute('products', 'Products:default');

Häufig enthalten Masken sogenannte Parameter. Diese werden in spitzen Klammern angegeben (z. B. <year>) und an den Ziel-Presenter übergeben, zum Beispiel an die Methode renderShow(int $year) oder an den persistenten Parameter $year:

$router->addRoute('chronicle/<year>', 'History:show');

Das Beispiel besagt: Öffnen wir im Browser https://example.com/chronicle/2020, wird der Presenter History mit der Aktion show und dem Parameter year: 2020 angezeigt.

Parametern können wir direkt in der Maske einen Standardwert zuweisen, wodurch sie optional werden:

$router->addRoute('chronicle/<year=2020>', 'History:show');

Die Route akzeptiert nun auch die URL https://example.com/chronicle/, die wiederum History:show mit dem Parameter year: 2020 anzeigt.

Parameter können natürlich auch der Name des Presenters und der Aktion sein. Zum Beispiel so:

$router->addRoute('<presenter>/<action>', 'Home:default');

Die angegebene Route akzeptiert z. B. URLs in der Form /article/edit oder auch /catalog/list und versteht sie als die Presenter und Aktionen Article:edit bzw. Catalog:list.

Zugleich gibt sie den Parametern presenter und action die Standardwerte Home und default, wodurch auch sie optional sind. Die Route akzeptiert also auch eine URL in der Form /article und versteht sie als Article:default. Oder umgekehrt: Ein Link auf Product:default erzeugt den Pfad /product, ein Link auf den Standard Home:default den Pfad /.

Die Maske kann nicht nur den relativen Pfad vom Wurzelverzeichnis der Website beschreiben, sondern auch einen absoluten Pfad, wenn sie mit einem Schrägstrich beginnt, oder sogar eine ganze absolute URL, wenn sie mit zwei Schrägstrichen beginnt:

// relativ zum Document-Root
$router->addRoute('<presenter>/<action>', /* ... */);

// absoluter Pfad (relativ zur Domain)
$router->addRoute('/<presenter>/<action>', /* ... */);

// absolute URL inklusive Domain (relativ zum Schema)
$router->addRoute('//<lang>.example.com/<presenter>/<action>', /* ... */);

// absolute URL inklusive Schema
$router->addRoute('https://<lang>.example.com/<presenter>/<action>', /* ... */);

Validierungsausdrücke

Für jeden Parameter lässt sich eine Validierungsbedingung mit einem regulären Ausdruck angeben. Für den Parameter id legen wir zum Beispiel mit dem Regex \d+ fest, dass er nur Ziffern enthalten darf:

$router->addRoute('<presenter>/<action>[/<id \d+>]', /* ... */);

Der Standard-Regex für alle Parameter ist [^/]+, also alles außer einem Schrägstrich. Soll ein Parameter auch Schrägstriche akzeptieren, setzen wir den Ausdruck auf .+:

// akzeptiert https://example.com/a/b/c, path ist dann 'a/b/c'
$router->addRoute('<path .+>', /* ... */);

Optionale Sequenzen

In der Maske lassen sich optionale Teile mit eckigen Klammern kennzeichnen. Jeder Teil der Maske kann optional sein und Parameter enthalten:

$router->addRoute('[<lang [a-z]{2}>/]<name>', /* ... */);

// Akzeptiert die Pfade:
//    /en/download  => lang => en, name => download
//    /download     => lang => null, name => download

Ist ein Parameter Teil einer optionalen Sequenz, wird natürlich auch er optional. Hat er keinen angegebenen Standardwert, ist er null.

Optionale Teile können auch in der Domain vorkommen:

$router->addRoute('//[<lang=en>.]example.com/<presenter>/<action>', /* ... */);

Sequenzen lassen sich beliebig verschachteln und kombinieren:

$router->addRoute(
	'[<lang [a-z]{2}>[-<sublang>]/]<name>[/page-<page=0>]',
	'Home:default',
);

// Akzeptiert die Pfade:
// 	/en/hello
// 	/en-us/hello
// 	/hello
// 	/hello/page-12

Beim Erzeugen von URLs wird die kürzeste Variante bevorzugt, es wird also alles weggelassen, was weggelassen werden kann. Deshalb erzeugt zum Beispiel die Route index[.html] den Pfad /index. Dieses Verhalten lässt sich umkehren, indem man hinter die linke eckige Klammer ein Ausrufezeichen setzt:

// akzeptiert /hello und /hello.html, erzeugt /hello
$router->addRoute('<name>[.html]', /* ... */);

// akzeptiert /hello und /hello.html, erzeugt /hello.html
$router->addRoute('<name>[!.html]', /* ... */);

Optionale Parameter (also Parameter mit einem Standardwert) ohne eckige Klammern verhalten sich im Grunde so, als wären sie folgendermaßen eingeklammert:

$router->addRoute('<presenter=Home>/<action=default>/<id=>', /* ... */);

// entspricht diesem:
$router->addRoute('[<presenter=Home>/[<action=default>/[<id>]]]', /* ... */);

Wollen wir das Verhalten des abschließenden Schrägstrichs beeinflussen, damit zum Beispiel /home statt /home/ erzeugt wird, erreichen wir das so:

$router->addRoute('[<presenter=Home>[/<action=default>[/<id>]]]', /* ... */);

Wildcards

In der Maske einer absoluten URL können wir die folgenden Wildcards verwenden, um zum Beispiel die Domain nicht in die Maske schreiben zu müssen, die sich zwischen Entwicklungs- und Produktionsumgebung unterscheiden kann:

  • %tld% = Top-Level-Domain, z. B. com oder org
  • %sld% = Second-Level-Domain, z. B. example
  • %domain% = Domain ohne Subdomains, z. B. example.com
  • %host% = gesamter Host, z. B. www.example.com
  • %basePath% = Pfad zum Wurzelverzeichnis
$router->addRoute('//www.%domain%/%basePath%/<presenter>/<action>', /* ... */);
$router->addRoute('//www.%sld%.%tld%/%basePath%/<presenter>/<action>', /* ... */);

Erweiterte Schreibweise

Das Ziel der Route, üblicherweise im Format Presenter:action geschrieben, lässt sich auch mit einem Array angeben, das die einzelnen Parameter und ihre Standardwerte definiert:

$router->addRoute('<presenter>/<action>[/<id \d+>]', [
	'presenter' => 'Home',
	'action' => 'default',
]);

Für eine genauere Angabe lässt sich eine noch ausführlichere Form verwenden, in der wir neben Standardwerten auch weitere Eigenschaften der Parameter setzen können, etwa einen regulären Ausdruck zur Validierung (siehe den Parameter id):

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>[/<id>]', [
	'presenter' => [
		Route::Value => 'Home',
	],
	'action' => [
		Route::Value => 'default',
	],
	'id' => [
		Route::Pattern => '\d+',
	],
]);

Wichtig ist: Sind im Array definierte Parameter nicht in der Pfadmaske aufgeführt, lassen sich ihre Werte nicht ändern, auch nicht über Query-Parameter, die in der URL hinter dem Fragezeichen angegeben werden.

Das ist nützlich für feste Parameter – um einer bestimmten Seite eine kurze, einprägsame URL zu geben. Damit zum Beispiel /tos immer Article:view mit id: 123 öffnet:

$router->addRoute('tos', [
	'presenter' => 'Article',
	'action' => 'view',
	'id' => 123,
]);

Filter und Übersetzungen

Den Quellcode der Anwendung schreiben wir auf Englisch, wenn die Website aber tschechische URLs haben soll, dann erzeugt ein einfaches Routing wie:

$router->addRoute('<presenter>/<action>', 'Home:default');

englische URLs, etwa /product/123 oder /cart. Wollen wir, dass Presenter und Aktionen in der URL durch tschechische Wörter dargestellt werden (z. B. /produkt/123 oder /kosik), können wir ein Übersetzungswörterbuch verwenden. Um es zu schreiben, brauchen wir bereits die „gesprächigere“ Variante des zweiten Parameters:

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>', [
	'presenter' => [
		Route::Value => 'Home',
		Route::FilterTable => [
			// String in der URL => Presenter
			'produkt' => 'Product',
			'kosik' => 'Cart',
			'katalog' => 'Catalog',
		],
	],
	'action' => [
		Route::Value => 'default',
		Route::FilterTable => [
			'seznam' => 'list',
		],
	],
]);

Mehrere Schlüssel im Übersetzungswörterbuch können auf denselben Presenter führen. So entstehen für ihn verschiedene Aliase. Der letzte Schlüssel gilt als die kanonische Variante (also die, die in der erzeugten URL steht).

Die Übersetzungstabelle lässt sich auf diese Weise für jeden Parameter verwenden. Existiert keine Übersetzung, wird der ursprüngliche Wert genommen. Dieses Verhalten können wir mit Route::FilterStrict => true ändern, die Route weist die URL dann zurück, wenn der Wert nicht im Wörterbuch steht.

Neben dem Übersetzungswörterbuch in Form eines Arrays lassen sich eigene Übersetzungsfunktionen einsetzen.

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>/<id>', [
	'presenter' => [
		Route::Value => 'Home',
		Route::FilterIn => function (string $s): string { /* ... */ },
		Route::FilterOut => function (string $s): string { /* ... */ },
	],
	'action' => 'default',
	'id' => null,
]);

Die Funktion Route::FilterIn wandelt zwischen dem Parameter in der URL und dem String um, der dann an den Presenter übergeben wird; die Funktion FilterOut sorgt für die Umwandlung in die Gegenrichtung.

Die Parameter presenter, action und module haben bereits vordefinierte Filter, die zwischen dem PascalCase- bzw. camelCase-Stil und dem in URLs verwendeten kebab-case umwandeln. Der Standardwert der Parameter wird in der Form geschrieben, in der er an die Anwendung übergeben wird (PascalCase bei Presenter und Modul, camelCase bei der Aktion), im Fall eines Presenters schreiben wir also <presenter=ProductEdit>, nicht <presenter=product-edit>.

Allgemeine Filter

Neben Filtern für konkrete Parameter können wir auch allgemeine Filter definieren, die ein assoziatives Array aller Parameter erhalten, das sie beliebig verändern und dann zurückgeben können. Allgemeine Filter werden unter dem leeren Schlüssel definiert.

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>', [
	'presenter' => 'Home',
	'action' => 'default',
	'' => [
		Route::FilterIn => function (array $params): array { /* ... */ },
		Route::FilterOut => function (array $params): array { /* ... */ },
	],
]);

Allgemeine Filter bieten die Möglichkeit, das Verhalten der Route auf absolut beliebige Weise zu verändern. Wir können sie zum Beispiel nutzen, um Parameter anhand anderer Parameter zu verändern. Etwa <presenter> und <action> anhand des aktuellen Werts des Parameters <lang> zu übersetzen.

Hat ein Parameter einen eigenen Filter definiert und existiert zugleich ein allgemeiner Filter, wird das eigene FilterIn vor dem allgemeinen ausgeführt und umgekehrt das allgemeine FilterOut vor dem eigenen. Innerhalb des allgemeinen Filters sind die Werte der Parameter presenter und action also im PascalCase- bzw. camelCase-Stil geschrieben.

Eine praktische Anwendung dieser Filter – das Erzeugen SEO-freundlicher URLs wie /article/123-how-to-bake-bread ohne jede Änderung an den Templates – finden Sie unter Schöne URLs mit Slugs.

Flag OneWay

Einwegrouten dienen dazu, die Funktionsfähigkeit alter URLs zu erhalten, die die Anwendung nicht mehr erzeugt, aber weiterhin akzeptiert. Wir kennzeichnen sie mit dem Flag OneWay:

// alte URL /product-info?id=123
$router->addRoute('product-info', 'Product:detail', oneWay: true);
// neue URL /product/123
$router->addRoute('product/<id>', 'Product:detail');

Beim Aufruf der alten URL leitet der Presenter automatisch auf die neue URL weiter, damit Suchmaschinen diese Seiten nicht doppelt indexieren (siehe SEO und Kanonisierung).

Dynamisches Routing mit Callbacks

Dynamisches Routing mit Callbacks erlaubt es, Routes direkt Funktionen (Callbacks) zuzuweisen, die beim Aufruf des jeweiligen Pfads ausgeführt werden. Diese flexible Funktionalität ermöglicht es, schnell und effizient verschiedene Endpunkte für Ihre Anwendung zu erstellen:

$router->addRoute('test', function () {
	echo 'Sie befinden sich auf der Adresse /test';
});

In der Maske können Sie auch Parameter definieren, die automatisch an Ihren Callback übergeben werden:

$router->addRoute('<lang cs|en>', function (string $lang) {
	echo match ($lang) {
		'cs' => 'Willkommen in der tschechischen Version unserer Website!',
		'en' => 'Willkommen in der englischen Version unserer Website!',
	};
});

Neben den Parametern aus der Maske kann der Callback auch Services aus dem DI-Container erhalten. Sie werden anhand des Parametertyps übergeben. Zusätzlich erhält der Parameter $presenter eine Instanz von MicroPresenter, die die Route verarbeitet:

$router->addRoute('<lang cs|en>', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) {
	// ...
});

Module

Haben wir mehrere Routes, die zu einem gemeinsamen Modul gehören, verwenden wir withModule(). Das angegebene Modul wird dem Presenter jeder Route in der Gruppe automatisch vorangestellt und verschwindet vollständig aus der URL:

$router = new RouteList;
$router->withModule('Forum') // die folgenden Routes sind Teil des Moduls Forum
	->addRoute('rss', 'Feed:rss') // Presenter ist Forum:Feed
	->addRoute('<presenter>/<action>')

	->withModule('Admin') // die folgenden Routes sind Teil des Moduls Forum:Admin
		->addRoute('sign:in', 'Sign:in');

Eine Alternative ist der Parameter module, der ebenfalls ein festes Modul setzt und es aus der URL heraushält:

// URL manage/dashboard/default wird auf den Presenter Admin:Dashboard abgebildet
$router->addRoute('manage/<presenter>/<action>', [
	'module' => 'Admin',
]);

Der Name eines Presenters ist erst zusammen mit seinem Modul vollständig, z. B. Front:Admin:ProductList. Immer wenn ein solcher vollständiger Name in einem URL-Parameter landet, kodiert ihn der Router nach zwei einfachen Regeln: Jeder Doppelpunkt : (der Modultrenner) wird zu einem Punkt, und jede Wortgrenze in einem PascalCase-Namen wird zu einem Bindestrich. Front:Admin:ProductList erscheint in der URL also als front.admin.product-list und wird genauso wieder dekodiert. Genau deshalb erzeugt eine modulare Anwendung ohne eines der obigen Werkzeuge URLs voller Punkte.

Sowohl withModule() als auch der Parameter module vermeiden das gerade dadurch, dass sie den bekannten Modulpräfix vom Presenter-Namen abschneiden, bevor er in die URL gelangt: Da das Modul eine Konstante ist, muss es überhaupt nicht kodiert werden.

Manchmal wollen wir, dass sich das Modul selbst ändert und in der URL erscheint, und greifen daher direkt in der Maske zu <module>. Achten Sie dabei auf eine entscheidende Kleinigkeit: <module> verschlingt den gesamten Modulpfad – alles bis zum letzten Doppelpunkt im Presenter-Namen. Beim Presenter Shop:Admin:Product ist das also das Modul Shop:Admin und der Presenter Product, und weil Doppelpunkte zu Punkten werden, erhalten wir:

Subdomains

Routensammlungen lassen sich nach Subdomains aufteilen:

$router = new RouteList;
$router->withDomain('example.com')
	->addRoute('rss', 'Feed:rss')
	->addRoute('<presenter>/<action>');

Im Domainnamen lassen sich auch Wildcards verwenden:

$router = new RouteList;
$router->withDomain('example.%tld%')
	// ...

Pfadpräfix

Routensammlungen lassen sich nach dem Pfad in der URL aufteilen:

$router = new RouteList;
$router->withPath('eshop')
	->addRoute('rss', 'Feed:rss') // passt auf die URL /eshop/rss
	->addRoute('<presenter>/<action>'); // passt auf die URL /eshop/<presenter>/<action>

Kombinationen

Die obigen Gruppierungen lassen sich miteinander kombinieren:

$router = (new RouteList)
	->withDomain('admin.example.com')
		->withModule('Admin')
			->addRoute(/* ... */)
			->addRoute(/* ... */)
		->end()
		->withModule('Images')
			->addRoute(/* ... */)
		->end()
	->end()
	->withDomain('example.com')
		->withPath('export')
			->addRoute(/* ... */)
			// ...

Query-Parameter

Masken können auch Query-Parameter enthalten (Parameter hinter dem Fragezeichen in der URL). Für sie lässt sich kein Validierungsausdruck definieren, wohl aber der Name ändern, unter dem sie an den Presenter übergeben werden:

// wir wollen den Query-Parameter 'cat' in der Anwendung unter dem Namen 'categoryId' verwenden
$router->addRoute('product ? id=<productId> & cat=<categoryId>', /* ... */);

Foo-Parameter

Jetzt gehen wir tiefer. Foo-Parameter sind im Grunde unbenannte Parameter, die es erlauben, einen regulären Ausdruck abzugleichen. Ein Beispiel ist eine Route, die /index, /index.html, /index.htm und /index.php akzeptiert:

$router->addRoute('index<? \.html?|\.php|>', /* ... */);

Es lässt sich auch ausdrücklich der String festlegen, der beim Erzeugen der URL verwendet wird. Der String muss direkt hinter dem Fragezeichen stehen. Die folgende Route ähnelt der vorherigen, erzeugt aber /index.html statt /index, weil der String .html als Wert für die Erzeugung gesetzt ist:

$router->addRoute('index<?.html \.html?|\.php|>', /* ... */);

Integration

Um den erstellten Router in die Anwendung einzubinden, müssen wir dem DI-Container von ihm erzählen. Am einfachsten ist es, eine Factory vorzubereiten, die das Router-Objekt erzeugt, und dem Container in der Konfiguration zu sagen, dass er sie verwenden soll. Nehmen wir an, wir schreiben dafür die Methode App\Core\RouterFactory::createRouter():

namespace App\Core;

use Nette\Application\Routers\RouteList;

class RouterFactory
{
	public static function createRouter(): RouteList
	{
		$router = new RouteList;
		$router->addRoute(/* ... */);
		return $router;
	}
}

Dann schreiben wir in die Konfiguration:

services:
	- App\Core\RouterFactory::createRouter

Eventuelle Abhängigkeiten, etwa von einer Datenbank usw., werden der Factory-Methode über Autowiring als ihre Parameter übergeben:

public static function createRouter(Nette\Database\Connection $db): RouteList
{
	// ...
}

SimpleRouter

Ein weitaus einfacherer Router als die Routensammlung ist der SimpleRouter. Wir verwenden ihn, wenn wir keine besonderen Anforderungen an die Form der URL haben, wenn mod_rewrite (oder seine Alternativen) nicht verfügbar ist oder wenn wir uns mit pretty URLs noch nicht befassen wollen.

Er erzeugt Adressen ungefähr in dieser Form:

http://example.com/?presenter=Product&action=detail&id=123

Der Parameter des Konstruktors von SimpleRouter ist der Standard-Presenter & die Standardaktion, also die Aktion, die ausgeführt wird, wenn wir z. B. http://example.com/ ohne weitere Parameter öffnen.

// der Standard-Presenter wird 'Home' und die Aktion 'default' sein
$router = new Nette\Application\Routers\SimpleRouter('Home:default');

Wir empfehlen, den SimpleRouter direkt in der Konfiguration zu definieren:

services:
	- Nette\Application\Routers\SimpleRouter('Home:default')

SEO und Kanonisierung

Das Framework trägt zum SEO (Search Engine Optimization) bei, indem es verhindert, dass derselbe Inhalt unter verschiedenen URLs existiert. Führen mehrere Adressen zu einem bestimmten Ziel, z. B. /index und /index.html, bestimmt das Framework die erste als primär (kanonisch) und leitet die übrigen mit dem HTTP-Code 301 dorthin weiter. Dadurch indexieren Suchmaschinen die Seiten nicht doppelt und verwässern deren Page Rank nicht.

Dieser Vorgang heißt Kanonisierung. Die kanonische URL ist diejenige, die der Router erzeugt, also die erste passende Route in der Collection ohne das Flag OneWay. Deshalb führen wir in der Collection die primären Routes zuerst auf.

Die Kanonisierung führt der Presenter durch, mehr im Kapitel Kanonisierung.

HTTPS

Um das Protokoll HTTPS zu verwenden, muss es beim Hosting aktiviert und der Server korrekt konfiguriert sein.

Die Weiterleitung der gesamten Website auf HTTPS muss auf Serverebene eingerichtet werden, zum Beispiel über die Datei .htaccess im Wurzelverzeichnis unserer Anwendung, mit dem HTTP-Code 301. Die Einstellung kann sich je nach Hosting unterscheiden und sieht ungefähr so aus:

<IfModule mod_rewrite.c>
	RewriteEngine On
	...
	RewriteCond %{HTTPS} off
	RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
	...
</IfModule>

Der Router erzeugt URLs mit demselben Protokoll, mit dem die Seite geladen wurde, es muss also nichts weiter eingestellt werden.

Brauchen wir jedoch ausnahmsweise, dass verschiedene Routes unter verschiedenen Protokollen laufen, geben wir das in der Maske der Route an:

// Erzeugt eine HTTP-Adresse
$router->addRoute('http://%host%/<presenter>/<action>', /* ... */);

// Erzeugt eine HTTPS-Adresse
$router->addRoute('https://%host%/<presenter>/<action>', /* ... */);

Debugging des Routers

Das in der Tracy Bar angezeigte Routing-Panel ist ein nützlicher Helfer, der eine Liste der Routes anzeigt und außerdem die Parameter, die der Router aus der URL gewonnen hat.

Der grüne Balken mit dem Symbol ✓ stellt die Route dar, die die aktuelle URL verarbeitet hat; blaue Farbe und das Symbol ≈ kennzeichnen Routes, die die URL ebenfalls verarbeitet hätten, wenn ihnen die grüne nicht zuvorgekommen wäre. Weiter sehen wir den aktuellen Presenter & die aktuelle Aktion.

Kommt es zugleich wegen der Kanonisierung zu einer unerwarteten Weiterleitung, lohnt sich ein Blick in den Balken redirect im Panel, wo Sie herausfinden, wie der Router die URL ursprünglich verstanden hat und warum er weitergeleitet hat.

Beim Debuggen des Routers empfehlen wir, im Browser die Developer Tools zu öffnen (Strg+Umschalt+I oder Cmd+Option+I) und im Panel Network den Cache abzuschalten, damit die Weiterleitungen nicht darin gespeichert werden.

Leistung

Die Anzahl der Routes beeinflusst die Geschwindigkeit des Routers. Ihre Anzahl sollte auf keinen Fall einige Dutzend überschreiten. Hat Ihre Website eine zu komplizierte URL-Struktur, können Sie einen Eigener Router schreiben.

Hat der Router keine Abhängigkeiten, etwa von einer Datenbank, und nimmt seine Factory keine Argumente entgegen, können wir seine kompilierte Form direkt in den DI-Container serialisieren und die Anwendung damit etwas beschleunigen.

routing:
	cache: true

Eigener Router

Die folgenden Zeilen sind für sehr fortgeschrittene Benutzer gedacht. Sie können einen eigenen Router erstellen und ihn ganz natürlich in die Routensammlung einbinden. Der Router ist eine Implementierung des Interfaces Nette\Routing\Router mit zwei Methoden:

use Nette\Http\IRequest as HttpRequest;
use Nette\Http\UrlScript;

class MyRouter implements Nette\Routing\Router
{
	public function match(HttpRequest $httpRequest): ?array
	{
		// ...
	}

	public function constructUrl(array $params, UrlScript $refUrl): ?string
	{
		// ...
	}
}

Die Methode match verarbeitet den aktuellen Request $httpRequest, aus dem sich nicht nur die URL, sondern auch Header usw. gewinnen lassen, in ein Array mit dem Namen des Presenters und seinen Parametern. Kann sie den Request nicht verarbeiten, gibt sie null zurück. Bei der Verarbeitung des Requests müssen wir mindestens den Presenter zurückgeben; die Aktion ist optional und ist standardmäßig default, wenn sie nicht angegeben wird. Der Name des Presenters ist vollständig und enthält eventuelle Module:

[
	'presenter' => 'Front:Home',
	'action' => 'default',
]

Die Methode constructUrl konstruiert umgekehrt aus dem Array der Parameter die resultierende absolute URL. Sie kann dabei Informationen aus dem Parameter $refUrl nutzen, der die aktuelle URL ist.

Zur Routensammlung fügen Sie ihn mit add() hinzu:

$router = new Nette\Application\Routers\RouteList;
$router->add($myRouter);
$router->addRoute(/* ... */);
// ...

Eigenständige Verwendung

Mit eigenständiger Verwendung meinen wir den Einsatz der Fähigkeiten des Routers in einer Anwendung, die Nette Application und Presenter nicht verwendet. Fast alles, was wir in diesem Kapitel gezeigt haben, gilt auch dafür, mit diesen Unterschieden:

Wir erstellen also wieder eine Methode, die uns den Router zusammenbaut, z. B.:

namespace App\Core;

use Nette\Routing\RouteList;

class RouterFactory
{
	public static function createRouter(): RouteList
	{
		$router = new RouteList;
		$router->addRoute('rss.xml', [
			'controller' => 'RssFeedController',
		]);
		$router->addRoute('article/<id \d+>', [
			'controller' => 'ArticleController',
		]);
		// ...
		return $router;
	}
}

Wenn Sie einen DI-Container verwenden, was wir empfehlen, fügen Sie die Methode wieder in die Konfiguration ein und holen sich dann den Router samt HTTP-Request aus dem Container:

$router = $container->getByType(Nette\Routing\Router::class);
$httpRequest = $container->getByType(Nette\Http\IRequest::class);

Oder erzeugen Sie die Objekte direkt:

$router = App\Core\RouterFactory::createRouter();
$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals();

Nun bleibt nur noch, den Router seine Arbeit tun zu lassen:

$params = $router->match($httpRequest);
if ($params === null) {
	// keine passende Route gefunden, Fehler 404 senden
	exit;
}

// die gewonnenen Parameter verarbeiten
$controller = $params['controller'];
// ...

Und umgekehrt den Router zum Konstruieren eines Links verwenden:

$params = ['controller' => 'ArticleController', 'id' => 123];
$url = $router->constructUrl($params, $httpRequest->getUrl());