フォームの単体利用

Nette Forms はウェブのフォームの作成と処理を劇的に簡単にします。この章で見るように、フレームワークのほかの部分なしで、まったく単独でアプリケーションに使えます。

とはいえ Nette Application とプレゼンターを使っているなら、あなた向けの案内があります。プレゼンターでのフォームです。

はじめてのフォーム

始める前に、Composerでパッケージを入れてください。

composer require nette/forms

単純な登録のフォームを書いてみましょう。そのコードは次のようになります(完全なコード をご覧ください)。

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 クラスを使います)。そこに ‘name’、‘password’ という名前の要素と、送信のボタンを足しました。

ではフォームに命を吹き込みましょう。$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 は前後の空白を自動的に取り除きます。試してみてください。1 行の入力ではいつもそうすべきことですが、よく忘れられます。Nette は自動的にやってくれます。(名前として複数行の文字列を送ってフォームをだまそうとしてみてください。ここでも Nette はだまされず、改行は空白に変えられます。)

フォームはいつもサーバー側で検証されますが、JavaScript の検証も生成されます。これはすぐに走るので、利用者はフォームをサーバーへ送らなくてもエラーにすぐ気づけます。これは netteForms.js のスクリプトが受け持ちます。ページに読み込んでください。

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

フォームのあるページのソースコードを見ると、Nette が必須の要素を required という CSS クラスの要素に入れているのに気づくかもしれません。次のスタイルシートをテンプレートに足してみてください。「名前」のラベルが赤くなります。これで必須の要素を利用者に優雅に示せます。

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

さらに検証の規則を addRule() メソッドで足します。第 1 パラメータは規則、第 2 パラメータはやはりエラーのメッセージの文で、そのあとに省略できる検証の規則への引数が続くことがあります。それはどういうことでしょうか。

フォームに新しい、省略できる項目「年齢」を足しましょう。これは整数でなければならず(addInteger())、許される範囲に収まっていなければなりません($form::Range)。ここでは addRule() メソッドの第 3 パラメータを使って、必要な範囲を [最小, 最大] の組として検証器に渡します。

$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 という項目を足しましょう。利用者は確認のためにもう一度パスワードを入力します。検証の規則を使って、2 つのパスワードが同じかを確かめます($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() は要素を定義したあとに呼んでください。

すでに送信されたフォームでは setDefaults() は何もしません。利用者が入力したものを上書きしないので、フォームのファクトリの中で条件なしに呼んでも安全です。送信後にも値を強いる必要があるなら、代わりに setValues() を使ってください。

フォームの描画

既定では、フォームは表として描かれます。個々の要素はアクセシビリティの基本の指針に従っていて、すべてのラベルは <label> 要素として生成され、それぞれのフォームの要素と結び付けられています。ラベルをクリックすると、自動的にフォームの項目にカーソルが移ります。

要素ごとに好きな HTML の属性を設定できます。たとえばプレースホルダを足します。

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

フォームを描く方法はたくさんあるので、描画には独立した章を用意しています。

Latte での描画

Latteテンプレートエンジンが手元にあるなら、フォームの描画を任せて、できあがる HTML を完全に思いどおりにできます。エンジンを作り、フォームの拡張を登録して、フォームを変数としてテンプレートに渡します。

$latte = new Latte\Engine;
$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension);

$latte->render('form.latte', ['form' => $form]);

テンプレートでは $form 変数と {input}{label}n:name といったタグを通してフォームを扱います。テンプレートを含む完全な例は examplesディレクトリ(latte.phplatte/)にあります。個々のタグは描画の章で説明しています。

クラスへの対応づけ

フォームのデータの処理に戻りましょう。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) メソッドで生成でき、ブラウザのページに出力されます。あとはクリックして選び、そのコードをプロジェクトにコピーするだけです。

複数の送信ボタン

フォームにボタンが 2 つ以上あるなら、ふつうはどれが押されたかを見分ける必要があります。ボタンの 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 の文字コードとして正しいかを確かめるので、フォームから来るデータはいつもきれいです。選択肢やラジオの一覧では、選ばれた項目が本当に提示されたものの中にあり、偽造がなかったことを確かめます。1 行のテキストの入力では、攻撃者が送るかもしれない改行の文字を空白に置き換えることはすでに触れました。複数行の入力では改行の文字をそろえます。ほかにもいろいろあります。

多くのプログラマーが存在すら知らないセキュリティリスクを、Nette があなたの代わりに片付けています。

先ほどの CSRF の攻撃では、攻撃者が被害者をあるページへ誘い込み、そのページが被害者のブラウザの中で、被害者が今ログインしているサーバーへのリクエストを黙って実行します。するとサーバーは、そのリクエストが被害者の意思で行われたと信じてしまいます。ですから Nette は、よそのオリジンから送信された POST のフォームを拒みます。同じサイトの違うサブドメインでも「よそ」と見なされます。別のオリジンからの送信を許す必要があるなら、次のようにして保護を切ります。

$form->allowCrossOrigin(); // 注意。保護が完全に切れます。

ただしこれはどのオリジンに対しても保護を切ります。特定のオリジンだけを許したいなら、保護を切ったうえで Origin ヘッダーを自分の許可の一覧と自分で照らし合わせてください。

この保護はブラウザの Sec-Fetch-Site ヘッダー(Fetch Metadata)に頼っています。これはブラウザが自動的に送るもので、XSS の弱点があっても偽れません。これらのヘッダーを送らない古いブラウザは、この検査を通りません。記事 ブラウザがついに CSRF を解決するで詳しく説明しています。

セッションに保存した認可のトークンを使う以前の保護($form->addProtection() で有効にするもの)はもう要らず、バージョン 3.3 から非推奨です。

以上で、Nette のフォームの手早い入門を見てきました。もっと着想が欲しければ、配布物の examplesディレクトリをのぞいてみてください。

バージョン: 4.x