開発コストを削減「BiTT開発」って?
開発コストを削減「BiTT開発」って?
2019.09.11

Laravelのイベントを使ってみる

エリカ

こんにちは、ディレクターのエリカです。
今回は Laravel のイベントとリスナを使ってみます。

イベントとは

ドキュメントでは、次のように説明されています。

Laravelのイベントはシンプルなオブザーバの実装で、アプリケーションで発生する様々なイベントを購読し、リッスンするために使用します。

簡単に言うと、「何かイベントが発生したら、何かの処理をする」ことができる、ということかと思います。

説明にもあります「アプリケーションで発生する様々なイベント」は自由に定義することができます。

例えば、ブログのようなアプリケーションであれば、記事が投稿されたとき、記事にコメントがついた、などをイベントとして定義することができます。

リスナーとは

リスナーを定義しておくと、あるイベントが発生したときに対して、何かしらの処理を実行することができます。

例えば、「記事が投稿された」というイベントが発生したら、該当記事のデータを利用した処理を実行するといったような実装することができます。

実際の利用シーンを考えてみる

それでは、イベントとリスナーを使って、前述の例について考えてみます。
ブログアプリケーションで、「記事が投稿されたとき」、「記事にコメントがついたとき」をそれぞれイベントとして定義し、その内容をロギングしたり、通知するような機能を実装する場合です。

前提として、ブログアプリケーションの記事自体は、 BlogPost というEloquentで扱うものとします。

まずは、どんなイベントがあって、どういう処理をさせるかを EventServiceProvider に定義します。先に定義しておくことにより、 artisan で簡単にスキャフォルドを作成することができます。

リスナーには、ロギングするものと、通知するものを用意するものとし、通知については、「記事にコメントがついたとき」のみ行うといったケースです。

app/Providers/EventServiceProvider.php

// イベントとリスナクラスの場所を指定
use App\Events\BlogPostCreated;
use App\Events\BlogPostCommented;
use App\Listeners\BlogPostCreatedLogging;
use App\Listeners\BlogPostCommentedLogging;
use App\Listeners\BlogPostCommentedNotification;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
                
                // ブログ記事についてのイベントとそのリスナを指定
        BlogPostCreated::class => [
            BlogPostCreatedLogging::class,
        ],
        BlogPostCommented::class => [
            BlogPostCommentedLogging::class,
            BlogPostCommentedNotification::class,
        ],  
    ];

準備ができたら、 artisan event:generate を実行してみましょう。指定した場所に、それぞれのクラスが書き出されます。まずは、Eventsディレクトリに書き出されたイベントクラスを見てみます。

<?php

namespace App\Events;

use App\BlogPost;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class BlogPostCreated
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $post;

    /**
     * Create a new event instance.
     *
     * @param BlogPost $post
     * @return void
     */
    public function __construct(BlogPost $post)
    {
        $this->post = $post;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('channel-name');
    }
}
<?php

namespace App\Events;

use App\BlogPost;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class BlogPostCommented
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $post;

    /**
     * Create a new event instance.
     *
     * @param BlogPost $post
     * @return void
     */
    public function __construct(BlogPost $post)
    {
        $this->post = $post;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('channel-name');
    }
}

すべてのイベントで、記事のインスタンスを受け取ります。「記事にコメントがついたとき」のイベントは、コメントもあわせて受け取ります。

次に、Listenersディレクトリに書き出されたリスナクラスを見てみます。

<?php

namespace App\Listeners;

use App\Events\BlogPostCreated;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class BlogPostCreatedLogging
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  BlogPostCreated  $event
     * @return void
     */
    public function handle(BlogPostCreated $event)
    {
        // $event->post がBlogPostインスタンスになっているので、その情報を利用できる。
        // BlogPostインスタンスのtitleプロパティには記事のタイトルが定義されているとする

        logger("[{$event->post->title}] was created.");

    }
}
<?php

namespace App\Listeners;

use App\Events\BlogPostCommented;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class BlogPostCommentedLogging
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  BlogPostCommented  $event
     * @return void
     */
    public function handle(BlogPostCommented $event)
    {
        // $event->post がBlogPostインスタンスになっているので、その情報を利用できる。
        // BlogPostインスタンスのtitleプロパティには記事のタイトルが定義されているとする

        logger("[{$event->post->title}] was commented.");

    }
}

BlogPostCreatedLogging および BlogPostCommentedLogging では、イベントを受け取ってロギングします。

<?php

namespace App\Listeners;

use App\Events\BlogPostCommented;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class BlogPostCommentedNotification
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  BlogPostCommented  $event
     * @return void
     */
    public function handle(BlogPostCommented $event)
    {
        // $event->post がBlogPostインスタンスになっているので、その情報を利用できる。
        // BlogPostインスタンスのtitleプロパティには記事のタイトルが定義されているとする
        // $event->comment にはコメントが指定されている。

        // 上記を利用した通知の処理
    }
}

BlogPostCommentedNotification では、イベントを受け取って通知します。

これで、イベントとリスナの準備ができました。
あとは、それぞれのタイミングでイベントの発行をすれば完成です。

イベントを発行するには、ブログ記事を投稿するときの処理でそれぞれ以下のように実行するだけです。

// $post は投稿されたBlogPostインスタンス
event(new BlogPostCreated($post));

ブログ記事にコメントを投稿するときの処理で以下を実行。

// $post はコメントの対象のBlogPostインスタンス
// $comment はコメントの内容
event(new BlogPostCommented($post, $comment));

以上で、各種イベントにあわせた処理が実装できました。

まとめ

このような処理自体はコントローラレベルでも直接実装できそうですが、それぞれの機能(責務)をイベントやリスナに分散することにより、コード自体の見通しがよくなり、テストもそれぞれでしやすく、保守性にすぐれたものになるのではないかと思います(今回は触れていませんが、イベントの処理にはキューを利用(非同期)することができるので、重い処理がある場合にも有用です)。

積極的に利用していきたいですね。

2