54.限制回复频率
- 本系列文章为
laracasts.com
的系列视频教程——Let's Build A Forum with Laravel and TDD 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频, 支持正版 ;- 视频源码地址:github.com/laracasts/Lets-Build-a-...;
- 本项目为一个 forum(论坛)项目,与本站的第二本实战教程 《Laravel 教程 - Web 开发实战进阶》 类似,可互相参照。
本节说明
- 对应视频教程第 54 小节:A User May Not Reply More Than Once Per Minute
本节内容
本节让我们限制用户的回复频率,防止程序恶意刷评论。首先我们按照惯例新建测试:
forum\tests\Feature\ParticipateInForumTest.php
.
.
/** @test */
public function users_may_only_reply_a_maximum_of_once_per_minute()
{
$this->signIn();
$thread = create('App\Thread');
$reply = make('App\Reply',[
'body' => 'My simple reply.'
]);
$this->post($thread->path() . '/replies',$reply->toArray())
->assertStatus(200);
$this->post($thread->path() . '/replies',$reply->toArray())
->assertStatus(422);
}
}
连续地发布两条回复,我们禁止这种操作,所以我们返回 422 状态。现在运行测试会报错,因为我们还没有建立相关逻辑。我们利用已经建立的授权策略,新建一个create()
方法:
app\Policies\ReplyPolicy.php
.
.
public function create(User $user)
{
return ! $user->lastReply->wasJustPublished();
}
}
我们通过lastReply
关联取出最新的回复,通过wasJustPublished()
方法判断是否刚刚已经发表过回复。这是我们接下来要做的两件事情,但是在此之前,我们要为lastReply
关联建立测试。我们新建一个单元测试文件:
forum\tests\Unit\UserTest.php
<?php
namespace Tests\Unit;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class UserTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function a_user_can_fetch_their_most_recent_reply()
{
$user = create('App\User');
$reply = create('App\Reply',['user_id' => $user->id]);
$this->assertEquals($reply->id,$user->lastReply->id);
}
}
建立lastReply
关联:
forum\app\User.php
.
.
public function threads()
{
return $this->hasMany(Thread::class)->latest();
}
public function lastReply()
{
return $this->hasOne(Reply::class)->latest();
}
.
.
运行刚刚建立的测试:
我们接着新建wasJustPublished()
方法:
forum\app\Reply.php
use Carbon\Carbon;
.
.
public function thread()
{
return $this->belongsTo(Thread::class);
}
public function wasJustPublished()
{
return $this->created_at->gt(Carbon::now()->subMinute());
}
.
.
我们依然要建立测试:
forum\tests\Unit\ReplyTest.php
<?php
namespace Tests\Unit;
use Carbon\Carbon;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class ReplyTest extends TestCase
{
use DatabaseMigrations;
/** @test */
function it_has_an_owner()
{
$reply = create('App\Reply');
$this->assertInstanceOf('App\User',$reply->owner);
}
/** @test */
public function it_knows_if_it_was_just_published()
{
$reply = create('App\Reply');
$this->assertTrue($reply->wasJustPublished());
$reply->created_at = Carbon::now()->subMonth();
$this->assertFalse($reply->wasJustPublished());
}
}
运行测试:
接下来我们来完善我们的授权策略。在前面的代码中,我们根据$user->lastReply->wasJustPublished()
来判断授权是否通过,但是我们没有考虑到如果一个用户没法发表过回复,那么$user->lastReply
的值将会是null
,这就会产生报错。我们要进行完善:
forum\app\Policies\ReplyPolicy.php
.
.
public function create(User $user)
{
if(! $lastReply = $user->lastReply) {
return true;
}
return ! $lastReply->wasJustPublished();
}
}
接着我们应用授权策略:
forum\app\Http\Controllers\RepliesController.php
.
.
public function store($channelId,Thread $thread)
{
try{
$this->authorize('create',new Reply);
$this->validate(request(),['body' => 'required|spamfree']);
$reply = $thread->addReply([
'body' => request('body'),
'user_id' => auth()->id(),
]);
}catch (\Exception $e){
return response(
'Sorry,your reply could not be saved at this time.',422
);
}
return $reply->load('owner');
}
.
.
现在我们可以运行一开始建立的测试了:
发现仍然失败了。我们来梳理一下我们的测试:
/** @test */
public function users_may_only_reply_a_maximum_of_once_per_minute()
{
$this->signIn();
$thread = create('App\Thread');
$reply = make('App\Reply',[
'body' => 'My simple reply.'
]);
$this->post($thread->path() . '/replies',$reply->toArray())
->assertStatus(200);
$this->post($thread->path() . '/replies',$reply->toArray())
->assertStatus(422);
}
在测试中,我们模拟新建了一个话题,并连续添加了两条回复。我们现在已经在添加回复时应用了create
授权策略:
public function create(User $user)
{
if(! $lastReply = $user->lastReply) {
return true;
}
return ! $lastReply->wasJustPublished();
}
看出问题的所在了吗?问题出在$user->lastReply
上面。其实这个问题我们在前面已经遇见过,我们知道,当作为属性访问 Eloquent 关联时,关联数据是「懒加载」的。这意味着在你第一次访问该属性时,才会加载关联数据。所以我们需要进行以下修改:
$lastReply = $user->fresh()->lastReply;
现在我们再来运行测试:
现在我们去应用进行测试:
你会发现仍然可以成功添加回复。这是怎么回事呢?明明我们的测试都已经通过了。不要忘了我们曾经在第 24 节设置用户 NoNo1 为管理员,所以对于 NoNo1 而言,所有的授权都会通过。我们换一个其他的账号进行测试:
我们还需要修改一下消息提示:
forum\app\Http\Controllers\RepliesController.php
.
.
public function store($channelId,Thread $thread)
{
if(Gate::denies('create',new Reply)) {
return response(
'You are posting too frequently.Please take a break.:)',422
);
}
try{
$this->validate(request(),['body' => 'required|spamfree']);
$reply = $thread->addReply([
'body' => request('body'),
'user_id' => auth()->id(),
]);
}catch (\Exception $e){
return response(
'Sorry,your reply could not be saved at this time.',422
);
}
return $reply->load('owner');
}
.
.
最后,运行全部测试: