0

Quản lý Real-time Notification trong Laravel với Pusher

Real-Time Notifications

Trong thực tế chúng ta gặp rất nhiều trường hợp cần phải sử dụng đến notifications, và đặc biệt là việc thông báo các notifications này là ngay lập tức (real-time).

Một cách thường gặp nhất đó là sử dụng AJAX request liên tục đến back-end và lấy thông báo mới nhất về nếu có. Tốt hơn chúng ta có thể sử dụng WebSockets để nhận các notifications, đây cũng là cách mà mình muốn đề cập trong bài viết này

Pusher

Pusher là một dịch vụ cloud, cung cấp các hàm xử lý dữ liệu với thời gian thực thông qua WebSockets gửi đến web và các ứng dụng mobile.

Nó đơn giản là một API, để sử dụng dễ dàng trong bài viết này chúng ta sẽ sử dụng thêm Laravel Broadcasting và Laravel Echo. Trong bài viết này chúng ta sẽ thêm real-time notification và 1 blog đã tồn tại (clone từ trên git về)

Khởi tạo project

1. Clone 1 blog Laravel

git clone   https://github.com/marslan-ali/laravel-blog

2. Chỉnh sửa các biến config

cp .env.example .env
###Edit file .env
DB_HOST=localhost
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

3. Install dependencies và migrate database

composer install
php artisan migrate --seed

4. Tạo bảng followers quan hệ n-n với bảng users

php artisan make:migration create_followers_table --create=followers

Tạo migrate file quan hệ với bảng users

public function up()
{
    Schema::create('followers', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('user_id')->index();
        $table->integer('follows_id')->index();
        $table->timestamps();
    });
}

Chạy migrate để tạo table

php artisan migrate

*Thêm quan hệ với bảng User, thêm code vào User model

// ...

class extends Authenticatable
{
    // ...

    public function followers() 
    {
        return $this->belongsToMany(self::class, 'followers', 'follows_id', 'user_id')
                    ->withTimestamps();
    }

    public function follows() 
    {
        return $this->belongsToMany(self::class, 'followers', 'user_id', 'follows_id')
                    ->withTimestamps();
    }
    // Helper function
    public function follow($userId) 
    {
        $this->follows()->attach($userId);
        return $this;
    }
  // Helper function
    public function unfollow($userId)
    {
        $this->follows()->detach($userId);
        return $this;
    }
    // Helper function
    public function isFollowing($userId) 
    {
        return (boolean) $this->follows()->where('follows_id', $userId)->first(['id']);
    }
}

Tạo các thành phần liên quan đến Users

1. Add routes

/...
Route::group(['middleware' => 'auth'], function () {
    Route::get('users', 'UsersController@index')->name('users');
    Route::post('users/{user}/follow', 'UsersController@follow')->name('follow');
    Route::delete('users/{user}/unfollow', 'UsersController@unfollow')->name('unfollow');
});

2. Tạo UserController và các file blade liên quan

php artisan make:controller UsersController

Add index function vào UserController.php

// ...
use App\User;
class UsersController extends Controller
{
    //..
    public function index()
    {
        $users = User::where('id', '!=', auth()->user()->id)->get();
        return view('users.index', compact('users'));
    }
}

Add user.index.php blade

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="col-sm-offset-2 col-sm-8">

            <!-- Following -->
            <div class="panel panel-default">
                <div class="panel-heading">
                    All Users
                </div>

                <div class="panel-body">
                    <table class="table table-striped task-table">
                        <thead>
                        <th>User</th>
                        <th> </th>
                        </thead>
                        <tbody>
                        @foreach ($users as $user)
                            <tr>
                                <td clphpass="table-text"><div>{{ $user->name }}</div></td>
                                @if (auth()->user()->isFollowing($user->id))
                                    <td>
                                        <form action="{{route('unfollow', ['id' => $user->id])}}" method="POST">
                                            {{ csrf_field() }}
                                            {{ method_field('DELETE') }}

                                            <button type="submit" id="delete-follow-{{ $user->id }}" class="btn btn-danger">
                                                <i class="fa fa-btn fa-trash"></i>Unfollow
                                            </button>
                                        </form>
                                    </td>
                                @else
                                    <td>
                                        <form action="{{route('follow', ['id' => $user->id])}}" method="POST">
                                            {{ csrf_field() }}

                                            <button type="submit" id="follow-user-{{ $user->id }}" class="btn btn-success">
                                                <i class="fa fa-btn fa-user"></i>Follow
                                            </button>
                                        </form>
                                    </td>
                                @endif
                            </tr>
                        @endforeach
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
@endsection

