🎻

[Symfony] Doctrineの便利機能「Entity Listener」の使い方

2020/04/23に公開

Doctrineの便利機能「Entity Listener」について紹介し、具体的な使い方の例を示します。

Entity Listenerとは

Entity Listener は、特定のエンティティの Lifecicle Events をフックできる機構です。

例えば、 エンティティが更新される度にそのエンティティの updatedAt プロパティを現在日時で更新したい というようなケースを考えてみましょう。

普通にやるなら、コントローラ等で

$foo->setBar($bar);
$foo->setUpdatedAt(new \DateTime());
$em->persist($foo);
$em->flush();

こんな風にやれば実現できます。

が、これだとFooエンティティが更新される処理が複数あった場合に、すべての箇所で忘れずに $entity->setUpdatedAt(new \DateTime()); を実行する必要があり、面倒だし絶対どこかで忘れます。

こんなときに、「Fooエンティティが更新されたら自動で updatedAt を現在日時に変更する」という処理をしてくれるEntity Listenerを用意しておけば、コントローラ側では updatedAt の更新については完全に気にしなくてよくなります。

最高ですね。

具体的な使い方

手順としては、

  1. FooListener クラスを作る
  2. FooListener クラスをEntity Listenerとして登録
  3. FooListener クラスを Foo エンティティのEntity Listenerとして指定する

という3ステップが必要です。

1. FooListener クラスを作る

<?php

namespace App\EntityListener;

use App\Entity\Foo;
use Doctrine\ORM\Event\PreUpdateEventArgs;

class FooListener
{
    public function preUpdate(Foo $foo, PreUpdateEventArgs $event)
    {
        $foo->setUpdatedAt(new \DateTime());
    }
}

こんな感じで、Lifecycle Eventsのイベント名をメソッド名にすれば、自動でそのイベントをフックしてくれます。

第二引数で受け取れるDoctrineのイベントクラスは、フックするLifecycle Eventsによって異なります。詳しくは 公式ドキュメント をご参照ください。

2. FooListener クラスをEntity Listenerとして登録

次に、 FooListener クラスがEntity ListenerであるということをSymfonyに登録する必要があります。

services.yaml に以下のように書けばOKです。(詳しくは 公式ドキュメント 参照)

services:
    # :
    App\EntityListener\FooListener:
        tags: ['doctrine.orm.entity_listener']

これで、 App\EntityListener\FooListener サービスがEntity Listenerとして登録されます。

ちなみに、 App\EntityListener namespaceの配下に他にもたくさんEntity Listenerを作る場合は、以下のように一括で登録するようにしておくと記述量が減らせます。

services:
    # :
    App\EntityListener\:
        resource: '../src/EntityListener'
        tags: ['doctrine.orm.entity_listener']

ただし、Entity Listenerにコンストラクタインジェクションが必要な場合は、そのクラスについては別途定義する必要があります。

services:
    # :
    App\EntityListener\:
        resource: '../src/EntityListener'
        tags: ['doctrine.orm.entity_listener']

    App\EntityListener\BarListener:
        arguments: ['@some_service']
        tags: ['doctrine.orm.entity_listener']

3. FooListener クラスを Foo エンティティのEntity Listenerとして指定する

最後に、「Entity Listenerとして登録済みの FooListener というクラス」を、「 Foo エンティティに対するEntityListener」として登録する必要があります。

と言ってもエンティティクラスにアノテーションを追加するだけです。(公式ドキュメント 参照)

  /**
   * @ORM\Entity(repositoryClass="App\Repository\FooRepository")
+  * @ORM\EntityListeners({"App\EntityListener\FooListener"})
   */
  class Foo
  {
      // ...
  }

4. 完成

以上で完了です!

あとはコントローラなどから

$foo->setBar($bar);
$em->persist($foo);
$em->flush();

とするだけで、ちゃんと $foo::updatedAt が更新されます👍

Lifecycle CallbackやLifecycle Listener/Subscriberとの違いは?

今回紹介したEntity Listenerのように、エンティティのLifecycle Eventsをフックして何か処理を実行するための機構はいくつか用意されています。

  • Lifecycle Callback
  • Lifecycle Listener
  • Lifecycle Subscriber
  • Entity Listener

すごくややこしいですが、Symfonyの こちらのドキュメント に比較がまとめられています。

  • Lifecycle callbacks, they are defined as methods on the entity classes and they are called when the events are triggered;
  • Lifecycle listeners and subscribers, they are classes with callback methods for one or more events and they are called for all entities;
  • Entity listeners, they are similar to lifecycle listeners, but they are called only for the entities of a certain class.

特徴を表にまとめてみると以下のような感じですね。

対象エンティティ 処理を書く場所
Lifecycle Callback 特定のエンティティ そのエンティティクラスのメソッド
Lifecycle Listener/Subscriber すべてのエンティティ 別クラス
Entity Listener 特定のエンティティ 別クラス

使い分け方としては以下のような感じがいいのかなと思います。

ちょっとした処理 サービスに依存するような複雑な処理
特定のエンティティだけに対して行いたい処理 Lifecycle Callback Entity Listener
すべてのエンティティに共通して行いたい処理 Lifecycle Listener/Subscriber Lifecycle Listener/Subscriber

まとめ

  • DoctrineのEntity Listenerという機能を使うと、特定のエンティティに対するLifecycle Eventsをフックして自由に処理を足すことができて便利
  • 似た機構としてLifecycle CallbackやLifecycle Listener/Subscriberもあるけど、「やりたい処理が複雑か」「すべてのエンティティが対象か特定のエンティティだけが対象か」に応じて使い分けるのがよい
GitHubで編集を提案

Discussion