通知送信機能
メール通知とデータベース通知を行います。
通知テーブルを作成
php artisan notifications:table
php artisan migrate
作成されたマイグレーションファイル
<?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');
}
};
作成されたテーブル
カラム | データ型 | 説明 |
---|---|---|
id | uuid | PK |
type | string | 通知のタイプ |
notifiable_type | string | 通知対象のモデル |
notifiable_id | bigint | 通知対象のユーザーid |
data | text | 通知内容 |
read_at | time_stamps | 既読日時 |
通知クラスを作成
今回はメール通知とデータベース通知を行います。
Markdownメール通知ではBladeコンポーネントとMarkdown記法が利用でき、メールメッセージを簡単に構築できると同時に、Laravelが用意している通知コンポーネントも活用できます。
php artisan make:notification InformationNotification --markdown=mail.notification
コード全体図
<?php
namespace App\Notifications;
use App\Mail\AdminNotificationMail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class InformationNotification extends Notification
{
use Queueable;
protected $title;
protected $content;
/**
* Create a new notification instance.
*/
public function __construct(string $title, string $content)
{
$this->title = $title;
$this->content = $content;
//
}
/**
* メール、データベース通知を指定
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail', 'database'];
}
/**
* メール通知の送信
*/
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject($this->title)
->markdown('mail.notification', ['title' => $this->title, 'content' => $this->content]);
}
/**
* 通知をデータベースに保存
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
'title' => $this->title,
'content' => $this->content,
];
}
}
対応するMarkdownテンプレートを指定し、Mailableを生成するには、make:notification Artisanコマンドを--markdownオプション付きで使用します。
<x-mail::message>
# {{ $title }}
{{ $content }}
Thanks,<br>
</x-mail::message>
他にも
- Buttonコンポーネント
- Panelコンポーネント
- Tableコンポーネント
などでカスタマイズができます。
Notifiableトレイトの使用
Notifiableトレイトは、アプリケーションのApp\Models\Userモデルにデフォルトで含まれています。
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use Notifiable;
}
通知作成
通知を送信したい相手にnotify
メソッドで引数を渡します。
$user->notify(new InformationNotification('title', 'content'));
通知表示と既読機能
ビューコンポーザークラスを作成
まず、ビューコンポーザーとして機能するクラスを作成します。このクラスでは、ビューに渡すデータを定義します。
php artisan make:provider ViewServiceProvider
このコマンドでApp\Providers\ViewServiceProvider.phpが作成されます。
ViewServiceProviderの編集
今回はヘッダーで通知を表示させたかったので*
で全てのviewに適応されるようにしました。
認証ユーザーを取得し、unreadNotifications
は未読の通知。notifications
は既読・未読の区別なく、全ての通知が含まれます。
class ViewServiceProvider extends ServiceProvider
{
/**
* ログインユーザーの未読通知をビューに渡す
*
* @return void
*/
public function boot(): void
{
View::composer('*', function ($view) {
$unreadNotifications = 0;
$notifications = collect();
if (Auth::check()) {
$unreadNotifications = Auth::user()->unreadNotifications->count();
$notifications = Auth::user()->notifications;
}
$view->with(compact('notifications', 'unreadNotifications'));
});
}
}
config/app.phpの編集
config/app.phpのproviders配列に、作成したViewServiceProviderを追加します。
'providers' => [
// Other Service Providers
App\Providers\ViewServiceProvider::class,
],
コントローラーを作成
php artisan make:controller NotificationController
NotificationControllerを作成し、markAsReadメソッドを作成します。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class NotificationController extends Controller
{
public function markAsRead()
{
$user = Auth::user();
$user->unreadNotifications->markAsRead();
return redirect()->back();
}
}
ルーティング作成
routes/web.phpへ既読機能に対応するルートを追加します。
/**
* 通知を既読にする
*/
Route::controller(NotificationController::class)->group(function () {
Route::get('/notifications/mark-as-read', 'markAsRead')->name('mark-as-read');
});
Viewに表示
Alpine.jsでベールマークをクリックしたら、通知がドロップダウンで表示されるようになっています。
レイアウトはTailwindcssで実装しました。
<header class="fixed top-0 left-0 w-full z-30 bg-white shadow">
<div class="flex justify-end flex-1 gap-x-4 self-stretch lg:gap-x-6">
<div class="flex items-center gap-x-4 lg:gap-x-6">
<!-- Notification dropdown -->
<div x-data="{ notificationsOpen: false }">
<button @click="notificationsOpen = !notificationsOpen" type="button" class="relative">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
</svg>
<!-- 未読通知数を表示 -->
<span class="absolute top-0.5 -right-6 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-red-100 bg-red-600 rounded-full">
{{$unreadNotifications}}
</span>
</button>
<!-- 通知ドロップダウンメニュー -->
<div x-show="notificationsOpen" @click.away="notificationsOpen = false" class="absolute mt-2 w-64 bg-gray-200 rounded-md shadow-lg overflow-hidden z-50 right-0 mr-10">
@if($unreadNotifications === 0)
<div>
<p>何もありません</p>
</div>
@else
<a href="{{ route('mark-as-read') }}" class="inline-block bg-gray-500 text-white font-bold m-2 py-1 px-1 rounded hover:bg-gray-700 focus:outline-none focus:shadow-outline">
全て既読にする
</a>
<div class="max-h-64 overflow-y-auto">
@foreach ($notifications as $notification)
<div class="d-flex flex-row align-items-center justify-content-center border-b-2 border-gray-700">
<p class="font-bold p-3">
{{ $notification->data['title'] }}
</p>
<p class="p-3">
{{ $notification->data['content'] }}
</p>
</div>
@endforeach
</div>
@endif
</div>
<!-- Profile dropdown -->
<div class="relative" x-data="{ open: false }">
<button @click="open = !open" type="button" class="-m-1.5 flex items-center p-1.5" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
<span class="sr-only">ユーザーメニューを開く</span>
<span class="hidden lg:flex lg:items-center">
<span class="ml-4 text-sm font-semibold leading-6" aria-hidden="true">テスト</span>
<svg class="ml-2 h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
</svg>
</span>
</button>
</div>
</div>
</div>
</header>