見出し画像

inertia.js+react通知(2) - queueの完了後に通知

こんな記事もあったな、、、途中で投げだしてるやんか

laravel通知の準備

artisanコマンドで作る

php artisan notifications:table

すると

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('notifications', function (Blueprint $table) {
            $table->uuid('id')->primary();
            $table->string('type');
            $table->morphs('notifiable');
            $table->text('data');
            $table->timestamp('read_at')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('notifications');
    }
};

こんなmigrationができるけど、あんま気にしなくていいからとりあえず何らかの形でmigrateしてこれをDBにとりこんでおくこと。

sessionのインクリメンタルをやめてqueueに切り替える準備をする

これはDashboardにボタンを付けていて、今どうなってたかというと
resources/js/Pages/Dashboard.jsx

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { useLaravelReactI18n } from 'laravel-react-i18n';
import { Head, useForm } from '@inertiajs/react';
import PrimaryButton from '@/Components/PrimaryButton';

export default function Dashboard({ auth }) {
  const { post, processing} = useForm();
  const submit = (e) => {
    e.preventDefault();
    post(route('dashboard'));
  };
  const { t, currentLocale } = useLaravelReactI18n();

  return (
    <AuthenticatedLayout
      user={auth.user}
      header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">{t("Dashboard")}</h2>}
    >
      <Head title={t("Dashboard")} />

      <div className="py-12">
        <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
          <div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
            <div className="p-6 text-gray-900">{t("You're logged in!")}</div>


            <form onSubmit={submit}>
              <PrimaryButton className="ml-4" disabled={processing}>
                Click
              </PrimaryButton>
            </form>

          </div>
        </div>
      </div>
    </AuthenticatedLayout>
  );
}

となっていた。ここでdashboardルートにpostしてるんだけど

routes/web.php

use Illuminate\Http\Request;
Route::post('/dashboard', function (Request $request) {
    $notificationsCount = $request->session()->get('notificationsCount', 0);
    $request->session()->put('notificationsCount', $notificationsCount + 1);
    return redirect()->back();
})->middleware(['auth', 'verified'])->name('dashboard');

sessionのincrementではあまりにも意味がないのでこれをやめて、queueに変更してみよう。

jobを作る

ここではAnalyzeFileJobという名前で作ってみよう

artisan make:job AnalyzeFileJob

こんなのが出来る app/Jobs/AnalyzeFileJob.php 

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class AnalyzeFileJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        //
    }
}

ここでダミーのIDを__constructから格納させる

    protected $fileId;
    protected $userId;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($fileId, $userId)
    {
        $this->fileId = $fileId;
        $this->userId = $userId;
    }

handle()には適当にlogを送る。sleepで時間かかってるテイを出しとく

    public function handle(): void
    {
        sleep(10); // なんとなく10秒かかるような処理をシミュレート
        \Log::info("Called: fileId {$this->fileId} / User ID: {$this->userId}");
    }

とりあえずqueueに送る

では、やってみよう。これはroutes/web.php

Route::post('/dashboard', function (Request $request) {

 # ここ
})->middleware(['auth', 'verified'])->name('dashboard');

を改良するのだった。

use Illuminate\Http\Request;
use App\Jobs\AnalyzeFileJob;
Route::post('/dashboard', function (Request $request) {

    $fileId = 1;  // dummy
    $userId = $request->user()->id;
    AnalyzeFileJob::dispatch($fileId, $userId);
    return redirect()->back();

})->middleware(['auth', 'verified'])->name('dashboard');

このようにfileIdに1をダミーで詰め、送信している。実際にこのような処理を行う場合は対象となるfileIdが実際に入るんだろう。

で、このボタンは何度か押してもいいが、sleep(10)とか関係なくすぐ処理されるはずだ。これはまだ処理されておらず、実際の処理はqueue workerを起動するからである。要するに処理の重いやつを裏で処理させることでfrontからはすぐ断ち切ってしまうということである。

実際にやってみよう。

% ./vendor/bin/sail artisan queue:work

   INFO  Processing jobs from the [default] queue.

  2024-05-08 15:12:34 App\Jobs\AnalyzeFileJob .............................. RUNNING
  2024-05-08 15:12:44 App\Jobs\AnalyzeFileJob ............................. 10秒 DONE
  2024-05-08 15:12:44 App\Jobs\AnalyzeFileJob .............................. RUNNING
  2024-05-08 15:12:54 App\Jobs\AnalyzeFileJob ............................. 10秒 DONE
  2024-05-08 15:12:54 App\Jobs\AnalyzeFileJob .............................. RUNNING
  2024-05-08 15:13:04 App\Jobs\AnalyzeFileJob ............................. 10秒 DONE
  2024-05-08 15:13:04 App\Jobs\AnalyzeFileJob .............................. RUNNING
  2024-05-08 15:13:14 App\Jobs\AnalyzeFileJob ............................. 10秒 DONE

このように10秒かかって実行しきったのがわかる。logをみると

[2024-05-08 15:12:44] local.INFO: Called: fileId 1 / User ID: 1
[2024-05-08 15:12:54] local.INFO: Called: fileId 1 / User ID: 1
[2024-05-08 15:13:04] local.INFO: Called: fileId 1 / User ID: 1
[2024-05-08 15:13:14] local.INFO: Called: fileId 1 / User ID: 1

など出ており、うまくいっているようだ。

queueが成功したら通知を送る

ファイルの分析成功の通知なのでそれっぽい名前を付けておく

artisan make:notification FileAnalyzedNotification

そうすると、定型文のようなものが出力されてくる

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class FileAnalyzedNotification extends Notification
{
    use Queueable;

    /**
     * Create a new notification instance.
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the notification's delivery channels.
     *
     * @return array<int, string>
     */
    public function via(object $notifiable): array
    {
        return ['mail'];
    }

    /**
     * Get the mail representation of the notification.
     */
    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
                    ->line('The introduction to the notification.')
                    ->action('Notification Action', url('/'))
                    ->line('Thank you for using our application!');
    }

    /**
     * Get the array representation of the notification.
     *
     * @return array<string, mixed>
     */
    public function toArray(object $notifiable): array
    {
        return [
            //
        ];
    }
}

ここで通知ドライバーをvia()に書くのだが、mailまたはdatabaseあるいはその両方みたいな設定ができるんだけど、今回はdatabaseしか取り扱わない。

    /**
     * Get the notification's delivery channels.
     *
     * @return array<int, string>
     */
    public function via(object $notifiable): array
    {
        return ['database'];
    }

    /**
     * Get the mail representation of the notification.
     */
    /*
    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
                    ->line('The introduction to the notification.')
                    ->action('Notification Action', url('/'))
                    ->line('Thank you for using our application!');
    }
     */

toDatabase()を書く。toArray()との違いはここでは議論しない

    public function toDatabase(object $notifiable): array
    {
        return [
            //
        ];
    }

ここでのキーは何でもいいんだけど、複数の通知classから集約してトップで通知する場合に関しては共通キーが必要であり、ここではtitlemessageとする。そして当該のジャンプ先urlも指定する、ただ、ここではダミーとする。

    public function toDatabase(object $notifiable): array
    {
        return [
            'title' => 'ファイル分析完了',
            'message' => '',
            'url' => url('/'), // ダミーのジャンプ先
        ];
    }

ここでmessageに関してはとりあえず空とし、jobに戻って再度考えてみよう。

app/Jobs/AnalyzeFileJob.php に戻る

結局

    public function handle(): void
    {
        sleep(10);
        \Log::info("Called: fileId {$this->fileId} / User ID: {$this->userId}");
    }

ここんところのLogに書いてるものをそのまんまNotificationのconstructorに渡せばよいと思う。とりあえずはね。

use App\Notifications\FileAnalyzedNotification;
class AnalyzeFileJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $fileId;
    protected $userId;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($fileId, $userId)
    {
        $this->fileId = $fileId;
        $this->userId = $userId;
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        sleep(10);
        $message = "Called: fileId {$this->fileId} / User ID: {$this->userId}";
        $user = \App\Models\User::findOrFail($this->userId);

        if ($user) {
            $user->notify(new FileAnalyzedNotification($message));
        }
    }
}

ここでの注目点は

$user->notify(new FileAnalyzedNotification($message));

であり、このようにするとユーザーごとにnotificationが送られるようになっている。その実態の詳細が知りたければUserモデルとか見てみてね。Userクラスに関しては冒頭でuseしても、もちろんいい

app/Notifications/FileAnalyzedNotification.php に戻る

