39.筛选零回复的话题
- 本系列文章为
laracasts.com
的系列视频教程——Let's Build A Forum with Laravel and TDD 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频, 支持正版 ;- 视频源码地址:github.com/laracasts/Lets-Build-a-...;
- 本项目为一个 forum(论坛)项目,与本站的第二本实战教程 《Laravel 教程 - Web 开发实战进阶》 类似,可互相参照。
本节说明
- 对应视频教程第 39 小节:A User Can Filter By Unanswered Threads
本节内容
上一节的最后我们运行了测试,但是有失败的测试我们没有修复。我们现在进行修复:
首先修复an_authenticated_user_may_participate_in_forum_threads
:
forum\tests\Feature\ParticipateInForumTest.php
.
.
/** @test */
function an_authenticated_user_may_participate_in_forum_threads()
{
// Given we have a authenticated user
$this->signIn();
// And an existing thread
$thread = create('App\Thread');
// When the user adds a reply to the thread
$reply = make('App\Reply');
$this->post($thread->path() .'/replies',$reply->toArray());
// Then their reply should be visible on the page
// $this->get($thread->path())
// ->assertSee($reply->body);
$this->assertDatabaseHas('replies',['body' => $reply->body]);
}
.
.
我们用Ajax
的方式提交回复,所以我们的测试需要进行如上的修改。再次运行测试:
接着修复a_user_can_read_replies_that_are_associated_with_a_thread
。由于我们建立了a_user_can_request_all_replies_for_a_given_thread
测试,所以实际上a_user_can_read_replies_that_are_associated_with_a_thread
测试我们已经不需要了,删除即可。再次运行测试:
wo我们在上一节定义的每页回复数是 1 ,而现在每页回复数是 20。所以我们需要进行修改:
forum\tests\Feature\ReadThreadsTest.php
.
.
/** @test */
public function a_user_can_request_all_replies_for_a_given_thread()
{
$thread = create('App\Thread');
create('App\Reply',['thread_id' => $thread->id],40);
$response = $this->getJson($thread->path() . '/replies')->json();
$this->assertCount(20,$response['data']);
$this->assertEquals(40,$response['total']);
}
}
我们生成了 40 个回复,并且测试返回的首页数据是 20 个,且总数是40个。现在我们再次运行测试:
由此我们想到一个问题,现在我们每页的回复数是 20 个,我们想让用户点击翻页后聚焦到回复区域的最上方,给用户良好的使用体验:
forum\resources\assets\js\components\Replies.vue
.
.
refresh({data}) {
this.dataSet = data;
this.items = data.data;
window.scrollTo(0,0);
}
.
.
现在正式开始本节的内容:刷选零回复的话题。首先添加入口:
forum\resources\views\layouts\nav.blade.php
.
.
<ul class="dropdown-menu">
<li><a href="/threads">ALL Threads</a> </li>
@if(auth()->check())
<li><a href="/threads?by={{ auth()->user()->name }}">My Threads</a> </li>
@endif
<li><a href="/threads?popularity=1">Popular Threads</a> </li>
<li><a href="/threads?unanswered=1">Unanswered Threads</a> </li>
</ul>
.
.
接下来按照惯例新建测试:
forum\tests\Feature\ReadThreadsTest.php
.
.
/** @test */
public function a_user_can_filter_threads_by_those_that_are_unanswered()
{
$thread = create('App\Thread');
create('App\Reply',['thread_id' => $thread->id]);
$response = $this->getJson('threads?unanswered=1')->json();
$this->assertCount(1,$response);
}
/** @test */
public function a_user_can_request_all_replies_for_a_given_thread()
.
.
注:在我们的模型工厂文件中,当我们
create
一个reply
时,也会新建一个thread
,所以我们实际上会有两条话题,一个有回复,一个没有回复。所以我们期望筛选得到的没有回复的话题数量是 1.。
得益于我们之前的工作,我们现在想要新增一种筛选条件,只需要很少的代码就能实现:
forum\app\Filters\ThreadsFilters.php
.
.
class ThreadsFilters extends Filters
{
protected $filters = ['by','popularity','unanswered'];
.
.
public function unanswered()
{
return $this->builder->where('replies_count',0);
}
}
运行测试:
但是现在我们有一个新的想法:将replies_count
加到threads
表结构当中。因为我们在很多地方都用到replies_count
,并且我们在Thread.php
模型中使用了全局作用域预先进行加载replies_count
属性。首先我们去掉全局作用域:
forum\app\Thread.php
.
.
protected static function boot()
{
parent::boot();
static::deleting(function ($thread) {
$thread->replies->each->delete();
});
}
.
.
接下来我们修改迁移文件:
forum\database\migrations\2018_04_27_023037_create_threads_table.php
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateThreadsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('threads', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('user_id');
$table->unsignedInteger('channel_id');
$table->unsignedInteger('replies_count')->default(0);
$table->string('title');
$table->text('body');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('threads');
}
}
运行迁移:
$ php artisan migrate:refresh
我们还需要重新填充数据。进入Tinker
:
$ php artisan tinker
填充数据:
>>> factory('App\Thread',30)->create();
注意,我们修改了数据库表结构,相应的测试也需要进行修改:
forum\tests\Feature\ParticipateInForumTest.php
.
.
/** @test */
function an_authenticated_user_may_participate_in_forum_threads()
{
// Given we have a authenticated user
$this->signIn();
// And an existing thread
$thread = create('App\Thread');
// When the user adds a reply to the thread
$reply = make('App\Reply');
$this->post($thread->path() .'/replies',$reply->toArray());
// Then their reply should be visible on the page
// $this->get($thread->path())
// ->assertSee($reply->body);
$this->assertDatabaseHas('replies',['body' => $reply->body]);
$this->assertEquals(1,$thread->fresh()->replies_count);
}
.
.
运行测试会失败,因为我们还没有添加回复时replies_count
的处理逻辑:
模型事件可以轻松地帮助我们完成处理逻辑:
forum\app\Reply.php
.
.
class Reply extends Model
{
use Favoritable,RecordsActivity;
protected $guarded = [];
protected $with = ['owner','favorites'];
protected $appends = ['favoritesCount','isFavorited'];
protected static function boot()
{
parent::boot(); //
static::created(function ($reply){
$reply->thread->increment('replies_count');
});
static::deleted(function ($reply){
$reply->thread->decrement('replies_count');
});
}
.
.
再次运行测试:
还有一个测试需要修改:
forum\tests\Feature\ParticipateInForumTest.php
.
.
/** @test */
public function authorized_users_can_delete_replies()
{
$this->signIn();
$reply = create('App\Reply',['user_id' => auth()->id()]);
$this->delete("/replies/{$reply->id}")->assertStatus(302);
$this->assertDatabaseMissing('replies',['id' => $reply->id]);
$this->assertEquals(0,$reply->thread->fresh()->replies_count);
}
.
.
运行测试:
现在我们回到测试a_user_can_filter_threads_by_those_that_are_unanswered
当中来:
现在我们填充回复数据,然后在应用中进行测试。我们先进入Tinker
:
$ php artisan tinker
填充数据:
>>> factory('App\Reply',30)->create(['thread_id' => App\Thread::latest()->first()->id]);
进行测试:
最后运行全部测试: