Presenter

Wir werden lernen, wie man Presenter und Templates in Nette schreibt. Nach dem Lesen werden Sie wissen:

  • wie ein Presenter funktioniert
  • was persistente Parameter sind
  • wie Templates gerendert werden

Wir wissen bereits, dass ein Presenter eine Klasse ist, die eine bestimmte Seite einer Webanwendung repräsentiert, z. B. die Homepage, ein Produkt in einem E-Shop, ein Anmeldeformular, einen Sitemap-Feed usw. Eine Anwendung kann einen bis tausende Presenter haben. In anderen Frameworks werden sie auch Controller genannt.

Normalerweise ist mit dem Begriff Presenter ein Nachkomme der Klasse Nette\Application\UI\Presenter gemeint, der für die Generierung von Weboberflächen geeignet ist und dem wir uns im Rest dieses Kapitels widmen werden. Im allgemeinen Sinn ist ein Presenter jedes Objekt, das das Interface Nette\Application\IPresenter implementiert.

Lebenszyklus des Presenters

Die Aufgabe des Presenters ist es, eine Anfrage zu bearbeiten und eine Antwort zurückzugeben (dies kann eine HTML-Seite, ein Bild, eine Weiterleitung usw. sein).

Zu Beginn wird ihm also eine Anfrage übergeben. Dies ist keine direkte HTTP-Anfrage, sondern ein Objekt Nette\Application\Request, in das die HTTP-Anfrage mithilfe des Routers umgewandelt wurde. Mit diesem Objekt kommen wir normalerweise nicht in Berührung, da der Presenter die Verarbeitung der Anfrage geschickt an weitere Methoden delegiert, die wir uns jetzt ansehen werden.

Lebenszyklus des Presenters

Das Bild zeigt eine Liste von Methoden, die der Reihe nach von oben nach unten aufgerufen werden, sofern sie existieren. Keine davon muss existieren, wir können einen völlig leeren Presenter ohne eine einzige Methode haben und darauf eine einfache statische Website aufbauen.

__construct()

Der Konstruktor gehört nicht ganz zum Lebenszyklus des Presenters, da er im Moment der Objekterstellung aufgerufen wird. Aber wir erwähnen ihn wegen seiner Wichtigkeit. Der Konstruktor (zusammen mit der inject-Methode) dient zur Übergabe von Abhängigkeiten.

Ein Presenter sollte nicht die Geschäftslogik der Anwendung handhaben, aus der Datenbank schreiben und lesen, Berechnungen durchführen usw. Dafür gibt es Klassen aus der Schicht, die wir als Model bezeichnen. Zum Beispiel kann die Klasse ArticleRepository für das Laden und Speichern von Artikeln zuständig sein. Damit der Presenter damit arbeiten kann, lässt er sie sich mittels Dependency Injection übergeben:

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private ArticleRepository $articles,
	) {
	}
}

startup()

Unmittelbar nach Erhalt der Anfrage wird die Methode startup() aufgerufen. Sie können sie zur Initialisierung von Properties, zur Überprüfung von Benutzerberechtigungen usw. verwenden. Es ist erforderlich, dass die Methode immer den Vorfahren parent::startup() aufruft.

action<Action>(args...)

Ähnlich der Methode render<View>(). Während render<View>() dazu dient, Daten für ein bestimmtes Template vorzubereiten, das anschließend gerendert wird, wird in action<Action>() die Anfrage ohne Bezug zum Rendern des Templates verarbeitet. Zum Beispiel werden Daten verarbeitet, der Benutzer an- oder abgemeldet usw., und danach woandershin weitergeleitet.

Wichtig ist, dass action<Action>() vor render<View>() aufgerufen wird, sodass wir darin gegebenenfalls den weiteren Verlauf ändern können, d.h. das zu rendernde Template und auch die aufzurufende render<View>()-Methode ändern können. Und zwar mittels setView('anderesView').

Der Methode werden Parameter aus der Anfrage übergeben. Es ist möglich und empfohlen, den Parametern Typen anzugeben, z.B. actionShow(int $id, ?string $slug = null) – wenn der Parameter id fehlt oder kein Integer ist, gibt der Presenter einen Fehler 404 zurück und beendet die Tätigkeit.

