🎻
【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