Laravel 论坛系统之消息通知功能

消息通知

这篇文章我们来开发消息通知功能,当话题有新回复时,我们将通知作者『你的话题有新回复,请查看』类似的信息。

Laravel 的消息通知系统

Laravel 自带了一套极具扩展性的消息通知系统,尤其还支持多种通知频道,我们将利用此套系统来向用户发送消息提醒。

什么是通知频道?

通知频道是通知传播的途径,Laravel 自带的有数据库、邮件、短信(通过 Nexmo)以及 Slack。本章节中我们将使用数据库通知频道,后面也会使用到邮件通知频道。

1. 准备数据库

数据通知频道会在一张数据表里存储所有通知信息。包含了比如通知类型、JSON 格式数据等描述通知的信息。我们后面会通过查询这张表的内容在应用界面上展示通知。但是在这之前,我们需要先创建这张数据表,Laravel 自带了生成迁移表的命令,执行以下命令即可:

$ php artisan notifications:table

会生成 database/migrations/{$timestamp}_create_notifications_table.php 迁移文件,执行 migrate 命令将表结构写入数据库中:

$ php artisan migrate

我们还需要在 users 表里新增 notification_count 字段,用来跟踪用户有多少未读通知,如果未读通知大于零的话,就在站点的全局顶部导航栏显示红色的提醒。

$ php artisan make:migration add_notification_count_to_users_table --table=users

打开生成的文件,修改为以下:

database/migrations/{$timestamp}_add_notification_count_to_users_table.php

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddNotificationCountToUsersTable extends Migration
{
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->integer('notification_count')->unsigned()->default(0);
        });
    }

    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('notification_count');
        });
    }
}

再次应用数据库修改:

$ php artisan migrate

2. 生成通知类

Laravel 中一条通知就是一个类(通常存在 app/Notifications 文件夹里)。看不到的话不要担心,运行一下以下命令即可创建:

$ php artisan make:notification TopicReplied

修改文件为以下:

app/Notifications/TopicReplied.php

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use App\Models\Reply;

class TopicReplied extends Notification
{
    use Queueable;

    public $reply;

    public function __construct(Reply $reply)
    {
        // 注入回复实体,方便 toDatabase 方法中的使用
        $this->reply = $reply;
    }

    public function via($notifiable)
    {
        // 开启通知的频道
        return ['database'];
    }

    public function toDatabase($notifiable)
    {
        $topic = $this->reply->topic;
        $link =  $topic->link(['#reply' . $this->reply->id]);

        // 存入数据库里的数据
        return [
            'reply_id' => $this->reply->id,
            'reply_content' => $this->reply->content,
            'user_id' => $this->reply->user->id,
            'user_name' => $this->reply->user->name,
            'user_avatar' => $this->reply->user->avatar,
            'topic_link' => $link,
            'topic_id' => $topic->id,
            'topic_title' => $topic->title,
        ];
    }
}

每个通知类都有个 via() 方法,它决定了通知在哪个频道上发送。我们写上 database 数据库来作为通知频道。

因为使用数据库通知频道,我们需要定义 toDatabase()。这个方法接收 $notifiable 实例参数并返回一个普通的 PHP 数组。这个返回的数组将被转成 JSON 格式并存储到通知数据表的 data 字段中。

3. 触发通知

我们希望当用户回复主题后,通知到主题作者。故触发通知的时机是:『回复发布成功后』,在模型监控器里,我们可以在 created 方法里实现此部分代码,修改 created() 方法为以下:

app/Observers/ReplyObserver.php

<?php
.
.
.

use App\Notifications\TopicReplied;

class ReplyObserver
{
    public function created(Reply $reply)
    {
        $reply->topic->reply_count = $reply->topic->replies->count();
        $reply->topic->save();

        // 通知话题作者有新的评论
        $reply->topic->user->notify(new TopicReplied($reply));
    }

    .
    .
    .
}

请注意顶部引入 TopicReplied 。默认的 User 模型中使用了 trait —— Notifiable,它包含着一个可以用来发通知的方法 notify() ,此方法接收一个通知实例做参数。虽然 notify() 已经很方便,但是我们还需要对其进行定制,我们希望每一次在调用 $user->notify() 时,自动将 users 表里的 notification_count +1 ,这样我们就能跟踪用户未读通知了。

打开 User.php 文件,将 use Notifiable, MustVerifyEmailTrait; 修改为以下:

app/Models/User.php

<?php
.
.
.

use Auth;

class User extends Authenticatable implements MustVerifyEmailContract
{
    use MustVerifyEmailTrait;

