【PHP】Laravelで通知(Notifications)を実装する方法

対象バージョン: Laravel 13.x / PHP 8.3+

Laravel の通知(Notifications)を使うと、「請求書の支払いが完了した」といった短い知らせを、メールやデータベースなど複数のチャネルへ一つのクラスから届けられます。この記事では、通知クラスの作成からメールとデータベースへの送信、既読管理、テストまでを順に説明します。新規プロジェクトの初期設定のまま試せます。

この記事で分かること

  • php artisan make:notification で通知クラスを作る方法と、生成直後のコードの注意点
  • notify() と Notification::send() でメール・データベースへ通知する方法
  • データベース通知の保存内容と、未読・既読の扱い
  • 保存されていない宛先へ送るオンデマンド通知
  • Notification::fake() を使ったテスト

前提

Laravel 13.x は PHP 8.3 以上が必要です。動作確認は laravel/framework v13.35.0 で行いました。インストールやルーティングの基本は【PHP】Laravel入門:インストールから簡単なサンプルアプリケーションの作成を参照してください。

メールそのものの設定や Mailable の作り方は【PHP】LaravelでMailableを使ってメールを送信する方法で扱っています。この記事では通知に絞って説明します。

通知クラスを作成する

次のコマンドで通知クラスを作ります。

php artisan make:notification InvoicePaid

app/Notifications/InvoicePaid.php が作られます。生成直後のコードは次のとおりです(コメントは省略)。

<?php

namespace App\Notifications;

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

class InvoicePaid extends Notification
{
    use Queueable;

    public function __construct()
    {
        //
    }

    public function via(object $notifiable): array
    {
        return ['mail'];
    }

    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!');
    }

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

生成直後のコードには、次の点に注意してください。

  • via() が返すのは ['mail'] だけです。データベースにも保存するには 'database' を自分で追加します。
  • データベース用のメソッドは toDatabase() ではなく toArray() が用意されています。
  • ShouldQueue は use 文で読み込まれているだけで、クラスには実装されていません。このままでは通知はキューに入らず、その場で送信されます。

通知の内容を定義する

請求書番号と金額を受け取り、メールとデータベースの両方へ通知するように書き換えます。

<?php

namespace App\Notifications;

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

class InvoicePaid extends Notification
{
    use Queueable;

    public function __construct(public int $invoiceId, public int $amount)
    {
    }

    public function via(object $notifiable): array
    {
        return ['mail', 'database'];
    }

    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            ->greeting('Hello '.$notifiable->name.'!')
            ->line('Invoice #'.$this->invoiceId.' has been paid.')
            ->action('View Invoice', url('/invoices/'.$this->invoiceId))
            ->line('Thank you for using our application!');
    }

    public function toArray(object $notifiable): array
    {
        return [
            'invoice_id' => $this->invoiceId,
            'amount' => $this->amount,
        ];
    }
}
  • via() は通知を届けるチャネルの一覧です。['mail', 'database'] にすると、1回の通知でメール1通とデータベースの1行が作られます。
  • toMail() は MailMessage を返します。greeting()、line()、action() で挨拶・本文・ボタンを組み立てると、既定の HTML テンプレートで表示されます。
  • subject() を指定しない場合、件名はクラス名を単語に分けた形(InvoicePaid なら Invoice Paid)になります。
  • toArray() の戻り値が、データベースに保存される内容です。

通知を送信する

既定の App\Models\User は Notifiable トレイトを使っているため、そのまま notify() を呼べます。

use App\Models\User;
use App\Notifications\InvoicePaid;
use Illuminate\Support\Facades\Notification;

// 1人に送る
$user = User::find(1);
$user->notify(new InvoicePaid(1001, 5000));

// 複数人にまとめて送る
$users = User::all();
Notification::send($users, new InvoicePaid(1002, 3000));

