【Laravel】データベース通知を実装する

データベース通知とは

  • 通知の内容 (通知種別や通知内容を含むJSONデータ)をデータベースに保存する
  • 上記データを使用して、Webアプリ上で通知を表示することが可能
  • よくあるWebサイトのヘッダーのベル通知などに使用できて便利

以下、実装手順を記載してく

1. 通知データを保存するテーブルを作成する

  • Laravelが用意してくれてるコマンドを利用して通知テーブルを作成する
php artisan notifications:table
php artisan migrate

これで notificationsテーブルが作成された
構成内容はこんな感じ

カラム内容
iduuidユニークID
typevarchar通知の種別
notifiable_typevarchar通知対象のモデル
notifiable_idbigint通知対象のモデルのレコードID
datatext通知内容 (json)
read_attimestamp既読日時

上記に加え、従来の$timestampsカラムも完備
notifiable_typeとnotifiable_idはmorphsで自動作成される

実際入るデータはこんな感じ

notifiable_typeに登録されているモデルのデータうち、
notifiable_idと同一IDのレコードに対して、dataの内容を通知する仕組み

またread_atの値がnullかどうかで未読・既読の判断が可能

2. 通知クラスを作成する

通知テーブルに保存するためには、通知クラスtoArrayメソッドを定義する必要がある
ここで返す配列がJSONエンコードされてnortificatonsテーブルのdataカラムに保存される

  • 実装例
public function toArray($notifiable)
{
    return [
        'invoice_id' => $this->invoice->id,
        'amount' => $this->invoice->amount,
    ];
}

toArrayメソッドではなくtoDatabaseメソッドでも可

toArrayメソッドは「databaseチャンネル」と「broadcastチャンネル」両方に使用され、toDatabaseメソッドは「databaseチャンネル」でのみ使用される

databasebroadcastでデータの内容を分けたい場合は
toDatabaseのほうを使用すると良い感じ

  • さっそく通知クラスを作成する
    今回は、お知らせ (日付・タイトル・内容) の通知クラスを作成してみる
php artisan make:notification InformationNotification
  • app/Notifications/InformationNotification.php
<?php

namespace App\Notifications;

use App\Models\Information;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;

class InformationNotification extends Notification
{
    use Queueable;

    private Information $information;