    use Notifiable {
        notify as protected laravelNotify;
    }
    public function notify($instance)
    {
        // 如果要通知的人是当前用户,就不必通知了!
        if ($this->id == Auth::id()) {
            return;
        }

        // 只有数据库类型通知才需提醒,直接发送 Email 或者其他的都 Pass
        if (method_exists($instance, 'toDatabase')) {
            $this->increment('notification_count');
        }

        $this->laravelNotify($instance);
    }

    .
    .
    .
}

请注意顶部 Auth 的引入。

我们对 notify() 方法做了一个巧妙的重写,现在每当你调用 $user->notify() 时, users 表里的 notification_count 将自动 +1。

接下来我们需要将通知展示出来

4. 新建路由器

首先我们需要新增路由入口:

routes/web.php

.
.
.

Route::resource('notifications', 'NotificationsController', ['only' => ['index']]);

5. 修改顶部导航栏入口,新增通知标识

我们希望用户在访问网站时,能在很显眼的地方提醒他你有未读信息,接下来我们会利用上 notification_count 字段,新增下面的 消息通知标记 区块:

resources/views/layouts/_header.blade.php

<li class="nav-item notification-badge">
  <a class="nav-link mr-3 badge badge-pill badge-{{ Auth::user()->notification_count > 0 ? 'hint' : 'secondary' }} text-white" href="{{ route('notifications.index') }}">
   {{ Auth::user()->notification_count }}
  </a>
</li>

刷新页面即可看到消息提醒标示:

file

样式有点乱,我们稍加调整:

resources/sass/app.scss

.
.
.

/* 消息通知 */
.notification-badge {
  .badge {
    font-size: 12px;
    margin-top: 14px;
  }

  .badge-secondary {
    background-color: #EBE8E8;
  }

  .badge-hint {
    background-color: #d15b47 !important;;
  }
}

默认情况下样式很低调:

file

我们重新使用 Summer 用户登录,可看到显眼的红色标示,并且带有未读消息数量:

file

6. 新建 Notifications 控制器

如果你点击红色标示,会报错 —— 控制器文件并不存在:
接下来我们使用命令行生成控制器:

$ php artisan make:controller NotificationsController

修改控制器的代码如下:

app/Http/Controllers/NotificationsController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Auth;

class NotificationsController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }

    public function index()
    {
        // 获取登录用户的所有通知
        $notifications = Auth::user()->notifications()->paginate(20);
        return view('notifications.index', compact('notifications'));
    }
}

控制器的构造方法 __construct() 里调用 Auth 中间件,要求必须登录以后才能访问控制器里的所有方法。

7. 新建通知列表视图

再次刷新页面,你会看到视图文件未找到的异常:

接下来新建此模板:

resources/views/notifications/index.blade.php

@extends('layouts.app')

@section('title', '我的通知')

@section('content')
  <div class="container">
    <div class="col-md-10 offset-md-1">
      <div class="card ">

        <div class="card-body">

          <h3 class="text-xs-center">
            <i class="far fa-bell" aria-hidden="true"></i> 我的通知
          </h3>
          <hr>

          @if ($notifications->count())

            <div class="list-unstyled notification-list">
              @foreach ($notifications as $notification)
                @include('notifications.types._' . snake_case(class_basename($notification->type)))
              @endforeach

              {!! $notifications->render() !!}
            </div>

          @else
            <div class="empty-block">没有消息通知!</div>
          @endif

        </div>
      </div>
    </div>
  </div>
@stop

通知数据库表的 Type 字段保存的是通知类全称,如 :App\Notifications\TopicRepliedsnake_case(class_basename($notification->type)) 渲染以后会是 —— topic_repliedclass_basename() 方法会取到 TopicReplied,Laravel 的辅助方法 snake_case() 会字符串格式化为下划线命名。

刷新页面,会提示我们对应类型的模板文件不存在:

创建此文件:

resources/views/notifications/types/_topic_replied.blade.php

<li class="media @if ( ! $loop->last) border-bottom @endif">
  <div class="media-left">
    <a href="{{ route('users.show', $notification->data['user_id']) }}">
      <img class="media-object img-thumbnail mr-3" alt="{{ $notification->data['user_name'] }}" src="{{ $notification->data['user_avatar'] }}" style="width:48px;height:48px;" />
    </a>
  </div>

  <div class="media-body">
    <div class="media-heading mt-0 mb-1 text-secondary">
      <a href="{{ route('users.show', $notification->data['user_id']) }}">{{ $notification->data['user_name'] }}</a>
      评论了
      <a href="{{ $notification->data['topic_link'] }}">{{ $notification->data['topic_title'] }}</a>

      {{-- 回复删除按钮 --}}
      <span class="meta float-right" title="{{ $notification->created_at }}">
        <i class="far fa-clock"></i>
        {{ $notification->created_at->diffForHumans() }}
      </span>
    </div>
    <div class="reply-content">
      {!! $notification->data['reply_content'] !!}
    </div>
  </div>
