27.将查询从控制器抽取到模型中

未匹配的标注

本节说明

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

运行测试:
file
前往添加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)
        ]);
    }
}

再次运行测试:
file
测试只是初步通过,因为我们在测试中新建了两个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')
        ));
    }
}

再次测试:
file
测试未通过,让我们来排查是哪里出来问题。我们将结果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')
    ));
}
.

运行测试查看打印出来的结果:
file
可以很清楚地看到:第一个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_atupdated_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')
        ));
    }
}

再次运行测试:
file
现在我们再做一点点小修改:
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)
        ]);
    }
}

再次测试,仍然通过:
file
运行全部测试:
file
我们看一下失败的测试:
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);
}

再次运行测试:
file

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 0
发起讨论 只看当前版本


暂无话题~