フォームイベントを使用してフォームを動的に生成する方法

動的なフォーム生成に入る前に、初期状態のフォームクラスがどうなっているか見てみましょう。

//src/Acme/DemoBundle/Form/ProductType.php
namespace Acme\DemoBundle\Form

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

class ProductType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder->add('name');
        $builder->add('price');
    }

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

Note

上記の記事のコードが理解できない方は、先に Forms chapter を参照してから、この記事を読んでください。

仮想的な “Product” クラスを使用したフォームがあることを想定してください。 Product クラスには、 “name” と “price” の2つのプロパティがあるとします。このクラスによって生成されたフォームは、新しく Product を作成する際にも、データベース等で保存されている内容を編集する際にも、同じものが使われます。

オブジェクトを作成した後の編集の際に name の値を変更させたくないとしましょう。そうするには、Symfony のイベントディスパッチャー Event Dispatcher を使用してオブジェクトのデータを解析して、 Product のデータに基づいたフォームを変更することができます。この記事では、そのフォームの柔軟性について学びます。

フォームクラスにイベントサブスライバ(Event Subscriber)を追加する

ProductType のフォームクラスに “name” ウィジェットを追加する代わりに、イベントサブスクライバ(Event Subscriber)にフィールド作成の責任を移譲しましょう。

//src/Acme/DemoBundle/Form/ProductType.php
namespace Acme\DemoBundle\Form

use Symfony\Component\Form\AbstractType
use Symfony\Component\Form\FormBuilder;
use Acme\DemoBundle\Form\EventListener\AddNameFieldSubscriber;

class ProductType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $subscriber = new AddNameFieldSubscriber($builder->getFormFactory());
        $builder->addEventSubscriber($subscriber);
        $builder->add('price');
    }

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

イベントサブスライバは、 FormFactory オブジェクトのコンストラクタに渡されるので、このサブスクライバは、フォーム作成時にディスパッチされたイベントを通知されたときにフォームウィジェットを作成することができます。

イベントサブスライバ(Event Subscriber)クラスの内部

まだデータベースに保存されていないときなど、フォーム内の Product オブジェクトが新規のとき のみ “name” フィールドを作成することが今回のゴールです。

// src/Acme/DemoBundle/Form/EventListener/AddNameFieldSubscriber.php
namespace Acme\DemoBundle\Form\EventListener;

use Symfony\Component\Form\Event\DataEvent;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;

class AddNameFieldSubscriber implements EventSubscriberInterface
{
    private $factory;

    public function __construct(FormFactoryInterface $factory)
    {
        $this->factory = $factory;
    }

    public static function getSubscribedEvents()
    {
        // ディスパッチャに form.pre_set_data イベントをリッスンして
        // preSetData メソッドが呼ばれるように伝えます
        return array(FormEvents::PRE_SET_DATA => 'preSetData');
    }

    public function preSetData(DataEvent $event)
    {
        $data = $event->getData();
        $form = $event->getForm();

        // フォーム作成時に FormBuilder のコンストラクタによって setData() は null の引数で呼ばれます。
        // Doctrine に新規に保存するとき、またはデータを取ってきたときなど
        // 実際のエンティティオブジェクトを操作する際の setData() のみを対象にします。
        // そのため、この if 文は null の条件をスキップさせます。
        if (null === $data) {
            return;
        }

        // Product オブジェクトが "new" かどうか調べます
        if (!$data->getId()) {
            $form->add($this->factory->createNamed('text', 'name'));
        }
    }
}

Caution

このイベントディスパッチャの if (null === $data) 部の目的がよく間違って理解されます。正しく理解するために、 Form class の中を参照し、特にコンストラクタの最後で setData() メソッドが呼ばれるところと setData() メソッド自体に注目してください。

FormEvents::PRE_SET_DATA の行は実際に form.pre_set_data 文字列となります。 `FormEvent class`_ は構造上の目的を担います。 `FormEvent class`_ は、中央の場所となり、そこで利用可能ないろんなフォームイベントを探すことができます。

今回の例では、 form.set_data イベントや form.post_set_data イベントを使用することができました。しかし、 form.pre_set_data を使用することで、 Event オブジェクトから取り出したデータが他のサブスライバやリスナによって変更されることがないということを保証することができる点で違います。なぜなら form.pre_set_dataform.set_data` イベントによって渡される FilterDataEvent オブジェクトではなく、 DataEvent オブジェクトを渡すからです。 DataEventFilterDataEvent の親クラスで setData() メソッドはありませんので、サブスライバやリスナによって変更されることはないのです。

Note

フォームイベントの完全な一覧は FormEvents class を参照してください。