    /**
     * Create a new notification instance.
     *
     * @param Information $information
     */
    public function __construct(Information $information)
    {
        $this->information = $information;
    }

    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return ['database'];
    }

    /**
     * Get the array representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function toArray($notifiable)
    {
        return [
            'date' => $this->information->date,
            'title' => $this->information->title,
            'content' => $this->information->content,
             // ⭐️ 通知からリンクしたいURLがあれば設定しておくと便利
             'url' => route('infos.show', ['information' => $this->information])
        ];
    }
変更内容

コマンドで自動生成されたデフォルトの通知クラスから変更した点はざっくり下記

  • __construct()Informationモデル (=お知らせ) を受け取る
  • via() では ['database'] を返す
  • メール通知は使用しないのでtoMail()を削除
  • toArray() メソッドで通知に使用したいデータを配列で返す

3. 通知を行う (notificationsテーブルへ保存)

続いて、作成した通知クラスを使用して通知テーブルに登録してみる
通知を行う方法は2つある

  1. Notifiableトレイトを使用する
    $model->notify(new 通知クラス)
  2. Notificationファサードを使用する
    Notification::send(モデルのCollection, new 通知クラス))

どちらでも大丈夫だけど、
単独のユーザーに送る場合はNotifiableトレイト
複数のユーザーに送る場合はNotificationファサードが便利なので
2通り実装してみる

1. 単独のユーザーに通知する (Notifiableトレイト使用)

まず、通知を行うモデルでNotifiableトレイトをインポートしているか確認する

<?php

namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; // ⭐️ ここ

class User extends Authenticatable
{
    use Notifiable; // ⭐️ ここ
}

続いて、登録部分の実装

  • InformationController
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Notifications\InformationNotification;

class InformationController extends Controller
{
    public function store(Request $request)
    {
        // お知らせテーブルへ登録
        $information = Information::create([
            'date' => $request->get('date'),
            'title' => $request->get('title'),
            'content' => $request->get('content'),
        ]);

        // お知らせ内容を対象ユーザー宛てに通知登録
        $user = User::find($request->get('user_id'));
        $user->notify(
            new InformationNotification($information)
        );
    }
}

通知するモデルクラスでNotifiableトレイトuseしておかないと、
$model->notify()を実行してもそんなメソッドないですよと怒られる

デフォルトのUserモデルではすでに記載済みだが、
新しく作成したモデルなどでは注意が必要

2. 複数のユーザーに通知する (Notificationファサード使用)

  • InformationController
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Notifications\InformationNotification;
use Illuminate\Support\Facades\Notification;

class InformationController extends Controller
{
    public function store(Request $request)
    {
        // お知らせテーブルへ登録
        $information = Information::create([
            'date' => $request->get('date'),
            'title' => $request->get('title'),
            'content' => $request->get('content'),
        ]);

        // お知らせ内容を全ユーザー宛てに通知登録
        $users = User::all();
        Notification::send($users, new InformationNotification($information));
    }
}

これでDBを確認してみると、登録されていることを確認

補足

$model->notify(new 通知クラス)Notification::send(モデルのCollection, new 通知クラス))実行により、対象モデル宛てへの通知レコードがnotificationsテーブルに作成される

  • notifications.notifiable_type = $modelのEloquentクラスの場所
  • notifications.notifiable_id = $model->id

4. 登録した通知を取得 & 表示する

登録した通知をWeb上でユーザーに伝えるためには、まず通知を取得する必要がある
これも、前述のNotificationトレイトを使用することで簡単に取得することができる

  • まず、通知対象のモデルでNotificationトレイトをuseする
    (※ 既にしている場合は不要)
<?php

namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; // ⭐️ ここ

class User extends Authenticatable
{
    use Notifiable; // ⭐️ ここ
}

  • Notificationトレイトが提供するnotificationsリレーションを使って取得する
// ユーザーはとりあえず決めうち
$user = User::find(1);

// ⭐️ 全通知を取得
foreach ($user->notifications as $notification) {
    echo $notification->type;
}

// ⭐️ 未読の通知のみを取得
foreach ($user->unreadNotifications as $notification) {
    echo $notification->type;
}

上記のようにNotificationトレイトは、
対象のモデルに紐づく通知データを取得するための
各種notificationsリレーションを用意してくれてる
これにより、従来のリレーションと同様の方法で通知へのアクセスが可能になる

  • viewで表示する場合はこんな感じ
    ( Controller側であらかじめ変数に入れてviewに渡してあげてももちろんOK )
<div class="notifications">
    @forelse(auth()->user()->notifications()->take(5)->get() as $notification)
        <div class="{{ is_null($notification->read_at) ? 'un-read' : '' }}">
            <p>{{ $notification->data['date'] }}</p>
            <p>{{ $notification->data['title'] }}</p>
            <p>{{ $notification->data['content'] }}</p>
        </div>
    @empty
        <p>まだ通知はありません</p>
    @endforelse
</div>

リレーションなので、上記のようにtake()などで取得件数指定などもできて便利
また、notifications.dataのjsonには配列でアクセスできるようにしてくれてるので、
$notification->data['key名']で取得可能になってる

5. 通知を既読にする

ユーザーが通知を確認すると、その通知は既読としてマークする必要がある
今回は呼び出し元のルーティングは省略してメソッドだけ記載する
1つのみ既読にする場合と、ユーザーに紐づく全ての通知を既読にする場合の2種類

  • NotificationController
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Notifications\DatabaseNotification;

class NotificationController extends Controller
{
    /**
     * 通知を既読にする
     *
     * @param DatabaseNotification $notification
     * @return \Illuminate\Http\RedirectResponse
     */
    public function read(DatabaseNotification $notification)
    {
        $notification->markAsRead();

        return redirect($notification->data['url']);
    }

    /**
     * 全ての通知を既読にする
     *
     * @param DatabaseNotification $notification
     * @return \Illuminate\Http\RedirectResponse
     */
    public function readAll(DatabaseNotification $notification)
    {
        auth()->user()->unreadNotifications->markAsRead();

        return redirect(route('notifications.index'));
    }
}

  • read()メソッド:
    既読にする対象の通知モデルを引数に受け取って更新後、通知に設定されているURLにリダイレクトする
    通知一覧から任意の通知を押下された時などにpostで呼び出すと良き
  • readAll() メソッド:
    ログインユーザーに紐づく通知を全て既読に更新する
    通知一覧などに「全て既読にする」などのボタンを設置して押下時にpostで呼び出すと良き

  • routes/web.php
Route::post('/notifications/{notification}/read', [App\Http\Controllers\NotificationController::class, 'read'])->name('notifications.read');
Route::post('/notifications/read-all', [App\Http\Controllers\NotificationController::class, 'readAll'])->name('notifications.read-all');

  • viewでリンク設定
<div class="notifications">
    @forelse(auth()->user()->notifications()->take(5)->get() as $notification)
        <a href="{{ route('notifications.read', $notification) }}" class="{{ is_null($notification->read_at) ? 'un-read' : '' }}">
            <p>{{ $notification->data['date'] }}</p>
            <p>{{ $notification->data['title'] }}</p>
            <p>{{ $notification->data['content'] }}</p>
        </a>
    @empty
        <p>まだ通知はありません</p>
    @endforelse
    <a href="{{ route('notifications.read-all') }}">全て既読にする</a>
</div>

その他応用

  • notifications通知一覧をページネーションで取得する
public function index(Request $request)
{
    // 対象のページ番号取得
    $page =  $request->get('page', 1);
    // ページネーションで取得
    $notifications = auth()->user()->notifications()
                         ->paginate(20, ['*'], 'page', $page);

    return view('notifications.index', compact('notifications'));
}
  • notificationsテーブルのdataカラムのJSON値を対象にwhere検索する
public function index(Request $request)
{
    // 対象のページ番号取得
    $page =  $request->get('page', 1);
    // dataカラムをwhere検索
    $infoNotifications = auth()->user()->notifications()
            ->where('data->title', $request->title)
            ->get();

    return view('notifications.index', compact('notifications'));
}
  • 通知を削除する (ユーザー削除時などに)
$user->notifications()->delete();

\ 案件のご依頼・ご相談はこちらから /