Menu

43.话题订阅(四)

本节说明

  • 对应视频教程第 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);
    }
    .
    .

如果我们运行次测试,当然是不会通过的:
file

准备数据库

我们将使用 Laravel 的消息通知系统 来进行消息通知。我们需要先创建notifications数据表,Laravel 自带了生成迁移表的命令,执行以下命令即可:

$ php artisan notifications:table

再执行 migrate 命令将表结构写入数据库中:

$ php artisan migrate

现在我们再次运行测试:
file
我们设定的触发消息通知的时机是『订阅的话题有新的回复 』,所以我们需要更改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 数据库来作为通知频道。

触发通知

我们继续完善消息通知的逻辑:

    .
    .
    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);
    }
}

现在我们再次运行测试:
file
我们想要测试地更加严谨:


    .
    .
    /** @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()方法的使用。

再次测试:
file
看上去我们的测试已经完善了,但是我们并不十分满意。因为现在的测试变得功能不单一了,它既测试了订阅功能,又测试了消息通知功能。所以我们打算将消息通知功能抽取成一个全新的测试文件:

$ php artisan make:test NotificationsTest

我们修改a_user_can_subscribe_to_threads测试:

    .
    .
    /** @test */
    public function a_user_can_unsubscribe_from_threads()
    {
        $this->signIn();

        // Given we have a thread
        $thread = create('App\Thread');

        $thread->subscribe();

        // And the user unsubscribes from the thread
        $this->delete($thread->path() . '/subscriptions');

        $this->assertCount(0,$thread->subscriptions);
    }
}

运行一下全部的测试:
file
有一个警告,是因为我们刚刚新建了测试文件,但是还没有添加测试导致的。现在我们添加第一个测试:
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')->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')->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。我们来运行测试:
file
接下来我们对消息通知的代码进行下重构:

    .
    .
    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));
    }
}

运行测试:
file

阅读通知

既然我们发送了通知,当然我们可以阅读通知消息。我们下面来进行这一功能,首先我们新建测试:
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()方法:

$ 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;
    }
}

运行测试:
file

清除通知

最后我们来开发清除已读消息的功能:当用户已读消息通知后,我们应该将所有已通知消息的状态设定为已读,并清除已通知消息。首先我们依然是先建立测试:
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()方法:

$ make:controller UserNotificationsController

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);
    }
}

运行测试:
file

最后检验

最后,让我们来运行一下全部的测试:
file
OK,下一节我们继续前进。

本文章首发在 Laravel China 社区
上一篇 下一篇
讨论数量: 1
发起讨论


zh117
关于 43 节的遗漏?
1 个点赞 | 1 个回复 | 问答