Presenter内のフォーム

Nette Formsは、Webフォームの作成と処理を大幅に簡素化します。この章では、Presenter内でフォームを使用する方法を学びます。

フレームワークの残りの部分なしで完全にスタンドアロンで使用する方法に興味がある場合は、スタンドアロンでの使用のガイドが用意されています。

最初のフォーム

簡単な登録フォームを作成してみましょう。そのコードは次のようになります:

use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', '名前:');
$form->addPassword('password', 'パスワード:');
$form->addSubmit('send', '登録');
$form->onSuccess[] = [$this, 'formSucceeded'];

そして、ブラウザでは次のように表示されます:

Presenter内のフォームは Nette\Application\UI\Form クラスのオブジェクトであり、その前身である Nette\Forms\Form はスタンドアロンでの使用を目的としています。名前、パスワード、送信ボタンといういわゆる要素を追加しました。そして最後に、$form->onSuccess の行は、送信され、正常に検証された後、$this->formSucceeded() メソッドを呼び出す必要があることを示しています。

Presenterの観点から見ると、フォームは通常のコンポーネントです。したがって、コンポーネントとして扱われ、ファクトリメソッドを使用してPresenterに組み込まれます。次のようになります:

use Nette;
use Nette\Application\UI\Form;

class HomePresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentRegistrationForm(): Form
	{
		$form = new Form;
		$form->addText('name', '名前:');
		$form->addPassword('password', 'パスワード:');
		$form->addSubmit('send', '登録');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// ここでフォームから送信されたデータを処理します
		// $data->name には名前が含まれます
		// $data->password にはパスワードが含まれます
		$this->flashMessage('正常に登録されました。');
		$this->redirect('Home:');
	}
}

そして、テンプレートでは {control} タグを使用してフォームをレンダリングします:

<h1>登録</h1>

{control registrationForm}

そして、それがすべてです :-) 機能的で完全に保護されたフォームがあります。

そして今、あなたはおそらくそれが速すぎたと思って、formSucceeded() メソッドがどのように呼び出されるのか、そしてそれが受け取るパラメータは何なのか疑問に思っているでしょう。確かに、あなたは正しいです、これは説明に値します。

Netteは、ハリウッドスタイルと呼ばれる新鮮なメカニズムを導入しています。開発者として常に何かが起こったかどうか(「フォームは送信されましたか?」、「有効に送信されましたか?」、「改ざんされませんでしたか?」)を尋ねる代わりに、フレームワークに「フォームが有効に記入されたら、このメソッドを呼び出して」と言い、残りの作業を任せます。JavaScriptでプログラミングしている場合、このプログラミングスタイルには精通しています。特定のイベントが発生したときに呼び出される関数を記述します。そして、言語はそれらに適切な引数を渡します。

上記Presenterコードもまさにこのように構築されています。$form->onSuccess 配列は、フォームが送信され、正しく記入された(つまり、有効である)瞬間にNetteが呼び出すPHPコールバックのリストを表します。presenterのライフサイクルのコンテキストでは、これはいわゆるシグナルであり、action* メソッドの後、render* メソッドの前に呼び出されます。そして、各コールバックに最初のパラメータとしてフォーム自体を渡し、2番目のパラメータとして送信されたデータをArrayHashオブジェクト(または指定されたクラス)の形式で渡します。フォームオブジェクトが必要ない場合は、最初のパラメータを省略できます。そして、2番目のパラメータはより賢くなることができますが、それについては後で説明します。

$data オブジェクトには、ユーザーが入力したデータを含む name および password プロパティが含まれています。通常、データはさらに処理するために直接送信されます。たとえば、データベースへの挿入などです。ただし、処理中にエラーが発生する可能性があります。たとえば、ユーザー名がすでに使用されている場合などです。その場合、addError() を使用してエラーをフォームに戻し、エラーメッセージとともに再度レンダリングさせます。

$form->addError('申し訳ありませんが、そのユーザー名は既に使用されています。');

onSuccess に加えて、onSubmit もあります:コールバックは、フォームが送信されたときに常に呼び出されます。正しく記入されていない場合でも。さらに onError:コールバックは、送信が有効でない場合にのみ呼び出されます。onSuccess または onSubmitaddError() を使用してフォームを無効にした場合でも呼び出されます。

フォームを処理した後、次のページにリダイレクトします。これにより、更新ボタン、戻るボタン、またはブラウザ履歴の移動によってフォームが意図せず再送信されるのを防ぎます。

