Interaktive Komponenten
Komponenten sind eigenständige, wiederverwendbare Objekte, die wir in Seiten einbetten. Das können Formulare, Datagrids, Umfragen sein – kurz alles, was sich sinnvoll wiederholt verwenden lässt. Wir zeigen:
- wie man Komponenten verwendet?
- wie man sie schreibt?
- was Signale sind?
Nette hat ein eingebautes Komponentensystem. Etwas Ähnliches kennen Veteranen vielleicht aus Delphi oder ASP.NET Web Forms; React oder Vue.js bauen auf entfernt Ähnlichem auf. In der Welt der PHP-Frameworks ist das jedoch eine einzigartige Eigenschaft.
Zugleich beeinflussen Komponenten die Herangehensweise an die Anwendungsentwicklung grundlegend. Sie können Seiten aus vorbereiteten Einheiten zusammensetzen. Brauchen Sie in Ihrer Administration ein Datagrid? Finden Sie es auf Componette, einem Repository von Open-Source-Erweiterungen (nicht nur Komponenten) für Nette, und fügen Sie es einfach in den Presenter ein.
Sie können beliebig viele Komponenten in den Presenter einbinden. Und in manche Komponenten können Sie weitere Komponenten einbetten. So entsteht ein Komponentenbaum, dessen Wurzel der Presenter ist.
Factory-Methoden
Wie werden Komponenten in den Presenter eingefügt und anschließend verwendet? Üblicherweise über Factory-Methoden.
Eine Factory für Komponenten ist ein eleganter Weg, Komponenten erst dann zu erzeugen, wenn sie tatsächlich gebraucht werden
(lazy / on demand). Die ganze Magie steckt in der Implementierung einer Methode namens createComponent<Name>(),
wobei <Name> der Name der erzeugten Komponente ist und die die Komponente erzeugt und zurückgibt.
class DefaultPresenter extends Nette\Application\UI\Presenter
{
protected function createComponentPoll(): PollControl
{
$poll = new PollControl;
$poll->items = $this->items;
return $poll;
}
}
Weil alle Komponenten in eigenen Methoden erzeugt werden, wird der Code übersichtlicher.
Namen von Komponenten beginnen immer mit einem Kleinbuchstaben, obwohl sie im Methodennamen großgeschrieben werden.
Wir rufen Factories nie direkt auf; sie werden automatisch beim ersten Verwenden der Komponente aufgerufen. Dadurch wird die Komponente im richtigen Moment erzeugt und nur dann, wenn sie tatsächlich gebraucht wird. Verwenden wir die Komponente nicht (z. B. bei einem AJAX-Request, bei dem nur ein Teil der Seite übertragen wird, oder beim Cachen des Templates), wird sie überhaupt nicht erzeugt, was Serverleistung spart.
// wir greifen auf die Komponente zu, und wenn es das erste Mal war,
// wird createComponentPoll() aufgerufen, die sie erzeugt
$poll = $this->getComponent('poll');
// alternative Schreibweise: $poll = $this['poll'];
Im Template lässt sich eine Komponente mit dem Tag {control} rendern. Komponenten müssen daher nicht von Hand an das Template übergeben werden.
<h2>Bitte stimmen Sie ab</h2>
{control poll}
Um dynamisch eine variable Anzahl von Komponenten zu erzeugen, verwenden Sie den Multiplier.
Die Factory-Methoden createComponent<Name>() funktionieren nicht nur in Presentern. Auf dieselbe Weise
können Sie eine Komponente in eine andere Komponente verschachteln und sie zu einem Baum zusammensetzen – praktisch zum
Beispiel für ein separat gerendertes Formular innerhalb einer Komponente.
Hollywood Style
Komponenten nutzen gewöhnlich eine erfrischende Technik, die wir gerne Hollywood Style nennen. Sie kennen sicher das Klischee, das Teilnehmer von Filmcastings oft hören: „Rufen Sie uns nicht an, wir rufen Sie an.“ Und genau darum geht es.
In Nette müssen Sie nicht ständig Fragen stellen („wurde das Formular abgeschickt?“, „war es gültig?“, oder „hat der Benutzer diese Schaltfläche gedrückt?“), sondern Sie sagen dem Framework „wenn das passiert, rufe diese Methode auf“ und überlassen ihm die weitere Arbeit. Wenn Sie in JavaScript programmieren, ist Ihnen dieser Programmierstil bestens vertraut. Sie schreiben Funktionen, die aufgerufen werden, wenn ein bestimmtes Ereignis eintritt. Und die Sprache übergibt ihnen die passenden Parameter.
Das ändert die Sicht auf das Schreiben von Anwendungen vollständig. Je mehr Aufgaben Sie dem Framework überlassen können, desto weniger Arbeit haben Sie. Und desto weniger können Sie übersehen.
Eine Komponente schreiben
Mit dem Begriff Komponente meinen wir üblicherweise einen Nachfahren der Klasse Nette\Application\UI\Control. (Genauer wäre
der Begriff „Controls“, aber der hat in manchen Sprachen eine andere Bedeutung, und „Komponenten“ hat sich stärker
eingebürgert.) Auch der Presenter Nette\Application\UI\Presenter selbst ist ein
Nachfahre der Klasse Control.
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
Rendering
Wir wissen bereits, dass zum Rendern einer Komponente der Tag {control componentName} dient. Er ruft in
Wirklichkeit die Methode render() der Komponente auf, in der wir uns um das Rendering kümmern. Wir haben, genau wie
im Presenter, in der Variablen $this->template ein Latte-Template zur Verfügung, dem
wir Parameter übergeben. Anders als im Presenter müssen wir die Template-Datei angeben und sie rendern lassen:
public function render(): void
{
// einige Parameter in das Template einfügen
$this->template->param = $value;
// und es rendern
$this->template->render(__DIR__ . '/poll.latte');
}
Der Tag {control} erlaubt es, der Methode render() Parameter 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 wollen. Für jeden davon legen wir eine
eigene Rendering-Methode an, im Beispiel hier renderPaginator():
public function renderPaginator(): void
{
// ...
}
Und im Template rufen wir sie dann so auf:
{control poll:paginator}
Zum besseren Verständnis ist es gut zu wissen, wie sich dieser Tag in PHP-Code übersetzt.
{control poll}
{control poll:paginator 123, 'hello'}
übersetzt sich zu:
$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');
Die Methode getComponent() gibt die Komponente poll zurück, und auf dieser Komponente wird die
Methode render() aufgerufen, bzw. renderPaginator(), wenn im Tag nach dem Doppelpunkt eine andere
Rendering-Methode angegeben ist.
Achtung: Erscheint in den Parametern außerhalb eckiger Klammern ein =>, werden alle
Parameter in ein Array verpackt und als erstes Argument übergeben:
{control poll, id: 123, message: 'hello'}
übersetzt sich zu:
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
Rendering einer Unterkomponente:
{control cartControl-someForm}
übersetzt sich zu:
$control->getComponent("cartControl-someForm")->render();
Komponenten übergeben Templates, ebenso wie Presenter, automatisch mehrere nützliche Variablen:
$basePathist der absolute URL-Pfad zum Wurzelverzeichnis (z. B./eshop)$baseUrlist die absolute URL zum Wurzelverzeichnis (z. B.http://localhost/eshop)$userist ein Objekt, das den Benutzer repräsentiert$presenterist der aktuelle Presenter$controlist die aktuelle Komponente$flashesist ein Array von Meldungen, die mit der FunktionflashMessage()gesendet wurden
Signal
Wir wissen bereits, dass die Navigation in einer Nette-Anwendung im Verlinken oder Weiterleiten auf Paare
Presenter:action besteht. Was aber, wenn wir nur eine Aktion auf der aktuellen Seite ausführen wollen? Zum
Beispiel die Sortierung von Spalten einer Tabelle ändern; ein Element löschen; zwischen hellem und dunklem Modus umschalten; ein
Formular absenden; in einer Umfrage abstimmen usw.
Diese Art von Request nennt man Signal. Und so, wie Aktionen die Methoden action<Aktion>() oder
render<Aktion>() aufrufen, rufen Signale die Methoden handle<Signal>() auf. Während sich
der Begriff Aktion (oder View) rein auf Presenter bezieht, betreffen Signale alle Komponenten. Und damit auch Presenter, denn
UI\Presenter ist ein Nachfahre von UI\Control.
public function handleClick(int $x, int $y): void
{
// ... Verarbeitung des Signals ...
}
Einen Link, der ein Signal aufruft, erstellt man auf die übliche Weise, also im Template mit dem Attribut n:href
oder dem Tag {link}, im Code mit der Methode link(). Mehr im Kapitel Erstellen von URL-Links.
<a n:href="click! $x, $y">hier klicken</a>
Ein Signal wird immer auf dem aktuellen Presenter und der aktuellen Aktion aufgerufen; es lässt sich nicht auf einem anderen Presenter oder einer anderen Aktion auslösen.
Ein Signal bewirkt also, dass die Seite genau wie beim ursprünglichen Request neu geladen wird, ruft zusätzlich aber die Methode zur Verarbeitung des Signals mit den passenden Parametern auf. Existiert die Methode nicht, wird eine Exception Nette\Application\UI\BadSignalException geworfen, 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 mit AJAX aufgerufen, und anschließend werden nur die geänderten Teile der Seite in den Browser übertragen. Diese nennt man Snippets. Mehr dazu finden Sie auf der Seite über AJAX.
Flash-Meldungen
Eine Komponente hat einen eigenen, vom Presenter unabhängigen Speicher für Flash-Meldungen. Das sind Meldungen, die zum Beispiel über das Ergebnis einer Operation informieren. Eine wichtige Eigenschaft von Flash-Meldungen ist, dass sie im Template auch nach einer Weiterleitung verfügbar sind. Auch nach der Anzeige bleiben sie noch 30 Sekunden aktiv – zum Beispiel für den Fall, dass der Benutzer die Seite wegen eines Übertragungsfehlers neu lädt, verschwindet die Meldung nicht sofort.
Das Senden übernimmt die Methode flashMessage. Der erste
Parameter ist der Text der Meldung (string, Stringable) oder ein Objekt stdClass, das die
Meldung repräsentiert. Der optionale zweite Parameter ist ihr Typ (error, warning, info usw.). Die Methode
flashMessage() gibt eine Instanz der Flash-Meldung als Objekt stdClass zurück, dem sich weitere
Informationen hinzufügen lassen.
$this->flashMessage('Das Element wurde gelöscht.');
$this->redirect(/* ... */); // und weiterleiten
Diese Meldungen stehen dem Template in der Variablen $flashes als Objekte stdClass zur Verfügung,
die die Properties message (Text der Meldung) und type (Typ der Meldung) enthalten und die erwähnten
eigenen Informationen enthalten können. Wir rendern sie zum Beispiel so:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Weiterleitung nach Verarbeitung eines Signals
Auf die Verarbeitung eines Signals einer Komponente folgt oft eine Weiterleitung. Das ist ähnlich wie bei Formularen – nach dem Absenden leiten wir ebenfalls weiter, damit die Daten beim Neuladen der Seite im Browser nicht erneut abgeschickt werden.
$this->redirect('this'); // leitet auf den aktuellen Presenter und die aktuelle Aktion weiter
Da eine Komponente ein wiederverwendbares Element ist und üblicherweise keine direkte Bindung an konkrete Presenter haben
sollte, interpretieren die Methoden redirect() und link() den Parameter automatisch als Signal der
Komponente:
$this->redirect('click'); // leitet auf das Signal 'click' derselben Komponente weiter
Müssen Sie auf einen anderen Presenter oder eine andere Aktion weiterleiten, geht das über den Presenter:
$this->getPresenter()->redirect('Product:show'); // leitet auf einen anderen Presenter/eine andere Aktion weiter
Persistente Parameter
Persistente Parameter dienen dazu, den Zustand von Komponenten über verschiedene Requests hinweg zu erhalten. Ihr Wert bleibt auch nach dem Klick auf einen Link derselbe. Anders als Daten in der Session werden sie in der URL übertragen. Und das geschieht vollautomatisch, einschließlich der Links, die in anderen Komponenten auf derselben Seite erstellt werden.
Sie haben zum Beispiel eine Komponente zur Paginierung von Inhalten. Auf einer Seite können mehrere solcher Komponenten sein.
Und wir wollen, dass alle Komponenten nach dem Klick auf einen Link auf ihrer aktuellen Seite bleiben. Deshalb machen wir die
Seitennummer (page) zu einem persistenten Parameter.
Einen persistenten Parameter in Nette zu erstellen ist ausgesprochen einfach. Legen Sie einfach eine public Property an und
kennzeichnen Sie sie mit dem Attribut: (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
}
Wir empfehlen, für die Property den Datentyp anzugeben (z. B. int), und Sie können auch einen Standardwert
angeben. Die Werte der Parameter lassen sich validieren.
Beim Erstellen eines Links lässt sich der Wert eines persistenten Parameters ändern:
<a n:href="this page: $page + 1">weiter</a>
Oder er lässt sich zurücksetzen, also aus der URL entfernen. Er nimmt dann seinen Standardwert an:
<a n:href="this page: null">zurücksetzen</a>
Persistente Komponenten
Nicht nur Parameter, sondern auch Komponenten können persistent sein. Ihre persistenten Parameter werden dann auch zwischen
verschiedenen Aktionen des Presenters oder zwischen mehreren Presentern übertragen. Persistente Komponenten kennzeichnen wir mit
einem Attribut an der Klasse des Presenters. So kennzeichnen wir zum Beispiel die Komponenten calendar und
poll:
use Nette\Application\Attributes\Persistent;
#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Unterkomponenten innerhalb dieser Komponenten müssen nicht gekennzeichnet werden; auch sie werden persistent.
Die ältere Annotation @persistent funktioniert weiterhin, ist aber veraltet und löst eine Warnung aus:
/**
* @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 „verstopfen“? Dank der klugen Fähigkeiten des DI-Containers in Nette lässt sich, ähnlich wie bei klassischen Services, der Großteil der Arbeit dem Framework überlassen.
Nehmen wir als Beispiel eine Komponente, die eine Abhängigkeit vom Service 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($this->id, $voteId);
// ...
}
}
Würden wir einen klassischen Service schreiben, gäbe es nichts zu besprechen. Der DI-Container würde die Übergabe aller
Abhängigkeiten unsichtbar erledigen. Bei Komponenten lösen wir das jedoch üblicherweise so, dass wir in den Factory-Methoden createComponent…() direkt im Presenter eine neue Instanz
erzeugen. Aber alle Abhängigkeiten aller Komponenten in den Presenter zu übergeben, nur um sie an die Komponenten
weiterzureichen, ist umständlich. Und die Menge des geschriebenen Codes …
Die logische Frage lautet: Warum registrieren wir die Komponente nicht einfach als klassischen Service, übergeben sie dem
Presenter und geben sie dann in der Methode createComponent…() zurück? Dieser Ansatz ist jedoch ungeeignet, weil
wir die Möglichkeit haben wollen, die Komponente bei Bedarf mehrfach zu erzeugen.
Die richtige Lösung ist, für die Komponente eine Factory zu schreiben, also eine Klasse, die die Komponente für uns erzeugt:
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 der Konfiguration in unserem Container:
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 Großartige daran ist, dass Nette DI solche einfachen Factories generieren kann, sodass Sie statt des gesamten Codes nur ihr Interface schreiben müssen:
interface PollControlFactory
{
public function create(int $id): PollControl;
}
Und das ist alles. Nette implementiert dieses Interface intern und injiziert es in den Presenter, wo wir es verwenden können.
Es ergänzt unsere Komponente auf magische Weise um den Parameter $id und eine Instanz der Klasse
PollFacade.
Komponenten im Detail
Komponenten stellen in Nette Application wiederverwendbare Teile einer Webanwendung dar, die wir in Seiten einbetten und denen dieses ganze Kapitel gewidmet ist. Was genau kann eine solche Komponente?
- Sie ist in einem Template renderbar
- Sie weiß, welchen Teil von sich sie bei einem AJAX-Request rendern soll (Snippets)
- Sie kann ihren Zustand in der URL speichern (persistente Parameter)
- Sie kann auf Benutzeraktionen reagieren (Signale)
- Sie bildet eine hierarchische Struktur (deren Wurzel der Presenter ist)
Jede dieser Funktionen übernimmt eine der Klassen in der Vererbungslinie. Um das Rendering (1 + 2) kümmert sich Nette\Application\UI\Control, um die Einbindung in den Lebenszyklus (3, 4) die Klasse Nette\Application\UI\Component, und um den Aufbau 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 aus der URL empfangenen Werte der Persistente Parameter werden von der Methode
loadState() in die Properties geschrieben. Sie prüft außerdem, ob der für die Property angegebene Datentyp passt;
andernfalls antwortet sie mit dem Fehler 404 und die Seite wird nicht angezeigt.
Vertrauen Sie persistenten Parametern niemals blind, denn sie lassen sich vom Benutzer in der URL leicht überschreiben. So
prüfen wir zum Beispiel, ob die Seitennummer $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();
}
}
}
Den umgekehrten Vorgang, also das Einsammeln der Werte aus den persistenten Properties, übernimmt die Methode
saveState().
Verbindung mit dem Presenter
In dem Moment, in dem eine Komponente Teil der Hierarchie des Presenters wird, werden ihre im Array $onAnchor
gespeicherten Callbacks aufgerufen. Ab diesem Zeitpunkt hat die Komponente den Presenter zur Verfügung, kann gefahrlos Links
erstellen, persistente Parameter lesen und so weiter.
$control->onAnchor[] = function ($control): void {
// die Komponente hat nun den Presenter zur Verfügung
};
Signale im Detail
Ein Signal bewirkt, dass die Seite genau wie beim ursprünglichen Request neu geladen wird (außer beim Aufruf über AJAX), und
ruft die Methode signalReceived($signal) auf, deren Standardimplementierung in der Klasse
Nette\Application\UI\Component versucht, eine aus den Wörtern handle<Signal> zusammengesetzte
Methode aufzurufen. Die weitere Verarbeitung liegt beim jeweiligen Objekt. Objekte, die von Component erben (also
Control und Presenter), reagieren so, dass sie versuchen, die Methode handle<Signal>
mit den passenden Parametern aufzurufen.
Mit anderen Worten: Es wird die Definition der Funktion handle<Signal> genommen, samt allen Parametern, die
mit dem Request kamen, und die Parameter aus der URL werden den Argumenten anhand des Namens zugeordnet, dann wird versucht, die
Methode aufzurufen. Zum Beispiel wird der Wert des Parameters id aus der URL als Argument $id
übergeben, something aus der URL als $something usw. Und existiert die Methode nicht, wirft die Methode
signalReceived eine Exception.
Neben den Parametern aus der URL liest ein Signal auch die im POST-Body des Requests gesendeten Parameter. Das ist praktisch, weil Signale oft über JavaScript aufgerufen werden, wo es natürlich ist, Daten mit der Methode POST zu senden. Kommt ein Parameter gleichen Namens jedoch sowohl aus der URL als auch aus dem POST-Body, hat der Wert aus der URL Vorrang. Vermeiden Sie es daher, einem POST-Feld denselben Namen wie einem URL- oder Route-Parameter zu geben, sonst würde der Wert aus der URL ihn stillschweigend überschreiben. Die Signalparameter teilen sich einen gemeinsamen Raum mit den Parametern der Aktion und den persistenten Parametern, siehe Gemeinsamer Parameterraum.
Ein Signal kann jede Komponente, jeder Presenter oder jedes Objekt empfangen, das das Interface SignalReceiver
implementiert und mit dem Komponentenbaum verbunden ist.
Die Hauptempfänger von Signalen werden Presenter und visuelle Komponenten sein, die von Control
erben. Ein Signal soll einem Objekt als Zeichen dienen, dass es etwas tun soll – eine Umfrage soll eine Stimme des Benutzers
zählen, ein Nachrichtenblock soll sich ausklappen und doppelt so viele Nachrichten anzeigen, ein Formular wurde abgeschickt und
soll Daten verarbeiten und so weiter.
Die URL für ein Signal wird mit der Methode Component::link() erstellt. Als
Parameter $destination übergeben wir den String {signal}! und als $args ein Array von
Argumenten, die wir dem Signal übergeben wollen. Das Signal wird immer auf dem aktuellen Presenter und der aktuellen Aktion mit
den aktuellen Parametern aufgerufen; die Parameter des Signals kommen lediglich hinzu. Zusätzlich wird der Parameter
?do, der das Signal angibt, ergänzt.
Sein Format lautet entweder {signal} oder {signalReceiver}-{signal}. {signalReceiver}
ist der Name der Komponente im Presenter. Deshalb darf im Namen der Komponente kein Bindestrich vorkommen – er dient der
Trennung von Komponentenname und Signal, wobei sich auf diese Weise mehrere Komponenten verschachteln lassen.
Die Methode isSignalReceiver()
prüft, ob die Komponente (erstes Argument) Empfänger des Signals (zweites Argument) ist. Das zweite Argument kann weggelassen
werden – dann wird geprüft, ob die Komponente Empfänger irgendeines Signals ist. Wird der zweite Parameter auf
true gesetzt, wird geprüft, ob die angegebene Komponente oder 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 sich um die Verarbeitung des Signals kümmert – sie nimmt die als Signalempfänger bestimmte Komponente (ist kein
Empfänger angegeben, ist es der Presenter selbst) und sendet ihr das Signal.
Beispiel:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
Dadurch wird das Signal vorzeitig ausgeführt und nicht erneut aufgerufen.