レコードの作成と編集のためのフォーム

Nette でレコードの追加と編集を、同じフォームを使って正しく実装するにはどうすればよいでしょうか。

多くの場合、レコードの追加と編集のフォームは同一で、違うのはボタンのラベルくらいです。ここでは簡単なプレゼンターの例を通して、まずフォームをレコードの追加に使い、次に編集に使い、最後にその 2 つをひとつにまとめます。

レコードの追加

レコードを追加するプレゼンターの例です。実際のデータベースとのやり取りは 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() メソッドから、ID にアクセスするために 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() メソッドでの確認がまったく行われないからです。

追加と編集で同じフォームを使う

では 2 つのプレゼンターをひとつにまとめましょう。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('...');
	}
}