handle<Signal>(args...)

Diese Methode verarbeitet sogenannte Signale, die wir im Kapitel über Komponenten kennenlernen werden. Sie ist nämlich hauptsächlich für Komponenten und die Verarbeitung von AJAX-Anfragen gedacht.

Der Methode werden Parameter aus der Anfrage übergeben, wie im Fall von action<Action>(), einschließlich Typüberprüfung.

beforeRender()

Die Methode beforeRender, wie der Name schon sagt, wird vor jeder render<View>()-Methode aufgerufen. Sie wird für die gemeinsame Konfiguration des Templates, die Übergabe von Variablen für das Layout und ähnliches verwendet.

render<View>(args...)

Der Ort, an dem wir das Template für das anschließende Rendern vorbereiten, ihm Daten übergeben usw.

Der Methode werden Parameter aus der Anfrage übergeben, wie im Fall von action<Action>(), einschließlich Typüberprüfung.

public function renderShow(int $id): void
{
	// Wir holen Daten aus dem Model und übergeben sie an das Template
	$this->template->article = $this->articles->getById($id);
}

afterRender()

Die Methode afterRender, wie der Name wiederum andeutet, wird nach jeder render<View>()-Methode aufgerufen. Sie wird eher selten verwendet.

shutdown()

Wird am Ende des Lebenszyklus des Presenters aufgerufen.

Ein guter Rat, bevor wir weitermachen. Ein Presenter kann, wie man sieht, mehrere Aktionen/Views bedienen, also mehrere render<View>()-Methoden haben. Wir empfehlen jedoch, Presenter mit einer oder möglichst wenigen Aktionen zu entwerfen.

Senden der Antwort

Die Antwort des Presenters ist normalerweise das Rendern eines Templates mit einer HTML-Seite, es kann aber auch das Senden einer Datei, JSON oder eine Weiterleitung zu einer anderen Seite sein.

Wir können jederzeit während des Lebenszyklus mit einer der folgenden Methoden eine Antwort senden und gleichzeitig den Presenter beenden:

Wenn Sie keine dieser Methoden aufrufen, greift der Presenter automatisch auf das Rendern des Templates zurück. Warum? Weil wir in 99 % der Fälle ein Template rendern möchten, daher betrachtet der Presenter dieses Verhalten als Standard und möchte uns die Arbeit erleichtern.

Der Presenter verfügt über die Methode link(), mit der URL-Links zu anderen Presentern erstellt werden können. Der erste Parameter ist der Ziel-Presenter & Aktion, gefolgt von den übergebenen Argumenten, die als Array angegeben werden können:

$url = $this->link('Product:show', $id);

$url = $this->link('Product:show', [$id, 'lang' => 'cs']);

Im Template werden Links zu anderen Presentern & Aktionen auf diese Weise erstellt:

<a n:href="Product:show $id">Produktdetail</a>

Statt der echten URL schreiben Sie einfach das bekannte Paar Presenter:action und geben eventuelle Parameter an. Der Trick liegt in n:href, das besagt, dass dieses Attribut von Latte verarbeitet wird und die echte URL generiert. In Nette müssen Sie also überhaupt nicht über URLs nachdenken, nur über Presenter und Aktionen.

Weitere Informationen finden Sie im Kapitel Erstellen von URL-Links.

Weiterleitung

Zum Wechsel zu einem anderen Presenter dienen die Methoden redirect() und forward(), die eine sehr ähnliche Syntax wie die Methode link() haben.

Die Methode forward() wechselt sofort zum neuen Presenter ohne HTTP-Weiterleitung:

$this->forward('Product:show');

Ein Beispiel für eine sogenannte temporäre Weiterleitung mit dem HTTP-Code 302 (oder 303, wenn die Methode der aktuellen Anfrage POST ist):

$this->redirect('Product:show', $id);

Eine permanente Weiterleitung mit dem HTTP-Code 301 erreichen Sie so:

$this->redirectPermanent('Product:show', $id);

