27.将查询从控制器抽取到模型中
- 本系列文章为
laracasts.com
的系列视频教程——Let's Build A Forum with Laravel and TDD 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频, 支持正版 ;- 视频源码地址:github.com/laracasts/Lets-Build-a-...;
- 本项目为一个 forum(论坛)项目,与本站的第二本实战教程 《Laravel 教程 - Web 开发实战进阶》 类似,可互相参照。
本节说明
- 对应视频第 27 小节:Extracting a Controller Query to the Model
本节内容
上一节我们查询了动作流,并且在页面进行了显示。但是我们的查询动作放在控制器中,我们想把与数据库的交互放到模型中,然后用类似Activity::feed($user)
的方法获取。首先我们编写测试逻辑:
forum\tests\Unit\ActivityTest.php
.
.
/** @test */
public function it_fetches_a_feed_for_any_user()
{
// Given we have a thread
// And another thread from a week ago
// When we fetch their feed
// Then,it should be returned in the proper format.
}
}
按照测试逻辑填充具体的代码:
<?php
namespace Tests\Unit;
use App\Activity;
use Carbon\Carbon;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class ActivityTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function it_records_activity_when_a_thread_is_created()
{
$this->signIn();
$thread = create('App\Thread');
$this->assertDatabaseHas('activities',[
'type' => 'created_thread',
'user_id' => auth()->id(),
'subject_id' => $thread->id,
'subject_type' => 'App\Thread'
]);
$activity = Activity::first(); // 当前测试中,表里只存在一条记录
$this->assertEquals($activity->subject->id,$thread->id);
}
/** @test */
public function it_records_activity_when_a_reply_is_created()
{
$this->signIn();
$reply = create('App\Reply');
$this->assertEquals(2,Activity::count());
}
/** @test */
public function it_fetches_a_feed_for_any_user()
{
// Given we have a thread
$this->signIn();
create('App\Thread');
// And another thread from a week ago
create('App\Thread',[
'user_id' => auth()->id(),
'created_at' => Carbon::now()->subWeek()
]);
// When we fetch their feed
$feed = Activity::feed(auth()->user());
// Then,it should be returned in the proper format.
$this->assertTrue($feed->keys()->contains(
Carbon::now()->format('Y-m-d')
));
}
}
运行测试:
前往添加feed()
方法:
forum\app\Activity.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Activity extends Model
{
protected $guarded = [];
public function subject()
{
return $this->morphTo();
}
public static function feed($user)
{
return $user->activity()->latest()->with('subject')->take(50)->get()->groupBy(function ($activity) {
return $activity->created_at->format('Y-m-d');
});
}
}
控制器调用:
forum\app\Http\Controllers\ProfilesController.php
<?php
namespace App\Http\Controllers;
use App\Activity;
use App\User;
class ProfilesController extends Controller
{
public function show(User $user)
{
return view('profiles.show',[
'profileUser'=> $user,
'activities' => Activity::feed($user)
]);
}
}
再次运行测试:
测试只是初步通过,因为我们在测试中新建了两个thread
,但是只测试了当前创建的那条。我们将测试补充完整:
forum\tests\Unit\ActivityTest.php
.
.
/** @test */
public function it_fetches_a_feed_for_any_user()
{
// Given we have a thread
$this->signIn();
create('App\Thread',['user_id' => auth()->id()]);
// And another thread from a week ago
create('App\Thread',[
'user_id' => auth()->id(),
'created_at' => Carbon::now()->subWeek()
]);
// When we fetch their feed
$feed = Activity::feed(auth()->user());
// Then,it should be returned in the proper format.
$this->assertTrue($feed->keys()->contains(
Carbon::now()->format('Y-m-d')
));
$this->assertTrue($feed->keys()->contains(
Carbon::now()->subWeek()->format('Y-m-d')
));
}
}
再次测试:
测试未通过,让我们来排查是哪里出来问题。我们将结果dd()
出来:
.
.
/** @test */
public function it_fetches_a_feed_for_any_user()
{
// Given we have a thread
$this->signIn();
create('App\Thread',['user_id' => auth()->id()]);
// And another thread from a week ago
create('App\Thread',[
'user_id' => auth()->id(),
'created_at' => Carbon::now()->subWeek()
]);
// When we fetch their feed
$feed = Activity::feed(auth()->user());
dd($feed->toArray()); -->将 $feed 打印出来
// Then,it should be returned in the proper format.
$this->assertTrue($feed->keys()->contains(
Carbon::now()->format('Y-m-d')
));
$this->assertTrue($feed->keys()->contains(
Carbon::now()->subWeek()->format('Y-m-d')
));
}
.
运行测试查看打印出来的结果:
可以很清楚地看到:第一个thread
正常,但是第二个thread
虽然创建时间是一个星期之前,但是它动作流的创建时间却仍旧是当前时间。
注:当前的
subject
即代表的是thread
。
我们来看看为什么会产生这样的问题。我们是使用RecordsActivity
Trait来创建动作流,所以我们看一下动作流是如何被创建的:
forum\app\RecordsActivity.php
.
.
protected function recordActivity($event)
{
$this->activity()->create([
'user_id' => auth()->id(),
'type' => $this->getActivityType($event)
]);
}
.
.
因为我们没有对created_at
和updated_at
字段做设置,所以 Laravel 会默认设置为当前时间。那么我们就知道了,动作流的updaed_at
字段会默认为当前时间,所以我们的测试才会失败。既然知道了原因,那么修复就很简单了:
forum\tests\Unit\ActivityTest.php
.
.
/** @test */
public function it_fetches_a_feed_for_any_user()
{
$this->signIn();
// Given we have a thread
// And another thread from a week ago
create('App\Thread',['user_id' => auth()->id()],2);
auth()->user()->activity()->first()->update(['created_at' => Carbon::now()->subWeek()]);
// When we fetch their feed
$feed = Activity::feed(auth()->user());
// Then,it should be returned in the proper format.
$this->assertTrue($feed->keys()->contains(
Carbon::now()->format('Y-m-d')
));
$this->assertTrue($feed->keys()->contains(
Carbon::now()->subWeek()->format('Y-m-d')
));
}
}
再次运行测试:
现在我们再做一点点小修改:
forum\app\Activity.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Activity extends Model
{
protected $guarded = [];
public function subject()
{
return $this->morphTo();
}
public static function feed($user,$take = 50)
{
return static::where('user_id',$user->id)
->latest()->
with('subject')
->take($take)
->get()
->groupBy(function ($activity) {
return $activity->created_at->format('Y-m-d');
});
}
}
forum\app\Http\Controllers\ProfilesController.php
<?php
namespace App\Http\Controllers;
use App\Activity;
use App\User;
class ProfilesController extends Controller
{
public function show(User $user)
{
return view('profiles.show',[
'profileUser'=> $user,
'activities' => Activity::feed($user)
]);
}
}
再次测试,仍然通过:
运行全部测试:
我们看一下失败的测试:
forum\tests\Feature\ProfilesTest.php
.
.
/** @test */
public function profiles_display_all_threads_created_by_the_associated_user()
{
$user = create('App\User');
$thread = create('App\Thread',['user_id' => $user->id]);
$this->get("/profiles/{$user->name}")
->assertSee($thread->title)
->assertSee($thread->body);
}
.
.
我们在新建thread
之后会创建动作流,而动作流的user_id
是当前登录用户的id
。但是在我们的测试中,我们并不存在已登录用户。我们进行修复:
forum\tests\Feature\ProfilesTest.php
.
.
/** @test */
public function profiles_display_all_threads_created_by_the_associated_user()
{
$this->signIn();
$thread = create('App\Thread',['user_id' => auth()->id()]);
$this->get("/profiles/" . auth()->user()->name)
->assertSee($thread->title)
->assertSee($thread->body);
}
再次运行测试: