LaravelではEvent(イベント)/Listener(リスナー)を設定した場合、アプリケーションの中でイベントが発生するとそのイベントを監視しているリスナーがイベントの発生を検知し、予め決められている処理を実行することができます。つまりある処理をトリガーにして別の処理を行うことができます。

ユーザ登録の機能を実装したLaravelアプリケーションの例を使ってイベントとリスナーがどのように利用されているか簡単に説明します。

新規のユーザがユーザ登録を行うとLaravelアプリケーション側で登録イベント(ユーザの登録が完了したよという信号をアプリケーションの内部で送る)を発生させます。ユーザ登録によってイベントが発生することを事前に知っているリスナーがユーザ登録によって発生したイベントを検知します。イベント検知後は事前に登録された処理を実行します。リスナーには事前に登録ユーザへのWelcomeメール送信、メルマガへの登録や管理者へのSlack送信などの処理を登録しておく必要があります。

本文書ではLaravelのイベントを使う場合と使わない場合で比較して動作確認を行うことでイベントの理解だけではなくイベントの利点についても解説していきます。

なぜイベントを使うのか?

本書ではユーザ登録後に行うメール送信やメルマガ登録をイベントを利用して行います。それらの処理はイベントを使用しなくても実装することができますがイベントを利用することでコントローラー(メインのファイル)に記述するコード量が少なくなる上、メール送信、メルマガ登録という処理を別のクラス(別ファイル)にわけることでコードのメンテナンスが楽になります。処理毎にファイルを分けることで例えば新たにSlack送信という処理を追加することになってもコントローラー(メインのファイル)のコード変更を行うことなく他への影響を最小限に抑えて処理の追加を行うことができます。不要になった処理の削除についてもコントローラーとは別に処理毎にファイルが作成されているので削除も簡単に行うことができます。

イベントがある場合ない場合
イベントがある場合ない場合の違い

イベントを使わない場合と使った場合の上記の図を念頭においてイベントを理解していきましょう。

環境の構築

イベントとリスナーの動作確認を行うためには、Laravel環境の構築を行う必要があります。Laravelのインストールやデータベースの設定のみではなくメールマガジンを送信するメールアドレスを保存するテーブル、メール送信の機能の設定も行います。

Laravelのインストールと設定

composerコマンドでLaravelのインストールを行います。


$ composer create-project --prefer-dist laravel/laravel event_listener   

データベースにはsqliteを利用します。touchコマンドでデータベースファイルを作成します。コマンドはLaravelのインストールディレクトリで実行します。


$ touch database/database.sqlite

.envファイルでデータベースの設定をmysqlからsqliteに変更します。


DB_CONNECTION=sqlite
DB_CONNECTION以外のデータベースに関わる設定は削除します。
fukidashi

ユーザの登録作業を行うために認証機能が必須となります。php artisan make:authコマンドを実行し認証機能の追加を行います。


$ php artisan make:auth
Authentication scaffolding generated successfully.

メールマガジン登録用のテーブルを作成します。php artisan make:modelコマンドでMailMagazineモデルを作成します。-mオプションをつけることで同時にマイグレーションファイルも作成します。


$ php artisan make:model MailMagazine -m
Model created successfully.
Created Migration: 2019_08_20_112646_create_mail_magazines_table

マイグレーションファイルには、メールマガジンに登録するメールアドレス列のみ作成します。


public function up()
{
    Schema::create('mail_magazines', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->string('email')->index();
        $table->timestamps();
    });
}

php artisan migrateコマンドでテーブルの作成を行います。users, password_resets, mail_magazinesテーブルの3つが作成されます。


$ php artisan migrate
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (0.07 seconds)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (0 seconds)
Migrating: 2019_08_20_112646_create_mail_magazines_table
Migrated:  2019_08_20_112646_create_mail_magazines_table (0 seconds)

メール送信のSMTP設定

ユーザ登録後にWelocomeメールが送信できるようにメールの設定を行います。

ここではダミーのSMTPサーバのmailtrapを利用します。.envファイルを開いてmailTrapの接続情報を設定します。


MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=XXXXXXXXXXXXX
MAIL_PASSWORD= XXXXXXXXXXXXX
MAIL_FROM_ADDRESS=from@example.com
MAIL_FROM_NAME=Example
SMTPサーバが準備できない場合はMAIL_DRIVERにlogを設定してください。送信したメールの内容がログファイルに書き込まれます。ログファイルはstorage/logsの下に作成されます。
fukidashi

