Interaktive Komponenten

Komponenten sind eigenständige, wiederverwendbare Objekte, die wir in Seiten einfügen. Das können Formulare, Datengrids, Umfragen sein, eigentlich alles, was sinnvoll wiederverwendet werden kann. Wir zeigen Ihnen:

  • Wie man Komponenten verwendet?
  • Wie man sie schreibt?
  • Was sind Signale?

Nette verfügt über ein eingebautes Komponentensystem. Etwas Ähnliches kennen Kenner vielleicht noch aus Delphi oder ASP.NET Web Forms, und React oder Vue.js bauen auf etwas entfernt Ähnlichem auf. In der Welt der PHP-Frameworks ist dies jedoch eine einzigartige Angelegenheit.

Dabei beeinflussen Komponenten den Ansatz zur Anwendungsentwicklung grundlegend. Sie können Seiten aus vorgefertigten Einheiten zusammenstellen. Benötigen Sie ein Datagrid in der Administration? Sie finden es auf Componette, einem Repository für Open-Source-Add-ons (also nicht nur Komponenten) für Nette, und fügen es einfach in den Presenter ein.

Sie können beliebig viele Komponenten in einen Presenter integrieren. Und in einige Komponenten können Sie weitere Komponenten einfügen. So entsteht ein Komponentenbaum, dessen Wurzel der Presenter ist.

Factory-Methoden

Wie werden Komponenten in den Presenter eingefügt und anschließend verwendet? Normalerweise über Factory-Methoden.

Eine Komponenten-Factory stellt eine elegante Möglichkeit dar, Komponenten erst dann zu erstellen, wenn sie tatsächlich benötigt werden (lazy / on demand). Der ganze Zauber besteht darin, eine Methode mit dem Namen createComponent<Name>() zu implementieren, wobei <Name> der Name der zu erstellenden Komponente ist, und die die Komponente erstellt und zurückgibt.

class DefaultPresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentPoll(): PollControl
	{
		$poll = new PollControl;
		$poll->items = $this->item;
		return $poll;
	}
}

Dadurch, dass alle Komponenten in separaten Methoden erstellt werden, gewinnt der Code an Übersichtlichkeit.

Komponentennamen beginnen immer mit einem Kleinbuchstaben, obwohl sie im Methodennamen großgeschrieben werden.

Factories werden niemals direkt aufgerufen, sie rufen sich selbst auf, wenn die Komponente zum ersten Mal verwendet wird. Dadurch wird die Komponente zum richtigen Zeitpunkt erstellt und nur dann, wenn sie tatsächlich benötigt wird. Wenn wir die Komponente nicht verwenden (z. B. bei einer AJAX-Anfrage, bei der nur ein Teil der Seite übertragen wird, oder beim Caching des Templates), wird sie überhaupt nicht erstellt und wir sparen Serverleistung.

// Wir greifen auf die Komponente zu, und wenn es das erste Mal war,
// wird createComponentPoll() aufgerufen, die sie erstellt
$poll = $this->getComponent('poll');
// alternative Syntax: $poll = $this['poll'];

Im Template kann eine Komponente mit dem Tag {control} gerendert werden. Es ist daher nicht notwendig, Komponenten manuell an das Template zu übergeben.

<h2>Stimmen Sie ab</h2>

{control poll}

Hollywood Style

Komponenten verwenden üblicherweise eine frische Technik, die wir gerne Hollywood Style nennen. Sie kennen sicher den geflügelten Satz, den Teilnehmer von Filmcastings so oft hören: „Rufen Sie uns nicht an, wir rufen Sie an“. Und genau darum geht es.

In Nette sagen Sie dem Framework nämlich, anstatt ständig nachfragen zu müssen („wurde das Formular abgeschickt?“, „war es gültig?“ oder „hat der Benutzer diesen Button gedrückt?“), „wenn das passiert, ruf diese Methode auf“ und überlassen die weitere Arbeit ihm. Wenn Sie in JavaScript programmieren, kennen Sie diesen Programmierstil genau. Sie schreiben Funktionen, die aufgerufen werden, wenn ein bestimmtes Ereignis eintritt. Und die Sprache übergibt ihnen die entsprechenden Parameter.

Dies verändert die Sichtweise auf das Schreiben von Anwendungen grundlegend. Je mehr Aufgaben Sie dem Framework überlassen können, desto weniger Arbeit haben Sie. Und desto weniger können Sie vielleicht übersehen.

Wir schreiben eine Komponente