他のフォーム要素も追加してみてください。

要素へのアクセス

フォームはPresenterのコンポーネントであり、この場合は registrationForm という名前です(ファクトリメソッド createComponentRegistrationForm の名前に基づく)。したがって、Presenter内のどこからでもフォームにアクセスできます:

$form = $this->getComponent('registrationForm');
// 代替構文: $form = $this['registrationForm'];

フォームの個々の要素もコンポーネントであるため、同じ方法でアクセスできます:

$input = $form->getComponent('name'); // または $input = $form['name'];
$button = $form->getComponent('send'); // または $button = $form['send'];

要素はunsetを使用して削除されます:

unset($form['name']);

検証ルール

有効という言葉が出ましたが、フォームにはまだ検証ルールがありません。それを修正しましょう。

名前は必須なので、setRequired() メソッドでマークします。その引数は、ユーザーが名前を入力しなかった場合に表示されるエラーメッセージのテキストです。引数を指定しない場合は、デフォルトのエラーメッセージが使用されます。

$form->addText('name', '名前:')
	->setRequired('名前を入力してください');

名前を入力せずにフォームを送信してみてください。エラーメッセージが表示され、フィールドに入力するまでブラウザまたはサーバーがそれを拒否することがわかります。

同時に、フィールドにスペースだけを入力してもシステムをだますことはできません。いいえ。Netteは左右の空白を自動的に削除します。試してみてください。これは、すべての一行入力で常に行うべきことですが、忘れられがちです。Netteはそれを自動的に行います。(フォームをだまして、名前として複数行の文字列を送信してみてください。ここでもNetteはだまされず、改行をスペースに変更します。)

フォームは常にサーバー側で検証されますが、JavaScript検証も生成されます。これは瞬時に実行され、ユーザーはフォームをサーバーに送信することなく、すぐにエラーを知ることができます。これは netteForms.js スクリプトが担当します。 レイアウトテンプレートに挿入します:

<script src="https://unpkg.com/nette-forms@3"></script>

フォームのあるページのソースコードを見ると、Netteが必須要素をHTML要素の required 属性としてマークしていることに気付くかもしれません(またはCSSクラス required を持つ要素に挿入)。テンプレートに次のスタイルシートを追加してみてください。「名前」ラベルが赤くなります。これにより、必須要素をユーザーにエレガントに示すことができます:

<style>
.required label { color: maroon }
</style>

他の検証ルールは addRule() メソッドで追加します。最初のパラメータはルール、2番目は再びエラーメッセージのテキストであり、検証ルールの引数が続く場合があります。これはどういう意味ですか?

フォームに新しいオプションのフィールド「年齢」を追加します。これは整数(addInteger())であり、さらに許容範囲($form::Range)内である必要があります。そして、ここで addRule() メソッドの3番目のパラメータを使用します。これにより、必要な範囲をペア [from, to] としてバリデータに渡します:

$form->addInteger('age', '年齢:')
	->addRule($form::Range, '年齢は18歳から120歳の間である必要があります', [18, 120]);

ユーザーがフィールドを入力しない場合、要素はオプションであるため、検証ルールはチェックされません。

ここでは、小さなリファクタリングの余地があります。エラーメッセージと3番目のパラメータで数値が重複して記載されており、これは理想的ではありません。多言語フォームを作成し、数値を含むメッセージが複数の言語に翻訳された場合、値の変更が困難になります。このため、プレースホルダー %d を使用でき、Netteが値を補完します:

	->addRule($form::Range, '年齢は %d 歳から %d 歳の間である必要があります', [18, 120]);

password 要素に戻りましょう。これも必須にし、パスワードの最小長(Form::MinLength)も検証します。ここでもプレースホルダーを使用します:

$form->addPassword('password', 'パスワード:')
	->setRequired('パスワードを選択してください')
	->addRule($form::MinLength, 'パスワードは少なくとも %d 文字必要です', 8);

フォームにフィールド passwordVerify を追加します。ここでユーザーは確認のためにパスワードをもう一度入力します。検証ルールを使用して、両方のパスワードが同じかどうかを確認します(Form::Equal)。そして、パラメータとして、角括弧を使用して最初のパスワードへの参照を与えます:

$form->addPassword('passwordVerify', '確認用パスワード:')
	->setRequired('確認のため、もう一度パスワードを入力してください')
	->addRule($form::Equal, 'パスワードが一致しません', $form['password'])
	->setOmitted();

