Wielokrotne użycie formularzy w wielu miejscach

W Nette masz do dyspozycji kilka opcji, jak użyć tego samego formularza w wielu miejscach i nie duplikować kodu. W tym artykule pokażemy różne rozwiązania, w tym te, których powinieneś unikać.

Fabryka formularzy

Jednym z podstawowych podejść do użycia tego samego komponentu w wielu miejscach jest utworzenie metody lub klasy, która generuje ten komponent, a następnie wywoływanie tej metody w różnych miejscach aplikacji. Taka metoda lub klasa nazywana jest fabryką. Proszę nie mylić z wzorcem projektowym factory method, który opisuje specyficzny sposób wykorzystania fabryk i nie jest związany z tym tematem.

Jako przykład stworzymy fabrykę, która będzie budować formularz edycyjny:

use Nette\Application\UI\Form;

class FormFactory
{
	public function createEditForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Tytuł:');
		// tutaj dodawane są kolejne pola formularza
		$form->addSubmit('send', 'Wyślij');
		return $form;
	}
}

Teraz możesz użyć tej fabryki w różnych miejscach w swojej aplikacji, na przykład w presenterach lub komponentach. A to tak, że zażądamy jej jako zależności. Najpierw więc zapiszemy klasę do pliku konfiguracyjnego:

services:
	- FormFactory

A potem użyjemy jej w prezenterze:

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

	protected function createComponentEditForm(): Form
	{
		$form = $this->formFactory->createEditForm();
		$form->onSuccess[] = function () {
			// przetwarzanie wysłanych danych
		};
		return $form;
	}
}

Fabrykę formularzy możesz rozszerzyć o kolejne metody do tworzenia innych rodzajów formularzy zgodnie z potrzebami Twojej aplikacji. I oczywiście możemy dodać również metodę, która stworzy podstawowy formularz bez elementów, a tę będą wykorzystywać inne metody:

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

	public function createEditForm(): Form
	{
		$form = $this->createForm();
		$form->addText('title', 'Tytuł:');
		// tutaj dodawane są kolejne pola formularza
		$form->addSubmit('send', 'Wyślij');
		return $form;
	}
}

Metoda createForm() na razie nie robi nic użytecznego, ale to się szybko zmieni.

Zależności fabryki

Z czasem okaże się, że potrzebujemy, aby formularze były wielojęzyczne. Oznacza to, że wszystkim formularzom musimy ustawić tzw. translator. W tym celu zmodyfikujemy klasę FormFactory tak, aby przyjmowała obiekt Translator jako zależność w konstruktorze, i przekażemy go formularzowi:

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;
	}

	// ...
}

Ponieważ metodę createForm() wywołują również inne metody tworzące specyficzne formularze, wystarczy translator ustawić tylko w niej. I gotowe. Nie ma potrzeby zmieniać kodu żadnego presentera ani komponentu, co jest świetne.

Wiele klas fabryk

Alternatywnie możesz utworzyć wiele klas dla każdego formularza, który chcesz użyć w swojej aplikacji. Takie podejście może zwiększyć czytelność kodu i ułatwić zarządzanie formularzami. Pierwotną FormFactory pozostawimy do tworzenia tylko czystego formularza z podstawową konfiguracją (na przykład ze wsparciem tłumaczeń), a dla formularza edycyjnego stworzymy nową fabrykę EditFormFactory.

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

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


// ✅ użycie kompozycji
class EditFormFactory
{
	public function __construct(
		private FormFactory $formFactory,
	) {
	}

	public function create(): Form
	{
		$form = $this->formFactory->create();
		// tutaj dodawane są kolejne pola formularza
		$form->addSubmit('send', 'Wyślij');
		return $form;
	}
}

Bardzo ważne jest, aby powiązanie między klasami FormFactory i EditFormFactory było realizowane przez kompozycję, a nie przez dziedziczenie obiektowe:

// ⛔ TAK NIE! DZIEDZICZENIE TU NIE PASUJE
class EditFormFactory extends FormFactory
{
	public function create(): Form
	{
		$form = parent::create();
		$form->addText('title', 'Tytuł:');
		// tutaj dodawane są kolejne pola formularza
		$form->addSubmit('send', 'Wyślij');
		return $form;
	}
}