Unter dem Begriff Komponente verstehen wir normalerweise einen Nachfahren der Klasse Nette\Application\UI\Control. (Genauer wäre es also, den Begriff „Controls“ zu verwenden, aber „Kontrollen“ hat im Deutschen eine ganz andere Bedeutung, und eher haben sich „Komponenten“ durchgesetzt.) Der Presenter Nette\Application\UI\Presenter selbst ist übrigens auch ein Nachfahre der Klasse Control.

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

Rendern

Wir wissen bereits, dass zum Rendern einer Komponente der Tag {control componentName} verwendet wird. Dieser ruft eigentlich die Methode render() der Komponente auf, in der wir uns um das Rendern kümmern. Uns steht, genau wie im Presenter, eine Latte-Vorlage in der Variablen $this->template zur Verfügung, an die wir Parameter übergeben. Im Gegensatz zum Presenter müssen wir die Template-Datei angeben und sie rendern lassen:

public function render(): void
{
	// Wir fügen einige Parameter in das Template ein
	$this->template->param = $value;
	// und rendern es
	$this->template->render(__DIR__ . '/poll.latte');
}

Der {control}-Tag ermöglicht es, Parameter an die render()-Methode zu übergeben:

{control poll $id, $message}
public function render(int $id, string $message): void
{
	// ...
}

Manchmal kann eine Komponente aus mehreren Teilen bestehen, die wir getrennt rendern möchten. Für jeden davon erstellen wir eine eigene Rendering-Methode, hier im Beispiel etwa renderPaginator():

public function renderPaginator(): void
{
	// ...
}

Und im Template rufen wir sie dann auf mit:

{control poll:paginator}

Zum besseren Verständnis ist es gut zu wissen, wie dieser Tag in PHP übersetzt wird.

{control poll}
{control poll:paginator 123, 'hello'}

wird übersetzt als:

$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');

Die Methode getComponent() gibt die Komponente poll zurück und ruft für diese Komponente die Methode render() bzw. renderPaginator() auf, wenn im Tag nach dem Doppelpunkt eine andere Rendering-Art angegeben ist.

Achtung, wenn irgendwo in den Parametern => vorkommt, werden alle Parameter in ein Array verpackt und als erstes Argument übergeben:

{control poll, id: 123, message: 'hello'}

wird übersetzt als:

$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);

Rendern einer Sub-Komponente:

{control cartControl-someForm}

wird übersetzt als:

$control->getComponent("cartControl-someForm")->render();

Komponenten, ebenso wie Presenter, übergeben automatisch einige nützliche Variablen an die Templates:

  • $basePath ist der absolute URL-Pfad zum Wurzelverzeichnis (z. B. /eshop)
  • $baseUrl ist die absolute URL zum Wurzelverzeichnis (z. B. http://localhost/eshop)
  • $user ist das Objekt, das den Benutzer repräsentiert
  • $presenter ist der aktuelle Presenter
  • $control ist die aktuelle Komponente
  • $flashes ist ein Array von Nachrichten, die mit der Funktion flashMessage() gesendet wurden

Signal

Wir wissen bereits, dass die Navigation in einer Nette-Anwendung im Verlinken oder Weiterleiten auf Paare von Presenter:action besteht. Aber was ist, wenn wir nur eine Aktion auf der aktuellen Seite durchführen wollen? Zum Beispiel die Sortierreihenfolge von Spalten in einer Tabelle ändern; einen Eintrag löschen; den Hell-/Dunkelmodus umschalten; ein Formular absenden; in einer Umfrage abstimmen; usw.

Diese Art von Anfragen wird als Signale bezeichnet. Und ähnlich wie Aktionen die Methoden action<Action>() oder render<Action>() aufrufen, rufen Signale die Methoden handle<Signal>() auf. Während der Begriff Aktion (oder View) rein mit Presentern zusammenhängt, betreffen Signale alle Komponenten. Und somit auch Presenter, da UI\Presenter ein Nachfahre von UI\Control ist.

public function handleClick(int $x, int $y): void
{
	// ... Verarbeitung des Signals ...
}

Einen Link, der ein Signal aufruft, erstellen wir auf die übliche Weise, d. h. im Template mit dem Attribut n:href oder dem Tag {link}, im Code mit der Methode link(). Mehr dazu im Kapitel Erstellen von URL-Links.

<a n:href="click! $x, $y">Hier klicken</a>

Ein Signal wird immer im aktuellen Presenter und der aktuellen Action aufgerufen, es kann nicht in einem anderen Presenter oder einer anderen Action ausgelöst werden.

Ein Signal bewirkt also das Neuladen der Seite genau wie bei der ursprünglichen Anfrage, ruft aber zusätzlich die Signal-Handler-Methode mit den entsprechenden Parametern auf. Wenn die Methode nicht existiert, wird eine Ausnahme Nette\Application\UI\BadSignalException ausgelöst, die dem Benutzer als Fehlerseite 403 Forbidden angezeigt wird.

Snippets und AJAX

Signale erinnern Sie vielleicht ein wenig an AJAX: Handler, die auf der aktuellen Seite aufgerufen werden. Und Sie haben Recht, Signale werden tatsächlich oft mittels AJAX aufgerufen, und anschließend übertragen wir nur die geänderten Teile der Seite an den Browser. Also sogenannte Snippets. Weitere Informationen finden Sie auf der AJAX gewidmeten Seite.

Flash-Nachrichten

Eine Komponente hat ihren eigenen Speicher für Flash-Nachrichten, unabhängig vom Presenter. Dies sind Nachrichten, die z. B. ü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 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.

Das Senden übernimmt die Methode flashMessage. Der erste Parameter ist der Nachrichtentext oder ein stdClass-Objekt, das die Nachricht repräsentiert. Der optionale zweite Parameter ist ihr Typ (error, warning, info usw.). Die Methode flashMessage() gibt eine Instanz der Flash-Nachricht als stdClass-Objekt zurück, dem weitere Informationen hinzugefügt werden können.

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

Dem Template stehen diese Nachrichten in der Variablen $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}

