🎻
【symfony/form】query_builderとForm Eventsで激重なEntityTypeのviewを軽くできた話
長いことSymfonyを使ってますが、今回初めて query_builder オプションや Form Events という機能を知ったので、その使い方について共有しようと思います。
僕が置かれていた状況
query_builder や Form Events について説明する前に、そもそも僕がどういう状況でそれらを使って嬉しかったのかを伝えておきます。
-
ParentとChildという、親子関係を持った2つのエンティティがある -
Childエンティティは、訳あって「どのParentの子供か」が分からない状態で一旦作成される- つまり、
Child::parent_idがnullの状態で作成される
- つまり、
- その後、「どの
Childが どのParentの子供か」を示す情報を別途インポートすることで、一括でParentとChildの紐付け処理を行う- このとき、インポートされたデータを元に、紐付けのためのFormを動的に作って確認画面を表示する
-
ParentもChildもそこそこの数がある(数千〜数万ぐらいのオーダー)
ちょっと特殊な状況だとは思いますが、頑張って想像してみてください😅
この「紐付け処理」を行うための確認画面(動的に作った巨大な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は奥が深い😇
Discussion