setOmitted() を使用して、実際には値に関心がなく、検証目的でのみ存在する要素をマークしました。値は $data に渡されません。

これで、PHPとJavaScriptの両方で検証を備えた完全に機能するフォームが完成しました。Netteの検証機能ははるかに広範であり、条件を作成したり、それらに基づいてページのパーツを表示および非表示にしたりできます。すべてはフォームの検証に関する章で学びます。

デフォルト値

フォーム要素には通常、デフォルト値を設定します:

$form->addEmail('email', 'メールアドレス')
	->setDefaultValue($lastUsedEmail);

すべての要素に同時にデフォルト値を設定すると便利なことがよくあります。たとえば、フォームがレコードの編集に使用される場合などです。データベースからレコードを読み取り、デフォルト値を設定します:

//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);

要素を定義した後に setDefaults() を呼び出します。

フォームのレンダリング

デフォルトでは、フォームはテーブルとしてレンダリングされます。個々の要素は基本的なアクセシビリティルールを満たしています – すべてのラベルは <label> として記述され、対応するフォーム要素に関連付けられています。ラベルをクリックすると、カーソルが自動的にフォームフィールドに表示されます。

各要素に任意のHTML属性を設定できます。たとえば、プレースホルダーを追加します:

$form->addInteger('age', '年齢:')
	->setHtmlAttribute('placeholder', '年齢を入力してください');

フォームをレンダリングする方法は本当にたくさんあるので、レンダリングに関する別の章があります。

クラスへのマッピング

2番目のパラメータ $data で送信されたデータを ArrayHash オブジェクトとして受け取る formSucceeded() メソッドに戻りましょう。これは stdClass のようなジェネリッククラスであるため、エディタでのプロパティの補完や静的コード分析など、特定の快適さが欠けています。これは、各フォームに特定のクラスを持たせることで解決できます。そのプロパティは個々の要素を表します。例:

class RegistrationFormData
{
	public string $name;
	public ?int $age;
	public string $password;
}

または、コンストラクタを使用することもできます:

class RegistrationFormData
{
	public function __construct(
		public string $name,
		public ?int $age,
		public string $password,
	) {
	}
}

データクラスのプロパティはenumにすることもでき、自動的にマッピングされます。

Netteにこのクラスのオブジェクトとしてデータを返すように指示するにはどうすればよいですか?思ったより簡単です。ハンドラメソッドの $data パラメータの型としてクラスを指定するだけです:

public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $data は RegistrationFormData のインスタンスです
	$name = $data->name;
	// ...
}

型として array を指定することもでき、その場合、データは配列として渡されます。

同様に、getValues() 関数を使用することもできます。これには、クラス名またはハイドレートするオブジェクトをパラメータとして渡します:

$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;

フォームがコンテナで構成される多層構造を形成する場合、それぞれに個別のクラスを作成します:

$form = new Form;
$person = $form->addContainer('person');
$person->addText('firstName');
/* ... */

class PersonFormData
{
	public string $firstName;
	public string $lastName;
}

class RegistrationFormData
{
	public PersonFormData $person;
	public ?int $age;
	public string $password;
}

マッピングは、プロパティ $person の型から、コンテナを PersonFormData クラスにマッピングする必要があることを認識します。プロパティにコンテナの配列が含まれている場合は、型 array を指定し、マッピングするクラスをコンテナに直接渡します:

$person->setMappedType(PersonFormData::class);

フォームのデータクラスの設計は、Nette\Forms\Blueprint::dataClass($form) メソッドを使用して生成できます。これはブラウザページに出力されます。コードをクリックして選択し、プロジェクトにコピーするだけです。

複数のボタン

フォームに複数のボタンがある場合、通常、どちらが押されたかを区別する必要があります。各ボタンに独自のハンドラ関数を作成できます。イベント onClick のハンドラとして設定します:

$form->addSubmit('save', '保存')
	->onClick[] = [$this, 'saveButtonPressed'];

$form->addSubmit('delete', '削除')
	->onClick[] = [$this, 'deleteButtonPressed'];

これらのハンドラは、onSuccess イベントの場合と同様に、有効に記入されたフォームの場合にのみ呼び出されます。違いは、最初のパラメータとしてフォームの代わりに送信ボタンを渡すことができる点です。指定した型によって異なります:

public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
	$form = $button->getForm();
	// ...
}

