6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

BEAR.SundayAdvent Calendar 2015

Day 13

BEAR.Sundayのフォーム

Last updated at Posted at 2015-12-12

フォームバリデーションはバリデーションするメソッドに@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

フォームはインジェクトされるオブジェクトなのでコンテキストやリソースの状態によって動的に変えることができます。例えばログインしてるかしてないかでページクラスの変更なしに"コメント投稿"のフォームを変えるができます。

参考リンク

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?