Повторно използване на форми на няколко места

В Nette имате на разположение няколко опции как да използвате една и съща форма на няколко места и да не дублирате код. В тази статия ще покажем различни решения, включително тези, които трябва да избягвате.

Фабрика за форми

Един от основните подходи за използване на един и същ компонент на няколко места е създаването на метод или клас, който генерира този компонент, и последващото извикване на този метод на различни места в приложението. Такъв метод или клас се нарича фабрика. Моля, не го бъркайте с дизайн патърна factory method, който описва специфичен начин за използване на фабрики и не е свързан с тази тема.

Като пример ще създадем фабрика, която ще изгражда форма за редактиране:

use Nette\Application\UI\Form;

class FormFactory
{
	public function createEditForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Заглавие:');
		// тук се добавят други полета на формата
		$form->addSubmit('send', 'Изпрати');
		return $form;
	}
}

Сега можете да използвате тази фабрика на различни места във вашето приложение, например в презентери или компоненти. И това става, като я поискаме като зависимост. Първо, записваме класа в конфигурационния файл:

services:
	- FormFactory

И след това я използваме в презентера:

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

	protected function createComponentEditForm(): Form
	{
		$form = $this->formFactory->createEditForm();
		$form->onSuccess[] = function () {
			// обработка на изпратените данни
		};
		return $form;
	}
}

Можете да разширите фабриката за форми с допълнителни методи за създаване на други видове форми според нуждите на вашето приложение. И разбира се, можем да добавим и метод, който създава основна форма без елементи, и този метод ще бъде използван от другите методи:

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

	public function createEditForm(): Form
	{
		$form = $this->createForm();
		$form->addText('title', 'Заглавие:');
		// тук се добавят други полета на формата
		$form->addSubmit('send', 'Изпрати');
		return $form;
	}
}

Методът createForm() засега не прави нищо полезно, но това бързо ще се промени.

Зависимости на фабриката

С времето ще се окаже, че се нуждаем формите да бъдат многоезични. Това означава, че трябва да зададем т.нар. translator на всички форми. За тази цел ще модифицираме класа FormFactory, така че да приема обект Translator като зависимост в конструктора и ще го предадем на формата:

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

	// ...
}

Тъй като методът createForm() се извиква и от другите методи, създаващи специфични форми, е достатъчно да зададем translator-а само в него. И сме готови. Няма нужда да променяме кода на нито един презентер или компонент, което е страхотно.

Множество фабрични класове

Алтернативно, можете да създадете множество класове за всяка форма, която искате да използвате във вашето приложение. Този подход може да увеличи четимостта на кода и да улесни управлението на формите. Ще оставим оригиналната FormFactory да създава само чиста форма с основна конфигурация (например с поддръжка на преводи) и ще създадем нова фабрика EditFormFactory за формата за редактиране.

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

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


// ✅ използване на композиция
class EditFormFactory
{
	public function __construct(
		private FormFactory $formFactory,
	) {
	}

	public function create(): Form
	{
		$form = $this->formFactory->create();
		// тук се добавят други полета на формата
		$form->addSubmit('send', 'Изпрати');
		return $form;
	}
}

Много е важно връзката между класовете FormFactory и EditFormFactory да се реализира чрез композиция, а не чрез обектно наследяване:

// ⛔ НЕ ТАКА! НАСЛЕДЯВАНЕТО НЕ Е ЗА ТУК
class EditFormFactory extends FormFactory
{
	public function create(): Form
	{
		$form = parent::create();
		$form->addText('title', 'Заглавие:');
		// тук се добавят други полета на формата
		$form->addSubmit('send', 'Изпрати');
		return $form;
	}
}

Използването на наследяване в този случай би било напълно контрапродуктивно. Много бързо ще се сблъскате с проблеми. Например, в момента, в който искате да добавите параметри към метода create(); PHP ще съобщи за грешка, че неговата сигнатура се различава от родителската. Или при предаване на зависимост към класа EditFormFactory чрез конструктора. Ще възникне ситуация, която наричаме constructor hell.

Като цяло е по-добре да се дава предимство на композицията пред наследяването.

Обработка на формата

Обработката на формата, която се извиква след успешно изпращане, също може да бъде част от фабричния клас. Тя ще работи, като предава изпратените данни на модела за обработка. Евентуални грешки ще предаде обратно на формата. Моделът в следващия пример е представен от класа Facade:

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

	public function create(): Form
	{
		$form = $this->formFactory->create();
		$form->addText('title', 'Заглавие:');
		// тук се добавят други полета на формата
		$form->addSubmit('send', 'Изпрати');
		$form->onSuccess[] = [$this, 'processForm'];
		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// обработка на изпратените данни
			$this->facade->process($data);

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

Самото пренасочване обаче ще оставим на презентера. Той ще добави към събитието onSuccess допълнителен handler, който ще извърши пренасочването. Благодарение на това ще бъде възможно да се използва формата в различни презентери и във всеки да се пренасочва към различно място.

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('Записът е запазен');
			$this->redirect('Homepage:');
		};
		return $form;
	}
}

Това решение използва свойството на формите, че когато се извика addError() върху формата или неин елемент, следващият handler onSuccess вече не се извиква.

Наследяване от класа Form

Изградената форма не трябва да бъде наследник на формата. С други думи, не използвайте това решение:

// ⛔ НЕ ТАКА! НАСЛЕДЯВАНЕТО НЕ Е ЗА ТУК
class EditForm extends Form
{
	public function __construct(Translator $translator)
	{
		parent::__construct();
		$this->addText('title', 'Заглавие:');
		// тук се добавят други полета на формата
		$this->addSubmit('send', 'Изпрати');
		$this->setTranslator($translator);
	}
}

Вместо да изграждате формата в конструктора, използвайте фабрика.

Трябва да се осъзнае, че класът Form е преди всичко инструмент за изграждане на форма, т.е. form builder. А изградената форма може да се разглежда като неин продукт. Но продуктът не е специфичен случай на builder-а, между тях няма връзка is a, която е основата на наследяването.

Компонент с форма

Напълно различен подход представлява създаването на компонент, чиято част е форма. Това дава нови възможности, например да се рендира формата по специфичен начин, тъй като компонентът включва и шаблон. Или могат да се използват сигнали за AJAX комуникация и дозареждане на информация във формата, например за подсказки и т.н.

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', 'Заглавие:');
		// тук се добавят други полета на формата
		$form->addSubmit('send', 'Изпрати');
		$form->onSuccess[] = [$this, 'processForm'];

		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// обработка на изпратените данни
			$this->facade->process($data);

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

		// извикване на събитие
		$this->onSave($this, $data);
	}
}

Ще създадем и фабрика, която ще произвежда този компонент. Достатъчно е да запишем нейния интерфейс:

interface EditControlFactory
{
	function create(): EditControl;
}

И да добавим в конфигурационния файл:

services:
	- EditControlFactory

И сега вече можем да поискаме фабриката и да я използваме в презентера:

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');
			// или пренасочваме към резултата от редактирането, напр.:
			// $this->redirect('detail', ['id' => $data->id]);
		};

		return $control;
	}
}