Formulare an mehreren Stellen wiederverwenden

In Nette haben Sie mehrere Möglichkeiten, dasselbe Formular an mehreren Stellen wiederzuverwenden, ohne Code zu duplizieren. In diesem Artikel gehen wir auf die verschiedenen Lösungen ein, einschließlich derer, die Sie vermeiden sollten.

Formular-Fabrik

Ein grundlegender Ansatz für die Verwendung derselben Komponente an mehreren Stellen besteht darin, eine Methode oder Klasse zu erstellen, die die Komponente erzeugt, und diese Methode dann an verschiedenen Stellen in der Anwendung aufzurufen. Eine solche Methode oder Klasse wird als Factory bezeichnet. Bitte nicht mit dem Entwurfsmuster Fabrikmethode verwechseln, das eine spezielle Art der Verwendung von Fabriken beschreibt und nicht mit diesem Thema zusammenhängt.

Lassen Sie uns als Beispiel eine Fabrik erstellen, die ein Bearbeitungsformular erstellt:

use Nette\Application\UI\Form;

class FormFactory
{
	public function createEditForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Title:');
		// zusätzliche Formularfelder werden hier hinzugefügt
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Jetzt können Sie diese Fabrik an verschiedenen Stellen in Ihrer Anwendung verwenden, zum Beispiel in Presentern oder Komponenten. Und wir tun dies, indem wir sie als Abhängigkeit anfordern. Zuerst schreiben wir also die Klasse in die Konfigurationsdatei:

services:
	- FormFactory

Und dann verwenden wir sie im Präsentator:

class MyPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private FormFactory $formFactory,
	) {
	}

	protected function createComponentEditForm(): Form
	{
		$form = $this->formFactory->createEditForm();
		$form->onSuccess[] = function () {
			// Verarbeitung der gesendeten Daten
		};
		return $form;
	}
}

Sie können die Formularfabrik mit zusätzlichen Methoden erweitern, um andere Arten von Formularen für Ihre Anwendung zu erstellen. Und natürlich können Sie auch eine Methode hinzufügen, die ein Basisformular ohne Elemente erstellt, das die anderen Methoden verwenden werden:

class FormFactory
{
	public function createForm(): Form
	{
		$form = new Form;
		return $form;
	}

