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

Как правильно реализовать добавление и редактирование записи в 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;
	}

	private function recordFormSucceeded(Form $form, array $data): void
	{
		$this->facade->add($data); // добавляем запись в базу данных
		$this->flashMessage('Successfully added');
		$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;
	}

	private function recordFormSucceeded(Form $form, array $data): void
	{
		$this->facade->update($this->record->id, $data); // обновляем запись
		$this->flashMessage('Successfully updated');
		$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() и настраивать форму соответственно, либо переложить это на сами методы действий и избавиться от условной проверки:

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

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

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