Laravel 通知機能実装　通知送信から既読まで

# 通知送信機能
メール通知とデータベース通知を行います。
### 通知テーブルを作成
```zsh
php artisan notifications:table
php artisan migrate
```
作成されたマイグレーションファイル
```php
<?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が用意している通知コンポーネントも活用できます。
```zsh
php artisan make:notification InformationNotification --markdown=mail.notification
```
コード全体図
```app/Notifications/InformationNotification.php
<?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オプション付きで使用します。

[メッセージカスタマイズ方法](https://readouble.com/laravel/10.x/ja/notifications.html#:~:text=%E7%94%9F%E6%88%90%E3%81%97%E3%81%BE%E3%81%99%E3%80%82-,%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E7%94%9F%E6%88%90,-%E5%AF%BE%E5%BF%9C%E3%81%99%E3%82%8BMarkdown)

```resources/views/mail/notification.blade.php
<x-mail::message>
# {{ $title }}

{{ $content }}



Thanks,<br>
</x-mail::message>

```
![スクリーンショット 2024-03-24 14.51.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/8df19c82-444e-08fa-1d53-008ae078fe0f.png)

他にも
- Buttonコンポーネント
- Panelコンポーネント
- Tableコンポーネント
などでカスタマイズができます。



### Notifiableトレイトの使用
Notifiableトレイトは、アプリケーションのApp\Models\Userモデルにデフォルトで含まれています。
```php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;
}
```
## 通知作成
通知を送信したい相手に`notify`メソッドで引数を渡します。
```php
$user->notify(new InformationNotification('title', 'content'));
```


# 通知表示と既読機能
### ビューコンポーザークラスを作成
まず、ビューコンポーザーとして機能するクラスを作成します。このクラスでは、ビューに渡すデータを定義します。
```zsh
php artisan make:provider ViewServiceProvider
```
このコマンドでApp\Providers\ViewServiceProvider.phpが作成されます。

### ViewServiceProviderの編集
今回はヘッダーで通知を表示させたかったので`*`で全てのviewに適応されるようにしました。
認証ユーザーを取得し、`unreadNotifications`は未読の通知。`notifications`は既読・未読の区別なく、全ての通知が含まれます。
```app\Providers\ViewServiceProvider.php
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を追加します。
```php
  'providers' => [
        // Other Service Providers

        App\Providers\ViewServiceProvider::class,
    ],
```
### コントローラーを作成
```zsh
php artisan make:controller NotificationController
```
NotificationControllerを作成し、markAsReadメソッドを作成します。

```app/Http/Controllers/NotificationController.php
    <?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へ既読機能に対応するルートを追加します。
```web.php
/**
 * 通知を既読にする
 */
Route::controller(NotificationController::class)->group(function () {
    Route::get('/notifications/mark-as-read', 'markAsRead')->name('mark-as-read');
});

```

[Readとしての通知作成](https://readouble.com/laravel/10.x/ja/notifications.html#:~:text=%E9%80%81%E4%BF%A1%E3%81%97%E3%81%BE%E3%81%99%E3%80%82-,Read%E3%81%A8%E3%81%97%E3%81%A6%E3%81%AE%E9%80%9A%E7%9F%A5%E4%BD%9C%E6%88%90,-%E9%80%9A%E5%B8%B8%E3%80%81%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%8C)


### Viewに表示
Alpine.jsでベールマークをクリックしたら、通知がドロップダウンで表示されるようになっています。
レイアウトはTailwindcssで実装しました。

```header_blade.php
<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>

```

<img width="400" alt="スクリーンショット 2024-03-24 15.29.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/c18d1878-6909-3935-7b8e-a6af93d0bd0c.png">