3. Follow và Unfollow

Thêm function follow và unfollow và UserController

//...
class UsersController extends Controller
{
    //...
    public function follow(User $user)
    {
        $follower = auth()->user();
        if ($follower->id == $user->id) {
            return back()->withError("You can't follow yourself");
        }
        if(!$follower->isFollowing($user->id)) {
            $follower->follow($user->id);

            // sending a notification
            $user->notify(new UserFollowed($follower));

            return back()->withSuccess("You are now friends with {$user->name}");
        }
        return back()->withError("You are already following {$user->name}");
    }

    public function unfollow(User $user)
    {
        $follower = auth()->user();
        if($follower->isFollowing($user->id)) {
            $follower->unfollow($user->id);
            return back()->withSuccess("You are no longer friends with {$user->name}");
        }
        return back()->withError("You are not following {$user->name}");
    }
}

Notifications

Laravel cung cấp API để gửi thông báo thông qua nhiều kênh khác nhau: Emails, SMS, web notifications, và bất kỳ loại notifications nào thông qua Notification class.

Chúng ta có 2 loại thông báo chính:

  • Follow notification: gửi đến người dùng khi bạn được người khác theo dõi
  • Thông báo tạo bài viết: được gửi đến người dùng khi họ tạo 1 bài viết

1. User Followed Notification

Tạo table notifications

php artisan notifications:table
php artisan migrate

Tạo table UserFollowed

php artisan make:notification UserFollowed

Thêm vào file app/Notifications/UserFollowed.php

Các user được followed sẽ được thêm 1 bản ghi trong DB thông qua function via

class UserFollowed extends Notification implements ShouldQueue
{
    use Queueable;

    protected $follower;

    public function __construct(User $follower)
    {
        $this->follower = $follower;
    }

    public function via($notifiable)
    {
        return ['database'];
    }

    public function toDatabase($notifiable)
    {
        return [
            'follower_id' => $this->follower->id,
            'follower_name' => $this->follower->name,
        ];
    }
}

Thêm follow function vào file UserController.php

// ...
use App\Notifications\UserFollowed;
class UsersController extends Controller
{
   // ...
   public function follow(User $user)
   {
       $follower = auth()->user();
       if ( ! $follower->isFollowing($user->id)) {
           $follower->follow($user->id);

           // add this to send a notification
           $user->notify(new UserFollowed($follower));

           return back()->withSuccess("You are now friends with {$user->name}");
       }

       return back()->withSuccess("You are already following {$user->name}");
   }

   //...
}

2. Đánh dấu các thông báo đã được đọc

Thông báo sẽ chưa thông tin và link đến nguồn. Ví dụ: khi người dùng nhận được thông báo tạo bài viết mới, thông báo sẽ bao gồm thông tin thông báo (text) , link đến bài viết đó khi bạn click vào và 1 cờ đánh dấu là "read"

Tạo 1 middleware như sau:

php artisan make:middleware MarkNotificationAsRead 

Thêm code handle như sau:

class MarkNotificationAsRead
{
   public function handle($request, Closure $next)
   {
       if($request->has('read')) {
           $notification = $request->user()->notifications()->where('id', $request->read)->first();
           if($notification) {
               $notification->markAsRead();
           }
       }
       return $next($request);
   }
}

Add middleware to Kernel (app/Http/Kernel.php)

//...
class Kernel extends HttpKernel
{
   //...
   protected $middlewareGroups = [
       'web' => [
           //...
           \App\Http\Middleware\MarkNotificationAsRead::class,
       ],
       // ...
   ];
   //...
}

3. Hiển thị notifications

Chúng ta sẽ sử dụng AJAX để hiển thị notification và PUSHER để quản lý real-time. Trước tiên ta thêm function notifications vào UserController

// ...
class UsersController extends Controller
{
   // ...
   public function notifications()
   {
       // return 5 notifications unread
       return auth()->user()->unreadNotifications()->limit(5)->get()->toArray();
   }
}

Add route to function

//...
Route::group([ 'middleware' => 'auth' ], function () {
   // ...
   Route::get('/notifications', 'UsersController@notifications');
});

Add code vào file resources/views/layouts/app.blade.php

