🎻

[Symfony] EntityType経由で取得したエンティティに対してはEntityListenerのpostLoadを無効にしたい

2020/06/19に公開

状況

  • EntityListenerが設定されているエンティティがある
  • EntityListenerの postLoad で、何か複雑なクエリを使って関連エンティティを取得してくる処理をしている
class FooListener
{
    private $barRepository;

    public function __construct(BarRepository $barRepository)
    {
        $this->barRepository = $barRepository;
    }

    public function postLoad(Foo $foo, LifecycleEventArgs $event)
    {
        $foo->setBars($this->barRepository->findBarsWithTimeConsumingQuery($foo));
    }
}

EntityListenerについは こちらの過去記事 をご参照ください✋

問題

このエンティティに対してEntityTypeを使うフォームを作ると、当然1つ1つのエンティティに対してEntityListenerの postLoad が実行されます。

が、フォームに選択肢としてエンティティを出力したいだけなので、 id__toString() の出力さえあればよくて、関連エンティティが取得済みになっている必要は特にありません。

にもかかわらず、とても時間のかかるクエリがエンティティ全件に対して実行されてしまうため、パフォーマンス的にかなりもったいないです😓

解決策

EntityListenerを無効にすることができれば解決なので、ググってみたところ以下の情報を見つけました。

Symfony, Doctrine: how to disable Entity Listeners? - Stack Overflow
https://stackoverflow.com/questions/53501620/symfony-doctrine-how-to-disable-entity-listeners#answer-53503471

これを参考に、FormTypeで以下のようにしてEntityListenerを無効にすることで解決できました。

上記リンク先の回答者も言っているとおり、美しい方法ではありませんが…

class BazType extends AbstractType
{
    public function __construct(EntityManagerInterface $em)
    {
        $metadata = $em->getClassMetadata(Foo::class);
        foreach ($metadata->entityListeners as $eventName => $listeners) {
            foreach ($listeners as $i => $listener) {
                if ($listener['class'] === FooListener::class) {
                    unset($listeners[$i]);
                }
            }
            $metadata->entityListeners[$eventName] = $listeners;
        }
        $em->getMetadataFactory()->setMetadataFor(Foo::class, $metadata);
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('foo', EntityType::class, [
                'class' => Foo::class,
            ])
        ;
    }
}
Before After

ダメだった方法

ちなみに、同じ質問の こちらの回答 で提示されている、 EntityListenerResolverclear() メソッドを使うという方法も試してみたのですが、ダメでした。

class BazType extends AbstractType
{
    public function __construct(EntityManagerInterface $em)
    {
        $em->getConfiguration()->getEntityListenerResolver()->clear(FooListener::class);
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('foo', EntityType::class, [
                'class' => Foo::class,
            ])
        ;
    }
}

とやってみたのですが、PhpStormで ContainerEntityListenerResolver::clear() にブレイクポイントを張ってみたら、この時点ではそもそもまだ instances が空で、 serviceIds のほうにだけEntityListener(のクラス名)が登録されている状態でした。

これ以上コードは追っていませんが、実際にEntityListenerが登録されるタイミングよりも早く clear() を呼び出してしまっているので無意味なようです。

ということは?と思い、FormTypeのコンストラクタではなく

  • buildForm()
  • buildView()
  • finishView()
  • configureOptions()

それぞれの中で clear() してみるというのも試してみましたが、ダメでした😅

まとめ

  • Symfonyで、EntityType経由で取得したエンティティに対してはEntityListenerのpostLoadを無効にしたいという場合には、FormTypeのコンストラクタなどでエンティティの ClassMetadata を無理やり書き換えることで実現可能
    • (もっといい方法をご存知の方はぜひ Twitter などでフィードバックいただけると嬉しいです)
GitHubで編集を提案

Discussion