送信するメールの内容を記述するためにMailableクラスのWelcomeMailを作成します。


$ php artisan make:mail WelcomeMail
Mail created successfully.

app¥Mailディレクトリの下にWelcomeMail.phpが作成されます。

WelcomeMail.phpのbuildメソッドを下記のように書き換えます。viewメソッドで指定したemails.welcomeファイルにメールの内容を記述します。subjectメソッドはメールのタイトルです。


public function build()
{
    return $this->view('emails.welcome')
                ->subject('This is welcome mail');
}

resource¥viewsディレクトリの下にemails¥welcome.blade.phpファイルを作成します。

welcome.blade.phpファイルには以下を書き込みます。


ユーザ登録ありがとうございます。

MailMagazineモデルの設定

MailMagazineテーブルにemailの情報が追加できるようにMailMagazine.phpモデルファイルに以下を追加します。


class MailMagazine extends Model
{
    protected $fillable = [
        'email'
    ];
}

メールマガジン登録用の静的メソッドsubscribeも追加します。


public static function subscribe($email){

	return Self::create(['email' => $email ]);

}

php artisan serveコマンドを実行して開発用サーバを起動してブラウザからLaravelに接続しユーザ登録画面が表示されるか確認します。

ユーザ登録画面
ユーザ登録画面

イベントを使わない場合

ユーザ登録後に行うメール送信とメルマガ登録を行うコードを追加します。まずはイベントを使わない場合で作成し、動作確認を行います。

ユーザの登録の処理はApp¥Http¥Controllers¥Auth¥RegisterController.phpファイル内のcreateメソッドで行われています。メール送信とメルマガ登録処理はcreateメソッド内のユーザ登録処理の後に追加します。


// 中略 
use Mail;
use App\Mail\WelcomeMail;
use App\MailMagazine;

// 中略 

protected function create(array $data)
{
    $user = User::create([
        'name' => $data['name'],
        'email' => $data['email'],
        'password' => Hash::make($data['password']),
    ]);

    Mail::to($user->email)->send(new WelcomeMail());

    MailMagazine::subscribe($user->email);

    return $user;

}

実際にユーザ登録画面からユーザの登録を行うとメールが送信され、MailMagazineテーブルに登録を行ったユーザのemailアドレスが登録されます。

イベントを使用した場合

イベントとリスナーを利用して、メール送信とメルマガ登録の処理を行います。イベント、リスナーの作成とEventServiceProviderへの登録が必要になります。

先にEventServiceProviderでイベントとリスナーを登録することで一括でイベントとリスナーファイルを作成することができますが、ここでは個別にファイルを作成し、作成後にEventServiceProviderへの登録を行います。一括作成方法は後半の章の”その他”の中で記述しています。
fukidashi

イベントの作成

php artisan make:eventコマンドを使ってイベントを作成します。ユーザの登録後のイベントなので、名前はUserRegisteredとします。


$ php artisan make:event UserRegistered
Event created successfully.

実行後、ファイルはapp¥Eventsディレクトリの下に作成されるので、下記のように更新します。


// 中略 
use App\User;
// 中略 

class UserRegistered
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }

}

リスナーの作成

リスナーはphp artisan make:listenerコマンドで作成します。–eventオプションで対応するイベント名を指定すると作成したファイルにEventの情報が入力された状態で作成されます。–eventは必須ではありません。

Welcomeメール用のリスナーとメールマガジン登録用のリスナーの2つを作成します。


$ php artisan make:listener SendWelcomeEmail --event=UserRegistered
Listener created successfully.
$ php artisan make:listener RegisterMailMagazine --event=UserRegistered
Listener created successfully.
お互いに依存しない処理毎にリスナーを作成するため、場合によってはかなりの数のリスナーを作成することになります。例えばSlack送信を追加したい場合はSlack送信用のリスナーを追加することになります。
fukidashi

リスナーファイルはapp¥Listenersディレクトリの下に作成され、各ファイルに実行する処理を記述します。

SendWelcomeMail.phpファイルのhandleメソッドにメール送信の処理を記述します。


