プレゼンターでのフォーム

Nette Forms はウェブのフォームの作成と処理を大いに簡単にします。この章では、プレゼンターの中でフォームを使う方法を学びます。

フレームワークのほかの部分なしで、まったく単独で使うことに関心があるなら、単独での利用の案内があります。

はじめてのフォーム

単純な登録のフォームを書いてみましょう。そのコードは次のようになります。

use Nette\Application\UI\Form;

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

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

プレゼンターの中のフォームは Nette\Application\UI\Form クラスのオブジェクトです。その前身の Nette\Forms\Form は単独で使うためのものです。私たちは name と password という名前の要素と、送信のボタンを足しました。最後の $form->onSuccess の行は、送信されて検証を通ったあとに $this->formSucceeded() メソッドを呼ぶべきだと伝えています。

プレゼンターから見れば、フォームはふつうのコンポーネントです。ですからコンポーネントとして扱われ、ファクトリメソッドでプレゼンターに組み込まれます。次のようになります。

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

	private function formSucceeded(Form $form, $data): void
	{
		// ここでフォームから送られたデータを処理します
		// $data->name に名前が入っています
		// $data->password にパスワードが入っています
		$this->flashMessage('登録が完了しました。');
		$this->redirect('Home:');
	}
}

そしてテンプレートでは、フォームは {control} タグで描かれます。

<h1>登録</h1>

{control registrationForm}

これで基本はおしまいです :-) 動いて、しかも完璧に守られたフォームができました。

きっと今、話が速すぎる、formSucceeded() メソッドが呼ばれるのはどうしてで、どんなパラメータを受け取るのかと思っていることでしょう。そのとおりで、これは説明に値します。

Nette は ハリウッド流と呼ばれる新鮮なしくみを持ち込みます。開発者であるあなたが絶えず「フォームは送信されたか」「正しく送信されたか」「偽造されていないか」と問い続ける代わりに、「フォームが正しく埋められたら、このメソッドを呼んで」とフレームワークに伝えて、あとの仕事を任せます。JavaScript でプログラムしているなら、この書き方はよくご存じでしょう。あるイベントが起きたときに呼ばれる関数を書き、言語が適切な引数をそこに渡してくれます。

上のプレゼンターのコードは、まさにこのように組み立てられています。$form->onSuccess 配列は、フォームが送信されて正しく埋められた(つまり妥当な)瞬間に Nette が呼ぶ PHP のコールバックの一覧です。プレゼンターのライフサイクルの中ではこれはいわゆるシグナルなので、action* メソッドのあと、render* メソッドの前に呼ばれます。そしてそれぞれのコールバックには、第 1 パラメータとしてフォームそのものを、第 2 パラメータとして送信されたデータを ArrayHashオブジェクト(あるいは stdClass や独自のクラス)として渡します。フォームのオブジェクトが要らなければ第 1 パラメータは省けます。第 2 パラメータはもっと賢くできますが、それはのちほど

$data オブジェクトには、利用者が入力したデータの入った namepassword のプロパティがあります。ふつうはそのデータをそのまま次の処理へ、たとえばデータベースへの挿入へ送ります。しかし処理の途中でエラーが起きることもあります。たとえばそのユーザー名がすでに使われている場合です。そんなときは addError() でエラーをフォームに返し、エラーのメッセージとともにもう一度描かせます。

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

onSuccess のほかに onSubmit もあります。こちらのコールバックは、正しく埋められていなくてもフォームが送信されればいつでも呼ばれます。さらに onError もあり、こちらは送信が妥当でない場合にだけ呼ばれます。onSuccess の中で addError() を使ってフォームを妥当でなくした場合にも呼ばれます。

フォームを処理したあとは、別のページへリダイレクトします。これで 更新戻る のボタン、ブラウザの履歴をたどることによる、望まないフォームの再送信を防げます。

フォームが AJAX で送信された場合は、ふつうリダイレクトの代わりに、描き直したフォームを含むスニペットを再描画します。

ほかのフォームの要素も足してみてください。

要素へのアクセス

フォームはプレゼンターのコンポーネントで、この例ではファクトリメソッドの名前 createComponentRegistrationForm から registrationForm という名前です。ですからプレゼンターのどこからでも、次のようにフォームにアクセスできます。

$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 は前後の空白を自動的に取り除きます。試してみてください。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', '年齢を入力してください');

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

クラスへの対応づけ

formSucceeded() メソッドに戻りましょう。これは送信されたデータを第 2 パラメータ $dataArrayHash オブジェクト(あるいは stdClass)として受け取ります。これは 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;

フォームが検証される前に値を読む必要があるなら(ふつうは onValidate のハンドラの中です)、代わりに getUntrustedValues() メソッドを使ってください。getValues() と同じパラメータを受け取りますが、検証を通ったことを保証せずに、送信された値を返します。

フォームがコンテナから成る多階層の構造なら、それぞれに別のクラスを作ります。

$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 つ以上あるなら、ふつうはどれが押されたかを見分ける必要があります。ボタンごとに別のハンドラの関数を作れます。それを onClickイベントのハンドラとして設定します。

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

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

ハンドラは addSubmit() メソッドの第 3 引数として、ボタンに直接渡すこともできます。

これらのハンドラは、onSuccess イベントと同じく、フォームが正しく埋められた場合にだけ呼ばれます(そのボタンで検証が切られていない限り)。違うのは、指定した型宣言に応じて、第 1 パラメータにフォームではなく送信ボタンのオブジェクトが渡せる点です。

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

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

onAnchor イベント

ファクトリメソッド(createComponentRegistrationForm など)でフォームを組み立てるとき、そのフォームはまだ、自分が送信されたかどうかも、どんなデータで送信されたかも知りません。とはいえ、送信された値を知る必要がある場面もあります。フォームの見た目がそれに左右されたり、連動する選択肢に必要だったりする場合です。

そこで、フォームを組み立てるコードを、フォームが「錨を下ろした」とき、つまりすでにプレゼンターにつながって送信されたデータを知っているときにだけ呼ばせられます。そうしたコードは $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 の文字コードとして正しいかを確かめるので、フォームから来るデータはいつもきれいです。選択肢やラジオの一覧では、選ばれた項目が本当に提示されたものの中にあり、偽造がなかったことを確かめます。1 行のテキストの入力では、攻撃者が送るかもしれない改行の文字を空白に置き換えることはすでに触れました。複数行の入力では改行の文字をそろえます。ほかにもいろいろあります。

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

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

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

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

この保護はブラウザの Sec-Fetch-Site ヘッダー(Fetch Metadata)に頼っています。これはブラウザが自動的に送るもので、XSS の弱点があっても偽れません。これに対応していない古いブラウザには、Nette のアプリケーションが自動的に設定する SameSite のクッキーが代わりに働きます。記事 ブラウザがついに CSRF を解決するで詳しく説明しています。

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

ひとつのフォームを複数のプレゼンターで使う

同じフォームを複数のプレゼンターで使う必要があるなら、そのファクトリを作ってプレゼンターに注入することをおすすめします。そうしたクラスの置き場所としては、たとえば app/Forms ディレクトリが適しています。

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

use Nette\Application\UI\Form;

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

プレゼンターのコンポーネントのファクトリメソッドの中で、フォームを作ってもらうクラスを求めます。

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.x