新規プロジェクトは .env が MAIL_MAILER=log になっているため、メールは実際には送信されず、To: や Subject: を含む内容が storage/logs/laravel.log に記録されます。log メーラーの読み方はMailable の記事を参照してください。

メール通知をブラウザでプレビューする

toMail() の戻り値をルートから返すと、メールの HTML をブラウザで確認できます。

// routes/web.php
use App\Models\User;
use App\Notifications\InvoicePaid;
use Illuminate\Support\Facades\Route;

Route::get('/notification-preview', function () {
    $user = User::firstOrCreate(
        ['email' => 'taro@example.com'],
        ['name' => 'Taro', 'password' => bcrypt('password')]
    );

    return (new InvoicePaid(1001, 5000))->toMail($user);
});

/notification-preview を開くと、挨拶・本文・ボタンが既定のテンプレートで表示されます。プレビュー用のルートなので、確認が済んだら削除してください。

データベース通知を使う

notifications テーブルを作る

データベースチャネルには notifications テーブルが必要です。次のコマンドでマイグレーションを作り、実行します。

php artisan make:notifications-table
php artisan migrate

生成されるマイグレーションの中身は次のとおりです。

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();
});

通知を受け取るモデルの主キーが UUID や ULID の場合は、morphs の代わりに uuidMorphs や ulidMorphs を使います。マイグレーションの基本は【PHP】Laravelでデータベースと連携する方法を参照してください。

保存される内容

$user->notify(new InvoicePaid(1001, 5000)) を実行すると、次のような行が保存されます。

カラム 値
type App\Notifications\InvoicePaid
notifiable_type / notifiable_id App\Models\User / ユーザーの ID
data {"invoice_id":1001,"amount":5000}
read_at NULL

data には toArray() の戻り値が JSON で保存され、取り出すときは配列として扱えます。

通知の取得と既読

// すべての通知(created_at の新しい順)
foreach ($user->notifications as $notification) {
    echo $notification->data['invoice_id'];
}

// 未読の通知だけ
$unread = $user->unreadNotifications;

// 既読にする
$notification = $user->unreadNotifications->first();
$notification->markAsRead();

markAsRead() を呼ぶと read_at に現在時刻が入り、その通知は unreadNotifications に含まれなくなります。新規プロジェクトのタイムゾーンは UTC なので、read_at も UTC で保存されます。画面に表示するときは必要に応じて変換してください。

オンデマンド通知

ユーザーとして保存されていない宛先には、Notification::route() で送れます。ただし、この記事の InvoicePaid は toMail() で $notifiable->name を使っているため、このままではオンデマンドで送れません(理由は後述)。次のコードは、name を参照しない通知クラスで使う形として示します。

use App\Notifications\InvoicePaid;
use Illuminate\Support\Facades\Notification;

Notification::route('mail', 'guest@example.com')
    ->notify(new InvoicePaid(1003, 100));

オンデマンド通知では次の点に注意してください。

  • データベースの行は作られません。via() に 'database' が含まれていても、notifications テーブルには保存されません。
  • $notifiable はユーザーモデルではなく Illuminate\Notifications\AnonymousNotifiable のインスタンスなので、name プロパティを持ちません。上の InvoicePaid のように $notifiable->name を使う通知をオンデマンドで送ると、通常のアプリケーション(HTTP リクエスト、キューワーカー、Artisan コマンド)では ErrorException(Undefined property: Illuminate\Notifications\AnonymousNotifiable::$name)が発生し、メールは送信されません。オンデマンドでも使う通知では、name を参照しないか、値の有無を確認してから使ってください。

通知をキューで送る

通知クラスに ShouldQueue を実装すると、送信がキューに入ります。

class InvoicePaid extends Notification implements ShouldQueue
{
    use Queueable;

    // ...
}

ジョブは宛先とチャネルの組み合わせごとに作られます。1人のユーザーに ['mail', 'database'] で送ると、ジョブは2件です。新規プロジェクトは QUEUE_CONNECTION=database なので、ワーカーを動かすまで送信されません。

php artisan queue:work --stop-when-empty

ワーカーの運用やキューの考え方は【PHP】Laravelでキューとジョブを実装する方法とその利用シーンを参照してください。

通知をテストする

Notification::fake() を呼ぶと、通知は実際には送られず、送信内容を検証できます。新規プロジェクトのテストは PHPUnit です。

<?php

namespace Tests\Feature;

use App\Models\User;
use App\Notifications\InvoicePaid;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;

class InvoicePaidNotificationTest extends TestCase
{
    use RefreshDatabase;

    public function test_invoice_paid_notification_is_sent(): void
    {
        Notification::fake();

        Notification::assertNothingSent();

        $user = User::factory()->create();
        $user->notify(new InvoicePaid(1001, 5000));

        Notification::assertSentTo($user, InvoicePaid::class);
        Notification::assertSentTo(
            $user,
            function (InvoicePaid $notification, array $channels) {
                return $notification->invoiceId === 1001
                    && $channels === ['mail', 'database'];
            }
        );
        Notification::assertCount(1);
    }

    public function test_on_demand_notification(): void
    {
        Notification::fake();

        Notification::route('mail', 'guest@example.com')
            ->notify(new InvoicePaid(1003, 100));

        Notification::assertSentOnDemand(InvoicePaid::class);
    }
}
  • test_on_demand_notification では、$notifiable->name を使う InvoicePaid をオンデマンドで送っていますが、テストは成功します。Notification::fake() は送信を記録するだけで toMail() を呼ばないため、メールが組み立てられないからです。実際にオンデマンドで送る場合は、前述のとおり name を参照しない通知クラスを使ってください。
  • assertSentTo() の第2引数にクロージャを渡すと、通知インスタンスとチャネルの一覧を受け取って中身を検証できます。
  • assertCount() は送信された通知の総数を検証します。

1回だけ送られたことを確認する(Laravel 13.31 以降)

laravel/framework v13.31.0 以降では、1回だけ送られたことを確認する assertSentToOnce() と assertSentOnDemandOnce() が使えます。v13.31.0 より前(v13.30.x 以前)にはありません。

Notification::assertSentToOnce($user, InvoicePaid::class);
Notification::assertSentOnDemandOnce(InvoicePaid::class);

それより前の 13.x では、assertSentTo() と assertCount(1) を組み合わせます。

Notification::assertSentTo($user, InvoicePaid::class);
Notification::assertCount(1);

データベース通知は、fake() を使わずに実際に保存して確認することもできます。

public function test_database_notification_is_stored_and_marked_as_read(): void
{
    $user = User::factory()->create();
    $user->notify(new InvoicePaid(1001, 5000));

    $this->assertCount(1, $user->unreadNotifications);
    $notification = $user->notifications->first();
    $this->assertSame(['invoice_id' => 1001, 'amount' => 5000], $notification->data);
    $this->assertNull($notification->read_at);

    $notification->markAsRead();

    $this->assertCount(0, $user->fresh()->unreadNotifications);
    $this->assertNotNull($notification->fresh()->read_at);
}

テストの基本は【PHP】Laravelでテストを実装する方法を参照してください。

php artisan test

まとめ

  • 通知クラスは php artisan make:notification で作る。生成直後の via() は ['mail'] だけで、ShouldQueue は実装されていない
  • via() に 'database' を加えると、メールとデータベースの両方に届く。保存内容は toArray() で定義する
  • 送信は $user->notify() または Notification::send()
  • データベース通知は notifications / unreadNotifications で取得し、markAsRead() で既読にする。read_at は UTC で保存される
  • Notification::route() によるオンデマンド通知では、データベースの行は作られない
  • テストは Notification::fake() と assertSentTo() 系で行う。assertSentToOnce() / assertSentOnDemandOnce() は Laravel 13.31 以降

関連記事

参考リンク

コメントする