[Laravel] 複数の通知タイプの合計未読数を取得する

Laravel のデータベース通知を使って未読数を取得します。

## 通知タイプが1つの場合

通常，通知を取得する場合は1つの通知タイプ（`notificationsテーブル` の `typeカラム`）のみを扱うことが多いです。

例えば，未読のメッセージ数は以下のようにして取得できます。

```php
$user->unreadNotifications()->where('type', 'App\Notifications\MessageReceived')->count();
```

## 通知タイプが複数の場合

チャットアプリを例に挙げます。

「メッセージを受信した」「グループに招待された」のように，複数の通知タイプの合計数を表示したい場面があるかもしれません。

以下のように書きたくなりますが，これでは想定通りに動きません。

```php
$user()->unreadNotifications()->where('type', 'App\Notifications\MessageReceived')->orWhere('type', 'App\Notifications\GroupInvited')->count();
```

実行された SQL を見ると，`type` の条件がグループ化されていないのがわかります。
これだと `type = "App\Notifications\GroupInvited"` に該当する全てのレコードも含めてしまいます。

```sql
select count(*) as aggregate 
from `notifications` 
where `notifications`.`notifiable_id` = 1
and `notifications`.`notifiable_id` is not null 
and `notifications`.`notifiable_type` = "App\Models\User"
and `read_at` is null 
and `type` = "App\Notifications\MessageReceived"
or `type` = "App\Notifications\GroupInvited"
```

```sql
/* 以下の2行は等しい */
WHERE a AND b AND c OR d
WHERE (a AND b AND c) OR d
```

条件をグループ化して，以下のような SQL を組み立てる必要があります。

```sql
WHERE a AND b AND (c OR d)
```

### 正しい書き方

クロージャを使ってクエリをグループ化します。

```php
$user()->unreadNotifications()->where(function(Builder $query) {
    $query->where('type', 'App\Notifications\MessageReceived')
          ->orWhere('type', 'App\Notifications\GroupInvited');
})->count();
```

実行された SQL

```sql
select count(*) as aggregate 
from `notifications` 
where `notifications`.`notifiable_id` = 1
and `notifications`.`notifiable_id` is not null 
and `notifications`.`notifiable_type` = "App\Models\User"
and `read_at` is null 
and (`type` = "App\Notifications\MessageReceived" or `type` = "App\Notifications\GroupInvited") 
```

## メソッドとして定義する

毎回クエリを書くのは面倒なのでモデルに定義します。

```php:User.php
use Illuminate\Database\Eloquent\Builder;

public function getChatUnreadCount(): int
{
    return $this
        ->unreadNotifications()->where(function(Builder $query) {
            $query->where('type', 'App\Notifications\MessageReceived')
                  ->orWhere('type', 'App\Notifications\GroupInvited');
        })->count();
}
```

```php
// チャットの未読通知数を取得
$user->getChatUnreadCount();
```

## 任意の通知タイプを扱う
上で定義したメソッドでは条件が固定でした。
汎用的に使えるように，任意の通知タイプを指定できるようにします。

```php:User.php
use Illuminate\Database\Eloquent\Builder;

/**
 * 
 * @param  string[] $types
 * @return int
 */
public function getCombinedUnreadCount(string ...$types): int
{
    if(empty($types)){
        return $this->unreadNotifications->count();
    }
    return $this
        ->unreadNotifications()->where(function(Builder $query) use ($types) {
            $i = 0;
            $name = '';
            foreach($types as $type){
                $name = 'App\\Notifications\\' . $type;
                if($i === 0){
                    $query->where('type', $name);
                }else{
                    $query->orWhere('type', $name);
                }
                ++$i;
            }
        })->count();
}
```

通知クラス名を文字列で `,` 区切りにして渡します。

```php
$user()->getCombinedUnreadCount('MessageReceived', 'GroupInvited', 'FriendAdded');
```

```sql
select count(*) as aggregate 
from `notifications` 
where `notifications`.`notifiable_id` = 1
and `notifications`.`notifiable_id` is not null 
and `notifications`.`notifiable_type` = "App\Models\User" 
and `read_at` is null 
and (`type` = "App\Notifications\MessageReceived" or `type` = "App\Notifications\GroupInvited" or `type` = "App\Notifications\FriendAdded") 
```

## 最後に

実際は、わざわざメソッドとして定義する必要もないかもしれませんが、何か参考になればと思い書きました。

間違っている部分などがあれば、ご指摘いただけると嬉しいです。