複数の場所でのフォームの再利用

Netteでは、コードを複製することなく、同じフォームを複数の場所で使用するためのいくつかのオプションがあります。この記事では、避けるべきものも含め、さまざまな解決策を紹介します。

フォームファクトリ

同じコンポーネントを複数の場所で使用するための基本的なアプローチの1つは、このコンポーネントを生成するメソッドまたはクラスを作成し、その後、アプリケーションのさまざまな場所でこのメソッドを呼び出すことです。このようなメソッドまたはクラスは ファクトリ と呼ばれます。ファクトリの特定の利用方法を説明するデザインパターン factory method と混同しないでください。これはこのトピックとは関係ありません。

例として、編集フォームを組み立てるファクトリを作成します。

use Nette\Application\UI\Form;

class FormFactory
{
	public function createEditForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'タイトル:');
		// ここに他のフォームフィールドを追加します
		$form->addSubmit('send', '送信');
		return $form;
	}
}

これで、アプリケーションのさまざまな場所、たとえばPresenterやコンポーネントで、このファクトリを使用できます。それは、依存関係として要求します ことによって行います。まず、クラスを設定ファイルに記述します。

services:
	- FormFactory

そして、Presenterで使用します。

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', 'タイトル:');
		// ここに他のフォームフィールドを追加します
		$form->addSubmit('send', '送信');
		return $form;
	}
}

createForm() メソッドはまだ何も有用なことをしていませんが、それはすぐに変わります。

ファクトリの依存関係

やがて、フォームが多言語対応である必要があることがわかります。これは、すべてのフォームにいわゆる translator を設定する必要があることを意味します。この目的のために、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() メソッドは特定のフォームを作成する他のメソッドからも呼び出されるため、translatorを設定するのはそのメソッドだけで十分です。そして完了です。Presenterやコンポーネントのコードを変更する必要はありません。これは素晴らしいことです。

複数のファクトリクラス

あるいは、アプリケーションで使用したい各フォームに対して複数のクラスを作成することもできます。 このアプローチは、コードの可読性を向上させ、フォームの管理を容易にすることができます。元の 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', '送信');
		return $form;
	}
}

FormFactoryEditFormFactory クラス間の関連付けが、オブジェクト継承 ではなく コンポジション によって実現されることが非常に重要です。

// ⛔ これはダメ!継承はここには属しません
class EditFormFactory extends FormFactory
{
	public function create(): Form
	{
		$form = parent::create();
		$form->addText('title', 'タイトル:');
		// ここに他のフォームフィールドを追加します
		$form->addSubmit('send', '送信');
		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', 'タイトル:');
		// ここに他のフォームフィールドを追加します
		$form->addSubmit('send', '送信');
		$form->onSuccess[] = [$this, 'processForm'];
		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// 送信されたデータの処理
			$this->facade->process($data);

		} catch (AnyModelException $e) {
			$form->addError('...');
		}
	}
}

ただし、リダイレクト自体はPresenterに任せます。Presenterは onSuccess イベントにリダイレクトを実行する別のハンドラを追加します。これにより、フォームを異なるPresenterで使用し、それぞれで異なる場所にリダイレクトすることが可能になります。

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('レコードが保存されました');
			$this->redirect('Homepage:');
		};
		return $form;
	}
}

この解決策は、フォームまたはその要素に対して addError() が呼び出されると、次の onSuccess ハンドラが呼び出されないというフォームのプロパティを利用します。

Formクラスからの継承

組み立てられたフォームは、フォームの子孫であってはなりません。言い換えれば、この解決策を使用しないでください。

// ⛔ これはダメ!継承はここには属しません
class EditForm extends Form
{
	public function __construct(Translator $translator)
	{
		parent::__construct();
		$form->addText('title', 'タイトル:');
		// ここに他のフォームフィールドを追加します
		$form->addSubmit('send', '送信');
		$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', 'タイトル:');
		// ここに他のフォームフィールドを追加します
		$form->addSubmit('send', '送信');
		$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

そして今、ファクトリを要求してPresenterで使用できます。

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