Weiterleitung nach Signal

Nach der Verarbeitung eines Komponentensignals folgt oft eine Weiterleitung. Dies ist eine ähnliche Situation wie bei Formularen – nach dem Absenden leiten wir ebenfalls weiter, damit beim Neuladen der Seite im Browser die Daten nicht erneut gesendet werden.

$this->redirect('this') // leitet zum aktuellen Presenter und zur aktuellen Action weiter

Da eine Komponente ein wiederverwendbares Element ist und normalerweise keine direkte Bindung an bestimmte Presenter haben sollte, interpretieren die Methoden redirect() und link() den Parameter automatisch als Signal der Komponente:

$this->redirect('click') // leitet zum Signal 'click' derselben Komponente weiter

Wenn Sie zu einem anderen Presenter oder einer anderen Aktion weiterleiten müssen, können Sie dies über den Presenter tun:

$this->getPresenter()->redirect('Product:show'); // leitet zu einem anderen Presenter/Action weiter

Persistente Parameter

Persistente Parameter dienen dazu, den Zustand in Komponenten über verschiedene Anfragen hinweg zu erhalten. Ihr Wert bleibt auch nach dem Klick auf einen Link gleich. Im Gegensatz zu Daten in der Session werden sie in der URL übertragen. Und das vollautomatisch, einschließlich Links, die in anderen Komponenten auf derselben Seite erstellt wurden.

Sie haben z. B. eine Komponente für die Paginierung von Inhalten. Solche Komponenten können auf einer Seite mehrmals vorkommen. Und wir möchten, dass nach dem Klick auf einen Link alle Komponenten auf ihrer aktuellen Seite bleiben. Deshalb machen wir die Seitenzahl (page) zu einem persistenten Parameter.

Die Erstellung eines persistenten Parameters ist in Nette äußerst einfach. Es genügt, eine öffentliche Eigenschaft zu erstellen und sie mit einem Attribut zu kennzeichnen: (früher wurde /** @persistent */ verwendet)

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

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1; // muss public sein
}

Für die Eigenschaft empfehlen wir, auch den Datentyp anzugeben (z. B. int), und Sie können auch einen Standardwert angeben. Die Werte der Parameter können validiert werden.

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

<a n:href="this page: $page + 1">weiter</a>

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

<a n:href="this page: null">zurücksetzen</a>

Persistente Komponenten

Nicht nur Parameter, sondern auch Komponenten können persistent sein. Bei einer solchen Komponente werden ihre persistenten Parameter auch zwischen verschiedenen Aktionen des Presenters oder zwischen mehreren Presentern übertragen. Persistente Komponenten kennzeichnen wir mit einer Annotation in der Presenter-Klasse. So kennzeichnen wir beispielsweise die Komponenten calendar und poll:

/**
 * @persistent(calendar, poll)
 */
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

