LaravelとVue.jsで通知機能を実装してみた。(リアルタイム)

2021.01.18

見出し画像

今回は以前の記事でフォロー機能、いいね機能、コメント機能などを付けていたので受信する側が通知を受け取れるようにして行きたいと思います。

目次

  1. 前提
  2. 大まかな流れ
  3. Notificationテーブルの作成
  4. ルーティング
  5. notificationの作成,設定
  6. モデル
  7. コントローラーで呼び出し
  8. 通知をクリックした後の処理
  9. balde側
  10. コンポーネント側

すべて表示

前提

フォロー機能、いいね機能、コメント機能が実装されている。

pusherの設定も終わっている前提で進めます。pusher導入

大まかな流れ

Notificationテーブルの作成、モデル設定ルーティング

ルーティング

notificationの作成

コントローラーで呼び出し

pusherでリアルタイム化

上記のような流れで作成出来ます。

Notificationテーブルの作成

php artisan notifications:table
php artisan migrate

ルーティング

Route::get('/user/{user}/notice_get', 'UserController@notice_get')->name('notice_get');
Route::put('/user/{user}/notice_checked', 'UserController@notice_checked')->name('notice_checked');

notificationの作成,設定

php artisan make:notification ItemLiked
<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\User;
use App\Item;

class ItemLiked extends Notification implements ShouldQueue
{
   use Queueable;

   protected $liker;         1
   protected $item;

   /**
    * Create a new notification instance.
    *
    * @return void
    */
   public function __construct(User $liker, Item $item)        2
   {
       $this->liker = $liker;
       $this->item = $item;
   }

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

   /**
    * Get the mail representation of the notification.
    *
    * @param  mixed  $notifiable
    * @return \Illuminate\Notifications\Messages\MailMessage
    */
   public function toDatabase($notifiable)
   {         
       return [                                    4
           'liker_id' => $this->liker->id,
           'liker_name' => $this->liker->name,
           'item_id' => $this->item->id,
           'status' => false,
       ];
   }

   /**
    * Get the array representation of the notification.
    *
    * @param  mixed  $notifiable
    * @return array
    */
   public function toArray($notifiable)
   {
       return [
           //
       ];
   }
}

1.  このファイル内で使用する変数を設定します。

2.  $likerと$itemを設定します。

3. toMailからdatabaseに変更し、データベースへの保存ができるようにします。

4.  保存する値を決めています。注意点としては全て1つのカラムに保存される点です。

モデル

public function isLiked($user_id)    1
{
   return $this->likes()->where('user_id', $user_id)->exists();
}

public function getLike($user_id)     2
{
   return $this->likes()->where('user_id', $user_id)->first();
}

1. ユーザーがその商品に対していいねをしているか確認している。

2. ユーザーがその商品に対していいねしているレコードを取得している。

コントローラーで呼び出し

use App\Notifications\ItemLiked;     1

public function store(Item $item)
{
   $user = Auth::user();
   if($user->id != $item->user_id) {
       if($item->isLiked(Auth::id())) {         2
           // 対象のレコードを取得して、削除する。
           $delete_record = $item->getLike($user->id);
           $delete_record->delete();
       } else {
           $like = Like::firstOrCreate(
               array(
                   'user_id' => $user->id,
                   'item_id' => $item->id
               )
           );
           $item->user->notify(new ItemLiked($user, $item));      3
       }
   }
}

1.  ItemLikedを呼び出す為に記載します。

2. モデルに設定したメソッドを呼び出していいねしているかチェックしている。

3.  notifyでItemLikedを$userと$itemの引数を持って呼び出している。

通知をクリックした後の処理

viewでの表示でクリックした際に、その投稿やいいねしたユーザーページに飛ばしたりするようにします。また、クリックした後はデータベースの値を更新し、チェック済み扱いにします。

balde側

適当な位置に配置して、ログイン中のユーザー情報をコンポーネント側に送ります。

<notice :current_user="{{Auth::user()}}"></notice>

