【実務完全ガイド】Laravelで作る通知センター――Database Notifications、既読管理、メール連携、リアルタイム通知、アクセシブルな通知UI
この記事で学べること(要点)
- Laravel Notifications を使ったアプリ内通知の基本設計
- Database Notifications による通知保存、一覧表示、既読・未読管理
- メール通知、アプリ内通知、リアルタイム通知をどう使い分けるか
- 通知の種類、重要度、通知設定、配信停止、集約の設計
- 通知センター画面、バッジ、トースト、空状態のアクセシブルなUI
- キュー、再試行、冪等性、監査ログ、テストの実務パターン
想定読者
- Laravel 初〜中級エンジニア:メール通知だけでなく、アプリ内通知や既読管理も作りたい方
- SaaS / 管理画面 / 業務システム開発者:ユーザーごとの通知センターを実装したい方
- テックリード:通知の種類が増えてきて、設計や運用を整理したい方
- デザイナー / QA / アクセシビリティ担当:通知バッジ、トースト、通知一覧を分かりやすく整えたい方
アクセシビリティレベル:★★★★★
通知は、利用者の注意を引くUIです。だからこそ、過剰な表示や自動更新で操作を妨げないこと、通知件数や状態を色だけで伝えないこと、role="status" や aria-live を適切に使うことが重要です。本記事では、通知を「見える人だけに届く情報」ではなく、「すべての利用者が理解できる状態変化」として設計します。
1. はじめに:通知は「知らせる」だけでなく「行動につなげる」ための機能です
Webアプリケーションでは、さまざまな場面で通知が必要になります。コメントが届いた、申請が承認された、請求書が発行された、エクスポートが完了した、セキュリティ設定が変更された、期限が近づいている。こうした情報を利用者に届けることで、次に取るべき行動が分かりやすくなります。
ただし、通知は増やしすぎると逆効果です。何でも通知すると、利用者は通知を見なくなります。重要な通知と軽い通知が同じ見え方だと、本当に必要な情報が埋もれます。さらに、トーストがすぐ消える、バッジの赤色だけで状態を示す、読み上げでは通知内容が分からない、といったUIは、アクセシビリティ上も課題になります。
Laravelには Notifications という強力な仕組みがあります。メール、データベース、Slack、Broadcastなど複数チャネルに対応し、通知を整理しやすい構造を作れます。本記事では、Laravelで通知センターを作るための基本から、既読管理、メール連携、リアルタイム通知、アクセシブルなUIまで、実務で使える形にまとめます。
2. 通知の種類を整理する:すべてを同じ通知にしない
通知センターを作る前に、まず通知の種類を整理します。通知は、重要度や目的によって扱いを変える必要があります。
2.1 情報通知
例:
- 新しいコメントが届いた
- レポート生成が完了した
- プロフィールが更新された
比較的軽い通知です。アプリ内通知だけで十分なことも多いです。
2.2 行動が必要な通知
例:
- 申請の承認が必要
- 支払い方法の更新が必要
- 招待への回答が必要
利用者に操作を求める通知です。通知一覧から詳細画面や操作画面へ移動できる導線が必要です。
2.3 重要・セキュリティ通知
例:
- パスワードが変更された
- 2段階認証が無効化された
- 不審なログインが検知された
メール通知も併用した方がよい通知です。アプリ内通知だけでは見逃される可能性があります。
2.4 定期・集約通知
例:
- 週次レポート
- 未対応タスクのまとめ
- コメントのダイジェスト
通知が多くなりすぎる場合、個別通知ではなく集約した方が親切です。
通知を設計するときは、「これはすぐ知らせるべきか」「メールも必要か」「一覧で見られればよいか」「行動が必要か」を決めておくと、通知疲れを防ぎやすくなります。
3. Laravel Notifications の基本
Laravelでは、通知クラスを作って、ユーザーなどの Notifiable モデルへ送信します。多くの場合、User モデルには Notifiable trait が含まれています。
通知クラスを作成します。
php artisan make:notification ReportGeneratedNotification
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class ReportGeneratedNotification extends Notification
{
use Queueable;
public function __construct(
public int $reportId,
public string $title
) {}
public function via(object $notifiable): array
{
return ['database'];
}
public function toArray(object $notifiable): array
{
return [
'type' => 'report.generated',
'report_id' => $this->reportId,
'title' => $this->title,
'message' => 'レポートの生成が完了しました。',
'url' => route('reports.show', $this->reportId),
];
}
}
送信側:
$user->notify(new ReportGeneratedNotification(
reportId: $report->id,
title: $report->title
));
この例では、通知を database チャネルに保存します。つまり、通知センターの一覧に表示できます。
4. Database Notifications の準備
アプリ内通知を保存するには、notifications テーブルを作成します。
php artisan notifications:table
php artisan migrate
これにより、notifications テーブルが作られます。通知データはJSONとして保存され、既読状態は read_at で管理されます。
ユーザーの通知一覧は、次のように取得できます。
$notifications = auth()->user()
->notifications()
->latest()
->paginate(20);
未読通知だけなら次のように取得します。
$unreadNotifications = auth()->user()
->unreadNotifications()
->latest()
->get();
既読にする場合:
$notification = auth()->user()
->notifications()
->whereKey($id)
->firstOrFail();
$notification->markAsRead();
すべて既読にする場合:
auth()->user()
->unreadNotifications
->markAsRead();
Database Notifications は、通知センターの基本として非常に扱いやすいです。
5. 通知データの設計:あとから表示しやすい形にする
通知の data には自由に情報を入れられます。ただし、何でも入れると表示側が混乱します。実務では、次のような構造を標準にすると扱いやすいです。
[
'type' => 'report.generated',
'title' => 'レポート生成完了',
'message' => '4月の売上レポートを確認できます。',
'url' => route('reports.show', $report->id),
'severity' => 'info',
'action_label' => 'レポートを確認する',
]
おすすめの項目は次の通りです。
type:通知の種類title:短い見出しmessage:本文url:詳細や操作へのリンクseverity:info / success / warning / danger などaction_label:リンクやボタンの文言meta:必要に応じた補足情報
このようにしておくと、通知一覧、トースト、メール通知などで再利用しやすくなります。
注意点として、通知データに機密情報を入れすぎないことが大切です。通知テーブルは後から表示される可能性があり、ログや管理画面でも扱われます。本文に個人情報や秘密情報を含める場合は、必要最小限にしましょう。
6. 通知センター画面:一覧、既読状態、空状態を整える
通知センターは、ただ通知を並べるだけではなく、状態が分かるように設計します。
<section aria-labelledby="notifications-title">
<h1 id="notifications-title" class="text-2xl font-semibold">
通知
</h1>
<p role="status" aria-live="polite" class="mt-2 text-sm">
未読通知が {{ auth()->user()->unreadNotifications()->count() }} 件あります。
</p>
@if($notifications->isEmpty())
<div class="border rounded p-4 mt-6">
<h2 class="text-lg font-semibold">通知はありません</h2>
<p class="mt-2">
新しいお知らせが届くと、この画面に表示されます。
</p>
</div>
@else
<ul class="mt-6 divide-y">
@foreach($notifications as $notification)
@php
$data = $notification->data;
$isUnread = is_null($notification->read_at);
@endphp
<li class="py-4">
<article aria-labelledby="notification-{{ $notification->id }}-title">
<div class="flex items-start justify-between gap-4">
<div>
<h2 id="notification-{{ $notification->id }}-title" class="font-semibold">
{{ $data['title'] ?? '通知' }}
@if($isUnread)
<span class="ml-2 rounded bg-blue-100 px-2 py-1 text-sm text-blue-900">
未読
</span>
@endif
</h2>
<p class="mt-1">
{{ $data['message'] ?? '' }}
</p>
<p class="mt-1 text-sm text-gray-600">
{{ $notification->created_at->diffForHumans() }}
</p>
</div>
@if(!empty($data['url']))
<a href="{{ $data['url'] }}" class="underline">
{{ $data['action_label'] ?? '詳細を見る' }}
</a>
@endif
</div>
</article>
</li>
@endforeach
</ul>
<div class="mt-6">
{{ $notifications->links() }}
</div>
@endif
</section>
この例では、未読状態を青色だけでなく「未読」という文字で表示しています。色だけに頼らないことが大切です。
また、空状態でも「通知はありません」だけで終わらせず、今後どこに表示されるかを説明しています。
7. 既読管理:自動既読と手動既読を分けて考える
通知の既読管理には、大きく2つの考え方があります。
7.1 表示したら既読にする
通知詳細を開いたり、通知リンクをクリックしたタイミングで既読にします。
これは自然ですが、一覧を表示しただけで既読にするのは少し注意が必要です。利用者が本当に読んだとは限らないためです。
7.2 手動で既読にする
「既読にする」ボタンや「すべて既読にする」操作を用意します。
利用者が自分で状態を管理しやすい反面、操作が少し増えます。
実務では、通知リンクを開いたときに既読化し、必要に応じて「すべて既読」も用意するのが扱いやすいです。
Route::patch('/notifications/{id}/read', function (string $id) {
$notification = auth()->user()
->notifications()
->whereKey($id)
->firstOrFail();
$notification->markAsRead();
return back()->with('status', '通知を既読にしました。');
})->name('notifications.read');
UI側:
<form method="POST" action="{{ route('notifications.read', $notification->id) }}">
@csrf
@method('PATCH')
<button type="submit" class="underline">
既読にする
</button>
</form>
既読化後は role="status" で結果を知らせると親切です。
8. 通知バッジ:数字とラベルで状態を伝える
ヘッダーやサイドバーに通知バッジを表示する場合、赤い丸だけでは意味が伝わりません。数字とテキストを含めます。
<a href="{{ route('notifications.index') }}" class="relative inline-flex items-center">
<span>通知</span>
@if($unreadCount > 0)
<span class="ml-2 rounded-full bg-red-600 px-2 py-1 text-sm text-white">
{{ $unreadCount }}
</span>
<span class="sr-only">
未読通知が {{ $unreadCount }} 件あります
</span>
@endif
</a>
見える人には数字で、スクリーンリーダー利用者には sr-only の説明で伝えます。
「赤い丸があるから分かる」ではなく、「未読通知が何件あるか」をテキストで伝えることが重要です。
9. トースト通知:使いすぎず、消え方に注意します
保存完了や軽い通知には、トーストUIを使うことがあります。便利ですが、アクセシビリティ上の注意点があります。
- すぐ消えすぎない
- マウスだけでなくキーボードでも閉じられる
- 重要な通知をトーストだけにしない
- 自動でフォーカスを奪わない
role="status"またはrole="alert"を適切に使う
成功通知の例:
@if(session('status'))
<div role="status" aria-live="polite" class="border rounded p-3 mb-4">
{{ session('status') }}
</div>
@endif
重大なエラーの場合:
@if(session('error'))
<div role="alert" class="border rounded p-3 mb-4">
{{ session('error') }}
</div>
@endif
基本的には、重要度の低い完了通知は role="status"、緊急性の高い失敗は role="alert" と考えると分かりやすいです。
ただし、role="alert" は読み上げを強く割り込ませることがあるため、乱用しないようにします。
10. メール通知との使い分け:重要度と行動必要性で判断します
同じ通知でも、アプリ内通知だけでよいものと、メールでも届けるべきものがあります。
アプリ内通知だけでよい例
- コメントが追加された
- レポート生成が完了した
- 軽微な設定変更が完了した
メールも送るべき例
- パスワードが変更された
- 請求書が発行された
- セキュリティ設定が変更された
- 招待が送られた
- 重要な承認依頼がある
Laravel Notifications では、via() でチャネルを切り替えられます。
public function via(object $notifiable): array
{
return ['database', 'mail'];
}
メール本文でも、通知センターと同じように、具体的な件名、短い本文、明確なリンク文言を使います。
たとえば「こちら」ではなく「請求書を確認する」のように、行動が分かる文言にします。
11. 通知設定:利用者に選べる余地を用意します
通知が増えてくると、利用者ごとに受け取り方を選べる設計が必要になります。特にメール通知は、不要な通知が多いと配信停止や不満につながります。
通知設定の例:
- アプリ内通知:常に受け取る
- メール通知:重要なものだけ
- ダイジェスト通知:毎日または毎週
- セキュリティ通知:停止不可
- マーケティング通知:任意で解除可能
DB設計の例:
notification_preferences
- user_id
- type
- channel
- enabled
通知送信前に設定を確認します。
if ($user->wantsNotification('report.generated', 'mail')) {
$user->notify(new ReportGeneratedNotification($report->id, $report->title));
}
すべてをユーザーに選ばせる必要はありませんが、通知が増えるほど、設定画面は重要になります。
12. 通知の集約:通知疲れを防ぎます
同じ種類の通知が短時間に何件も届くと、利用者は疲れてしまいます。
たとえば、コメントが10件追加された場合に10通の通知を送るより、「新しいコメントが10件あります」とまとめた方が親切なことがあります。
集約の考え方:
- 短時間の同種通知をまとめる
- ダイジェストとして定期通知する
- 未読件数だけをバッジで示す
- 一覧では詳細を確認できるようにする
実務では、最初から複雑な集約を作る必要はありません。通知が増え始めたら、ログや利用状況を見ながら、よく発生する通知から集約していくと安全です。
13. キュー化:通知送信は非同期を基本にします
メールや外部通知は、同期で送ると画面のレスポンスが遅くなります。通知クラスに Queueable を使い、必要に応じて ShouldQueue を実装します。
use Illuminate\Contracts\Queue\ShouldQueue;
class ReportGeneratedNotification extends Notification implements ShouldQueue
{
use Queueable;
// ...
}
これにより、通知送信をキューで処理できます。
運用では、通知キューの遅延や失敗も監視しましょう。通知が遅れていると、ユーザーからは「処理が終わっていない」ように見える場合があります。
14. 冪等性:同じ通知を二重送信しない設計
キューやイベントを使うと、同じ通知が複数回送られる可能性があります。重要通知では、二重送信防止を考えます。
方法の例:
- 通知対象ごとに送信済みフラグを持つ
- 同じ
typeと対象IDの未読通知があれば作らない - キャッシュロックで短時間の重複を防ぐ
例:
$exists = $user->notifications()
->where('type', ReportGeneratedNotification::class)
->where('data->report_id', $report->id)
->exists();
if (! $exists) {
$user->notify(new ReportGeneratedNotification($report->id, $report->title));
}
通知が少ないうちは不要でも、請求やセキュリティ通知では特に重要です。
15. リアルタイム通知:便利ですが、操作を邪魔しない設計にします
Broadcastingを使うと、通知をリアルタイムに画面へ届けられます。
ただし、リアルタイム通知は注意が必要です。画面が勝手に更新されると、利用者の操作を妨げる可能性があります。
アクセシブルなリアルタイム通知の基本:
- フォーカスを勝手に動かさない
- 通知件数をテキストで更新する
aria-live="polite"を使う- 緊急通知だけ
role="alert"を検討する - トーストだけで重要情報を完結させない
例:
<div id="notification-live-region" role="status" aria-live="polite" class="sr-only"></div>
リアルタイム通知を受け取ったら、この領域に短い文言を入れます。
新しい通知が1件あります。
これにより、視覚的なバッジ更新だけでなく、読み上げでも状態変化を伝えられます。
16. 通知センターのテスト:保存、既読、表示を守ります
通知機能は状態が多いため、テストで守る価値があります。
16.1 通知が保存されるテスト
public function test_report_generated_notification_is_stored()
{
$user = User::factory()->create();
$user->notify(new ReportGeneratedNotification(
reportId: 1,
title: '月次レポート'
));
$this->assertDatabaseHas('notifications', [
'notifiable_id' => $user->id,
'notifiable_type' => User::class,
]);
}
16.2 既読化のテスト
public function test_user_can_mark_notification_as_read()
{
$user = User::factory()->create();
$user->notify(new ReportGeneratedNotification(
reportId: 1,
title: '月次レポート'
));
$notification = $user->notifications()->first();
$this->actingAs($user)
->patch(route('notifications.read', $notification->id))
->assertRedirect();
$this->assertNotNull($notification->fresh()->read_at);
}
16.3 他人の通知を既読にできないテスト
public function test_user_cannot_mark_others_notification_as_read()
{
$user = User::factory()->create();
$other = User::factory()->create();
$other->notify(new ReportGeneratedNotification(
reportId: 1,
title: '月次レポート'
));
$notification = $other->notifications()->first();
$this->actingAs($user)
->patch(route('notifications.read', $notification->id))
->assertNotFound();
}
通知は個人に紐づく情報なので、他人の通知を操作できないことを必ず確認します。
17. よくある落とし穴と回避策
17.1 通知が多すぎる
すべてを通知すると、重要な通知が埋もれます。
通知の種類と重要度を整理し、必要なら集約します。
17.2 未読バッジが色だけ
赤い丸だけでは意味が伝わりません。
数字とテキストで未読件数を示します。
17.3 トーストがすぐ消える
読み終わる前に消える通知は不親切です。
重要情報は通知センターにも残します。
17.4 メール通知とアプリ内通知の内容が違いすぎる
利用者が混乱します。
通知データの基本項目を共通化します。
17.5 他人の通知を操作できる
認可漏れです。
通知取得時は必ずログインユーザーの通知から検索します。
17.6 リアルタイム通知が操作を邪魔する
フォーカスを奪わず、aria-live="polite" で控えめに伝えます。
18. チェックリスト(配布用)
設計
- [ ] 通知の種類と重要度を整理している
- [ ] アプリ内通知、メール通知、リアルタイム通知の使い分けがある
- [ ] 通知データの共通項目がある
- [ ] 通知設定や配信停止の方針がある
Database Notifications
- [ ] notifications テーブルを作成している
- [ ] 通知一覧がある
- [ ] 既読・未読管理がある
- [ ] 他人の通知を操作できない
UI / アクセシビリティ
- [ ] 通知件数をテキストで示している
- [ ] 未読状態を色だけで示していない
- [ ] 空状態の説明がある
- [ ] 完了通知は
role="status" - [ ] 重要な失敗は
role="alert" - [ ] リアルタイム更新でフォーカスを奪わない
運用
- [ ] 通知送信をキュー化している
- [ ] 重要通知の二重送信を防いでいる
- [ ] 通知が増えたときの集約方針がある
- [ ] 通知失敗を監視している
テスト
- [ ] 通知保存のテストがある
- [ ] 既読化のテストがある
- [ ] 他人の通知を操作できないテストがある
- [ ] 通知一覧の表示テストがある
19. まとめ
Laravelの通知機能は、メールを送るだけでなく、アプリ内通知、通知センター、既読管理、リアルタイム更新まで広げられる強力な仕組みです。大切なのは、通知を増やすことではなく、必要な情報を、必要なタイミングで、分かりやすく届けることです。
まずは Database Notifications で通知を保存し、通知一覧、未読バッジ、既読化を整えましょう。次に、重要通知はメールと組み合わせ、通知設定や集約を検討します。リアルタイム通知は便利ですが、操作を邪魔しないように控えめに使います。
通知は、利用者の行動を助けるための道案内です。色だけに頼らず、テキストで状態を伝え、読み上げにも配慮し、必要な通知が埋もれないように設計することで、信頼される通知体験を作れます。
