スタンドアロンで使用されるフォーム

Nette Forms は、Web フォームの作成と処理を大幅に簡素化します。この章で示すように、フレームワークの残りの部分なしで、アプリケーションで完全に独立して使用できます。

ただし、Nette Application と Presenter を使用している場合は、Presenter での使用 ガイドが用意されています。

最初のフォーム

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

use Nette\Forms\Form;

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

非常に簡単にレンダリングできます:

$form->render();

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

フォームは Nette\Forms\Form クラスのオブジェクトです(Nette\Application\UI\Form クラスは Presenter で使用されます)。名前、パスワード、送信ボタンという要素を追加しました。

では、フォームを動作させてみましょう。$form->isSuccess() を問い合わせることで、フォームが送信され、有効に記入されたかどうかを確認します。もしそうなら、データを出力します。フォーム定義の後ろに以下を追加します:

if ($form->isSuccess()) {
	echo 'フォームは正しく記入され、送信されました';
	$data = $form->getValues();
	// $data->name には名前が含まれます
	// $data->password にはパスワードが含まれます
	var_dump($data);
}

getValues() メソッドは、送信されたデータを ArrayHash オブジェクトの形式で返します。これを変更する方法は後で示します。オブジェクト $data には、ユーザーが入力したデータを含む namepassword キーが含まれています。

通常、データはすぐにさらなる処理に送られます。これは、例えばデータベースへの挿入などです。しかし、処理中にエラーが発生する可能性があります。例えば、ユーザー名が既に使用されている場合などです。その場合、addError() を使用してエラーをフォームに戻し、エラーメッセージとともに再度レンダリングさせます。

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

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

フォームはデフォルトで POST メソッドを使用して同じページに送信されます。両方とも変更できます:

$form->setAction('/submit.php');
$form->setMethod('GET');

そして、これで終わりです :-) 機能的で完全に安全なフォームができました。

他のフォームコントロールも追加してみてください。

コントロールへのアクセス

フォームとその個々のコントロールをコンポーネントと呼びます。これらはコンポーネントツリーを形成し、フォームがルートになります。フォームの個々のコントロールには、次の方法でアクセスできます:

$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 が必須要素を 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', 'Eメール')
	->setDefaultValue($lastUsedEmail);

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

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

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

フォームのレンダリング

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

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

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

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

クラスへのマッピング

フォームデータの処理に戻りましょう。getValues() メソッドは、送信されたデータを ArrayHash オブジェクトとして返しました。これは 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 = $form->getValues(RegistrationFormData::class);
$name = $data->name;

パラメータとして 'array' を指定することもでき、その場合はデータを配列として返します。

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

$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) メソッドを使用して生成させることができます。これはブラウザページに出力されます。コードをクリックして選択し、プロジェクトにコピーするだけです。

複数のボタン

フォームに複数のボタンがある場合、通常、どのボタンが押されたかを区別する必要があります。この情報は、ボタンの isSubmittedBy() メソッドによって返されます:

$form->addSubmit('save', '保存');
$form->addSubmit('delete', '削除');

if ($form->isSuccess()) {
	if ($form['save']->isSubmittedBy()) {
		// ...
	}

	if ($form['delete']->isSubmittedBy()) {
		// ...
	}
}

$form->isSuccess() の問い合わせを省略しないでください。データの有効性を検証します。

フォームがEnterキーで送信されると、最初のボタンで送信されたものとして扱われます。

脆弱性からの保護

Nette Framework はセキュリティを非常に重視しており、そのためフォームの適切な保護に細心の注意を払っています。

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

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

Nette は、多くのプログラマーが存在することすら知らないセキュリティリスクをあなたに代わって解決します。

前述の CSRF 攻撃は、攻撃者が被害者を、被害者がログインしているサーバーに対して、被害者のブラウザで密かにリクエストを実行するページに誘導し、サーバーがそのリクエストが被害者自身の意志で行われたと誤解することに基づいています。そのため、Nette は他のドメインからの POST フォームの送信を防ぎます。何らかの理由で保護を無効にし、他のドメインからフォームを送信できるようにしたい場合は、次を使用します:

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

この保護は _nss という名前の SameSite クッキーを利用します。そのため、クッキーを送信できるように、最初の出力が送信される前にフォームオブジェクトを作成してください。

SameSite クッキーによる保護は 100% 信頼できるとは限らないため、トークンによる保護も有効にすることをお勧めします:

$form->addProtection();

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

これで、Nette のフォームの簡単な紹介は終わりです。配布物の examples ディレクトリも見て、さらなるインスピレーションを得てください。

バージョン: 4.0