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 Meldungen flashMessage()

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\UI\Application\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?

  1. sie ist in einer Vorlage renderbar
  2. es weiß, welcher Teil von sich selbst während einer AJAX-Anfrage zu rendern ist (Snippets)
  3. er kann seinen Zustand in einer URL speichern (persistente Parameter)
  4. es hat die Fähigkeit, auf Benutzeraktionen zu reagieren (Signale)
  5. 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.

Version: 4.0