Interaktive Komponenten
Komponenten sind separate, wiederverwendbare Objekte, die wir in Seiten einfügen. Sie können Formulare, Datenfelder, Umfragen sein, eigentlich alles, was sinnvoll ist, um es wiederholt zu verwenden. Wir werden es zeigen:
- wie man Komponenten verwendet?
- wie man sie schreibt?
- Was sind Signale?
Nette hat ein eingebautes Komponentensystem. Ältere von Ihnen erinnern sich vielleicht an etwas Ähnliches aus Delphi oder ASP.NET Web Forms. React oder Vue.js bauen auf etwas entfernt Ähnlichem auf. In der Welt der PHP-Frameworks ist dies jedoch eine völlig einzigartige Funktion.
Gleichzeitig verändern Komponenten die Herangehensweise an die Anwendungsentwicklung grundlegend. Sie können Seiten aus vorgefertigten Einheiten zusammenstellen. Benötigen Sie ein Datagrid in der Verwaltung? Sie finden es bei Componette, einem Repository von Open-Source-Add-ons (nicht nur Komponenten) für Nette, und fügen es einfach in den Presenter ein.
Sie können eine beliebige Anzahl von Komponenten in den Presenter einfügen. Und Sie können andere Komponenten in einige Komponenten einfügen. So entsteht ein Komponentenbaum mit einem Presenter als Wurzel.
Fabrik-Methoden
Wie werden Komponenten im Presenter platziert und anschließend verwendet? Normalerweise mit Hilfe von Fabrikmethoden.
Die Komponentenfabrik ist eine elegante Möglichkeit, Komponenten nur dann zu erstellen, wenn sie wirklich benötigt werden
(lazy / on-demand). Der ganze Zauber liegt in der Implementierung einer Methode namens createComponent<Name>()
,
wobei <Name>
der Name der Komponente ist, die erstellt und zurückgegeben wird.
class DefaultPresenter extends Nette\Application\UI\Presenter
{
protected function createComponentPoll(): PollControl
{
$poll = new PollControl;
$poll->items = $this->item;
return $poll;
}
}
Da alle Komponenten in separaten Methoden erstellt werden, ist der Code sauberer und leichter zu lesen.
Komponentennamen beginnen immer mit einem Kleinbuchstaben, obwohl sie im Methodennamen groß geschrieben werden.
Wir rufen die Factories nie direkt auf, sondern sie werden automatisch aufgerufen, wenn wir Komponenten zum ersten Mal verwenden. Dadurch wird eine Komponente im richtigen Moment erstellt, und nur dann, wenn sie wirklich benötigt wird. Wenn wir die Komponente nicht verwenden würden (z. B. bei einer AJAX-Anfrage, bei der nur ein Teil der Seite zurückgegeben wird, oder wenn Teile im Cache gespeichert sind), wird sie gar nicht erst erstellt, und wir sparen Leistung des Servers.
// wir greifen auf die Komponente zu und wenn es das erste Mal war,
// wird createComponentPoll() aufgerufen, um sie zu erstellen
$poll = $this->getComponent('poll');
// alternative Syntax: $poll = $this['poll'];
In der Vorlage können Sie eine Komponente mit dem Tag {control} rendern. Es besteht also keine Notwendigkeit, die Komponenten manuell an die Vorlage zu übergeben.
<h2>Please Vote</h2>
{control poll}
Hollywood-Stil
Komponenten verwenden häufig eine coole Technik, die wir gerne als Hollywood-Stil bezeichnen. Sicherlich kennen Sie das Klischee, das Schauspieler bei Casting-Aufrufen oft hören: “Rufen Sie uns nicht an, wir rufen Sie an.” Und genau darum geht es hier.
Anstatt in Nette ständig Fragen zu stellen (“wurde das Formular abgeschickt?”, “war es gültig?” oder “hat jemand diesen Knopf gedrückt?”), sagen Sie dem Framework “wenn das passiert, rufe diese Methode auf” und lassen die weitere Arbeit darauf beruhen. Wenn Sie in JavaScript programmieren, sind Sie mit dieser Art der Programmierung vertraut. Sie schreiben Funktionen, die aufgerufen werden, wenn ein bestimmtes Ereignis eintritt. Und die Engine übergibt die entsprechenden Parameter an sie.
Das verändert die Art und Weise, wie Sie Anwendungen schreiben, völlig. Je mehr Aufgaben Sie an das Framework delegieren können, desto weniger Arbeit haben Sie. Und desto weniger können Sie vergessen.
Wie man eine Komponente schreibt
Mit Komponente sind in der Regel Abkömmlinge der Klasse Nette\Application\UI\Control gemeint. Der
Präsentator Nette\Application\UI\Presenter selbst ist
ebenfalls ein Abkömmling der Klasse Control
.
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
Rendering
Wir wissen bereits, dass das Tag {control componentName}
zum Zeichnen einer Komponente verwendet wird. Es ruft die
Methode render()
der Komponente auf, in der wir uns um das Rendering kümmern. Wir haben, genau wie im Presenter,
eine Lattenvorlage in der Variablen $this->template
,
der wir die Parameter übergeben. Im Gegensatz zur Verwendung im Presenter müssen wir eine Vorlagendatei angeben und sie rendern
lassen:
public function render(): void
{
// wir werden einige Parameter in die Vorlage einfügen
$this->template->param = $value;
// und zeichnen es
$this->template->render(__DIR__ . '/poll.latte');
}
Das Tag {control}
erlaubt die Übergabe von Parametern an die Methode render()
:
{control poll $id, $message}
public function render(int $id, string $message): void
{
// ...
}
Manchmal kann eine Komponente aus mehreren Teilen bestehen, die wir separat rendern wollen. Für jedes dieser Teile wird eine
eigene Rendering-Methode erstellt, hier zum Beispiel renderPaginator()
:
public function renderPaginator(): void
{
// ...
}
Und in der Vorlage rufen wir sie dann mit auf:
{control poll:paginator}
Zum besseren Verständnis ist es gut zu wissen, wie der Tag in PHP-Code übersetzt wird.
{control poll}
{control poll:paginator 123, 'hello'}
Dies kompiliert zu:
$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');
getComponent()
Die Methode poll
gibt die Komponente zurück, und dann wird die Methode
render()
bzw. renderPaginator()
für sie aufgerufen.
Wenn irgendwo im Parameterteil =>
verwendet wird, werden alle Parameter in ein Array
eingeschlossen und als erstes Argument übergeben:
{control poll, id: 123, message: 'hello'}
kompiliert zu:
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
Rendering der Unterkomponente:
{control cartControl-someForm}
kompiliert zu:
$control->getComponent("cartControl-someForm")->render();
Komponenten, wie Präsentatoren, übergeben automatisch mehrere nützliche Variablen an Vorlagen:
$basePath
ist ein absoluter URL-Pfad zum Stammverzeichnis (z. B./CD-collection
)$baseUrl
ist eine absolute URL zum Stammverzeichnis (z. B.http://localhost/CD-collection
)$user
ist ein Objekt , das den Benutzer repräsentiert$presenter
ist der aktuelle Präsentator$control
ist die aktuelle Komponente$flashes
Liste der von der Methode gesendeten MeldungenflashMessage()
Signal
Wir wissen bereits, dass die Navigation in der Nette-Anwendung aus der Verknüpfung oder Routing zu Paaren
Presenter:action
besteht. Was aber, wenn wir nur eine Aktion auf der aktuellen Seite durchführen wollen? Zum
Beispiel die Sortierreihenfolge der Tabellenspalte ändern, ein Element löschen, den Hell/Dunkel-Modus umschalten, das Formular
abschicken, an der Umfrage teilnehmen usw.
Diese Art von Anfrage wird als Signal bezeichnet. Und wie Aktionen rufen Methoden auf action<Action>()
oder
render<Action>()
aufrufen, rufen Signale Methoden auf handle<Signal>()
. Während sich das
Konzept der Aktion (oder Ansicht) nur auf Präsentatoren bezieht, gelten Signale für alle Komponenten. Und damit auch für
Presenter, denn UI\Presenter
ist ein Abkömmling von UI\Control
.
public function handleClick(int $x, int $y): void
{
// ... Verarbeitung des Signals ...
}
Der Link, der das Signal aufruft, wird auf die übliche Weise erstellt, d.h. in der Vorlage durch das Attribut
n:href
oder den Tag {link}
, im Code durch die Methode link()
. Mehr dazu im Kapitel URL-Links erstellen.
<a n:href="click! $x, $y">click here</a>
Das Signal wird immer im aktuellen Präsentator und in der aktuellen Ansicht aufgerufen, es ist also nicht möglich, das Signal in einem anderen Präsentator / einer anderen Aktion zu verlinken.
Das Signal bewirkt also, dass die Seite genau so neu geladen wird wie in der ursprünglichen Anfrage, nur dass zusätzlich die Signalbehandlungsmethode mit den entsprechenden Parametern aufgerufen wird. Wenn die Methode nicht existiert, wird die Ausnahme Nette\Application\UI\BadSignalException ausgelöst, die dem Benutzer als Fehlerseite 403 Forbidden angezeigt wird.
Schnipsel und AJAX
Die Signale erinnern Sie vielleicht ein wenig an AJAX: Handler, die auf der aktuellen Seite aufgerufen werden. Und Sie haben Recht, Signale werden wirklich oft mit AJAX aufgerufen, und dann übertragen wir nur geänderte Teile der Seite an den Browser. Sie werden Snippets genannt. Mehr Informationen finden Sie auf der Seite über AJAX.
Flash-Meldungen
Eine Komponente verfügt über einen eigenen, vom Presenter unabhängigen Speicher für Flash-Nachrichten. Dies sind Meldungen, die z.B. über das Ergebnis der Operation informieren. Eine wichtige Eigenschaft von Flash-Meldungen ist, dass sie auch nach einer Routing in der Vorlage verfügbar sind. Selbst nachdem sie angezeigt wurden, bleiben sie noch 30 Sekunden lang erhalten – zum Beispiel für den Fall, dass der Benutzer die Seite ungewollt aktualisiert – die Nachricht geht nicht verloren.
Das Versenden erfolgt mit der Methode flashMessage. Der erste
Parameter ist der Nachrichtentext oder das stdClass
Objekt, das die Nachricht repräsentiert. Der optionale zweite
Parameter ist der Typ der Nachricht (Fehler, Warnung, Info, etc.). Die Methode flashMessage()
gibt eine Instanz von
flash message als Objekt stdClass zurück, an das Sie Informationen übergeben können.
$this->flashMessage('Artikel wurde gelöscht.');
$this->redirect(/* ... */); // und umleiten
In der Vorlage stehen diese Meldungen in der Variablen $flashes
als Objekte stdClass
zur Verfügung,
die die Eigenschaften message
(Meldungstext), type
(Meldungstyp) enthalten und die bereits erwähnten
Benutzerinformationen enthalten können. Wir zeichnen sie wie folgt:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Dauerhafte Parameter
Persistente Parameter werden verwendet, um den Zustand von Komponenten zwischen verschiedenen Anfragen zu erhalten. Ihr Wert bleibt gleich, auch wenn ein Link angeklickt wird. Im Gegensatz zu Sitzungsdaten werden sie in der URL übertragen. Und sie werden automatisch übertragen, einschließlich Links, die in anderen Komponenten auf derselben Seite erstellt wurden.
Sie haben zum Beispiel eine Komponente zum Paging von Inhalten. Es kann mehrere solcher Komponenten auf einer Seite geben. Und
Sie möchten, dass alle Komponenten auf ihrer aktuellen Seite bleiben, wenn Sie auf den Link klicken. Deshalb machen wir die
Seitennummer (page
) zu einem dauerhaften Parameter.
Das Erstellen eines dauerhaften Parameters ist in Nette extrem einfach. Erstellen Sie einfach eine öffentliche Eigenschaft und
versehen 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 öffentlich sein
}
Es wird empfohlen, den Datentyp (z. B. int
) mit der Eigenschaft zu verknüpfen, und Sie können auch einen
Standardwert angeben. Parameterwerte können validiert werden.
Sie können den Wert eines persistenten Parameters beim Erstellen eines Links ändern:
<a n:href="this page: $page + 1">next</a>
Oder er kann zurückgesetzt werden, d.h. aus der URL entfernt werden. Er nimmt dann seinen Standardwert an:
<a n:href="this page: null">reset</a>
Persistente Komponenten
Nicht nur Parameter, sondern auch Komponenten können persistent sein. Ihre persistenten Parameter werden auch zwischen
verschiedenen Aktionen oder zwischen verschiedenen Präsentatoren übertragen. Wir kennzeichnen persistente Komponenten mit diesen
Annotationen für die Presenter-Klasse. Zum Beispiel markieren wir hier die Komponenten calendar
und
poll
wie folgt:
/**
* @persistent(calendar, poll)
*/
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Sie müssen die Unterkomponenten nicht als persistent kennzeichnen, sie sind automatisch persistent.
In PHP 8 können Sie auch Attribute verwenden, um persistente Komponenten zu kennzeichnen:
use Nette\Application\Attributes\Persistent;
#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Komponenten mit Abhängigkeiten
Wie kann man Komponenten mit Abhängigkeiten erstellen, ohne die Präsentatoren, die sie verwenden werden, zu “versauen”? Dank der cleveren Funktionen des DI-Containers in Nette können wir, wie bei der Verwendung herkömmlicher Dienste, den Großteil der Arbeit dem Framework überlassen.
Nehmen wir als Beispiel eine Komponente, die von dem Dienst PollFacade
abhängig ist:
class PollControl extends Control
{
public function __construct(
private int $id, // Id einer Umfrage, für die die Komponente erstellt wird
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 befürchten. Der DI-Container würde sich unsichtbar um
die Übergabe aller Abhängigkeiten kümmern. Aber wir handhaben die Komponenten normalerweise so, dass wir eine neue Instanz von
ihnen direkt im Presenter in Factory-Methoden createComponent...()
erstellen. Aber
die Übergabe aller Abhängigkeiten aller Komponenten an den Presenter, um sie dann an die Komponenten weiterzugeben, ist
umständlich. Und die Menge des geschriebenen Codes…
Die logische Frage ist, warum registrieren wir die Komponente nicht einfach als klassischen Dienst, übergeben sie an den
Präsentator und geben sie dann in der Methode createComponent...()
zurück? Dieser Ansatz ist jedoch ungeeignet, da
wir die Komponente mehrfach erstellen können möchten.
Die richtige Lösung besteht darin, eine Fabrik für die Komponente zu schreiben, d. h. eine Klasse, die die Komponente für uns erstellt:
class PollControlFactory
{
public function __construct(
private PollFacade $facade,
) {
}
public function create(int $id): PollControl
{
return new PollControl($id, $this->facade);
}
}
Jetzt registrieren wir unseren Dienst am DI-Container zur Konfiguration:
services:
- PollControlFactory
Schließlich werden wir diese Fabrik in unserem Präsentator verwenden:
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 daran ist, dass Nette DI solche einfachen Fabriken generieren kann, so dass man nicht den ganzen Code schreiben muss, sondern nur die Schnittstelle:
interface PollControlFactory
{
public function create(int $id): PollControl;
}
Das ist alles. Nette implementiert diese Schnittstelle intern und injiziert sie in unseren Präsentator, wo wir sie verwenden
können. Außerdem werden auf magische Weise unser Parameter $id
und die Instanz der Klasse PollFacade
an unsere Komponente übergeben.
Komponenten im Detail
Komponenten in einer Nette-Anwendung sind die wiederverwendbaren Teile einer Web-Anwendung, die wir in Seiten einbetten, was das Thema dieses Kapitels ist. Was genau sind die Fähigkeiten einer solchen Komponente?
- sie ist in einer Vorlage renderbar
- es weiß, welcher Teil von sich selbst während einer AJAX-Anfrage zu rendern ist (Snippets)
- er kann seinen Zustand in einer URL speichern (persistente Parameter)
- es hat die Fähigkeit, auf Benutzeraktionen zu reagieren (Signale)
- er erstellt eine hierarchische Struktur (wobei die Wurzel der Präsentator ist)
Jede dieser Funktionen wird von einer der Klassen der Vererbungslinie ausgeführt. Die Darstellung (1 + 2) wird von Nette\Application\UI\Control übernommen, die Einbindung in den Lebenszyklus (3, 4) von der Klasse Nette\Application\UI\Component und die Erstellung der hierarchischen Struktur (5) von den 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 der Komponente
Lebenszyklus einer Komponente
Validierung von persistenten Parametern
Die Werte von persistenten Parametern, die von URLs empfangen werden, werden von der
Methode loadState()
in Eigenschaften geschrieben. Sie prüft auch, ob der für die Eigenschaft angegebene Datentyp
übereinstimmt, andernfalls antwortet sie mit einem 404-Fehler und die Seite wird nicht angezeigt.
Verlassen Sie sich niemals blind auf persistente Parameter, da sie leicht vom Benutzer in der URL überschrieben werden
können. So prüfen wir zum Beispiel, ob die Seitenzahl $this->page
größer als 0 ist. Eine gute Möglichkeit,
dies zu tun, ist, die oben 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 die $this->page gesetzt
// auf die Prüfung der Benutzerwerte:
if ($this->page < 1) {
$this->error();
}
}
}
Der umgekehrte Prozess, d. h. das Sammeln von Werten aus dauerhaften Propertys, wird von der Methode saveState()
übernommen.
Signale in der Tiefe
Ein Signal bewirkt ein Neuladen der Seite wie die ursprüngliche Anfrage (mit Ausnahme von AJAX) und ruft die Methode
signalReceived($signal)
auf, deren Standardimplementierung in der Klasse Nette\Application\UI\Component
versucht, eine Methode aufzurufen, die aus den Worten handle{Signal}
besteht. Die weitere Verarbeitung hängt von dem
angegebenen Objekt ab. Objekte, die Nachkommen von Component
sind (d.h. Control
und
Presenter
), versuchen, handle{Signal}
mit den entsprechenden Parametern aufzurufen.
Mit anderen Worten: die Definition der Methode handle{Signal}
wird genommen und alle Parameter, die in der Anfrage
empfangen wurden, werden mit den Parametern der Methode abgeglichen. Das bedeutet, dass der Parameter id
aus der URL
mit dem Parameter $id
der Methode abgeglichen wird, something
mit $something
und so weiter.
Und wenn die Methode nicht existiert, löst die Methode signalReceived
eine Ausnahme aus.
Das Signal kann von jeder Komponente empfangen werden, die die Schnittstelle SignalReceiver
implementiert, wenn
sie mit dem Komponentenbaum verbunden ist.
Die Hauptempfänger von Signalen sind Presenters
und visuelle Komponenten, die Control
erweitern. Ein
Signal ist ein Zeichen für ein Objekt, dass es etwas zu tun hat – eine Umfrage zählt eine Stimme vom Benutzer, eine Box mit
Nachrichten muss sich entfalten, ein Formular wurde gesendet und muss Daten verarbeiten und so weiter.
Die URL für das 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 an den Signalhandler übergeben wollen. Die Signalparameter werden an die URL des aktuellen Presenters/Views
angehängt. **Der Parameter ?do
in der URL bestimmt das aufgerufene Signal.
Sein Format ist {signal}
oder {signalReceiver}-{signal}
. {signalReceiver}
ist der Name
der Komponente im Presenter. Aus diesem Grund kann der Bindestrich nicht im Namen der Komponente vorkommen – er wird verwendet,
um den Namen der Komponente und des Signals zu trennen, aber es ist möglich, mehrere Komponenten zusammenzustellen.
Die Methode isSignalReceiver()
prüft, ob eine Komponente (erstes Argument) ein Empfänger eines Signals (zweites Argument) ist. Das zweite Argument kann
weggelassen werden – dann wird festgestellt, ob die Komponente ein Empfänger eines Signals ist. Wenn der zweite Parameter
true
lautet, wird geprüft, ob die Komponente oder ihre Nachkommen Empfänger eines Signals sind.
In jeder Phase, die handle{Signal}
vorausgeht, kann ein Signal manuell ausgeführt werden, indem die Methode processSignal()
aufgerufen wird, die die Verantwortung für die Signalausführung übernimmt. Nimmt die Empfängerkomponente (wenn nicht gesetzt,
ist es der Präsentator selbst) und sendet ihr das Signal.
Beispiel:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
Das Signal wird vorzeitig ausgeführt und wird nicht mehr aufgerufen.