Sub-Komponenten innerhalb dieser Komponenten müssen nicht gekennzeichnet werden, sie werden ebenfalls persistent.

In PHP 8 können Sie zur Kennzeichnung persistenter Komponenten auch Attribute verwenden:

use Nette\Application\Attributes\Persistent;

#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

Komponenten mit Abhängigkeiten

Wie erstellt man Komponenten mit Abhängigkeiten, ohne die Presenter, die sie verwenden, zu „verschmutzen“? Dank der intelligenten Eigenschaften des DI-Containers in Nette kann man, wie bei der Verwendung klassischer Dienste, den größten Teil der Arbeit dem Framework überlassen.

Nehmen wir als Beispiel eine Komponente, die eine Abhängigkeit vom Dienst PollFacade hat:

class PollControl extends Control
{
	public function __construct(
		private int $id, // ID der Umfrage, für die wir die Komponente erstellen
		private PollFacade $facade,
	) {
	}

	public function handleVote(int $voteId): void
	{
		$this->facade->vote($id, $voteId);
		// ...
	}
}

Wenn wir einen klassischen Dienst schreiben würden, gäbe es nichts zu lösen. Der DI-Container würde sich unsichtbar um die Übergabe aller Abhängigkeiten kümmern. Aber mit Komponenten gehen wir normalerweise so um, dass wir ihre neue Instanz direkt im Presenter in den Factory-Methoden createComponent…() erstellen. Aber alle Abhängigkeiten aller Komponenten an den Presenter zu übergeben, nur um sie dann an die Komponenten weiterzugeben, ist umständlich. Und der viele geschriebene Code…

Die logische Frage ist, warum registrieren wir die Komponente nicht einfach als klassischen Dienst, übergeben sie an den Presenter und geben sie dann in der Methode createComponent…() zurück? Dieser Ansatz ist jedoch ungeeignet, da wir die Komponente möglicherweise mehrmals erstellen möchten.

Die richtige Lösung ist, eine Factory für die Komponente zu schreiben, also eine Klasse, die uns die Komponente erstellt:

class PollControlFactory
{
	public function __construct(
		private PollFacade $facade,
	) {
	}

	public function create(int $id): PollControl
	{
		return new PollControl($id, $this->facade);
	}
}

Diese Factory registrieren wir in unserem Container in der Konfiguration:

services:
	- PollControlFactory

und schließlich verwenden wir sie in unserem Presenter:

class PollPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private PollControlFactory $pollControlFactory,
	) {
	}

	protected function createComponentPollControl(): PollControl
	{
		$pollId = 1; // wir können unseren Parameter übergeben
		return $this->pollControlFactory->create($pollId);
	}
}

Das Tolle ist, dass Nette DI solche einfachen Factories generieren kann, sodass statt des gesamten Codes nur ihr Interface geschrieben werden muss:

interface PollControlFactory
{
	public function create(int $id): PollControl;
}

Und das ist alles. Nette implementiert dieses Interface intern und übergibt es an den Presenter, wo wir es bereits verwenden können. Es fügt magischerweise auch den Parameter $id und eine Instanz der Klasse PollFacade zu unserer Komponente hinzu.

Komponenten im Detail

Komponenten in Nette Application stellen wiederverwendbare Teile einer Webanwendung dar, die wir in Seiten einfügen und denen dieses ganze Kapitel gewidmet ist. Welche Fähigkeiten hat eine solche Komponente genau?

  1. Sie ist im Template renderbar.
  2. Sie weiß, welchen Teil sie bei einer AJAX-Anfrage rendern soll (Snippets).
  3. Sie hat die Fähigkeit, ihren Zustand in der URL zu speichern (persistente Parameter).
  4. Sie hat die Fähigkeit, auf Benutzeraktionen zu reagieren (Signale).
  5. Sie bildet eine hierarchische Struktur (deren Wurzel der Presenter ist).

Jede dieser Funktionen wird von einer der Klassen der Vererbungslinie übernommen. Das Rendern (1 + 2) übernimmt Nette\Application\UI\Control, die Einbindung in den Lebenszyklus (3, 4) die Klasse Nette\Application\UI\Component und die Erstellung der hierarchischen Struktur (5) die Klassen Container und Component.

Nette\ComponentModel\Component  { IComponent }
|
+- Nette\ComponentModel\Container  { IContainer }
	|
	+- Nette\Application\UI\Component  { SignalReceiver, StatePersistent }
		|
		+- Nette\Application\UI\Control  { Renderable }
			|
			+- Nette\Application\UI\Presenter  { IPresenter }