</li>

我们可以通过 $notification->data 拿到在通知类 toDatabase() 里构建的数组。

刷新页面即可看到我们的消息通知列表:

file

8. 清除未读消息标示

下面我们来开发去除顶部未读消息标示的功能 —— 当用户访问通知列表时,将所有通知状态设定为已读,并清空未读消息数。

接下来在 User 模型中新增 markAsRead() 方法:

app/Models/User.php

<?php
.
.
.

class User extends Authenticatable implements MustVerifyEmailContract
{
    .
    .
    .

    public function markAsRead()
    {
        $this->notification_count = 0;
        $this->save();
        $this->unreadNotifications->markAsRead();
    }
}

修改控制器的 index() 方法,新增清空未读提醒的状态:

app/Http/Controllers/NotificationsController.php

<?php
.
.
.
class NotificationsController extends Controller
{
    .
    .
    .
    public function index()
    {
        // 获取登录用户的所有通知
        $notifications = Auth::user()->notifications()->paginate(20);
        // 标记为已读,未读数量清零
        Auth::user()->markAsRead();
        return view('notifications.index', compact('notifications'));
    }
}

现在进入消息通知页面,未读消息标示将被清除:

file

邮件通知

1. 开启 QQ 邮箱的 SMTP 支持

首先我们需要在 QQ 邮箱的账号设置里开启 POP3 和 SMTP 服务。具体请查看 如何打开POP3/SMTP/IMAP功能? 。

只需要开启以下:

file

复制方框里的『授权码』,授权码将作为我们的密码使用:

file

2. 邮箱发送配置

Laravel 中邮箱发送的配置存放于 config/mail.php 中。不过 mail.php 中我们所需的配置,都可以通过 .env 来配置。作为最佳实践,我们优先选择通过环境变量来配置:

.env

.
.
.
MAIL_DRIVER=smtp
MAIL_HOST=smtp.qq.com
MAIL_PORT=465
[email protected]
MAIL_PASSWORD=anoyvonurhqmcdad
MAIL_ENCRYPTION=ssl
[email protected]
MAIL_FROM_NAME=LewisCoder
.
.
.

3. 添加邮件通知频道

首先我们需要修改 via() 方法,并新增 mail 通知频道:

app/Notifications/TopicReplied.php

<?php
.
.
.
class TopicReplied extends Notification
{
    .
    .
    .
    public function via($notifiable)
    {
        // 开启通知的频道
        return ['database', 'mail'];
    }
    .
    .
    .
}

因为开启了 mail 频道,我们还需要新增 toMail 方法:

app/Notifications/TopicReplied.php

<?php
.
.
.
class TopicReplied extends Notification
{
    .
    .
    .

    public function toMail($notifiable)
    {
        $url = $this->reply->topic->link(['#reply' . $this->reply->id]);

        return (new MailMessage)
                    ->line('你的话题有新回复!')
                    ->action('查看回复', $url);
    }
}

4. 使用队列发送邮件

大家应该会发现我们提交回复时,服务器响应会变得非常缓慢,这是『邮件通知』功能请求了 QQ SMTP 服务器进行邮件发送所产生的延迟。对于处理此类延迟,最好的方式是使用队列系统。

我们可以通过对通知类添加 ShouldQueue 接口和 Queueable trait 把通知加入队列。它们两个在使用 make:notification 命令来生成通知文件时就已经被导入,我们只需添加到通知类接口即可。

修改 TopicReplied.php 文件,将以下这一行:

class TopicReplied extends Notification

改为:

class TopicReplied extends Notification implements ShouldQueue

Laravel 会检测 ShouldQueue 接口并自动将通知的发送放入队列中,所以我们不需要做其他修改。

测试下队列
将 QUEUE_DRIVER 的值改为 redis:

.env

QUEUE_CONNECTION=redis

命令行运行队列监控:

$ php artisan horizon

发送邮件测试,可以发现速度大大提高,队列监控也接收到队列任务,并成功处理:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Mr_Lewis/article/details/86153283