Повторное использование форм в нескольких местах
Nette предлагает несколько способов использовать одну и ту же форму в нескольких местах, не дублируя код. Эта статья разбирает разные решения, в том числе те, которых стоит избегать.
Фабрика форм
Основной подход к повторному использованию компонента в нескольких местах – создать метод или класс, который его порождает. Этот метод затем вызывается из разных мест приложения. Такой метод или класс называется фабрикой. Не путайте это с шаблоном проектирования factory method, который описывает конкретный способ использования фабрик и с этой темой напрямую не связан.
Для примера создадим фабрику, собирающую форму редактирования:
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;
}
private 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('Record was saved');
$this->redirect('Homepage:');
};
return $form;
}
}
Это решение опирается на особенность форм: если у формы или у одного
из её элементов вызван addError(), последующие обработчики
onSuccess не вызываются.
Наследование от класса Form
Собранная форма не должна быть потомком класса Form. Иными
словами, избегайте такого подхода:
// ⛔ НЕТ! НАСЛЕДОВАНИЮ ЗДЕСЬ НЕ МЕСТО
class EditForm extends Form
{
public function __construct(Translator $translator)
{
parent::__construct();
$this->addText('title', 'Title:');
// здесь добавляются остальные поля формы
$this->addSubmit('send', 'Save');
$this->setTranslator($translator);
}
}
Вместо сборки формы в конструкторе используйте фабрику.
Важно осознавать, что класс Form – это прежде всего инструмент
для сборки форм, то есть построитель форм. Собранную форму можно
считать его продуктом. Однако продукт не является разновидностью
построителя; между ними нет отношения является, которое лежит в
основе наследования.
Компонент с формой
Совершенно другой подход состоит в создании компонента, который скрывает в себе форму. Это открывает новые возможности, например отрисовку формы особым образом, потому что у компонента есть собственный шаблон. Кроме того, можно использовать сигналы для 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;
}
private 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;
}
}