Lebenszyklus einer Komponente

Lebenszyklus einer Komponente

Validierung persistenter Parameter

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

Vertrauen Sie niemals blind persistenten Parametern, da sie vom Benutzer leicht in der URL überschrieben werden können. So überprüfen wir beispielsweise, ob die Seitenzahl $this->page größer als 0 ist. Ein geeigneter Weg ist, die erwähnte Methode loadState() zu überschreiben:

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1;

	public function loadState(array $params): void
	{
		parent::loadState($params); // hier wird $this->page gesetzt
		// es folgt die eigene Wertprüfung:
		if ($this->page < 1) {
			$this->error();
		}
	}
}

Der umgekehrte Prozess, also das Sammeln von Werten aus persistenten Eigenschaften, wird von der Methode saveState() übernommen.

Signale im Detail

Ein Signal bewirkt das Neuladen der Seite genau wie bei der ursprünglichen Anfrage (außer wenn es per AJAX aufgerufen wird) und ruft die Methode signalReceived($signal) auf, deren Standardimplementierung in der Klasse Nette\Application\UI\Component versucht, eine Methode aufzurufen, die aus den Wörtern handle{signal} zusammengesetzt ist. Die weitere Verarbeitung liegt beim jeweiligen Objekt. Objekte, die von Component erben (d. h. Control und Presenter), reagieren, indem sie versuchen, die Methode handle{signal} mit den entsprechenden Parametern aufzurufen.

Mit anderen Worten: Es wird die Definition der Funktion handle{signal} und alle mit der Anfrage übermittelten Parameter genommen, und den Argumenten werden anhand des Namens Parameter aus der URL zugewiesen, und es wird versucht, die betreffende Methode aufzurufen. Z. B. wird als Parameter $id der Wert des Parameters id aus der URL übergeben, als $something wird something aus der URL übergeben, usw. Und wenn die Methode nicht existiert, löst die Methode signalReceived eine Ausnahme aus.

Ein Signal kann von jeder Komponente, jedem Presenter oder jedem Objekt empfangen werden, das das Interface SignalReceiver implementiert und in den Komponentenbaum eingebunden ist.

Die Hauptempfänger von Signalen sind Presenter und visuelle Komponenten, die von Control erben. Ein Signal soll als Zeichen für ein Objekt dienen, dass es etwas tun soll – die Umfrage soll eine Stimme vom Benutzer zählen, der Nachrichtenblock soll sich aufklappen und doppelt so viele Nachrichten anzeigen, das Formular wurde abgeschickt und soll die Daten verarbeiten und so weiter.

Die URL für ein Signal erstellen wir mit der Methode Component::link(). Als Parameter $destination übergeben wir den String {signal}! und als $args ein Array von Argumenten, die wir dem Signal übergeben möchten. Das Signal wird immer im aktuellen Presenter und der aktuellen Action mit den aktuellen Parametern aufgerufen, die Signalparameter werden nur hinzugefügt. Zusätzlich wird ganz am Anfang der Parameter ?do, der das Signal bestimmt, hinzugefügt.

Sein Format ist entweder {signal} oder {signalReceiver}-{signal}. {signalReceiver} ist der Name der Komponente im Presenter. Deshalb darf im Komponentennamen kein Bindestrich vorkommen – er wird zur Trennung von Komponentennamen und Signal verwendet, es ist jedoch möglich, mehrere Komponenten auf diese Weise zu verschachteln.

Die Methode isSignalReceiver() prüft, ob eine Komponente (erstes Argument) der Empfänger eines Signals ist (zweites Argument). Das zweite Argument kann weggelassen werden – dann wird geprüft, ob die Komponente Empfänger irgendeines Signals ist. Als zweites Parameter kann true angegeben werden, um zu prüfen, ob nicht nur die angegebene Komponente, sondern auch einer ihrer Nachfahren Empfänger ist.

In jeder Phase vor handle{signal} können wir das Signal manuell ausführen, indem wir die Methode processSignal() aufrufen, die die Bearbeitung des Signals übernimmt – sie nimmt die Komponente, die als Signalempfänger bestimmt wurde (wenn kein Signalempfänger bestimmt ist, ist es der Presenter selbst) und sendet ihr das Signal.

Beispiel:

if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
	$this->processSignal();
}

Damit wird das Signal vorzeitig ausgeführt und nicht mehr erneut aufgerufen.

Version: 4.0