namespace App\Listeners;

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

use Mail;
use App\Mail\WelcomeMail;

class SendWelcomeEmail
{

    public function __construct()
    {
        //
    }

    public function handle(UserRegistered $event)
    {
        Mail::to($event->user->email)->send(new WelcomeMail());
    }

}

RegisterMailMagazine.phpファイルのhandleメソッドにメールマガジン登録の処理を記述します。


namespace App\Listeners;

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

use App\MailMagazine;

class RegisterMailMagazine
{

    public function __construct()
    {
        //
    }

    public function handle(UserRegistered $event)
    {
        MailMagazine::subscribe($event->user->email);
    }
    
}

イベントとリスナーの登録

イベントとリスナーの作成が完了したら、EventServiceProviderに作成したイベントとリスナーの情報を登録する必要があります。EventServiceProviderに登録することでイベントとリスナーの関連付けを行うことができます。関連付けを行わなければイベントが発生してもそのイベントを検知するリスナーがないため何も処理が行われません。

バージョン5.8.9からはリスナーを自動で見つけてくれる機能が追加されました。説明は本文書の”その他”で行っています。

$listen変数に作成したイベントとリスナーを下記のように追加します。イベントに関連付けするリスナーは配列でいくつでも設定することができます。新たにリスナーを追加した場合は$listenに追加する必要があります。


namespace App\Providers;

use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

use App\Events\UserRegistered;
use App\Listeners\SendWelcomeEmail;
use App\Listeners\RegisterMailMagazine;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [       
        UserRegistered::class => [
            SendWelcomeEmail::class,
            RegisterMailMagazine::class,
        ],
    ];

    public function boot()
    {
        parent::boot();

        //
    }
}

eventの設定

イベントとリスナーの作成、EventServiceProviderへの登録が完了したので、RegisterController.phpのcreateメソッドの中でユーザ登録が完了した後にeventヘルパー関数を使ってイベントを追加します。


// 中略 
use App\Events\UserRegistered;
// 中略 

protected function create(array $data)
{
    $user = User::create([
        'name' => $data['name'],
        'email' => $data['email'],
        'password' => Hash::make($data['password']),
    ]);

    event(new UserRegistered($user));

    return $user;

}

ブラウザからユーザの登録を行うとユーザ登録の処理の後にUserRegisteredイベントが発生して、SendWelcomeEmailとRegisterMailMagazineリスナーの処理が実行されます。

ユーザ登録後の処理の追加

ここまでの動作確認でイベントを利用しない場合でも利用する場合でも同じ処理が行えることがわかりました。

メール送信とメルマガ登録の処理コードが短いため、あまりイベントの利点が感じられないかもしれません。しかし、運用後にSlackで管理者にメッセージを送る必要が出た場合を考えてみることでイベントの利点を確認していきましょう。

Slackでのメッセージ送信はLaravelのNotificationを利用して行います。

Slack Notificationの設定

ここではSlackの設定を行っていますが、イベント、リスナーを使用しない場合でもここで行うSlackの設定方法は同じです。

Slack経由でメッセージを送信するためには、Slack側で事前にWebhook URLを取得する必要があります。

LaravelでSlack Notificationを利用するためにcomposerを利用して、slack-notification-channelパッケージをインストールします。


 $ composer require laravel/slack-notification-channel
Using version ^2.0 for laravel/slack-notification-channel

php artisan make:notificationコマンドでNotification RegisterUserSlackを作成します。 RegisterUserSlackファイルの中では、通知方法の設定とSlackで送信したいメッセージを記述します。


$ php artisan make:notification RegisterUserSlack
Notification created successfully.
Notificationは通知という意味があり、Laravelではメール, SMS, Slackなどを利用してユーザへの通知を行うことができます。通知をデータベースに保存することも可能です。通知なので、ちょっとしたメッセージを送る時に使用します。
fukidashi

実行後はapp¥Notificationsディレクトリの下にファイルが作成されます。

デフォルトではviaメソッドでNotificationはデフォルトではMailに設定されているので、MailからSlackに変更します。


public function via($notifiable)
{
    return ['slack'];
}

Slackメッセージを送信するためのtoSlackメソッドを追加します。


use Illuminate\Notifications\Messages\SlackMessage;
// 中略
public function via($notifiable)
{
    return ['slack'];
}

