🎻

[Symfony] 他のエンティティの状態によって値が決まるような属性はEntityListenerのPostLoadでセットしてあげる

2020/04/30に公開

例えば、以下のような要件を考えます。

  • 「タスク」と「実施」というエンティティがある
  • タスクは実施を子に持つ(OneToMany)
  • タスクには「必要実施回数」という属性がある
  • タスクに紐づいている実施の数が必要実施回数に満たなければそのタスクは「未完了」であり、必要実施回数以上であれば「完了済み」であると見なされる

このような場合、タスクに「完了フラグ」というプロパティを持たせて永続化するのは、個人的には正規化の観点から違和感を覚えます🤔(タスクが完了済みかどうかという情報は実施がすでに持っているのに、それとは別でタスク自身に覚えさせるというのはデータ構造としてなんか変な気がする)

そのことの是非についてはちょっと僕の勉強不足で正解が分からないので、詳しい方がいたらぜひ Twitter などでフィードバックいただけると嬉しいです🙏

ともかく、こういうケースでタスク自身にフラグを持たせずにアプリ側で都度計算するような実装にする際に、僕はいつもこうしてますよいう方法についてこの記事では紹介したいと思います。

※ 追記

最近は↓こっちの方法を好んで使っています。
[Symfony] 他のエンティティの状態によって値が決まるような属性はエンティティ自体に持たせるよりTwig関数を作るほうがよさそう

例示したようなシンプルな要件なら

先ほどのタスクと実施の関係のようにシンプルな要件であれば、単純にタスクエンティティにメソッドを生やしてしまえばいいですよね。

class Task
{
    // ...

    /**
     * @ORM\Column(type="integer")
     */
    private $requiredTimes;

    /**
     * @ORM\OneToMany(targetEntity="Implementation", mappedBy="task")
     */
    private $implementations;

    // ...
    
    public function isComplete(): bool
    {
        return count($this->getImplementations()) >= $this->getRequiredTimes();
    }
}

こんな感じで。

もう少し複雑な要件の場合を考える

最初に示した例が完全にイマイチだったわけですが😓

もう少し複雑な親子関係の先にあるエンティティの状態が必要な場合とかを考えてみてください。

そういう場合、先ほどのようにエンティティに生やしたメソッドで対応しようとすると、getterメソッドを使ってリレーションのあるエンティティを何重にも渡ってすべて取得して、多重のforeachを回しながら条件を計算する、とかになってきます。

半端じゃない数のSQLが発行されて、どう考えてもパフォーマンスのボトルネックになりますよね。

となると当然DBレイヤーで条件の計算を片付けたくなるので、リポジトリに条件計算のためのメソッドを生やして、そこに問い合わせればタスクが完了済みかどうか一発で分かるような実装にするのがよさそうです。

で、それをエンティティ内でやっちゃうんですか?という話になります。

エンティティに他のエンティティのリポジトリをインジェクトして使うということは構造上はできてしまいますが、関心の分離の観点からあまり褒められた設計ではないですよね。

そこで、タイトルに書いたようにEntity Listenerを使ってPostLoadのタイミングで計算させるようにすると色々スッキリしてよいですよというお話です。(前置きがめっちゃ長くなった)

なお、Entity Listener自体の紹介は こちらの記事 に詳しく書いてありますので、よく知らないという方は先にこちらをご参照ください

具体的なコードの例

エンティティには、永続化しないプロパティとして完了済みフラグを持たせておきます。また、Entity Listenerも登録しておきます。

/**
 * @ORM\Entity(repositoryClass="App\Repository\TaskRepository")
 * @EntityListeners({"App\EntityListener\TaskListener"})
 */
class Task
{
    // ...

    /**
     * @ORM\Column(type="integer")
     */
    private $requiredTimes;

    /**
     * @ORM\OneToMany(targetEntity="Implementation", mappedBy="task")
     */
    private $implementations;

    // ※ ORMアノテーションがないことに注意
    private $isComplete;

    // ...
    
    public function getIsComplete(): ?bool
    {
        return $this->isComplete;
    }

    public function setIsComplete(?bool $isComplete): self
    {
        $this->isComplete = $isComplete;
        
        return $this;
    }
}

その上で、Entity Listenerをこんな感じで用意してあげます。

class TaskListener
{
    /**
     * @var FooRepository
     */
    private $FooRepository;

    public function __construct(FooRepository $fooRepository)
    {
        $this->fooRepository = $fooRepository;
    }

    public function postLoad(Task $task, LifecycleEventArgs $event)
    {
        $isComplete = someCalculation($this->fooRepository->findSomethingByTask($task));
        
        $task->setIsComplete($isComplete);
    }
}

例が悪いせいでいまいちピンと来ないかもしれませんが😓、「別のエンティティ(上記では Foo )のリポジトリを使って何か計算をした結果はじめてタスクが完了しているかどうか分かる」というケースを想定しています。

ともかく、エンティティにリポジトリをインジェクトするみたいな荒技を避けつつも、やりたかったことが上手く実現できました💪

まとめ

  • エンティティに「他のエンティティの状態によって決まる」ような属性がある場合は、Entity ListenerでPostLoadのタイミングでセットしてあげるとよさそう(タイトルのまんま)
GitHubで編集を提案

Discussion