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

В Nette у вас есть несколько вариантов повторного использования одной и той же формы в нескольких местах без дублирования кода. В этой статье мы рассмотрим различные решения, включая те, которых следует избегать.

Фабрика форм

Один из основных подходов к использованию одного и того же компонента в нескольких местах заключается в создании метода или класса, который генерирует компонент, а затем вызывает этот метод в разных местах приложения. Такой метод или класс называется фабрикой. Пожалуйста, не путайте с шаблоном проектирования фабричный метод, который описывает особый способ использования фабрик и не относится к данной теме.

В качестве примера давайте создадим фабрику, которая будет создавать форму редактирования:

use Nette\Application\UI\Form;

class FormFactory
{
	public function createEditForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Title:');
		// здесь добавляются дополнительные поля формы
		$form->addSubmit('send', 'Save');
		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', 'Title:');
		// здесь добавляются дополнительные поля формы
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Метод createForm() пока не делает ничего полезного, но это быстро изменится.

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

Со временем станет очевидно, что формы должны быть многоязычными. Это означает, что нам необходимо установить переводчик для всех форм. Для этого мы модифицируем класс 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() вызывается и другими методами, создающими конкретные формы, нам нужно установить транслятор только в этом методе. И все готово. Нет необходимости изменять код презентатора или компонента, что очень хорошо.

Другие фабричные классы

В качестве альтернативы вы можете создать несколько классов для каждой формы, которую вы хотите использовать в своем приложении. Такой подход может повысить читаемость кода и облегчить управление формами. Оставьте исходный 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', 'Save');
		return $form;
	}
}

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

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

Использование наследования в этом случае было бы совершенно непродуктивным. Вы бы очень быстро столкнулись с проблемами. Например, если бы вы захотели добавить параметры в метод create(), PHP выдал бы ошибку, что его сигнатура отличается от родительской. Или при передаче зависимости классу EditFormFactory через конструктор. Это привело бы к тому, что мы называем " ад конструктора".

В целом лучше отдать предпочтение композиции, а не наследованию.

Работа с формами

Обработчик формы, вызываемый после успешной отправки, также может быть частью класса-фабрики. Он будет работать, передавая отправленные данные в модель для обработки. Любые ошибки он будет передавать обратно в форму. Модель в следующем примере представлена классом Facade:

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

	public function create(): Form
	{
		$form = $this->formFactory->create();
		$form->addText('title', 'Title:');
		// здесь добавляются дополнительные поля формы
		$form->addSubmit('send', 'Save');
		$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, который будет выполнять перенаправление. Это позволит использовать форму в разных презентаторах, и каждый из них может перенаправлять на разные места.

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

Это решение использует свойство форм: когда addError() вызывается на форме или ее элементе, следующий обработчик onSuccess не вызывается.

Наследование от класса формы

Построенная форма не должна быть дочерней по отношению к форме. Другими словами, не используйте это решение:

// ⛔ НЕТ! НАСЛЕДСТВУ ЗДЕСЬ НЕ МЕСТО
class EditForm extends Form
{
	public function __construct(Translator $translator)
	{
		parent::__construct();
		$form->addText('title', 'Title:');
		// дополнительные поля формы добавляются здесь
		$form->addSubmit('send', 'Save');
		$form->setTranslator($translator);
	}
}

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

Важно понимать, что класс Form – это прежде всего инструмент для сборки формы, то есть конструктор форм. А собранную форму можно считать его продуктом. Однако продукт не является частным случаем конструктора; между ними нет отношения 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', 'Title:');
		// здесь добавляются дополнительные поля формы
		$form->addSubmit('send', 'Save');
		$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(): Form
	{
		$control = $this->controlFactory->create();

		$control->onSave[] = function (EditControl $control, $data) {
			$this->redirect('this');
			// или перенаправить на результат редактирования, например:
			// $this->redirect('detail', ['id' => $data->id]);
		};

		return $control;
	}
}