💨

SymfonyでObserverパターンを実装する

2024/08/11に公開

Observerパターンとは

Observerパターンは、オブジェクトの状態が変化したときに、そのオブジェクトに依存している他のオブジェクトに通知を送るためのデザインパターンです。
以下に概略図を示します。ある特定のイベントを発行するSubjectがあり、それを監視するObserverが複数存在します。
Observerパターンの概略図
Observerパターンでは、subjectはobserverの具象クラス、振る舞いなどを知る必要はなく、そのインターフェースのみを知っていれば良いのです。
また新たなオブジェクトをobserverとして追加することや、逆に削除することも容易です。

例えば、「受注が登録されたときにメールを送る、ログを出力する」といった場合を考えてみます。
Observerパターンを利用しない場合、以下のような実装になるでしょう。

class PurchaseFromFoo
{
    public function __construct(
        private RegisterOrderService $registerOrderService,
        private SendMailService $sendMailService,
        private LogService $logService,
        ){
    }

    public function register()
    {
        // 受注を登録するときにはメールを送る、ログを出力する
        $this->registerOrderService->execute();
        $this->sendMailService->sendMail();
        $this->logService->createLog();
    }
}

この場合は、受注を登録する、メールを送る、ログを出力する処理がPurchaseFromFooにまとまってしまっており、これらのロジックは互いに密結合しています。
新しく処理を追加する場合には、PurchaseFromFooクラスを修正し、新しいクラスに依存を追加する必要があります。
Observerパターンを利用することで、このような問題を解決することができます。

<?php

interface Observer {
    public function update($temperature);
}

class PurchaseFromFoo {

    /**
     * @var Observer[]
     */
    private array $observers = [];

    public function addObserver(Observer $observer) {
        $this->observers[] = $observer;
    }

    public function removeObserver(Observer $observer) {
        $this->observers = array_filter($this->observers, fn($o) => $o !== $observer);
    }

    public function register(Order $order) {
        $this->notify($order);
    }

    private function notify(Order $order) {
        foreach ($this->observers as $observer) {
            $observer->update($order);
        }
    }
}

class SendEmail implements Observer {
    public function onPlaced(Order $order) {
        $this->sendEmailLogic();
    }
}

class WriteLog implements Observer {
    public function onPlaced(Order $order) {
        $this->someLogLogic();
    }
}

// 利用例
$purchaseFromFoo = new PurchaseFromFoo();

$sendEmail = new SendEmail();
$writeLog = new WriteLog();

// observerを登録
$purchaseFromFoo.addObserver($sendEmail);
$purchaseFromFoo.addObserver($writeLog);

// 受注登録
$order = new Order();
$purchaseFromFoo->register($order);

このように、Observerパターンを利用することで、ロジックを疎結合にすることができます。

SymfonyでObserverパターンを実装する

SymfonyでObserverパターンを実装する場合、自前でObserverを実装する必要はありません。
EventDispatcherを利用することができます。
EventDispatcherは以下のコマンドでインストールできます。

composer require symfony/event-dispatcher

Eventクラスの作成

まずはEventクラスを作成します。
これは「受注が登録された」というイベントを表すクラスです。

<?php

namespace App\Event;

use App\Entity\Order;
use Symfony\Contracts\EventDispatcher\Event;

class OrderPlacedEvent extends Event
{
    public function __construct(private Order $order)
    {
    }

    public function getOrder(): Order
    {
        return $this->order;
    }
}

EventSubscriberクラスの作成

次に、EventSubscriberクラスを作成します。
これは、OrderPlacedEventを受け取るクラスです。
以下の例では手を抜いて、OrderPlacedEventを受け取ったときに、受注の金額を出力するだけの処理にしています。

<?php

namespace App\EventSubscriber;

use App\Event\OrderPlacedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class OrderSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            OrderPlacedEvent::class => 'onPlacedOrder',
        ];
    }

    public function onPlacedOrder(OrderPlacedEvent $event): void
    {
        $order = $event->getOrder();
        var_dump($order->getAmount());
    }
}

Symfonyは、EventSubscriberInterfaceを実装したクラスを自動的にEventDispatcherに登録してくれるので、クラスの中でEventをdispatchするだけで、EventSubscriberの処理が実行されます。

<?php

namespace App\Service;

use App\Entity\Order;
use App\Event\OrderPlacedEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class OrderService
{
    public function __construct(private EventDispatcherInterface $dispatcher)
    {
    }

    public function registerOrder(Order $order): void
    {
        // イベントを作ってdispatchするだけ
        $event = new OrderPlacedEvent($order);
        $this->dispatcher->dispatch($event);
    }
}

これで、Observerパターンを利用して、受注登録時に他の処理を実行することができるようになりました。
テストコードとその実行結果を以下に示します。

<?php

namespace App\Tests\Service;

use App\Entity\Order;
use App\Service\OrderService;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class OrderServiceTest extends KernelTestCase
{
    /**
     * @test
     */
    public function testOrderPlacedEvent()
    {
        $order = new Order(amount: 100);
        $dispather = $this->getContainer()->get('event_dispatcher');
        $service = new OrderService($dispather);
        $service->registerOrder($order);
    }
}

// 実行結果
// Testing started at 13:45 ...
// PHPUnit 9.6.19 by Sebastian Bergmann and contributors.
//
// Testing symfony-observer-pattern/tests/Service
// symfony-observer-pattern/src/EventSubscriber/OrderSubscriber.php:20:
// int(100) // 100円の受注が登録されたときに、金額が出力される

ちゃんと動いていそうです。

まとめ

「〜したときに〜する」といった処理はよく実装します。
プロダクトが大きくなるにつれてこの処理は複雑になるでしょう。
Observerパターンを利用することで、処理を疎結合にし、保守性を高めることができます。
SymfonyではEventDispatcherを利用することで、Observerパターンを簡単に実装することができるので、どんどん活用していきたいです。

参考

GitHubで編集を提案

Discussion