🎻

【symfony/form】query_builderとForm Eventsで激重なEntityTypeのviewを軽くできた話

2020/03/03に公開

長いことSymfonyを使ってますが、今回初めて query_builder オプションや Form Events という機能を知ったので、その使い方について共有しようと思います。

僕が置かれていた状況

query_builderForm Events について説明する前に、そもそも僕がどういう状況でそれらを使って嬉しかったのかを伝えておきます。

  • ParentChild という、親子関係を持った2つのエンティティがある
  • Child エンティティは、訳あって「どの Parent の子供か」が分からない状態で一旦作成される
    • つまり、 Child::parent_idnull の状態で作成される
  • その後、「どの Child が どの Parent の子供か」を示す情報を別途インポートすることで、一括で ParentChild の紐付け処理を行う
    • このとき、インポートされたデータを元に、紐付けのためのFormを動的に作って確認画面を表示する
  • ParentChild もそこそこの数がある(数千〜数万ぐらいのオーダー)

ちょっと特殊な状況だとは思いますが、頑張って想像してみてください😅

この「紐付け処理」を行うための確認画面(動的に作った巨大なFormをレンダリングしている画面)のイメージは以下のようになります。

(モザイクだらけでほとんど何のことか分かりませんが😅)この画面、

  • Parent の数だけ <option> を持っている <select> が、 Parent の数だけ並んでいる
  • その各行に、 Child の数だけ <option> を持っている <select> が置かれている

というかなり巨大すぎるHTMLになっていて、メモリも処理時間も掛かりすぎてビューのレンダリングができない状態でした。

これを、

  • 各行の Parent<select> は1つの <option> しか持たない(実際にはそれしか必要ないので)
  • 各行の Child<select> は、特定の条件で絞り込んで必要最小限の <option> しか持たない

ようにすることで、現実的な時間とメモリの範囲で実用に耐える状態にできたというお話です。

最初に書いたFormType

もともとの激重だったFormのFormTypeは以下のような実装でした。

class LinkCollectionType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('links', CollectionType::class, [
                'entry_type' => LinkType::class,
                'allow_add' => true, // これがないとhandleRequestできない
            ])
        ;
    }
}
class LinkType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('parent', EntityType::class, [
                'class' => Parent::class,
            ])
            ->add('children', EntityType::class, [
                'class' => Child::class,
                'expanded' => false,
                'multiple' => true,
            ])
        ;
    }
}

この LinkCollectionType のFormに、以下のような配列をデータとしてセットすることで、確認画面用のフォームが出来上がります。

[
    'links' => [
        [
            'parent' => $parent,
            'children' => [
                $child1,
                $child2,
                $child3,
                    :
            ],
        ],
        [
            'parent' => ...,
            'children' => ...,
        ],
        [
            'parent' => ...,
            'children' => ...,
        ],
            :
            :
    ],
],

軽くなるように修正したFormType

もとの実装のうち、確認画面の1行に相当する LinkType のほうだけを以下のように修正しました。

class LinkType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('parent', EntityType::class, [
                'class' => Parent::class,
            ])
            ->add('children', EntityType::class, [
                'class' => Child::class,
                'expanded' => false,
                'multiple' => true,
            ])
            ->addEventListener(
                FormEvents::PRE_SET_DATA,
                function (FormEvent $event) {
                    $data = $event->getData();

                    // 確認画面描画時のみ
                    if ($data) {
                        $parent = $data['parent'];
                        $children = $data['children'];

                        $event->getForm()
                            ->remove('parent')
                            ->remove('children')

                            ->add('parent', EntityType::class, [
                                'class' => Parent::class,
                                'query_builder' => function (ParentRepository $repository) use ($parent) {
                                    return $repository->createQueryBuilder('p')
                                        ->where('p = :parent')
                                        ->setParameter('parent', $parent)
                                    ;
                                },
                                'data' => $parent,
                            ])
                            ->add('children', EntityType::class, [
                                'class' => Child::class,
                                'expanded' => false,
                                'multiple' => true,
                                'query_builder' => function (ChildRepository $repository) use ($children) {
                                    return $repository->createQueryBuilder('c')
                                        ->where('必要最小限だけを選択肢に出すための絞り込み')
                                    ;
                                },
                                'data' => $children,
                            ])
                        ;
                    }
                }
            )
        ;
    }
}

かなりコードが長くなっていますが、以下のようなことをやっています。説明を見ながらコードを読んでみてください👍

  • addEventListener()FormEvents::PRE_SET_DATA をフックしてそのタイミングでフィールドを一度 remove() して add() しなおす
  • その add() の際に、 query_builder オプションを使って対象とするエンティティの絞り込みを行う
  • データをセットせずにFormインスタンスを作る場合(確認画面の送信先で handleRequest する際など)に必要となるため、EventListerner内だけでなく普通の add() も残しておくのがポイント

これにより、最初に言ったように

  • 各行の Parent<select> は1つの <option> しか持たない(実際にはそれしか必要ないので)
  • 各行の Child<select> は、特定の条件で絞り込んで必要最小限の <option> しか持たない

という出力になるようなFormが作れました🙌

参考リンク

まとめ

  • symfony/formには query_builder オプションや Form Events という機能があります
  • 柔軟にFormをいじれるので特殊な要件を実装するときに覚えておくといいかもです
  • symfony/formは奥が深い😇
GitHubで編集を提案

Discussion