Zu einer anderen URL außerhalb der Anwendung kann mit der Methode redirectUrl() weitergeleitet werden. Als zweiter Parameter kann der HTTP-Code angegeben werden, Standard ist 302 (oder 303, wenn die Methode der aktuellen Anfrage POST ist):

$this->redirectUrl('https://nette.org');

Die Weiterleitung beendet sofort die Tätigkeit des Presenters durch Auslösen der sogenannten stillen Beendigungs-Ausnahme Nette\Application\AbortException.

Vor der Weiterleitung können Flash-Nachrichten gesendet werden, also Nachrichten, die nach der Weiterleitung im Template angezeigt werden.

Flash-Nachrichten

Dies sind Nachrichten, die normalerweise über das Ergebnis einer Operation informieren. Ein wichtiges Merkmal von Flash-Nachrichten ist, dass sie auch nach einer Weiterleitung im Template verfügbar sind. Auch nach der Anzeige bleiben sie noch weitere 30 Sekunden aktiv – zum Beispiel für den Fall, dass der Benutzer aufgrund einer fehlerhaften Übertragung die Seite neu lädt – die Nachricht verschwindet also nicht sofort.

Rufen Sie einfach die Methode flashMessage() auf, und der Presenter kümmert sich um die Übergabe an das Template. Der erste Parameter ist der Text der Nachricht und der optionale zweite Parameter ihr Typ (error, warning, info usw.). Die Methode flashMessage() gibt eine Instanz der Flash-Nachricht zurück, der weitere Informationen hinzugefügt werden können.

$this->flashMessage('Der Eintrag wurde gelöscht.');
$this->redirect(/* ... */); // und wir leiten weiter

Im Template stehen diese Nachrichten in der Variable $flashes als stdClass-Objekte zur Verfügung, die die Eigenschaften message (Nachrichtentext), type (Nachrichtentyp) enthalten und die bereits erwähnten Benutzerinformationen enthalten können. Wir rendern sie zum Beispiel so:

{foreach $flashes as $flash}
	<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}

Fehler 404 und Co.

Wenn die Anfrage nicht erfüllt werden kann, zum Beispiel weil der Artikel, den wir anzeigen möchten, nicht in der Datenbank existiert, werfen wir einen 404-Fehler mit der Methode error(?string $message = null, int $httpCode = 404) aus.

public function renderShow(int $id): void
{
	$article = $this->articles->getById($id);
	if (!$article) {
		$this->error();
	}
	// ...
}

Der HTTP-Fehlercode kann als zweiter Parameter übergeben werden, Standard ist 404. Die Methode funktioniert so, dass sie die Ausnahme Nette\Application\BadRequestException auslöst, woraufhin die Application die Steuerung an den Error-Presenter übergibt. Dies ist ein Presenter, dessen Aufgabe es ist, eine Seite anzuzeigen, die über den aufgetretenen Fehler informiert. Die Einstellung des Error-Presenters erfolgt in der Anwendungskonfiguration.

Senden von JSON

Ein Beispiel für eine Action-Methode, die Daten im JSON-Format sendet und den Presenter beendet:

public function actionData(): void
{
	$data = ['hello' => 'nette'];
	$this->sendJson($data);
}

Anfrageparameter

Der Presenter und auch jede Komponente erhalten ihre Parameter aus der HTTP-Anfrage. Ihren Wert erhalten Sie mit der Methode getParameter($name) oder getParameters(). Die Werte sind Zeichenketten oder Arrays von Zeichenketten, es handelt sich im Grunde um Rohdaten, die direkt aus der URL stammen.

Für mehr Komfort empfehlen wir, die Parameter über Properties zugänglich zu machen. Markieren Sie sie einfach mit dem Attribut #[Parameter]:

use Nette\Application\Attributes\Parameter;  // diese Zeile ist wichtig

class HomePresenter extends Nette\Application\UI\Presenter
{
	#[Parameter]
	public string $theme; // muss public sein
}

Bei der Property empfehlen wir, auch den Datentyp anzugeben (z.B. string), und Nette wandelt den Wert entsprechend automatisch um. Die Parameterwerte können auch validiert werden.

