🫥

[Laravel Subscriber] 関数名を文字列で定義するの怖くない?

2024/04/05に公開1

LaravelのSubscriber is 何

イベントシステム内で特定のイベントが発生したときに、一連のリスナー(イベントハンドラー)を登録することを可能にするクラスです。イベントリスナーは、イベントが発生した際にアプリケーションが実行すべき具体的な動作を定義しますが、Subscriberを使用することで、関連する複数のイベントリスナーを一つのクラス内で管理し、効率的にイベントに対する反応を定義することができます。

詳しいことは、公式ドキュメントや日本語訳ドキュメントを読んでください。
https://laravel.com/docs/10.x/events#writing-event-subscribers

https://readouble.com/laravel/10.x/ja/events.html

Subscriber実装

各イベント毎にメッセージをカスタマイズしてSlack通知を送信したい。
そのために書いたSubscriberが以下のような感じです。

実装例

<?php

declare(strict_types=1);

namespace App\Listeners;

use App\Events\UserCreatedEvent;
use App\Events\UserDeletedEvent;
use App\Services\User\SendSlackNotificationService;
use Illuminate\Events\Dispatcher;

class SendSlackNotificationSubscriber
{
    public function __construct(private readonly SendSlackNotificationService $send_slack_notification_service)
    {
    }

    public function handleUserCreated(UserCreatedEvent $userCreatedEvent): void
    {
        $this->send_slack_notification_service->handle(
            $userCreatedEvent->getUser(),
            '新規登録がありました。'
        );
    }

    public function handleUserDeleted(UserDeletedEvent $userDeletedEvent): void
    {
        $this->send_slack_notification_service->handle(
            $userDeletedEvent->getTrashedUser(),
            '退会がありました。'
        );
    }

    /**
     * サブスクライバのリスナを登録
     *
     * @return array<string, string>
     */
    public function subscribe(Dispatcher $events): array
    {
        return [
            UserCreatedEvent::class   => 'handleUserCreated',
            UserDeletedEvent::class   => 'handleUserDeleted',
        ];
    }
}

湧き上がる恐怖

Subscriberを書いて恐怖を感じた部分は、イベントとメソッドを紐付けるsubscribeメソッドです。

return [
            UserCreatedEvent::class    => 'handleUserCreated',
            UserDeletedEvent::class   => 'handleUserDeleted',
        ];

テストを書いて恐怖に打ち勝つ

おそらく、Subscriberの各メソッドに対してテストを普通書こうとしたら、以下のようになると思います。

public function test_userCreatedEvent(){
    $user = User::factory()->create();

    $service = Mockery::mock(SendSlackNotificationService::class);
    $service->shouldReceive('handle')->once();

    $subscriber = new SendSlackNotificationSubscriber($service);

    $subscriber->handleUserCreated(new UserCreatedEvent($user));
}

これだと、subscribeメソッドは通さず直接メソッドを呼び出すので、まだ「タイポしてたらどうしよう」の恐怖には勝てません。。。

そこで、私はメソッドの実行にDispatcherを利用することにしました。

public function test_userCreatedEvent(){
    $service = Mockery::mock(SendSlackNotificationService::class);
    $service->shouldReceive('handle')->once();

    $this->app->instance(SendSlackNotificationService::class, $this->service);

    $dispatcher = app(Dispatcher::class);
    // UserCreatedEventと紐づくリスナーを一旦全削除(テスト対象外のリスナーを走らせないため)
    $dispatcher->forget(UserCreatedEvent::class);
    // 今回テスト対象のsubscriberを登録
    $dispatcher->subscribe(SendSlackNotificationSubscriber::class);
    // イベント発火
    $dispatcher->dispatch(new UserCreatedEvent($user));
}

このように書くことで、subscribeメソッドを通してイベントに紐づくメソッドを実行することができました。
もし、メソッドの文字列でタイポがある場合でも、下記のようにエラーが出てくれます。

php
return [
-           UserCreatedEvent::class    => 'handleUserCreated', 
+           UserCreatedEvent::class    => 'handleUserCreate',
            UserDeletedEvent::class     => 'handleUserDeleted',
        ];

これでもう安心だ!!!!

このテストが必須かと言われるとそんなことはないと思います。
ただ、個人的には文字列で関数名定義することにモヤモヤしたので、一案としては良いかなと思います。

他に良いテスト方法あれば教えてください🙇

Discussion

taterentateren

PHP8.1 で追加された第一級 callable 記法を使って

    public function subscribe(Dispatcher $events): array
    {
        return [
            UserCreatedEvent::class   => self::handleUserCreated(...),
            UserDeletedEvent::class   => self::handleUserDeleted(...),
        ];
    }

のようにするのはどうでしょうか?

これなら書く時に補完も効くし、IDE のリファクタリング機能でメソッドを変更する時も追従してくれると思います。

https://www.php.net/manual/ja/functions.first_class_callable_syntax.php