[Symfony] DoctrineのpreUpdateで他のエンティティの生成をやろうとしたけどできなかった話(解決策あり)
やりたかったこと
-
Foo
というエンティティのstate
というプロパティが変更されたら、その変更内容に応じてBar
というエンティティを自動で作成する
ということがやりたくて、 Foo
の preUpdate
のタイミングで $entityManager->persist($bar)
すればいいかと思ったらできませんでした🙄
なぜできないのか
Doctrineの公式ドキュメント を見ると、
PreUpdate is the most restrictive to use event, since it is called right before an update statement is called for an entity inside the
EntityManager#flush()
method. Note that this event is not triggered when the computed changeset is empty.Changes to associations of the updated entity are never allowed in this event, since Doctrine cannot guarantee to correctly handle referential integrity at this point of the flush operation.
とあり、 PreUpdate
は flush()
メソッド内で呼ばれる性質上、このタイミングで関連エンティティの更新はできない仕様のようです。
ちなみにググると
symfony - Persisting other entities inside preUpdate of Doctrine Entity Listener - Stack Overflow
とかが見つかって、「
preUpdate
じゃなくonFlush
でならできるよ〜」「getEntityChangeSet()
を使えば変更されたプロパティも分かるよ〜」などと書かれているんですが、実際にonFlush
でやってみたらやっぱりpersist
したものがDBに保存されないし、getEntityChangeSet()
の結果も空になります。
どうすればできるのか
preUpdate
のタイミングで persist
するだとダメなので、コントローラから自分でイベントをディスパッチして自作のEventListenerまたはEventSubscriberで persist
するようにすれば目的は果たせます。
具体的なコードのイメージは以下のような感じです。
コントローラ
/**
* @Route("/foo/{id}/edit", name="foo_edit", methods={"GET","POST"})
*/
public function edit(Request $request, Foo $foo, EntityManagerInterface $em, EventDispatcherInterface $dispatcher)
{
$form = $this->createForm(FooType::class, $foo);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// stateが変更されていたらイベントをディスパッチ.
$em->getUnitOfWork()->computeChangeSets();
$changeSets = $em->getUnitOfWork()->getEntityChangeSet($foo);
if (isset($changeSets['state']) && $changeSets['state'][0] !== $changeSets['state'][1]) {
$dispatcher->dispatch(new StateChangedEvent($foo));
}
$this->em->flush();
$this->addFlash('success', '編集が完了しました。');
return $this->redirectToRoute('foo_show', ['id' => $foo->getId()]);
}
return [
'foo' => $foo,
'form' => $form->createView(),
];
}
Foo
の state
が変更されているかどうかを調べるために、 UnitOfWork
から getEntityChangeSet()
で変更内容を取得しています。( preUpdate
なら hasChangedField()
で一撃なのにな〜と思いながら)
getEntityChangeSet()
する前に $em->getUnitOfWork()->computeChangeSets();
を先に実行しているところがポイントです。これをやらないとこの時点ではまだチェンジセットが空になっています。
ちなみに UnitOfWork
とかがピンと来ない方は 後藤さんのこのポスト がめちゃめちゃ分かりやすいのでぜひ読んでみてください。
EventSubscriber
class FooSubscriber implements EventSubscriberInterface
{
/**
* @var EntityManagerInterface
*/
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function onStateChanged(StateChangedEvent $event)
{
$foo = $event->getFoo();
$bar = new Bar();
// ... $foo->getState() の値に応じてBarの内容を設定.
$this->em->persist($task);
}
public static function getSubscribedEvents()
{
return [
StateChangedEvent::class => 'onStateChanged',
];
}
}
Event
class StateChangedEvent extends Event
{
/**
* @var Foo
*/
private $foo;
public function __construct(Foo $foo)
{
$this->foo = $foo;
}
public function getFoo(): Foo
{
return $this->foo;
}
}
まとめ
- Doctrineの
preUpdate
で他のエンティティの生成をやろうとしたけどできなかった - タイミングの問題なので、コントローラから自分でイベントをディスパッチして自作のEventListenerまたはEventSubscriberでエンティティの生成と
persist
をすれば目的は果たせる - 今回は複数のコントローラアクションでイベントをディスパッチする必要があったので、イベントディスパッチのコードが複数箇所に散らばるのが嫌だったけど、どうしようもなかった…
Discussion