コンポーネント側

<template>
 <div class="btn-group">
   <span v-if="not_checked_list.length != 0">{{not_checked_list.length}}</span>
   <a id="navbarDropdownSearch" class="pl-1 nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
     <font-awesome-icon icon="bell" />
   </a>
   <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownSearch">
     <div v-if="notice == 0" class="px-2">
       <p>通知はありません</p>
     </div>
1    <ul v-for="val in notice" :key="val.id" style="list-style: none;" class="mb-1 px-3">
2      <li v-if="val.data.liker_id">
3        <a v-if="val.data.status" class="dropdown-item" href="#">
4          <p><a @click="user_url(val, val.data.liker_id)" :url="url">{{val.data.liker_name}}</a>が<a @click="item_url(val, val.data.item_id)">あなたの投稿にLike</a>しました</p>
         </a>
         <a v-else class="dropdown-item not-check" href="#">
           <p><a @click="user_url(val, val.data.liker_id)" :url="url">{{val.data.liker_name}}</a>が<a @click="item_url(val, val.data.item_id)">あなたの投稿にLike</a>しました</p>
         </a>
       </li>
     </ul>
   </div>
 </div>
</template>

<script>
import {mapState} from 'vuex'
export default {
 props: ['current_user'],            5
 data() {
   return {
     notice: [],
     not_checked_list: []            6
   }
 },
 created() {
   this.get_notice()                   7
 },
 methods: {
   user_url(val, user_id) {                  8
     this.status_checked(val)
     const array = ["/users/", user_id];
     const url = array.join('')
     window.location.href = this.url
   },
   item_url(val, item_id) {                 9
     this.status_checked(val)
     const array = ["/items/", item_id];
     const link = array.join('')
     window.location.href = link
   },
   get_notice() {                        10
     const id = this.current_user.id
     const array = ["/user/",id,"/notice_get"];
     const path = array.join('')
     axios.get(path).then(res => {       11
       this.notice = res.data
       this.not_checked_list = this.notice.filter(val => val.data.status == false)
     }).catch(function(err) {
       console.log(err)
     })
   },
   status_checked(val) {                   12
     if(val.data.status == false) {
       const id = this.current_user.id
       const array = ["/user/",id,"/notice_checked"];
       const path = array.join('')
       axios.put(path, val).then(res => {
         this.get_notice()
       }).catch(function(err) {
         console.log(err)
       })
     }
   }
 }
}
</script>

<style scoped>
.not-check{
   background-color: cornsilk
}
p{
 margin-bottom: 0;
}
</style>

1.  noticeに入っている値をeachで回し、valとして取得、使用します。

2.  先ほど設定した通り、dataの中にliker_idとしてデータが入っているのでその値が入っているかチェックしています。

3.  同じくdataの中にstatusが入っているので、これを参考にチェック済みかどうか判断しています。v-eles側にはクラスでnot-checkを入れて、下のcssで背景色を変更しています。

4.  いいねしたユーザーといいねされた投稿にアクセスできるようクリックでメソッドが動くようにしています。

5.  blade側から現在ログイン中のユーザーの情報を受け取るようにしています。

6.  これはクリックしてない通知があるかどうか見ています。値がある場合は数字を、ない場合は何も表示しません。

7.   ライフサイクルメソッドのcreated()でget_notice()メソッドを動かすようにしています。

8.  user_idの変数を使用してユーザーページまでのリンク作成します。valの方はstatus_checked()メソッドの実行です。ただリンク飛ばしをwindow.location.hrefでやっているのでもっと他に良い方法があれば教えていただきたいです。

9.  8と非常に似ています。userではなくitemに対して同じような処理を書くだけです。

10.   axiosでコントローラーにアクセスし、通知を取得してきます。

11.  10で取得した値でstatusがfalseのものはnot_checked_listに代入しています。

12.こちらもaxiosでコントローラーにアクセスし、statusの値をtrueに変更しています。取得した後は10で説明したget_status()を再度実行し値を代入しています。

