イベントを制御する、"EventDispatcher"

[Symfony Component Advent Calendar 2022](https://qiita.com/advent-calendar/2022/symfony-component)の3日目の記事です。

## 最初に
[Symfony](https://symfony.com)はPHPのフレームワークのひとつです。しかし、公式サイトの説明文には

> Symfony is a set of PHP Components, a Web Application framework, a Philosophy, and a Community — all working together in harmony.
  （SymfonyはPHPコンポーネントのセットで、Webアプリケーションフレームワークで、哲学、そしてコミュニティです。それらがハーモニーを奏でながら動作しています。）

と書かれている通り、PHPコンポーネントのセットで、たくさんのコンポーネントを提供しており、それらを組み合わせてひとつのフレームワークとして動作しています。Symfonyのコンポーネントは、Symfony上だけで動作するのではなく、他のPHPフレームワークやアプリケーションでも動作している強力なものが揃っています。

今回はそれらの中から、役立ちそうなもの・お薦めしたいものを紹介していきたいと思います。

※記事内ではautoloadのインポートは省略します。


## イベントを制御する、"EventDispatcher"

[EventDispatcher](https://symfony.com/doc/current/components/http_kernel.html)は、イベントを使い、処理と処理の間に、独自の処理を挟み込むためのコンポーネントです。Symfonyのイベントは、[メディエーターパターン](https://qiita.com/tanakahisateru/items/dd3ae4c4c2953ab572e3)と[オブザーバーパターン](https://qiita.com/tanakahisateru/items/61cf521f0fe462324f36)を使用しています。

### インストール

```shell
composer require symfony/event-dispatcher
```

#### イベントの作成

イベントは`Event`クラスを拡張した子クラスを作成します。（なお、この**拡張する**になってるのが、筆者のSymfony若干もやっとポイント）

```php

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

class ItemPublishedEvent extends Event
{
    public const NAME = 'item.published';

    public function __construct(private readonly Item $item)
    {
    }

    public function getItem(): Item
    {
        return $this->item;
    }
}

```

コンストラクタに引数を設けることで、好きな値を扱うことができます。このイベントはGetterなどのデータを渡す程度のメソッドを持ちます。

#### イベント処理の実行

上記で作ったイベントを実行する（よく『（イベントを）発火する』って言われます）のに利用するのが`EventDispatcher`です。

```php
use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher = new EventDispatcher();　// イベントディスパッチャー

// 商品を公開する処理
...

// イベントの作成・発火
$event = new ItemPublishedEvent();
$dispatcher->dispatch($event);

// 事後処理
...

```

このイベントの作成・発火部分で、独自の処理を挟むことができます。独自処理は**イベントサブスクライバ**、**イベントリスナー**という2種類の方法で実装でき、それぞれのクラスを作って、そこに独自処理を記載します。イベントに対応した**イベントサブスクライバ**、**イベントリスナー**があれば、独自の処理を実行し、**対応したイベントがない場合は、何もしません**。

#### イベントリスナー

イベントリスナーは、**ディスパッチャー側に『どのイベントに対して処理を実行するか』を設定します**。関数で独自処理を実装するパターンと、イベントリスナークラスを作って独自処理を実装するパターンがあります。

```php

use Symfony\Contracts\EventDispatcher\Event;

// アイテム公開
$item = new Item();
...

$dispatcher = new EventDispatcher();

// 関数で独自処理を実装
$dispatcher->addListener('item.published', function (Event $event) {
    // 独自処理
});

// 関数で独自処理を実装（イベント名ではなくクラス名で指定）
$dispatcher->addListener(ItemPublishedEvent::class, function (Event $event) {
    // 独自処理
});

// クラスで独自処理を実装
$listener = new ItemPublishedListener($item);
$dispatcher->addListener('item.published', [$listener, 'onItemPublished']);
```

イベントリスナークラスパターンでは、[`イベントリスナーオブジェクト`, `メソッド名`]と引数を設定します。イベントリスナークラスには上記で設定したメソッド名を用意し、そこに独自処理を記載します。


```php
class ItemPublishedListener
{
    public function onItemPublished(ItemPublishedEvent $event): void
    {
        // 独自処理
        ...
    }
}
```

#### イベントサブスクライバ

イベントサブスクライバは、**イベントサブスクライバ側に『どのイベントに対して処理を実行するか』を設定します**。よって、こちらは独自実装をイベントサブスクライバクラスで記載するパターンしかありません。

```php


use Symfony\Contracts\EventDispatcher\Event;

// アイテム公開
$item = new Item();
...

$dispatcher = new EventDispatcher();

$subscriber = new ItemPublishedSubscriber();
$dispatcher->addSubscriber($subscriber);
```
イベントサブスクライバクラスは`EventSubscriberInterface`を実装して作ります。このインターフェイスには`getSubscribedEvents()`メソッドがあり、このメソッドを実装することで、どのイベントに対応するか設定できます。


```php

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class ItemPublisedSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            ItemPublishedEvent::class => [ // サブスクライバもイベント名、クラス名どちらでも可
                ['uploadImage', 10],
                ['sendEmail', -10],
            ],
        ];
    }

    public function uploadImage(ItemPublishedEvent $event): void
    {
        // 独自処理
        ...
    }

    public function sendEmail(ItemPublishedEvent $event): void
    {
        // 独自処理
        ...
    }
}
```

サブスクライバの場合、同時に複数のメソッド名を設定することで複数メソッドを実行することができます。その際、**優先順位**を設定することができ、[`メソッド名`, `優先順位`] という形で設定できます。優先順位は正数型で、**値が大きいほど優先して実行されます**。上記の例では`uploadImage`がまず実行され、その後に`sendMail`が実行されます。
なお、優先順位は任意で設定ができ、設定しない場合は**0**になります。

## まとめ

今回は`EventDispatcher`の紹介でした。イベントは若干とっつきにくいですが、使ってみれば便利です。
一つの処理で、複数のコンテキストに関わるユースケースを実行する必要があるときに役立ちます。