<head>
   <!-- // ... // -->
   <!-- Scripts -->
   <script>
       window.Laravel = <?php echo json_encode([
           'csrfToken' => csrf_token(),
       ]); ?>
   </script>
   <!-- This makes the current user's id available in javascript -->
   @if(!auth()->guest())
       <script>
           window.Laravel.userId = <?php echo auth()->user()->id; ?>
       </script>
   @endif
</head>
<body>
   <!-- // ... // -->
   @if (Auth::guest())
       <li><a href="{{ url('/login') }}">Login</a></li>
       <li><a href="{{ url('/register') }}">Register</a></li>
   @else
       <!-- // add this dropdown // -->
       <li class="dropdown">
           <a class="dropdown-toggle" id="notifications" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
               <span class="glyphicon glyphicon-user"></span>
           </a>
           <ul class="dropdown-menu" aria-labelledby="notificationsMenu" id="notificationsMenu">
               <li class="dropdown-header">No notifications</li>
           </ul>
       </li>
<!-- // ... // -->

4. JavaScript and SASS

Chúng ta sẽ sử dụng Laravel Mix để complie JavaScript và SASS. Đầu tiên ta sẽ cài đặt npm packages

npm install

Thêm code vào file app/resources/assets/js/app.js

window._ = require('lodash');
window.$ = window.jQuery = require('jquery');
require('bootstrap-sass');
var notifications = [];
const NOTIFICATION_TYPES = {
   follow: 'App\\Notifications\\UserFollowed'
};

“GET” thông báo thông qua AJAX

//...
$(document).ready(function() {
  // check if there's a logged in user
  if(Laravel.userId) {
      $.get('/notifications', function (data) {
          addNotifications(data, "#notifications");
      });
  }
});
function addNotifications(newNotifications, target) {
  notifications = _.concat(notifications, newNotifications);
  // show only last 5 notifications
  notifications.slice(0, 5);
  showNotifications(notifications, target);
}

Hiển thị thông báo

//...
function showNotifications(notifications, target) {
   if(notifications.length) {
       var htmlElements = notifications.map(function (notification) {
           return makeNotification(notification);
       });
       $(target + 'Menu').html(htmlElements.join(''));
       $(target).addClass('has-notifications')
   } else {
       $(target + 'Menu').html('<li class="dropdown-header">No notifications</li>');
       $(target).removeClass('has-notifications');
   }
}

Thêm 1 số helper functions để tạo thông báo

//...
// Make a single notification string
function makeNotification(notification) {
   var to = routeNotification(notification);
   var notificationText = makeNotificationText(notification);
   return '<li><a href="' + to + '">' + notificationText + '</a></li>';
}
// get the notification route based on it's type
function routeNotification(notification) {
   var to = '?read=' + notification.id;
   if(notification.type === NOTIFICATION_TYPES.follow) {
       to = 'users' + to;
   }
   return '/' + to;
}
// get the notification text based on it's type
function makeNotificationText(notification) {
   var text = '';
   if(notification.type === NOTIFICATION_TYPES.follow) {
       const name = notification.data.follower_name;
       text += '<strong>' + name + '</strong> followed you';
   }
   return text;
}

Thêm css cho notification app/resources/assets/sass/app.scss

//... 
#notifications.has-notifications {
 color: #bf5329
}

Complie asset

npm run dev

Thông báo bài viết mới

1. Tạo table NewPost và add code vào model

php artisan make:notification NewPost

Thêm code vào file app/Notifications/NewPost.php

// ..
use App\Post;
use App\User;
class NewArticle extends Notification implements ShouldQueue
{
   // ..
   protected $following;
   protected $post;
   public function __construct(User $following, Post $post)
   {
       $this->following = $following;
       $this->post = $post;
   }
   public function via($notifiable)
   {
       return ['database'];
   }
   public function toDatabase($notifiable)
   {
       return [
           'following_id' => $this->following->id,
           'following_name' => $this->following->name,
           'post_id' => $this->post->id,
       ];
   }
}

2. Sử dụng Eloquent Observers để quản lý notifications

Thêm code vào app/Observers/PostObserver.php

namespace App\Observers;
use App\Notifications\NewPost;
use App\Post;
class PostObserver
{
  public function created(Post $post)
  {
      $user = $post->user;
      foreach ($user->followers as $follower) {
          $follower->notify(new NewPost($user, $post));
      }
  }
}

Đăng ký observer với AppServiceProvider