Beim Erstellen eines Links kann der Wert der Parameter direkt festgelegt werden:

<a n:href="Home:default theme: dark">klicken</a>

Persistente Parameter

Persistente Parameter dienen dazu, den Zustand zwischen verschiedenen Anfragen aufrechtzuerhalten. Ihr Wert bleibt auch nach dem Klicken auf einen Link gleich. Im Gegensatz zu Daten in der Session werden sie in der URL übertragen. Und das völlig automatisch, es ist also nicht notwendig, sie explizit in link() oder n:href anzugeben.

Ein Anwendungsbeispiel? Sie haben eine mehrsprachige Anwendung. Die aktuelle Sprache ist ein Parameter, der ständig Teil der URL sein muss. Aber es wäre unglaublich mühsam, ihn in jedem Link anzugeben. Also machen Sie daraus einen persistenten Parameter lang, und er wird von selbst übertragen. Großartig!

Das Erstellen eines persistenten Parameters ist in Nette extrem einfach. Erstellen Sie einfach eine öffentliche Property und markieren Sie sie mit einem Attribut: (früher wurde /** @persistent */ verwendet)

use Nette\Application\Attributes\Persistent;  // diese Zeile ist wichtig

class ProductPresenter extends Nette\Application\UI\Presenter
{
	#[Persistent]
	public string $lang; // muss public sein
}

Wenn $this->lang beispielsweise den Wert 'en' hat, dann werden auch Links, die mit link() oder n:href erstellt wurden, den Parameter lang=en enthalten. Und nach dem Klicken auf den Link wird wieder $this->lang = 'en' sein.

Bei der Property empfehlen wir, auch den Datentyp anzugeben (z.B. string), und Sie können auch einen Standardwert angeben. Die Parameterwerte können validiert werden.

Persistente Parameter werden standardmäßig zwischen allen Aktionen des jeweiligen Presenters übertragen. Damit sie auch zwischen mehreren Presentern übertragen werden, müssen sie entweder definiert werden:

  • in einem gemeinsamen Vorfahren, von dem die Presenter erben
  • in einem Trait, den die Presenter verwenden:
trait LanguageAware
{
	#[Persistent]
	public string $lang;
}

class ProductPresenter extends Nette\Application\UI\Presenter
{
	use LanguageAware;
}

Beim Erstellen eines Links kann der Wert eines persistenten Parameters geändert werden:

<a n:href="Product:show $id, lang: cs">Detail auf Tschechisch</a>

Oder er kann zurückgesetzt werden, d.h. aus der URL entfernt werden. Dann nimmt er seinen Standardwert an:

<a n:href="Product:show $id, lang: null">klicken</a>

Interaktive Komponenten

Presenter haben ein eingebautes Komponentensystem. Komponenten sind separate, wiederverwendbare Einheiten, die wir in Presenter einfügen. Dies können Formulare, Datagrids, Menüs sein, eigentlich alles, was sinnvoll wiederverwendet werden kann.

Wie werden Komponenten in den Presenter eingefügt und anschließend verwendet? Das erfahren Sie im Kapitel Komponenten. Sie werden sogar herausfinden, was sie mit Hollywood gemeinsam haben.

Und wo kann ich Komponenten bekommen? Auf der Seite Componette finden Sie Open-Source-Komponenten sowie eine Reihe weiterer Add-ons für Nette, die von Freiwilligen aus der Community rund um das Framework hier platziert wurden.

Wir gehen in die Tiefe

Mit dem, was wir bisher in diesem Kapitel gezeigt haben, werden Sie wahrscheinlich vollkommen auskommen. Die folgenden Zeilen sind für diejenigen gedacht, die sich eingehender für Presenter interessieren und alles wissen möchten.

Validierung von Parametern

Die Werte der Anfrageparameter und persistenten Parameter, die aus der URL empfangen werden, schreibt die Methode loadState() in die Properties. Sie überprüft auch, ob der bei der Property angegebene Datentyp übereinstimmt, andernfalls antwortet sie mit einem 404-Fehler und die Seite wird nicht angezeigt.