コントローラー側

public function notice_get(User $user)         
{
   $notices = $user->unreadNotifications()->limit(5)->get()->toArray();      1
   return $notices;
}

public function notice_checked(User $user, Request $request)           
{
   $notice = $user->unreadNotifications()->where('id', $request->id)->first();       2
   // ネストしているのでarray_replaceを使用。 http://monteecristoo.hatenablog.com/entry/2019/09/08/044511
   $newValue = ['status' => true];         3
   $notice->data = array_replace($notice->data, $newValue);      4
   $notice->save();
}

1.  unreadNotifications()メソッドを使用することで、このユーザーに紐づくチェックしていないnotificationを取得しています。toArray()で配列の形にします。

2.  こちらもunreadNotifications()を使用してチェックしていないnotificationを取得します。またwhere()で$request->idとidが一致するものを見つけています。

3.  statusの値をtrueに変更しますが、1つのカラムに複数の値が入っているので少し複雑です。ここでは先に変更する値を用意します。

4.  array_replaceを使用して値の方を変更、save()で更新して行きます。

ここまでで一通りの機能は実装出来たかと思います。実際に動かして確認してみましょう。次は非同期にして行きます。

pusherでリアルタイム化

イベントを作成し、あるタイミングでイベントを発火するようにします。

イベント作成

php artisan make:event Notice

作成されたファイルを編集

ファイルはapp/events/Notice.phpって感じであるはずです。

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class Notice implements ShouldBroadcast      1
{
   use Dispatchable, InteractsWithSockets, SerializesModels;

   /**
    * Create a new event instance.
    *
    * @return void
    */
   public function __construct()
   {
       //
   }

   /**
    * Get the channels the event should broadcast on.
    *
    * @return \Illuminate\Broadcasting\Channel|array
    */
   public function broadcastOn()
   {
       return new Channel('notice');          2
   }
}

1.  ShouldBroadcastを実装することでリロードしなくてもイベントが発火されるようになります。

2.  privateChannelからChannelに変更。privateだとユーザー認証が確認できている状態という縛りがあるのですが今回は特に気にしないので外しています。()の中にはチャンネル名を文字列で記載します。

コントローラー側で呼び出し

イベント呼び出します。

use App\Events\Notice;   1


 $item->user->notify(new ItemLiked($user, $item));
 event(new Notice());  2

1.  設定したイベントを呼び出せるように記載を追加。

2.  notifyでnotificationレコードを作成した後にevent()でイベントを実行します。

コンポーネント側

created() {
   Echo.channel('notice').listen('Notice', (e) => {   1
     this.get_notice()
   })
   this.get_notice()
 },

1. ライフサイクルメソッドのcreated()に記載を追加します。Echo.channel()でチャンネル名、listenでクラス名を指定しget_notice()メソッドを発火しています。

こんな感じでリアルタイム化も実装出来ましたので、挙動の方を確認しましょう。

参考

参考記事

まとめ

・通知の保存にはnotificationを使う。

・保存する値は指定出来るが、1つのカラムに複数の値が入る形になる。

・更新作業ではarray_replace()を使って処理を行う。

・コンポーネント側はstatusの値によって表示を変更する。

・not_che….listでチェックしていない通知の数を表示する。

・いいねしたユーザーや、いいねされた投稿などをクリックで繊維できるメソッドも用意。

・axiosでコントローラーとやり取りをして値の取得、更新。

・echoでイベントが発火されたタイミングでメソッドを実行できるようにしている。

こんな感じでしょうか。。今回はいいね機能で実装しましたが、フォロー機能やコメント機能などにも使えるかなと思います!

初めて通知機能を実装したのでnotificationsとかわからない、値変更するのむずい!ってなりましたがなんとか実装出来ました。リアルタイム化はそこまで難しい処理ではないかなと思いますが、導入設定は大事です。

全く関係ないですが、自動車免許取得の合宿中です。ちゃんと取れますよーに。w

今回はこんな感じで!

以上!