BEAR.Sundayのフォーム

フォームバリデーションはバリデーションするメソッドに`@FormValidation`とアノテーションとすることで行われます。

# Formクラス

Formのページを作成するために最初にFormクラスを作成します。Formクラスはフォームの登録と初期化、ルール適用、HTML表現などフォームに関連する要素が１つに集約されています。


```php
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`をアトリビュートと一緒にセットします。

```php
$this->setField('name', 'text')->setAttribs(['id' => 'name']);
```

### バリデーション登録

次にセットしたフィールドに対してバリデーションをセットします。

```php
$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](https://github.com/auraphp/Aura.Filter/tree/2.x)です。バリデーションが必要な[filter](https://github.com/auraphp/Aura.Filter/blob/2.x/docs/validate.md)と指定のフォーマットに変換する[sanitize](https://github.com/auraphp/Aura.Filter/blob/2.x/docs/sanitize.md)があります。

バリデーションは設定ファイルやアノテーションで指定するのではなく、PHPで記述します。

### メッセージ

```php
$this->filter->useFieldMessage('name', 'Name must be alphabetic only.');
```

エラーメッセージを**フィールド**に対してセットします。

 * IDはアルファベットで指定してください
 * IDは４文字以上で指定してください

のようにルール単位でエラーが出るのではなく**「IDはアルファベット４文字以上で指定してください」**という風にフィールド単位でのエラーにすることができます。

## フォームのHTML

Formクラスの`__toString()`メソッドでhtml出力を返します。

```php
    /**
     * {@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;
    }
```

基本的に以下の２つの動的エレメントを使って他のタグを組み立てています。

```php
$this->input('name'); // ogフォームフィールド
$this->error('name'); // エラーメッセージ
```

実際には例えばBootstrap使用などフォームエレメントのため同じマークアップを行うことが多く、このようなメソッドを設置すると便利です。クラスにしてインジェクトすると再利用性が高まります。

```php
    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の確認ができます。


```php
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()`が呼ばれます。

```php

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](https://github.com/blongden/vnd.error)メディアタイプのエラーとして扱える文字列があります。

```php
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