Ponowne użycie formularzy w wielu miejscach

W Nette masz kilka opcji, aby ponownie wykorzystać ten sam formularz w wielu miejscach bez duplikowania kodu. W tym artykule omówimy różne rozwiązania, w tym te, których powinieneś unikać.

Fabryka formularzy

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

Jako przykład, stwórzmy fabrykę, która zbuduje formularz edycji:

use Nette\Application\UI\Form;

class FormFactory
{
	public function createEditForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Title:');
		// dodatkowe pola formularza są dodane tutaj
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Teraz można użyć tej fabryki w różnych miejscach w aplikacji, na przykład w prezenterach lub komponentach. A zrobimy to poprzez zażądanie jej jako zależności. Więc najpierw zapiszemy klasę do pliku konfiguracyjnego:

services:
	- FormFactory

A potem używamy 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 przesłanych danych
		};
		return $form;
	}
}

Możesz rozszerzyć fabrykę formularzy o dodatkowe metody, aby stworzyć inne typy formularzy, aby dopasować je do swojej aplikacji. I, oczywiście, możesz dodać metodę, która tworzy podstawowy formularz bez elementów, z którego będą korzystać inne metody:

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

	public function createEditForm(): Form
	{
		$form = $this->createForm();
		$form->addText('title', 'Title:');
		// dodatkowe pola formularza są dodane tutaj
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

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

Zależności fabryczne

Z czasem okaże się, że potrzebujemy, aby formularze były wielojęzyczne. Oznacza to, że musimy skonfigurować translator dla wszystkich formularzy. Aby to zrobić, modyfikujemy klasę FormFactory, aby zaakceptowała obiekt Translator jako zależność w konstruktorze i przekazała go do formularza:

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ż metoda createForm() jest wywoływana również przez inne metody tworzące konkretne formularze, musimy ustawić translator tylko w tej metodzie. I gotowe. Nie trzeba zmieniać żadnego kodu prezentera lub komponentu, co jest świetne.

Więcej klas fabrycznych

Alternatywnie, możesz stworzyć 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. Pozostaw oryginalną FormFactory, aby stworzyć tylko czysty formularz z podstawową konfiguracją (na przykład z obsługą tłumaczeń) i utwórz nową fabrykę EditFormFactory dla formularza edycji.

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();
		// dodatkowe pola formularza są dodawane tutaj
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

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

// ⛔ NIE! DZIEDZICZENIE NIE NALEŻY DO TEGO MIEJSCA
class EditFormFactory extends FormFactory
{
	public function create(): Form
	{
		$form = parent::create();
		$form->addText('title', 'Title:');
		// tutaj dodaje się dodatkowe pola formularza
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Używanie dziedziczenia w tym przypadku byłoby całkowicie przeciwne do zamierzonego. Bardzo szybko napotkałbyś problemy. Na przykład, gdybyś chciał dodać parametry do metody create(); PHP zgłosiłoby błąd, że jej podpis jest inny niż rodzica. Albo podczas przekazywania zależności do klasy EditFormFactory poprzez konstruktor. To spowodowałoby coś, co nazywamy piekłem konstruktora.

Ogólnie rzecz biorąc, lepiej jest preferować kompozycję niż dziedziczenie.

Obsługa formularzy

Obsługa formularza, która jest wywoływana po pomyślnym przesłaniu danych, może być również częścią klasy fabrycznej. Jego działanie będzie polegało na przekazaniu przesłanych danych do modelu w celu ich przetworzenia. Wszelkie błędy zostaną przekazane z powrotem do formularza. Model w poniższym przykładzie jest reprezentowany przez klasę Facade:

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

	public function create(): Form
	{
		$form = $this->formFactory->create();
		$form->addText('title', 'Title:');
		// tutaj dodaje się dodatkowe pola formularza
		$form->addSubmit('send', 'Save');
		$form->onSuccess[] = [$this, 'processForm'];
		return $form;
	}

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

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

Niech prezenter sam zajmie się przekierowaniem. Doda on kolejny handler do zdarzenia onSuccess, który wykona przekierowanie. Dzięki temu formularz będzie mógł być używany w różnych prezenterach, a każdy z nich może przekierować do innej lokalizacji.

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

Rozwiązanie to wykorzystuje właściwość formularzy polegającą na tym, że po wywołaniu addError() na formularzu lub jego elemencie nie jest wywoływany następny handler onSuccess.

Dziedziczenie po klasie Form

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

// ⛔ NIE! DZIEDZICZENIE NIE NALEŻY DO TEGO MIEJSCA
class EditForm extends Form
{
	public function __construct(Translator $translator)
	{
		parent::__construct();
		$form->addText('title', 'Title:');
		// tutaj dodaje się dodatkowe pola formularza
		$form->addSubmit('send', 'Save');
		$form->setTranslator($translator);
	}
}

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

Ważne jest, aby zdać sobie sprawę, że klasa Form jest przede wszystkim narzędziem do składania formularza, czyli konstruktorem formularzy. A złożony formularz można uznać za jej produkt. Produkt nie jest jednak szczególnym przypadkiem konstruktora; nie ma między nimi relacji is a, która stanowi podstawę dziedziczenia.

Komponent formularza

Zupełnie innym podejściem jest stworzenie komponentu, który zawiera formularz. Daje to nowe możliwości, na przykład renderowanie formularza w określony sposób, ponieważ komponent zawiera szablon. Albo sygnały mogą być użyte do komunikacji AJAX i ładowania informacji do formularza, na przykład do podpowiedzi itp.

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:');
		// tutaj dodaje się dodatkowe pola formularza
		$form->addSubmit('send', 'Save');
		$form->onSuccess[] = [$this, 'processForm'];

		return $form;
	}

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

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

		// wywoływanie zdarzeń
		$this->onSave($this, $data);
	}
}

Stwórzmy fabrykę, która będzie produkowała ten komponent. Wystarczy, że napiszemy jej interfejs:

interface EditControlFactory
{
	function create(): EditControl;
}

I dodać go do pliku konfiguracyjnego:

services:
	- EditControlFactory

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

		return $control;
	}
}