	public function createEditForm(): Form
	{
		$form = $this->createForm();
		$form->addText('title', 'Title:');
		// zusätzliche Formularfelder werden hier hinzugefügt
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Die Methode createForm() tut noch nichts Nützliches, aber das wird sich schnell ändern.

Abhängigkeiten von der Fabrik

Mit der Zeit wird sich herausstellen, dass die Formulare mehrsprachig sein müssen. Das bedeutet, dass wir einen Übersetzer für alle Formulare einrichten müssen. Zu diesem Zweck ändern wir die Klasse FormFactory so, dass sie das Objekt Translator als Abhängigkeit im Konstruktor akzeptiert und an das Formular übergibt:

use Nette\Localization\Translator;

class FormFactory
{
	public function __construct(
		private Translator $translator,
	) {
	}

	public function createForm(): Form
	{
		$form = new Form;
		$form->setTranslator($this->translator);
		return $form;
	}

	//...
}

Da die Methode createForm() auch von anderen Methoden aufgerufen wird, die bestimmte Formulare erstellen, müssen wir den Übersetzer nur in dieser Methode festlegen. Und schon sind wir fertig. Es ist nicht nötig, den Code des Presenters oder der Komponente zu ändern, was großartig ist.

Weitere Factory-Klassen

Alternativ können Sie für jedes Formular, das Sie in Ihrer Anwendung verwenden möchten, mehrere Klassen erstellen. Dieser Ansatz kann die Lesbarkeit des Codes erhöhen und die Verwaltung der Formulare erleichtern. Belassen Sie das Original FormFactory, um nur ein reines Formular mit Grundkonfiguration zu erstellen (z. B. mit Übersetzungsunterstützung), und erstellen Sie eine neue Fabrik EditFormFactory für das Bearbeitungsformular.

class FormFactory
{
	public function __construct(
		private Translator $translator,
	) {
	}

	public function create(): Form
	{
		$form = new Form;
		$form->setTranslator($this->translator);
		return $form;
	}
}


// ✅ Verwendung der Zusammensetzung
class EditFormFactory
{
	public function __construct(
		private FormFactory $formFactory,
	) {
	}

	public function create(): Form
	{
		$form = $this->formFactory->create();
		// hier werden zusätzliche Formularfelder hinzugefügt
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Es ist sehr wichtig, dass die Bindung zwischen den Klassen FormFactory und EditFormFactory durch Komposition und nicht durch Objektvererbung implementiert wird:

// ⛔ NEIN! VERERBUNG GEHÖRT HIER NICHT HIN
class EditFormFactory extends FormFactory
{
	public function create(): Form
	{
		$form = parent::create();
		$form->addText('title', 'Title:');
		// zusätzliche Formularfelder werden hier hinzugefügt
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Die Verwendung von Vererbung wäre in diesem Fall völlig kontraproduktiv. Sie würden sehr schnell auf Probleme stoßen. Wenn Sie z.B. der Methode create() Parameter hinzufügen wollten, würde PHP einen Fehler melden, dass sich die Signatur der Methode von der des Elternteils unterscheidet. Oder bei der Übergabe einer Abhängigkeit an die Klasse EditFormFactory über den Konstruktor. Dies würde zu dem führen, was wir Konstruktorhölle nennen.

Im Allgemeinen ist es besser, die Komposition der Vererbung vorzuziehen.

Handhabung von Formularen

Der Formular-Handler, der nach einer erfolgreichen Übermittlung aufgerufen wird, kann auch Teil einer Fabrikklasse sein. Er arbeitet, indem er die übermittelten Daten zur Verarbeitung an das Modell weitergibt. Eventuelle Fehler werden an das Formular zurückgegeben. Das Modell im folgenden Beispiel wird durch die Klasse Facade repräsentiert:

class EditFormFactory
{
	public function __construct(
		private FormFactory $formFactory,
		private Facade $facade,
	) {
	}

	public function create(): Form
	{
		$form = $this->formFactory->create();
		$form->addText('title', 'Title:');
		// zusätzliche Formularfelder werden hier hinzugefügt
		$form->addSubmit('send', 'Save');
		$form->onSuccess[] = [$this, 'processForm'];
		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// Verarbeitung der übermittelten Daten
			$this->facade->process($data);

		} catch (AnyModelException $e) {
			$form->addError('...');
		}
	}
}

Der Präsentator soll die Umleitung selbst durchführen. Er fügt dem Ereignis onSuccess einen weiteren Handler hinzu, der die Umleitung durchführt. Auf diese Weise kann das Formular in verschiedenen Präsentatoren verwendet werden, und jeder kann an eine andere Stelle weiterleiten.

class MyPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private EditFormFactory $formFactory,
	) {
	}

	protected function createComponentEditForm(): Form
	{
		$form = $this->formFactory->create();
		$form->onSuccess[] = function () {
			$this->flashMessage('Záznam byl uložen');
			$this->redirect('Homepage:');
		};
		return $form;
	}
}

Diese Lösung macht sich die Eigenschaft von Formularen zunutze, dass beim Aufruf von addError() auf einem Formular oder seinem Element der nächste onSuccess -Handler nicht aufgerufen wird.

Vererbung von der Formularklasse

Ein erstelltes Formular sollte nicht das Kind eines Formulars sein. Mit anderen Worten: Verwenden Sie diese Lösung nicht:

// ⛔ NEIN! VERERBUNG GEHÖRT HIER NICHT HIN
class EditForm extends Form
{
	public function __construct(Translator $translator)
	{
		parent::__construct();
		$form->addText('title', 'Title:');
		// zusätzliche Formularfelder werden hier hinzugefügt
		$form->addSubmit('send', 'Save');
		$form->setTranslator($translator);
	}
}

Anstatt das Formular im Konstruktor zu erstellen, verwenden Sie die Fabrik.

Es ist wichtig zu erkennen, dass die Klasse Form in erster Linie ein Werkzeug zum Zusammenstellen eines Formulars ist, d.h. ein Formularersteller. Und das zusammengesetzte Formular kann als ihr Produkt betrachtet werden. Das Produkt ist jedoch kein Sonderfall des Builders; es gibt keine ist eine Beziehung zwischen ihnen, die die Grundlage der Vererbung bildet.

Formular-Komponente

Ein völlig anderer Ansatz besteht darin, eine Komponente zu erstellen, die ein Formular enthält. Dadurch ergeben sich neue Möglichkeiten, z. B. um das Formular auf eine bestimmte Art und Weise zu rendern, da die Komponente eine Vorlage enthält. Oder es können Signale für die AJAX-Kommunikation und das Laden von Informationen in das Formular verwendet werden, z. B. für Hinting usw.

use Nette\Application\UI\Form;

class EditControl extends Nette\Application\UI\Control
{
	public array $onSave = [];

	public function __construct(
		private Facade $facade,
	) {
	}

	protected function createComponentForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Title:');
		// zusätzliche Formularfelder werden hier hinzugefügt
		$form->addSubmit('send', 'Save');
		$form->onSuccess[] = [$this, 'processForm'];

		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// Verarbeitung der übermittelten Daten
			$this->facade->process($data);

		} catch (AnyModelException $e) {
			$form->addError('...');
			return;
		}

		// Ereignisaufruf
		$this->onSave($this, $data);
	}
}

Lassen Sie uns eine Fabrik erstellen, die diese Komponente produzieren wird. Es genügt, ihre Schnittstelle zu schreiben:

interface EditControlFactory
{
	function create(): EditControl;
}

Und fügen Sie sie der Konfigurationsdatei hinzu:

services:
	- EditControlFactory

Jetzt können wir die Fabrik anfordern und sie im Präsentator verwenden:

class MyPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private EditControlFactory $controlFactory,
	) {
	}

	protected function createComponentEditForm(): EditControl
	{
		$control = $this->controlFactory->create();

		$control->onSave[] = function (EditControl $control, $data) {
			$this->redirect('this');
			// oder auf das Ergebnis der Bearbeitung umleiten, z. B:
			// $this->redirect('detail', ['id' => $data->id]);
		};

		return $control;
	}
}