Vertrauen Sie niemals blind Parametern, da sie vom Benutzer leicht in der URL überschrieben werden können. So überprüfen wir beispielsweise, ob die Sprache $this->lang zu den unterstützten gehört. Ein geeigneter Weg ist, die erwähnte Methode loadState() zu überschreiben:

class ProductPresenter extends Nette\Application\UI\Presenter
{
	#[Persistent]
	public string $lang;

	public function loadState(array $params): void
	{
		parent::loadState($params); // hier wird $this->lang gesetzt
		// es folgt die eigene Überprüfung des Wertes:
		if (!in_array($this->lang, ['en', 'cs'])) {
			$this->error();
		}
	}
}

Speichern und Wiederherstellen der Anfrage

Die Anfrage, die der Presenter bearbeitet, ist ein Objekt Nette\Application\Request und wird von der Presenter-Methode getRequest() zurückgegeben.

Die aktuelle Anfrage kann in der Session gespeichert oder daraus wiederhergestellt und vom Presenter erneut ausgeführt werden. Dies ist nützlich, zum Beispiel in einer Situation, in der ein Benutzer ein Formular ausfüllt und seine Anmeldung abläuft. Um die Daten nicht zu verlieren, speichern wir vor der Weiterleitung zur Anmeldeseite die aktuelle Anfrage mit $reqId = $this->storeRequest() in der Session. Diese Methode gibt ihren Identifikator in Form einer kurzen Zeichenkette zurück, den wir als Parameter an den Anmelde-Presenter übergeben.

Nach der Anmeldung rufen wir die Methode $this->restoreRequest($reqId) auf, die die Anfrage aus der Session holt und dorthin weiterleitet (forward). Die Methode überprüft dabei, ob die Anfrage vom selben Benutzer erstellt wurde, der sich jetzt angemeldet hat. Wenn sich ein anderer Benutzer angemeldet hat oder der Schlüssel ungültig ist, tut sie nichts und das Programm läuft weiter.

Schauen Sie sich die Anleitung Wie man zu einer früheren Seite zurückkehrt an.

Kanonisierung

Presenter haben eine wirklich großartige Eigenschaft, die zu besserem SEO (Optimierung der Auffindbarkeit im Internet) beiträgt. Sie verhindern automatisch das Vorhandensein von doppeltem Inhalt unter verschiedenen URLs. Wenn zu einem bestimmten Ziel mehrere URL-Adressen führen, z.B. /index und /index?page=1, bestimmt das Framework eine davon als primäre (kanonische) und leitet die anderen mit dem HTTP-Code 301 dorthin weiter. Dank dessen indizieren Suchmaschinen die Seiten nicht zweimal und verwässern nicht deren Page Rank.

Dieser Prozess wird Kanonisierung genannt. Die kanonische URL ist diejenige, die vom Router generiert wird, in der Regel also die erste passende Route in der Sammlung.

Die Kanonisierung ist standardmäßig aktiviert und kann über $this->autoCanonicalize = false deaktiviert werden.

Bei einer AJAX- oder POST-Anfrage erfolgt keine Weiterleitung, da dies zu Datenverlust führen würde oder aus SEO-Sicht keinen Mehrwert hätte.

Sie können die Kanonisierung auch manuell mit der Methode canonicalize() auslösen, der ähnlich wie der Methode link() der Presenter, die Aktion und die Parameter übergeben werden. Sie erstellt einen Link und vergleicht ihn mit der aktuellen URL-Adresse. Wenn sie sich unterscheiden, wird auf den generierten Link weitergeleitet.

public function actionShow(int $id, ?string $slug = null): void
{
	$realSlug = $this->facade->getSlugForId($id);
	// leitet weiter, wenn $slug sich von $realSlug unterscheidet
	$this->canonicalize('Product:show', [$id, $realSlug]);
}

Ereignisse