フォームがEnterキーで送信された場合、最初のボタンで送信されたかのように扱われます。

onAnchorイベント

ファクトリメソッド(例:createComponentRegistrationForm)でフォームを組み立てるとき、フォームはまだ送信されたかどうか、またはどのデータで送信されたかを知りません。しかし、送信された値を知る必要がある場合があります。たとえば、フォームのさらなる形状がそれらに依存する場合や、依存セレクトボックスなどに必要な場合などです。

したがって、フォームを組み立てるコードの一部は、いわゆるアンカーされたとき、つまりPresenterに接続され、送信されたデータを知っているときにのみ呼び出すことができます。そのようなコードを $onAnchor 配列に渡します:

$country = $form->addSelect('country', '国:', $this->model->getCountries());
$city = $form->addSelect('city', '市:');

$form->onAnchor[] = function () use ($country, $city) {
	// この関数は、フォームが送信されたかどうか、およびどのデータで送信されたかを知っているときにのみ呼び出されます
	// したがって、getValue() メソッドを使用できます
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

脆弱性からの保護

Nette Frameworkはセキュリティを非常に重視しており、したがってフォームの適切な保護に細心の注意を払っています。これは完全に透過的に行われ、手動で何も設定する必要はありません。

フォームをクロスサイトスクリプティング (XSS)およびクロスサイトリクエストフォージェリ (CSRF)攻撃から保護することに加えて、多くの小さなセキュリティ対策を実行するため、もはや考える必要はありません。

たとえば、入力からすべての制御文字を除去し、UTF-8エンコーディングの有効性を検証するため、フォームからのデータは常にクリーンになります。セレクトボックスとラジオリストでは、選択された項目が実際に提供されたものであり、改ざんされていないことを検証します。一行テキスト入力では、攻撃者がそこに送信した可能性のある改行文字を削除することをすでに述べました。複数行入力では、改行文字を正規化します。などなど。

Netteは、多くのプログラマーが存在することさえ知らないセキュリティリスクを処理します。

言及されたCSRF攻撃は、攻撃者が被害者をページに誘い込み、被害者のブラウザで被害者がログインしているサーバーへのリクエストを密かに実行し、サーバーがリクエストが被害者自身の意志で実行されたと信じ込ませることにあります。したがって、Netteは異なるドメインからのPOSTフォームの送信を防ぎます。何らかの理由で保護を無効にし、異なるドメインからのフォームの送信を許可したい場合は、次を使用します:

$form->allowCrossOrigin(); // 注意!保護を無効にします!

この保護は、_nss という名前のSameSite Cookieを使用します。SameSite Cookieによる保護は100%信頼できるとは限らないため、トークンによる保護も有効にすることをお勧めします:

$form->addProtection();

アプリケーション内の機密データを変更するWebサイトの管理部分のフォームをこのように保護することをお勧めします。フレームワークは、セッションに保存される認証トークンを生成および検証することによってCSRF攻撃から防御します。したがって、フォームを表示する前にセッションを開いておく必要があります。Webサイトの管理部分では、通常、ユーザーログインのためにセッションはすでに開始されています。 それ以外の場合は、Nette\Http\Session::start() メソッドでセッションを開始します。

複数のPresenterで同じフォームを使用する

複数のPresenterで1つのフォームを使用する必要がある場合は、そのためのファクトリを作成し、それをPresenterに渡すことをお勧めします。このようなクラスの適切な場所は、たとえば app/Forms ディレクトリです。

ファクトリクラスは次のようになります:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', '名前:');
		$form->addSubmit('send', 'ログイン');
		return $form;
	}
}

Presenterのコンポーネントファクトリメソッドで、クラスにフォームの作成を依頼します:

public function __construct(
	private SignInFormFactory $formFactory,
) {
}

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// フォームを変更できます。ここでは、たとえばボタンのキャプションを変更します
	$form['send']->setCaption('続行');
	$form->onSuccess[] = [$this, 'signInFormSuceeded']; // そしてハンドラを追加します
	return $form;
}

フォーム処理ハンドラは、ファクトリから提供することもできます:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', '名前:');
		$form->addSubmit('send', 'ログイン');
		$form->onSuccess[] = function (Form $form, $data): void {
			// ここでフォーム処理を実行します
		};
		return $form;
	}
}

これで、Netteのフォームの簡単な紹介が終わりました。examplesディレクトリを調べて、さらなるインスピレーションを見つけてみてください。

バージョン: 4.0