比較的symfony初心者向けの記事です。

私が初めてsymfonyプロジェクトに関わった時、formコンポーネントの使い方が良く分かりませんでした。 特に親エンティティとのリレーションの設定をどこで行えばよいかはForms (The Symfony Book)にも載っていなかったのでどうすべきか迷いました。 ですので、今回は上記の実装方法を現時点で私が良さそうだなーと思っているパターンを紹介してみます。

たどり着くまでに試行したパターン

コントローラー内でリレーションを設定

1番ベーシックであろうパターンです。 symfonyで普通に生成すると、1エンティティを作成できるフォームが完成します。 そして普通にコントローラーを実装すればフォームによってリクエストから生成されたエンティティに対してリレーションを設定する流れになるかと思います。 それでも要件は満たせますが、機能テストでなければテストが行えず個人的にはイマイチだなと感じました。

リポジトリにリレーションを設定させる

リポジトリに親と子のエンティティを受け取り保存するメソッドを追加し、リポジトリ経由で保存します。 メソッドにエンティティのタイプヒンティングを付けておけば、保存時にリレーションを設定する強制力を持たせる事が出来ます。 しかしコールされる場所は結局コントローラー内ですし、必ずリポジトリのメソッドをコールしてもらえるか分からないのでこれもイマイチに感じます。

フォームにリレーションを設定させる

いろいろと調べて実践してきた結果、フォーム内でリレーションの設定を行う方法が上手くいってるんじゃないかなと思っています。

上に挙げたパターンよりも少しだけ良い事があります。

  • エンティティに対するリレーションの設定をフォーム内で完結させる事ができる(単体テストができる)
  • 更にリレーションの設定を強制させる事ができる(リレーション設定忘れが早期に判明する)

この方法を実践するとコントローラーは下記のように変化します。

postAction(User $user, Request $request)
{
    $form = $this->createForm('post', $post = new Post(), [
        'method' => 'POST',
+       'user' => $user,
    ]);

    $form->handleRequest($request);
    if ($form->isValid()) {
-       $post->setUser($user);
        $em = $this->getDoctrine()->getManager();
        $em->persist($post);
        $em->flush();
        
        return $this->redirectToRoute('post_show', [...]);
    }

    return ['form' => $form];
}

どうでしょうか?元がシンプルなので見た目的にあまり代わり映えしませんが、シンプルさをキープしたまま強制力を持たせる事が出来ます。

フォームのオプションとイベント

この方法を実現するにはフォームのオプションを新たに定義して、フォームのイベントが発生したタイミングでリレーションを設定するようにします。

フォームのオプションは定義済みの物が沢山有りますが、自分が作ったフォームはこのオプションを自由に定義できます。 フォームのイベントはよくあるイベントシステムがフォーム上でも使えるというお話です。

これらを使い親エンティティをオプションから受け取るようにし、submit時にリレーションを設定するといった感じになります。

オプション経由で親エンティティをフォームに渡す

まずオプションの定義を追加しなければ親エンティティを渡す事は出来ません。 未定義のオプションは渡せない仕組みになっている為です。

なのでPostTypeUserエンティティを渡す事を強制するようなオプションを定義してみます。

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PostType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('subject')
            ->add('body')
        ;
    }

    public function setDefaultOptions(OptionsResolver $resolver)
    {
        $resolver
            ->setDefaults([
                'data_class' => 'Acme\Entity\Post',
            ])
+            ->setRequired([
+                'user',
+            ])
+            ->setAllowedTypes([
+                'user' => 'Acme\Entity\User',
+            ])
        ;
    }

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

これらを追加する事でこのPostTypeはオプションuserAcme\Entity\Userインスタンスを渡さなければならないようになりました。

必須とされているオプションを渡さず$formFactory->create('post', [])なんてしようものなら例外がスローされるようになります。 自動化ですね。いいですね。

受け取った親エンティティを使ってリレーションを設定する

さて、次は受け取ったエンティティを使ってリレーションを設定してみます。 フォームのイベントはいくつかありますが、今回は送信時にリレーションをセットするようにします。

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PostType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('subject')
            ->add('body')
+           ->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) use ($options) {
+               /* @var $post \Acme\Entity\Post */
+               $post = $event->getData();
+               $post->setUser($options['user']);
+           })
        ;
    }

    public function setDefaultOptions(OptionsResolver $resolver)
    {
        $resolver
            ->setDefaults([
                'data_class' => 'Acme\Entity\Post',
            ])
            ->setRequired([
               'user',
            ])
            ->setAllowedTypes([
                'user' => 'Acme\Entity\User',
            ])
        ;
    }

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

フォームを作成する時に渡されたオプションは、buildForm(FormBuilderInterface $builder, array $options)$optionsから受け取る事ができます。

なので、あとは言葉通り「送信時にリレーションをセット」するだけです。 可読性も高くいい感じですね。

単体テストする

FormTypeは単体でテストが行えます。なので今回作成したフォームも同様にテストする事が出来ます。

フォームのテスト方法は公式ドキュメントによくまとまっているのでそちらを参照頂ければ問題ないかと思います。 http://symfony.com/doc/current/cookbook/form/unit_testing.html