Neben den Methoden startup(), beforeRender() und shutdown(), die als Teil des Lebenszyklus des Presenters aufgerufen werden, können noch weitere Funktionen definiert werden, die automatisch aufgerufen werden sollen. Der Presenter definiert sogenannte Ereignisse, deren Handler Sie zu den Arrays $onStartup, $onRender und $onShutdown hinzufügen.

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct()
	{
		$this->onStartup[] = function () {
			// ...
		};
	}
}

Die Handler im Array $onStartup werden kurz vor der Methode startup() aufgerufen, $onRender zwischen beforeRender() und render<View>() und schließlich $onShutdown kurz vor shutdown().

Antworten

Die Antwort, die ein Presenter zurückgibt, ist ein Objekt, das das Interface Nette\Application\Response implementiert. Es stehen eine Reihe von vorbereiteten Antworten zur Verfügung:

Antworten werden mit der Methode sendResponse() gesendet:

use Nette\Application\Responses;

// Einfacher Text
$this->sendResponse(new Responses\TextResponse('Hello Nette!'));

// Sendet eine Datei
$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf'));

// Die Antwort wird ein Callback sein
$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) {
	if ($httpResponse->getHeader('Content-Type') === 'text/html') {
		echo '<h1>Hello</h1>';
	}
};
$this->sendResponse(new Responses\CallbackResponse($callback));

Zugriffsbeschränkung mit #[Requires]

Das Attribut #[Requires] bietet erweiterte Möglichkeiten zur Zugriffsbeschränkung für Presenter und deren Methoden. Es kann verwendet werden, um HTTP-Methoden zu spezifizieren, eine AJAX-Anfrage zu erfordern, den Zugriff auf den gleichen Ursprung (Same Origin) zu beschränken und den Zugriff nur über Forwarding zu erlauben. Das Attribut kann sowohl auf Presenter-Klassen als auch auf einzelne Methoden action<Action>(), render<View>(), handle<Signal>() und createComponent<Name>() angewendet werden.

Sie können folgende Beschränkungen festlegen:

  • auf HTTP-Methoden: #[Requires(methods: ['GET', 'POST'])]
  • Erfordern einer AJAX-Anfrage: #[Requires(ajax: true)]
  • Zugriff nur vom gleichen Ursprung: #[Requires(sameOrigin: true)]
  • Zugriff nur über Forwarding: #[Requires(forward: true)]
  • Beschränkung auf bestimmte Aktionen: #[Requires(actions: 'default')]

Details finden Sie in der Anleitung Verwendung des Requires-Attributs.

Überprüfung der HTTP-Methode

Presenter in Nette überprüfen automatisch die HTTP-Methode jeder eingehenden Anfrage. Der Grund für diese Überprüfung ist hauptsächlich die Sicherheit. Standardmäßig sind die Methoden GET, POST, HEAD, PUT, DELETE, PATCH erlaubt.

Wenn Sie zusätzlich beispielsweise die Methode OPTIONS erlauben möchten, verwenden Sie dazu das Attribut #[Requires] (ab Nette Application v3.2):

#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])]
class MyPresenter extends Nette\Application\UI\Presenter
{
}

In Version 3.1 erfolgt die Überprüfung in checkHttpMethod(), die prüft, ob die in der Anfrage angegebene Methode im Array $presenter->allowedMethods enthalten ist. Fügen Sie die Methode wie folgt hinzu:

class MyPresenter extends Nette\Application\UI\Presenter
{
    protected function checkHttpMethod(): void
    {
        $this->allowedMethods[] = 'OPTIONS';
        parent::checkHttpMethod();
    }
}

Es ist wichtig zu betonen, dass wenn Sie die Methode OPTIONS zulassen, Sie diese anschließend auch entsprechend in Ihrem Presenter behandeln müssen. Die Methode wird oft als sogenannter Preflight Request verwendet, den der Browser automatisch vor der eigentlichen Anfrage sendet, wenn überprüft werden muss, ob die Anfrage gemäß der CORS (Cross-Origin Resource Sharing) Richtlinie zulässig ist. Wenn Sie die Methode zulassen, aber keine korrekte Antwort implementieren, kann dies zu Inkonsistenzen und potenziellen Sicherheitsproblemen führen.

Weitere Lektüre

Version: 4.0