Форма для создания и редактирования записи

Как правильно реализовать добавление и редактирование записи в Nette, используя одну и ту же форму для обеих?

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

Добавление записи

Пример презентера, используемого для добавления записи. Мы оставим работу с базой данных классу Facade, код которого не имеет отношения к данному примеру.

use Nette\Application\UI\Form;

class RecordPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private Facade $facade,
	) {
	}

	protected function createComponentRecordForm(): Form
	{
		$form = new Form;

		// ... добавляем поля формы ...

		$form->onSuccess[] = [$this, 'recordFormSucceeded'];
		return $form;
	}

	public function recordFormSucceeded(Form $form, array $data): void
	{
		$this->facade->add($data); // добавляем запись в базу данных
		$this->flashMessage('Успешно добавлено');
		$this->redirect('...');
	}

	public function renderAdd(): void
	{
		// ...
	}
}

Редактирование записи

Теперь давайте посмотрим, как будет выглядеть презентер, используемый для редактирования записей:

use Nette\Application\UI\Form;

class RecordPresenter extends Nette\Application\UI\Presenter
{
	private $record;

	public function __construct(
		private Facade $facade,
	) {
	}

	public function actionEdit(int $id): void
	{
		$record = $this->facade->get($id);
		if (
			!$record // проверяем существование записи
			|| !$this->facade->isEditAllowed(/*...*/) // проверяем права доступа
		) {
			$this->error(); // ошибка 404
		}

		$this->record = $record;
	}

	protected function createComponentRecordForm(): Form
	{
		// проверяем, что выбрано действие 'edit'
		if ($this->getAction() !== 'edit') {
			$this->error();
		}

		$form = new Form;

		// ... добавляем поля формы ...

		$form->setDefaults($this->record); // устанавливаем значения по умолчанию
		$form->onSuccess[] = [$this, 'recordFormSucceeded'];
		return $form;
	}

	public function recordFormSucceeded(Form $form, array $data): void
	{
		$this->facade->update($this->record->id, $data); // обновляем запись
		$this->flashMessage('Успешно обновлено');
		$this->redirect('...');
	}
}

В методе action, который вызывается в самом начале жизненного цикла презентера, мы проверяем существование записи и разрешение пользователя на её редактирование.

Мы храним запись в свойстве $record, чтобы она была доступна в методе createComponentRecordForm() для установки значений по умолчанию и recordFormSucceeded() для идентификатора. Альтернативным решением может быть установка значений по умолчанию непосредственно в actionEdit() и значения ID, который является частью URL и извлекается с помощью getParameter('id'):

	public function actionEdit(int $id): void
	{
		$record = $this->facade->get($id);
		if (
			// проверяем существование и права доступа
		) {
			$this->error();
		}

		// устанавливаем значения по умолчанию для полей формы
		$this->getComponent('recordForm')
			->setDefaults($record);
	}

	public function recordFormSucceeded(Form $form, array $data): void
	{
		$id = (int) $this->getParameter('id');
		$this->facade->update($id, $data);
		// ...
	}
}

Однако, и это должно быть самым важным выводом из всего кода, нам нужно убедиться, что действие действительно edit, когда мы создаем форму. Потому что иначе валидация в методе actionEdit() вообще не произойдет!

Одна и та же форма для добавления и редактирования

А сейчас мы объединим оба презентера в один. Либо мы можем отличить, какое действие задействовано в методе createComponentRecordForm() и настроить форму соответствующим образом, либо мы можем оставить это непосредственно action-методам и избавиться от условия:

class RecordPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private Facade $facade,
	) {
	}

	public function actionAdd(): void
	{
		$form = $this->getComponent('recordForm');
		$form->onSuccess[] = [$this, 'addingFormSucceeded'];
	}

	public function actionEdit(int $id): void
	{
		$record = $this->facade->get($id);
		if (
			!$record // проверяем существование записи
			|| !$this->facade->isEditAllowed(/*...*/) // проверяем права доступа
		) {
			$this->error(); // ошибка 404
		}

		$form = $this->getComponent('recordForm');
		$form->setDefaults($record); // устанавливаем значения по умолчанию
		$form->onSuccess[] = [$this, 'editingFormSucceeded'];
	}

	protected function createComponentRecordForm(): Form
	{
		// проверяем, что текущее действие — 'add' или 'edit'
		if (!in_array($this->getAction(), ['add', 'edit'])) {
			$this->error();
		}

		$form = new Form;

		// ... добавляем поля формы ...

		return $form;
	}

	public function addingFormSucceeded(Form $form, array $data): void
	{
		$this->facade->add($data); // добавляем запись в базу данных
		$this->flashMessage('Успешно добавлено');
		$this->redirect('...');
	}

	public function editingFormSucceeded(Form $form, array $data): void
	{
		$id = (int) $this->getParameter('id');
		$this->facade->update($id, $data); // обновляем запись
		$this->flashMessage('Успешно обновлено');
		$this->redirect('...');
	}
}