Użycie dziedziczenia byłoby w tym przypadku całkowicie kontrproduktywne. Na problemy napotkałbyś bardzo szybko. Na przykład w chwili, gdy chciałbyś dodać parametry do metody create(); PHP zgłosiłoby błąd, że jej sygnatura różni się od rodzicielskiej. Lub przy przekazywaniu zależności do klasy EditFormFactory przez konstruktor. Powstałaby sytuacja, którą nazywamy constructor hell.

Ogólnie lepiej jest preferować kompozycję nad dziedziczeniem.

Obsługa formularza

Obsługa formularza, która jest wywoływana po pomyślnym wysłaniu, może być również częścią klasy fabryki. Będzie działać tak, że przekaże wysłane dane do modelu w celu przetworzenia. Ewentualne błędy przekazuje z powrotem do formularza. Model w poniższym przykładzie reprezentuje klasa Facade:

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

	public function create(): Form
	{
		$form = $this->formFactory->create();
		$form->addText('title', 'Tytuł:');
		// tutaj dodawane są kolejne pola formularza
		$form->addSubmit('send', 'Wyślij');
		$form->onSuccess[] = [$this, 'processForm'];
		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// przetwarzanie wysłanych danych
			$this->facade->process($data);

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

Samo przekierowanie pozostawimy jednak prezenterowi. Ten doda do zdarzenia onSuccess kolejny handler, który wykona przekierowanie. Dzięki temu będzie można użyć formularza w różnych prezenterach i w każdym przekierować gdzie indziej.

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('Rekord został zapisany');
			$this->redirect('Homepage:');
		};
		return $form;
	}
}

To rozwiązanie wykorzystuje właściwość formularzy, że gdy na formularzu lub jego elemencie zostanie wywołane addError(), kolejne handlery onSuccess nie są już wywoływane.

Dziedziczenie od klasy Form

Zbudowany formularz nie powinien być potomkiem formularza. Innymi słowy, nie używaj tego rozwiązania:

// ⛔ TAK NIE! DZIEDZICZENIE TU NIE PASUJE
class EditForm extends Form
{
	public function __construct(Translator $translator)
	{
		parent::__construct();
		$this->addText('title', 'Tytuł:');
		// tutaj dodawane są kolejne pola formularza
		$this->addSubmit('send', 'Wyślij');
		$this->setTranslator($translator);
	}
}

Zamiast budować formularz w konstruktorze, użyj fabryki.

Należy zdać sobie sprawę, że klasa Form jest przede wszystkim narzędziem do budowania formularza, czyli form builder. A zbudowany formularz można rozumieć jako jej produkt. Jednak produkt nie jest specyficznym przypadkiem buildera, nie ma między nimi relacji is a stanowiącej podstawę dziedziczenia.

Komponent z formularzem

Całkowicie inne podejście stanowi tworzenie komponentu, którego częścią jest formularz. Daje to nowe możliwości, na przykład renderowanie formularza w specyficzny sposób, ponieważ częścią komponentu jest również szablon. Lub można wykorzystać sygnały do komunikacji AJAX i doładowywania informacji do formularza, na przykład do podpowiadania, itd.

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', 'Tytuł:');
		// tutaj dodawane są kolejne pola formularza
		$form->addSubmit('send', 'Wyślij');
		$form->onSuccess[] = [$this, 'processForm'];

		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// przetwarzanie wysłanych danych
			$this->facade->process($data);

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

		// wywołanie zdarzenia
		$this->onSave($this, $data);
	}
}

Stworzymy jeszcze fabrykę, która będzie produkować ten komponent. Wystarczy zapisać jej interfejs:

interface EditControlFactory
{
	function create(): EditControl;
}

I dodać do pliku konfiguracyjnego:

services:
	- EditControlFactory

A teraz już możemy zażądać fabryki i użyć jej w prezenterze:

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');
			// lub przekierowujemy na wynik edycji, np.:
			// $this->redirect('detail', ['id' => $data->id]);
		};

		return $control;
	}
}