//...
use App\Observers\PostObserver;
use App\Post;
class AppServiceProvider extends ServiceProvider
{
  //...
  public function boot()
  {
      Post::observe(PostObserver::class);
  }
  //...
}

Add code to js file app/resources/assets/js/app.js

// ...
const NOTIFICATION_TYPES = {
  follow: 'App\\Notifications\\UserFollowed',
  newPost: 'App\\Notifications\\NewPost'
};
//...
function routeNotification(notification) {
  var to = `?read=${notification.id}`;
  if(notification.type === NOTIFICATION_TYPES.follow) {
      to = 'users' + to;
  } else if(notification.type === NOTIFICATION_TYPES.newPost) {
      const postId = notification.data.post_id;
      to = `posts/${postId}` + to;
  }
  return '/' + to;
}
function makeNotificationText(notification) {
  var text = '';
  if(notification.type === NOTIFICATION_TYPES.follow) {
      const name = notification.data.follower_name;
      text += `<strong>${name}</strong> followed you`;
  } else if(notification.type === NOTIFICATION_TYPES.newPost) {
      const name = notification.data.following_name;
      text += `<strong>${name}</strong> published a post`;
  }
  return text;
}

Quản lý notification real-time với Puser

1. Config Pusher

Truy cập trang web https://pusher.com/ , đăng ký 1 acct free và tạo một app

Edit file config .env

...
BROADCAST_DRIVER=pusher
PUSHER_KEY=
PUSHER_SECRET=
PUSHER_APP_ID=

Thêm config ở file config/broadcasting.php

//...
 'connections' => [
         'pusher' => [
             //...
             'options' => [
                 'cluster' => 'eu',
                 'encrypted' => true
             ],
         ],
 //...

Đăng ký với App\Providers\BroadcastServiceProvider vào trong file config/app.php

// ...
'providers' => [
 // ...
 App\Providers\BroadcastServiceProvider
 //...
],
//...

2. Cài đặt Pusher SDK và Laravel Echo

Cài đặt Pusher

composer require pusher/pusher-php-server
npm install --save laravel-echo pusher-js

Add code vào file app/Notifications/UserFollowed.php

//...
class UserFollowed extends Notification implements ShouldQueue
{
 // ..
 public function via($notifiable)
 {
     return ['database', 'broadcast'];
 }
 //...
 public function toArray($notifiable)
 {
     return [
         'id' => $this->id,
         'read_at' => null,
         'data' => [
             'follower_id' => $this->follower->id,
             'follower_name' => $this->follower->name,
         ],
     ];
 }
}

Thêm code vào file app/Notifications/NewPost.php

//...
class NewPost extends Notification implements ShouldQueue
{
 //...
 public function via($notifiable)
 {
     return ['database', 'broadcast'];
 }
 //...
 public function toArray($notifiable)
 {
     return [
         'id' => $this->id,
         'read_at' => null,
         'data' => [
             'following_id' => $this->following->id,
             'following_name' => $this->following->name,
             'post_id' => $this->post->id,
         ],
     ];
 }
}

Update js file (app/resources/assets/js/app.js)

// ...
window.Pusher = require('pusher-js');
import Echo from "laravel-echo";
window.Echo = new Echo({
  broadcaster: 'pusher',
  key: 'your-pusher-key',
  cluster: 'eu',
  encrypted: true
});
var notifications = [];
//...
$(document).ready(function() {
  if(Laravel.userId) {
      //...
      window.Echo.private(`App.User.${Laravel.userId}`)
          .notification((notification) => {
              addNotifications([notification], '#notifications');
          });
  }
});

Và cuối cùng chúng ta cũng hoàn thành, notification sẽ được gửi và quản lý real-time. Bạn có thể thử dùng và xem cách các thông báo được quản lý và update.

Tổng kết

Pusher là một service đơn giản có thể quản lý thông bảo real-time một cách dễ dàng. Pusher kết hợp với Laravel notification sẽ tiện lợi hơn, chúng ta có thể gửi notification thông qua nhiều kênh khác nhau (email, SMS, Slack,v..v..).

Bài viết trên mô tả cách hoạt động và kết hợp của Puser vào Laravel. Chúng ta còn có thể làm nhiều việc hơn nữa từ việc kết hợp Pusher và Laravel: như gửi thông báo song song, gửi đến các thiết bị mobile, IOT v..v...

Tham khảo

https://laravel.com/docs/8.x/notifications

https://pusher.com/

https://pusher.com/tutorials/chat-laravel


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí