43.话题订阅(四)
- 本系列文章为
laracasts.com
的系列视频教程——Let's Build A Forum with Laravel and TDD 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频, 支持正版 ;- 视频源码地址:github.com/laracasts/Lets-Build-a-...;
- 本项目为一个 forum(论坛)项目,与本站的第二本实战教程 《Laravel 教程 - Web 开发实战进阶》 类似,可互相参照。
本节说明
- 对应视频教程第 43 小节:Thread Subscriptions(Part 4)
本节内容
我们继续进行话题订阅功能。在之前的章节中,我们还有遗留的测试没有完成:
forum\tests\Feature\SubscribeToThreadsTest.php
.
.
/** @test */
public function a_user_can_subscribe_to_threads()
{
$this->signIn();
// Given we have a thread
$thread = create('App\Thread');
// And the user subscribes to the thread
$this->post($thread->path() . '/subscriptions');
// Then,each time a new reply is left...
$thread->addReply([
'user_id' => auth()->id(),
'body' => 'Some reply here'
]);
// A notification should be prepared for the user.
$this->assertCount(1,auth()->user()->notifications);
}
.
.
如果我们运行次测试,当然是不会通过的:
准备数据库
我们将使用 Laravel 的消息通知系统 来进行消息通知。我们需要先创建notifications
数据表,Laravel 自带了生成迁移表的命令,执行以下命令即可:
$ php artisan notifications:table
再执行 migrate
命令将表结构写入数据库中:
$ php artisan migrate
现在我们再次运行测试:
我们设定的触发消息通知的时机是『订阅的话题有新的回复 』,所以我们需要更改addReply
方法的逻辑:
forum\app\Thread.php
.
.
public function addReply($reply)
{
$reply = $this->replies()->create($reply);
// Prepare notifications for all subscribers
return $reply;
}
.
.
接下来编写代码:
.
.
public function addReply($reply)
{
$reply = $this->replies()->create($reply);
// Prepare notifications for all subscribers
foreach ($this->subscriptions as $subscription) {
$subscription->user->notify(new ThreadWasUpdated);
}
return $reply;
}
.
.
生成通知类
Laravel 中一条通知就是一个类(通常存在 app/Notifications 文件夹里)。我们运行以下命令创建ThreadWasUpdated
通知类:
$ php artisan make:notification ThreadWasUpdated
修改文件为以下:
forum\app\Notifications\ThreadWasUpdated.php
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
class ThreadWasUpdated extends Notification
{
use Queueable;
protected $thread;
protected $reply;
/**
* Create a new notification instance.
*
* @param $thread
* @param $reply
*/
public function __construct($thread,$reply)
{
$this->thread = $thread;
$this->reply = $reply;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['database'];
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
'message' => 'Temporary data.'
];
}
}
每个通知类都有个 via()
方法,它决定了通知在哪个频道上发送。我们使用 database
数据库来作为通知频道。
触发通知
我们继续完善消息通知的逻辑:
.
use App\Notifications\ThreadWasUpdated;
.
.
public function addReply($reply)
{
$reply = $this->replies()->create($reply);
// Prepare notifications for all subscribers
foreach ($this->subscriptions as $subscription) {
$subscription->user->notify(new ThreadWasUpdated($this,$reply));
}
return $reply;
}
.
.
注意,此时$subscription->user
的关联关系还未建立,我们需要先建立此关联关系:
forum\app\ThreadSubscription.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class ThreadSubscription extends Model
{
protected $guarded =[];
public function user()
{
return $this->belongsTo(User::class);
}
}
现在我们再次运行测试:
我们想要测试地更加严谨:
.
.
/** @test */
public function a_user_can_subscribe_to_threads()
{
$this->signIn();
// Given we have a thread
$thread = create('App\Thread');
$this->assertCount(0,auth()->user()->notifications);
// And the user subscribes to the thread
$this->post($thread->path() . '/subscriptions');
// Then,each time a new reply is left...
$thread->addReply([
'user_id' => auth()->id(),
'body' => 'Some reply here'
]);
// A notification should be prepared for the user.
$this->assertCount(1,auth()->user()->fresh()->notifications);
}
.
.
注意
fresh()
方法的使用。
再次测试:
看上去我们的测试已经完善了,但是我们并不十分满意。因为现在的测试变得功能不单一了,它既测试了订阅功能,又测试了消息通知功能。所以我们打算将消息通知功能抽取成一个全新的测试文件:
$ php artisan make:test NotificationsTest
我们修改a_user_can_subscribe_to_threads
测试:
.
.
/** @test */
public function a_user_can_subscribe_to_threads()
{
$this->signIn();
$thread = create('App\Thread');
$this->post($thread->path() . '/subscriptions');
$this->assertCount(1, $thread->fresh()->subscriptions);
}
/** @test */
public function a_user_can_unsubscribe_from_threads()
{
$this->signIn();
$thread = create('App\Thread');
$this->post($thread->path() . '/subscriptions');
$this->delete($thread->path() . '/subscriptions');
$this->assertCount(0,$thread->subscriptions);
}
}
运行一下全部的测试:
有一个警告,是因为我们刚刚新建了测试文件,但是还没有添加测试导致的。现在我们添加第一个测试:
forum\tests\Feature\NotificationsTest.php
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Tests\TestCase;
class NotificationsTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function a_notification_is_prepared_when_a_subscribed_thread_receives_a_new_reply_that_is_not_by_the_current_user()
{
$this->signIn();
$thread = create('App\Thread');
$thread->subscribe();
$this->assertCount(0,auth()->user()->notifications);
$thread->addReply([
'user_id' => auth()->id(),
'body' => 'Some reply here'
]);
$this->assertCount(1,auth()->user()->fresh()->notifications);
}
}
我们暂时将测试代码抽取了过来,但是消息通知的逻辑其实是有点小问题的。因为当话题的创建者添加回复时,也会触发消息通知。我们并不需要这样:
forum\app\Thread.php
.
.
public function addReply($reply)
{
$reply = $this->replies()->create($reply);
// Prepare notifications for all subscribers
foreach ($this->subscriptions as $subscription) {
if($subscription->user_id != $reply->user_id){
$subscription->user->notify(new ThreadWasUpdated($this,$reply));
}
}
return $reply;
}
.
.
public function subscribe($userId = null)
{
$this->subscriptions()->create([
'user_id' => $userId ?: auth()->id()
]);
return $this;
}
.
.
相应修改测试:
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Tests\TestCase;
class NotificationsTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function a_notification_is_prepared_when_a_subscribed_thread_receives_a_new_reply_that_is_not_by_the_current_user()
{
$this->signIn();
$thread = create('App\Thread');
$thread->subscribe();
$this->assertCount(0,auth()->user()->notifications);
$thread->addReply([
'user_id' => auth()->id(),
'body' => 'Some reply here'
]);
$this->assertCount(0,auth()->user()->fresh()->notifications);
$thread->addReply([
'user_id' => create('App\User')->id,
'body' => 'Some reply here'
]);
$this->assertCount(1,auth()->user()->fresh()->notifications);
}
}
现在我们的测试逻辑为:新建的话题无回复,通知数为 0;当创建者自己添加回复时,通知数仍旧为 0;当其他人添加回复时,通知数为 1。我们来运行测试:
接下来我们对消息通知的代码进行下重构:
.
.
public function addReply($reply)
{
$reply = $this->replies()->create($reply);
// Prepare notifications for all subscribers
$this->subscriptions
->filter (function ($sub) use ($reply){
return $sub->user_id != $reply->user_id;
})
->each (function ($sub) use ($reply){
$sub->user->notify(new ThreadWasUpdated($this,$reply));
});
return $reply;
}
.
.
现在我们不必再每次发送消息前都进行判断了。我们还可以继续重构:将发送消息通知的代码抽取出来。我们来修改消息通知的代码:
.
.
public function addReply($reply)
{
$reply = $this->replies()->create($reply);
// Prepare notifications for all subscribers
$this->subscriptions
->filter (function ($sub) use ($reply){
return $sub->user_id != $reply->user_id;
})
->each->notify($reply);
return $reply;
}
.
.
抽取代码:
forum\app\ThreadSubscription.php
.
.
public function thread()
{
return $this->belongsTo(Thread::class);
}
public function notify($reply)
{
$this->user->notify(new ThreadWasUpdated($this->thread,$reply));
}
}
运行测试:
阅读通知
既然我们发送了通知,当然我们可以阅读通知消息。我们下面来进行这一功能,首先我们新建测试:
forum\tests\Feature\NotificationsTest.php
.
.
/** @test */
public function a_user_can_fetch_their_unread_notifications()
{
$this->signIn();
$thread = create('App\Thread')->subscribe();
$thread->addReply([
'user_id' => create('App\User')->id,
'body' => 'Some reply here'
]);
$user = auth()->user();
$response = $this->getJson("/profiles/" . $user->name . "/notifications")->json();
$this->assertCount(1,$response);
}
}
添加路由:
forum\routes\web.php
.
.
Route::get('/profiles/{user}','ProfilesController@show')->name('profile');
Route::get('/profiles/{user}/notifications','UserNotificationsController@index');
新建控制器,并建立index()
方法:
$ php artisan make:controller UserNotificationsController
forum\app\Http\Controllers\UserNotificationsController.php
<?php
namespace App\Http\Controllers;
class UserNotificationsController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function index()
{
return auth()->user()->unreadNotifications;
}
}
运行测试:
清除通知
最后我们来开发清除已读消息的功能:当用户已读消息通知后,我们应该将所有已通知消息的状态设定为已读,并清除已通知消息。首先我们依然是先建立测试:
forum\tests\Feature\NotificationsTest.php
.
.
/** @test */
public function a_user_can_clear_a_notification()
{
$this->signIn();
$thread = create('App\Thread')->subscribe();
$thread->addReply([
'user_id' => create('App\User')->id,
'body' => 'Some reply here'
]);
$this->assertCount(1,auth()->user()->unreadNotifications);
$this->delete($uri);
$this->assertCount(0,auth()->user()->fresh()->unreadNotifications);
}
}
当订阅话题有新回复时,未读消息auth()->user()->unreadNotifications
的数量为 1;然后我们删除delete()
已读通知,此时,未读消息auth()->user()->unreadNotifications
的数量为 。但是此时我们delete
方法的路由还未设置,我们前往设置:
forum\routes\web.php
.
.
Route::get('/profiles/{user}','ProfilesController@show')->name('profile');
Route::get('/profiles/{user}/notifications','UserNotificationsController@index');
Route::delete('/profiles/{user}/notifications/{notification}','UserNotificationsController@destroy');
建立destroy()
方法:
forum\app\Http\Controllers\UserNotificationsController.php
<?php
namespace App\Http\Controllers;
use App\User;
class UserNotificationsController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function index()
{
return auth()->user()->unreadNotifications;
}
public function destroy(User $user,$notificationId)
{
auth()->user()->notifications()->findOrFail($notificationId)->markAsRead();
}
}
现在来完成我们的测试:
forum\tests\Feature\NotificationsTest.php
.
.
/** @test */
public function a_user_can_clear_a_notification()
{
$this->signIn();
$thread = create('App\Thread')->subscribe();
$thread->addReply([
'user_id' => create('App\User')->id,
'body' => 'Some reply here'
]);
$user = auth()->user();
$this->assertCount(1, $user->unreadNotifications);
$notificationId = $user->unreadNotifications->first()->id;
$this->delete("/profiles/" . $user->name . "/notifications/{$notificationId}");
$this->assertCount(0, $user->fresh()->unreadNotifications);
}
}
运行测试:
最后检验
最后,让我们来运行一下全部的测试:
OK,下一节我们继续前进。