Laravelのイベントとリスナを非同期で実行してみる

はじめに

イベントとリスナを実装するにあたって、具体例があった方がわかりやすいと思うので、今回は以下のような機能の実装を例にまとめていきたいと思います。

「画像レコード削除と同時に画像ファイルも削除する」

画像を扱うアプリケーションの場合、storageに画像ファイルを保存して、DBに画像のパスを保存する仕様が多いと思いますが、DBの方を削除すると同時に実データの画像ファイルも一緒に削除したい事があります。

今回はイベントとキューを使って画像ファイルを非同期で自動的に削除してみたいと思います。

前提

  • Laravel7
  • キューの実行環境ができている
  • 画像のパスを含むテーブルが既にある

方法

レコード削除時deletingというイベントが発生します。このイベントに独自のイベントを割り当てて、予め用意しておいた画像ファイル削除用クラス(リスナ)を呼び出します。この時、その場で実行せずキューへ登録します。

今回、キューについての詳細は省略します。(最後に簡単な実装方法あり)

イベントとリスナの関係

Laravelには、ある動作に対してイベントを発生させる仕組みがあります。そのイベントを受けて自動的に処理を実行するのがリスナです。

主な手順

  • オリジナルのイベントとリスナの組み合わせを作る
  • deletingイベント発生時に独自イベントを実行するようにする

イベント&リスナの作成

イベントとリスナをそれぞれ作っていきます。

イベント名をImageDeleted
リスナ名をDeleteImageFile

と仮定します。

EventServiceProviderにイベントとリスナを登録

まず、EventServiceProviderに以下のように登録します。

app/Providers/EventServiceProvider.php
protected $listen = [
    Registered::class => [
        SendEmailVerificationNotification::class,
    ],
    // これを追記
    'App\Events\ImageDeleted' => [         // ←イベント
        'App\Listeners\DeleteImageFile',    // ←リスナ
    ],
];

作成コマンド実行

以下のコマンドを実行すると、app/Eventsapp/Listenersにそれぞれファイルが作成されます。

$ php artisan event:generate

イベントファイルの編集

できたファイルを編集していきます。

対象テーブルのモデル名がImage
画像のパスのカラム名がImage_path

と仮定します。

app/Events/ImageDeleted.php
<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use App\Image; // ← 追記

class ImageDeleted
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $image_path; // ← 追記

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Image $image) // ← 追記
    {
        $this->image_path = $image->image_path; // ← 追記
    }

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

$imageから画像のパスを抜き出して$image_pathに入れておきます。(ここ重要)

通常は、$imageをそのまま$imageへ受け渡すだけでOKです。が、今回の場合、キューで処理する事と対象レコード自体が削除されるので、$imageをそのまま受け渡すと、キュー実行時「ModelNotFoundException」エラーが発生します。多分、実行時にはすでに対象レコードがないから?でしょうか。詳細は不明。

リスナファイルの編集

Storageファサードを使うのでuseで読み込みます。

implements ShouldQueue を追記します。(これがキューへ登録するための記述です。書かない場合は同期処理になります)

handle関数内に削除処理を書きます。引数の$eventにイベントで定義した変数(画像パス)が入っています。

app/Listeners/DeleteImageFile.php
<?php

namespace App\Listeners;

use App\Events\ImageDeleted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Storage; // ← 追記

class DeleteImageFile  implements ShouldQueue // ← 追記
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  ImageDeleted  $event
     * @return void
     */
    public function handle(ImageDeleted $event)
    {
        Storage::delete($event->image_path); // ← 削除処理
    }
}

‘image_path’には、storage/app以下のパスが入っているものとします。

削除イベントとの連携

イベントとリスナの組み合わせができたので、次に、deletingイベントが発生したら先程作ったイベントを実行するようにします。

Imageモデルに次のように追記します。

app/Image.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use App\Events\ImageDeleted; // ← 追記

class Image extends Model
{
    // これを追記
    protected $dispatchesEvents = [
        'deleting' => ImageDeleted::class,
    ];
}

確認

以上でレコードの削除と同時に画像ファイルが削除されるはずです。

キューが実行待機状態になっている事を確認してから、レコードの削除をしてみてください。

$images = Image::where('id', 1)->get();

foreach ($images as $image) {
    $image->delete();
}

注意点

レコードの削除を実行する場合、delete()の前にget()をしないとdeletingイベントが発生しません。削除するレコードのIDがわかっている場合は、delete()ではなくdestroy()を使えば常にイベントが発生します。

Image::where('id', 1)->delete(); // イベントが発生しない

まとめ

イベントとリスナを実装する具体例として「レコード削除と同時に画像ファイルを非同期で削除する」という少し特殊な場合を例にしてしまった為、わかりにくい部分もあったかもしれませんが、逆に理解が深まった部分もあったのではないでしょうか。

今回はキューの説明を含めると更に複雑になるので省略しましたが、最後に簡単な実装コマンドだけ載せておきます。

キューの実装方法

jobsテーブルを作る

$ php artisan queue:table
$ php artisan migrate

.env の以下の箇所を編集

QUEUE_CONNECTION=sync
↓
QUEUE_CONNECTION=database

.envのキャッシュをクリア

$ php artisan config:clear

キューを実行

$ php artisan queue:work