そしたら今constructormessageが渡ってきたので

class FileAnalyzedNotification extends Notification implements ShouldQueue
{
    use Queueable;

    protected $message;

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

    public function via($notifiable)
    {
        return ['database']; // 必要に応じて 'mail' も追加
    }

    public function toDatabase($notifiable)
    {
        return [
            'title' => 'ファイル処理完了',
            'message' => $this->message,
            'url' => url('/')
        ];
    }
}

を参考に改良すること。chatgptが吐いたのやつだ

実行してみよう!

さて、ここまでうまいこと組めれたら、Dashboardのボタンを3つくらい押せばまたまたqueueに貯まるので、またworkerを起動する

  2024-05-08 15:48:57 App\Jobs\AnalyzeFileJob .............................. RUNNING
  2024-05-08 15:49:07 App\Jobs\AnalyzeFileJob ............................. 10秒 DONE
  2024-05-08 15:49:16 App\Jobs\AnalyzeFileJob .............................. RUNNING
  2024-05-08 15:49:26 App\Jobs\AnalyzeFileJob ............................. 10秒 DONE
  2024-05-08 15:49:26 App\Jobs\AnalyzeFileJob .............................. RUNNING
  2024-05-08 15:49:36 App\Jobs\AnalyzeFileJob ............................. 10秒 DONE

などと正常に処理されたならばNotificationが届いているはずだ。失敗した場合はstorage/logs/laravel.logとかを見ること。

通知を確認する

tinkerを使ったらよい

> $u = User::find(1)
[!] Aliasing 'User' to 'App\Models\User' for this Tinker session.
= App\Models\User {#6407
    id: 1,
    name: "Admin User",
    email: "admin@example.com",
    email_verified_at: "2024-05-08 13:55:43",
    last_login_at: "2024-05-08 13:59:34",
    #password: "$2y$12$WBm8qcgFnFwfYQ9G4AwZyOAMJpXAqJJGZRtql949kQZj58cWESNYO",
    #remember_token: "fx3j0gPqdX",
    created_at: "2024-05-08 13:55:43",
    updated_at: "2024-05-08 13:59:34",
  }

> $n = $u->notifications;
= Illuminate\Notifications\DatabaseNotificationCollection {#6374
    all: [
      Illuminate\Notifications\DatabaseNotification {#6390
        id: "18877b90-3f3f-4004-b81d-8345886b60d9",
        type: "App\Notifications\FileAnalyzedNotification",
        notifiable_type: "App\Models\User",
        notifiable_id: 1,
        data: "{"title":"\u30d5\u30a1\u30a4\u30eb\u5206\u6790\u5b8c\u4e86","message":"Called: fileId 1 \/ User ID: 1","url":"http:\/\/localhost"}",
        read_at: null,
        created_at: "2024-05-08 15:49:36",
        updated_at: "2024-05-08 15:49:36",
      },
      Illuminate\Notifications\DatabaseNotification {#6391
        id: "79493bc4-cb11-4571-91c5-5a6818d42a2e",
        type: "App\Notifications\FileAnalyzedNotification",
        notifiable_type: "App\Models\User",
        notifiable_id: 1,
        data: "{"title":"\u30d5\u30a1\u30a4\u30eb\u5206\u6790\u5b8c\u4e86","message":"Called: fileId 1 \/ User ID: 1","url":"http:\/\/localhost"}",
        read_at: null,
        created_at: "2024-05-08 15:49:26",
        updated_at: "2024-05-08 15:49:26",
      },
      Illuminate\Notifications\DatabaseNotification {#6389
        id: "22639400-ff19-4f63-b541-46d348797aac",
        type: "App\Notifications\FileAnalyzedNotification",
        notifiable_type: "App\Models\User",
        notifiable_id: 1,
        data: "{"title":"\u30d5\u30a1\u30a4\u30eb\u5206\u6790\u5b8c\u4e86","message":"Called: fileId 1 \/ User ID: 1","url":"http:\/\/localhost"}",
        read_at: null,
        created_at: "2024-05-08 15:49:07",
        updated_at: "2024-05-08 15:49:07",
      },
    ],
  }

とまあこのように無事にjobから3つ、通知が送信されているものが受信できているはずだ。

次回は

この通知に関してベルのUIを完了させてみよう。





この記事が気に入ったらサポートをしてみませんか?