public function toSlack($notifiable){

     return (new SlackMessage)
                ->success()
                ->content('新規ユーザが登録されました。');

}
SlackMessageクラスを利用していますが、slack-notification-channelがインストールされていない場合はSlackMessageクラスは存在しないので利用することはできません。
fukidashi

User.phpファイルに事前に取得したWebhook URLを設定します。User.phpファイルにrouteNotificationForSlackメソッドを追加します。


public function routeNotificationForSlack($notification)
{
    return 'https://hooks.slack.com/services/...';
}

これでSlackの設定は完了です。

Slack用リスナーの追加作成

ユーザ登録後にSlackで通知を送るためには、新規にSlack用のリスナーを追加する必要があります。


$ php artisan make:listener RegisterUserNotification
Listener created successfully.

作成したRegisterUserNotification.phpファイルのhandleメソッドにNotificationの処理を追加します。


public function handle($event)
{
    $event->user->nofity(new RegisterUserSlack());
}

最後にEventServiceProvider.phpファイルに作成したリスナーを追加します。


protected $listen = [       
    UserRegistered::class => [
        SendWelcomeEmail::class,
        RegisterMailMagazine::class,
        RegisterUserNotification::class,
    ],
];

ユーザ登録を行うとメール送信、メールマガジンへの追加、管理者へのSlack通知を行うことができるようになりました。このようにイベントを利用するとメインのRegisterControllerファイルに変更を行うことなく、処理の追加を行うことができます。不必要になった処理はEventServiceProvider.phpファイルからリスナー情報を削除するだけなのでメンテナンスも楽になります。また、処理の中身の変更が行った場合も処理が独立しているので、他の処理への影響を心配することなく行うことができます。

その他

イベント、リスナーファイル同時作成

EventServiceProviderに事前にイベントとリスナーの情報を記述しておけば、php event:generateコマンドで一括でイベントとリスナーのファイルの作成することができます。

EventServiceProvider.phpを開いて、イベントとリスナーの名前を入力してください。この時名前空間も下記のように入力してください。


protected $listen = [             
    'App\Events\OrderReceived' => [
        'App\Listeners\SendThankEmail',
        'App\Listeners\SendOrderEmailToAdmin',
    ],
];

php artisan event:generateコマンドを実行するとapp¥Eventsの下にOrderReceived、app¥ListenersにSendThankEmailとSendOrderEmailToAdminファイルが保存されます。


$ php artisan event:generate
Events and listeners generated successfully!

モデルイベント

Laravelでは、createdやdeletedなどモデル対する処理にイベントを組み込むことができます。ここまではユーザ登録後にeventヘルパー関数を使ってイベントを発生させ処理を行っていましたが、ユーザ作成後にイベントを発生させることができます。発生させたいイベントはモデルファイルに追加します。

ここまではRegisterController.phpに下記を記述してeventを発生させていました。


event(new UserRegistered($user));

モデルイベントを利用するとUser.phpファイルにイベントを設定することができます。$dispatchesEvent変数を使ってイベントを発生させるポイントをイベントを設定します。

イベントやリスナーの作成、EventServeProvider.phpへの追加はこれまでと同様に行う必要があります。


namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

use App\Events\UserRegistered;

class User extends Authenticatable
{
    use Notifiable;

    protected $dispatchesEvents = [
        'created' => UserRegistered::class,
    ];

モデルイベントを使用するとeventを設定したRegisterController.phpファイルの変更は必要ありません。

モデルイベントはretrieved, creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restoredで組み込むことができます。

リスナー検知(Event Discovery)

Laravelのバージョン5.8.9以降ではEventServiceProvider.phpでイベントとリスナーの関連付けを行わなくても自動でリスナーを見つけてくれる機能が追加されました。

EventServiceProviderにshouldDiscoverEventsメソッドを追加しtrueを設定するだけです。


public function shouldDiscoverEvents()
{
    return true;
}

仕組みはapp¥Listenersディレクトリの下のリスナーファイルを確認し、handleメソッドの引数にあるイベントクラスを確認することで実現しています。下記のリスナーファイルのhandleメソッドのUserRegisteredのイベントクラスを確認します。


public function handle(UserRegistered $event)
{
    MailMagazine::subscribe($event->user->email);
}