SymfonyのFormコンポーネントを使いこなすために

どうもこんにちは。小川です。日付変わっちゃいましたが、Symfony2 Advent Calendar JP 2011の5日目です。今回はFormコンポーネントを使いこなす上でぜひ知っておきたいポイントを紹介します。

今回紹介するのは次の3つです。

  • 任意のプロパティをフィールドにマッピングする
  • どのプロパティにもマッピングしないフィールドを定義する
  • どのプロパティにもマッピングしないフィールドをバリデーションする

任意のプロパティをフィールドにマッピングする

Formコンポーネントを通常に使うと、オブジェクトの構造とフォームの構造を合わせるようにすると思います。たとえば次のクラスがあるとします。

<?php

class Foo 
{
    public $prop1;
    public $bar;
}

class Bar 
{
    public $prop2;
}

$foo = new Foo();
$foo->bar = new Bar();

このFooとBarを1つの画面で編集する場合、おそらくFooTypeとBarTypeを作って、FooTypeの中で $buider->add('bar', new BarType()) などとするでしょう。しかしちょっと別のオブジェクトのフィールドにマッピングするためにクラスを1つ用意するのは煩わしいと感じる場合もあります。そんな時は次のように、Property-Pathというオプションを指定します。

<?php

namespace Acme\DemoBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
    
class FooType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options
    {
        $builde->add('prop1', 'text')
            ->add('prop2', 'text', array('property_path' => 'bar.prop2'))
        ;
    }

    public function getName()
    
        return 'foo';
    
}

Property-Path(add()に指定されたproperty_pathオプション)は、指定したフィールドが、オブジェクトのどのプロパティへマッピングするかを記したもので、デフォルトだとadd()の第1引数に指定した値と同一です。FooTypeにはFooオブジェクトが渡されることを前提としています。たとえばProperty-Pathがprop1なら、$foo->prop1となり、Property-Pathがbar.prop2なら、$foo->bar->prop2のようになります。ドットがセパレータになっているのはTwigと同じなので、Twigでどうやってプロパティにアクセスするかというのと同じだと思ってください。つまりProperty-Pathを設定さえすれば、オブジェクトからたどれるすべての値へマッピングができるということです。Validatorの内部でもこのProperty-Pathという概念は使われているので、覚えておくといいでしょう。

追記:
ちなみにProperty-Pathという名前ですが、セッターが定義されている場合はセッターを経由してアクセスします。isXxx()系のメソッドでもOKです。

参考クラス:

  • Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper
  • Symfony\Component\Form\Extension\Core\Type\FieldType
  • Symfony\Component\Form\Util\PropertyPath

どのプロパティにもマッピングしないフィールドを定義する

Property-Pathの応用で、property_pathオプションにfalseもしくは空文字を指定すると、どのプロパティにもマッピングされなくなります。たとえばユーザの登録画面などで、ユーザ情報に加えて規約に同意されたかを確認するチェックボタンを設置するということはよくあると思います。次のRegistrationTypeではデータとはマッピングされないagreementというチェックボックスを追加する例です。

<?php

namespace Acme\DemoBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class RegistrationType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('email', 'email')
            ->add('password', 'password')
            ->add('agreement', 'checkbox', array('property_path' => false, 'required' => true))
        ;
    }

    public function getName()
    {
        return 'registration';
    }
}

このようにProperty-Pathを使うとわりと思いのままにフォームを組み立てることができるので、覚えておいて損はないと思います。

どのプロパティにもマッピングしないフィールドをバリデーションする

先ほどのRegistrationTypeでagreementフィールドを定義しましたが、このようなオブジェクトにマッピングされていないフィールドをバリデーションしたい場合もあると思います。その場合、CallbackValidatorというものを使ってバリデーション処理を記述します。

<?php

namespace Acme\DemoBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackValidator;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormError;

class RegistrationType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('email', 'email')
            ->add('password', 'password')
            ->add('agreement', 'checkbox', array('property_path' => false))
        ;

        $buider
            ->addValidator(new CallbackValidator(function($form) {
                if (!$form['agreement']->getData()) {
                    $form['agreement']->addError(new FormError('同意したまえ'));
                }
            }))
        ;
    }

    public function getName()
    {
        return 'registration';
    }
}

CallbackValidatorには任意のコールバックを渡すことができ、リクエストデータがバインドされた時に呼び出されます。その際引数にFormオブジェクトが渡されるので、ここからデータを取得してバリデーションを自前で行い、addError()メソッドにエラーを渡します。いくつかクラスをインポートしなきゃいけなかったり少し不便なので、その辺もうちょい簡単にできるように拡張しようかなーと考えてます。

ちなみにCallbackValidatorの他にCallbackTransformerというものもあり、コールバックを用いてデータの変換が可能です。これもまあ知っておくと便利ですが、これらのオブジェクトを意識せずに、思いついたときにぱっとコールバックを書けるといいなーと思います。


こんな感じで、Property-PathとCallbackValidatorを知っておくとよいですよということで、フォームの紹介は終わりです。

Advent Calendarの方ですが、次はuechocoさんです。がんばってください。