フォームバリデーションはバリデーションするメソッドに@FormValidation
とアノテーションとすることで行われます。
Formクラス
Formのページを作成するために最初にFormクラスを作成します。Formクラスはフォームの登録と初期化、ルール適用、HTML表現などフォームに関連する要素が1つに集約されています。
class MyForm extends AbstractForm
{
/**
* {@inheritdoc}
*/
public function init()
$this->setField('name')
->setAttribs([
'id' => 'name',
'name' => 'name',
'size' => 20,
'maxlength' => 20,
'class' => 'form-control',
'placeholder' => 'Your Name'
]);
$this->setField('submit', 'submit')
->setAttribs([
'name' => 'submit',
'value' => 'Submit'
]);
$this->filter->validate('name')->isNot('blank');
$this->filter->validate('name')->is('alnum');
$this->filter->useFieldMessage('name', 'Name must be alphabetic only !!.');
フォームで使う値でリソースやデータベースのアクセスが必要な場合は、インジェクトして使います。
フォーム初期化
init()
メソッドではフォームのフィールドにアトリビュートやルールを登録してフォームを初期化します。
フィールド登録
まずフォームフィールドname
をアトリビュートと一緒にセットします。
$this->setField('name', 'text')->setAttribs(['id' => 'name']);
バリデーション登録
次にセットしたフィールドに対してバリデーションをセットします。
$this->filter->validate('name')->is('alnum');
$this->filter->validate('name')->is('alpha');
$this->filter->validate('name')->is('between', $min, $max);
$this->filter->validate('name')->is('blank');
$this->filter->validate('name')->is('bool');
$this->filter->validate('name')->is('callback', function ($subject, $field) {
if ($subject->$field === 'foo') {
return true;
}
return false;
});
// ...
バリーデータはAura.Filter v2です。バリデーションが必要なfilterと指定のフォーマットに変換するsanitizeがあります。
バリデーションは設定ファイルやアノテーションで指定するのではなく、PHPで記述します。
メッセージ
$this->filter->useFieldMessage('name', 'Name must be alphabetic only.');
エラーメッセージをフィールドに対してセットします。
- IDはアルファベットで指定してください
- IDは4文字以上で指定してください
のようにルール単位でエラーが出るのではなく**「IDはアルファベット4文字以上で指定してください」**という風にフィールド単位でのエラーにすることができます。
フォームのHTML
Formクラスの__toString()
メソッドでhtml出力を返します。
/**
* {@inheritdoc}
*/
public function __toString()
{
$form = $this->form([
'method' => 'post',
'action' => '/min'
]);
// name
/** @var $tag Tag */
$tag = $this->helper->get('tag');
$form .= $tag('div', ['class' => 'form-group']);
$form .= $this->helper->tag('div', ['class' => 'form-group']);
$form .= $this->helper->tag('label', ['for' => 'name']);
$form .= 'Name:';
$form .= $this->helper->tag('/label') . PHP_EOL;
$form .= $this->input('name');
$form .= $this->error('name');
$form .= $this->helper->tag('/div') . PHP_EOL;
// submit
$form .= $this->input('submit');
$form .= $this->helper->tag('/form');
return $form;
}
基本的に以下の2つの動的エレメントを使って他のタグを組み立てています。
$this->input('name'); // ogフォームフィールド
$this->error('name'); // エラーメッセージ
実際には例えばBootstrap使用などフォームエレメントのため同じマークアップを行うことが多く、このようなメソッドを設置すると便利です。クラスにしてインジェクトすると再利用性が高まります。
private function inputGroup($input, $label)
{
/** @var $tag Tag */
$tag = $this->helper->get('tag');
$html = $tag('div', ['class' => 'form-group']);
$html .= $tag('label', ['for' => $input, 'class' => 'col-sm-4 control-label']);
$html .= $label;
$html .= $tag('/label') . PHP_EOL;
$html .= $tag('div', ['class' => 'col-sm-8']);
$html .= $this->input($input);
$html .= $this->error($input);
$html .= $tag('/div') . PHP_EOL;
$html .= $tag('/div') . PHP_EOL;
return $html;
}
Twigや他のテンプレートエンジンを使う場合はフォームクラスにインジェクトして使います。
フォームのテスト
フォームのテストは以下のようになりますapply()
でバリデーションが、(string)
評価でHTMLの確認ができます。
class NameFormTest extends \PHPUnit_Framework_TestCase
{
/**
* @var AbstractForm
*/
private $form;
protected function setUp()
{
parent::setUp();
/* @var $form MinForm */
$this->form = (new FormFactory)->newInstance(MinForm::class);
}
public function testValdationFailed()
{
$isValid = $this->form->apply([]);
$this->assertFalse($isValid);
}
public function testValidationSuccess()
{
$isValid = $this->form->apply([
'name' => 'BEAR'
]);
$this->assertTrue($isValid);
}
public function testHtml()
{
$html = (string) $this->form;
$this->assertContains('<form method="post"', $html);
$this->assertContains('<input id="name" type="text" name="name" size="20" maxlength="20"', $html);
}
}
重要:
フォームを実際にページに設置する前に、フォームクラスの作成とテストを終わらせておくことを勧めます。
フォームをページに設置
フォームをページにインジェクトして、フォームバリデーションを行うメソッドに@FormValidation
とアノテートします。
@FormValidation
ではバリデーションが失敗すればValidationFailed()
のサフィックスがついたメソッドonPostValidationFailed()
が呼ばれます。
class Min extends ResourceObject
{
/**
* @var FormInterface
*/
protected $form;
/**
* @param FormInterface $form
*
* @Inject
* @Named("name")
*/
public function __construct(FormInterface $form)
{
$this->form = $form;
}
public function onGet()
{
$this['form'] = $this->form;
return $this;
}
/**
* @FormValidation
*
* @param $name
*
* @return $this
*/
public function onPost($name)
{
$this->code = 201;
$this['name'] = $name;
return $this;
}
public function onPostValidationFailed()
{
$this->code = 400;
return $this->onGet();
}
}
@FormValidation
アノテーションではフォームオブジェクトのプロパティ名とバリデーション失敗時にコールするメソッド名が指定できます。無指定だとそれぞれform
, メソッド名 + ValidationFailed()
です。(onPostValidationFailed
など)
@FormValidation(form="loginForm", onFailure="onFailure")
例外
@FormValidation
の代わりに@InputValidation
とアノテートするとValidationException
例外が投げられます。
キャッチした$e
の$e->error
にはapplication/vnd.error+jsonメディアタイプのエラーとして扱える文字列があります。
header('Content-Type: application/vnd.error+json');
echo $e->error;
//{
// "message": "Validation failed",
// "path": "/path/to/error",
// "validation_messages": {
// "name": [
// "Name must be alphabetic only."
// ]
// }
//}
VndErrorModule
をインストールすると@FormValidation
とアノテートされていても@InputValidation
と同じように例外を発生します。
ページテンプレート
formのhtml全体は{{ form|raw }}
でレンダリングされます。
{% if _code == 201 %}
<h1>201</h1>
<p>Name: {{ name }}</p>
{% else %}
<h1>{{ _code }}</h1>
{{ form|raw }}
{% endif %}
_code
にはリソースのステータスコードが入るのでそれで内容を変えています。コードの規模が増えれば別のテンプレートにするのが良いでしょう。
html
,error
のメソッドを使って個別にエレメントだけをレンダリングすることもできます。
{{ form.input('name') }}
{{ form.error('name') }}
ページのテスト
フォームのテストでバリデーションやHTML表現のテストはできていれば、フォームに関連するページのテストはほとんど必要がありません。それぞれ別の場所で同等の効果を持つテストを終えているためです。フォームのためのWebブラウザを使った自動テストもあまり必要ありません。
フォームとDI
フォームはインジェクトされるオブジェクトなのでコンテキストやリソースの状態によって動的に変えることができます。例えばログインしてるかしてないかでページクラスの変更なしに"コメント投稿"のフォームを変えるができます。
参考リンク
- Ray.WebFormModule - https://github.com/ray-di/Ray.WebFormModule
- フォーム - http://bearsunday.github.io/manuals/1.0/ja/form.html