[Symfony] Doctrineの便利機能「Entity Listener」の使い方
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
の更新については完全に気にしなくてよくなります。
最高ですね。
具体的な使い方
手順としては、
-
FooListener
クラスを作る -
FooListener
クラスをEntity Listenerとして登録 -
FooListener
クラスをFoo
エンティティのEntity Listenerとして指定する
という3ステップが必要です。
FooListener
クラスを作る
1. <?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によって異なります。詳しくは 公式ドキュメント をご参照ください。
FooListener
クラスをEntity Listenerとして登録
2. 次に、 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']
FooListener
クラスを Foo
エンティティのEntity Listenerとして指定する
3. 最後に、「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もあるけど、「やりたい処理が複雑か」「すべてのエンティティが対象か特定のエンティティだけが対象か」に応じて使い分けるのがよい
Discussion