Laravel 專案:使用 TDD 構建論壇 Chapter 15

洛未必達發表於2018-05-05

0.寫在前面

  • 本系列文章為laracasts.com 的系列視訊教程——Let's Build A Forum with Laravel and TDD 的學習筆記。若喜歡該系列視訊,可去該網站訂閱後下載該系列視訊, 支援正版
  • 視訊原始碼地址:https://github.com/laracasts/Lets-Build-a-Forum-in-Laravel
  • *本專案為一個 forum(論壇)專案,與本站的第二本實戰教程 Laravel 教程 - Web 開發實戰進階 ( Laravel 5.5 ) 類似,可互相參照
  • 專案開發模式為TDD開發,教程簡介為:

    A forum is a deceptively complex thing. Sure, it's made up of threads and replies, but what else might exist as part of a forum? What about profiles, or thread subscriptions, or filtering, or real-time notifications? As it turns out, a forum is the perfect project to stretch your programming muscles. In this series, we'll work together to build one with tests from A to Z.

  • 專案版本為 laravel 5.4,教程後面會進行升級到 laravel 5.5 的教學
  • 視訊教程共計 102 個小節,筆記章節與視訊教程一一對應

1.本節說明

  • 對應視訊第 15 小節:A Lesson in Refactoring

2.本節內容

本節讓我們來對之前不那麼優雅的程式碼進行重構。很多人都不喜歡進行重構,一個很重要的原因就是害怕重構了程式碼之後,其他地方會發生錯誤。可是對於我們來說,在重構的時候完全不用擔心這一點。因為我們有測試來幫我們保證已有功能不會被破壞:
file
forum\app\Http\Controllers\ThreadsController.php

.
.
public function index(Channel $channel)
{
    if($channel->exists){
        $threads = $channel->threads()->latest();
    }else{
        $threads = Thread::latest();
    }

    if($username = request('by')){
        $user = \App\User::where('name',$username)->firstOrFail();

        $threads->where('user_id',$user->id);
    }

    $threads  = $threads->get();

    return view('threads.index',compact('threads'));
}
.
.

現在我們需要對上面這段程式碼進行重構,通常的一個方法是將獲取$threads的程式碼片段抽取出來:

.
.
public function index(Channel $channel)
{
    $threads = $this->getThreads($channel);

    return view('threads.index',compact('threads'));
}
.
.
protected function getThreads(Channel $channel)
{
    if ($channel->exists) {
        $threads = $channel->threads()->latest();
    } else {
        $threads = Thread::latest();
    }

    if ($username = request('by')) {
        $user = \App\User::where('name', $username)->firstOrFail();

        $threads->where('user_id', $user->id);
    }

    $threads = $threads->get();
    return $threads;
}

執行測試,功能任然完整:
file
但是我們不準備這麼做。因為在後面我們可能會根據不同的條件進行篩選,僅僅像上面那樣,功能太單一,無法滿足我們的需求。
首先新建forum\app\Filters\ThreadsFilters.php

<?php

namespace App\Filters;

class ThreadsFilters
{

}

重新重構ThreadsController.php

.
.
use App\Filters\ThreadsFilters;
.
.
public function index(Channel $channel,ThreadsFilters $filters)
{
    if ($channel->exists) {
        $threads = $channel->threads()->latest();
    } else {
        $threads = Thread::latest();
    }

    $threads = $threads->filter($filters)->get();

    return view('threads.index',compact('threads'));
}
.
.

我們在頭部引用了ThreadsFilters類,並且注入到index()方法的引數中。通過呼叫模型層的filter()方法,獲取到相應篩選條件下的$htreads。此時filter()方法還不存在,接下來新建filter()方法:
forum\app\Thread.php

.
.
public function scopeFilter($query,$filters)
{
    return $filters->apply($query);
}
.

注:這裡我們使用了 Laravel 本地作用域 。本地作用域允許我們定義通用的約束集合以便在應用中複用。要定義這樣的一個作用域,只需簡單在對應 Eloquent 模型方法前加上一個 scope 字首,作用域總是返回 查詢構建器。一旦定義了作用域,則可以在查詢模型時呼叫作用域方法。在進行方法呼叫時不需要加上 scope 字首。如以上程式碼中的 filter() 。

接下來只需補充完整ThreadsFilters.php即可:

<?php

namespace App\Filters;

use App\User;
use Illuminate\Http\Request;

class ThreadsFilters
{
    protected $request;

    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    public function apply($builder)
    {
        if(! $username = $this->request->by) return $builder;

        $user = User::where('name',$username)->firstOrfail();

        return $builder->where('user_id',$user->id);
    }
}

執行測試:
file
測試全部通過,這意味著一切正常。重新整理頁面:
file
讓我們繼續重構:
forum\app\Filters\ThreadsFilters.php

<?php

namespace App\Filters;

use App\User;
use Illuminate\Http\Request;

class ThreadsFilters
{
    protected $request;
    protected $builder;

    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    public function apply($builder)
    {
        $this->builder = $builder;

        if(! $username = $this->request->by) return $builder;

        return $this->by($username);
    }

    /**
     * @param $username
     * @return mixed
     */
    protected function by($username)
    {
        $user = User::where('name', $username)->firstOrfail();

        return $this->builder->where('user_id', $user->id);
    }
}

執行測試:
file
一切正常,但是我們任然可以繼續重構:
forum\app\Filters\ThreadsFilters.php

.
.
public function apply($builder)
{
    $this->builder = $builder;

    if($this->request->has('by')){
        $this->by($this->request->by);
    }

    return $this->builder;
}
.
.

執行測試:
file
如果我們想在後面根據其他的條件進行篩選,那我們幾乎需要重寫一遍ThreadsFilters.php。我們不希望這麼做,所以我們繼續重構:
forum\app\Filters\ThreadsFilters.php

 <?php

namespace App\Filters;

use App\User;

class ThreadsFilters extends Filters
{
    protected $filters = ['by'];

    /**
     * @param $username
     * @return mixed
     */
    protected function by($username)
    {
        $user = User::where('name', $username)->firstOrfail();

        return $this->builder->where('user_id', $user->id);
    }
}

並且新建Filters.php基類檔案:
forum\app\Filters\Filters.php

 <?php

namespace App\Filters;

use Illuminate\Http\Request;

abstract class Filters
{

    protected $request,$builder;
    protected $filters = [];

    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    /**
     * @param $builder
     * @return mixed
     */
    public function apply($builder)
    {
        $this->builder = $builder;

        foreach ($this->filters as $filter){
            if( ! $this->hasFilter($filter)) return;

            $this->$filter($this->request->$filter);

        }

        return $this->builder;
    }

    /**
     * @param $filter
     * @return bool
     */
    protected function hasFilter($filter)
    {
        return method_exists($this, $filter) && $this->request->has($filter);
    }
}

我們新建了基類Filters ,並且將可複用的程式碼抽取到了基類中,使得ThreadsFilters十分簡潔。執行測試:
file
依然一切正常。現在訪問 http://forum.test/threads?by=NoNo1 這樣的地址已經正常,但是我們想讓在使用者訪問 http://forum.test/threads?by=NoNo1&bad=thing 這樣的地址的時候,將其他的請求引數過濾,只保留下我們認可的篩選條件。當前我們希望只保留下by=NoNo1讓我們進行篩選。所以繼續重構,使用 only 方法達到我們的目的:
forum\app\Filters\Filters.php

<?php

namespace App\Filters;

use Illuminate\Http\Request;

abstract class Filters
{

    protected $request,$builder;
    protected $filters = [];

    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    /**
     * @param $builder
     * @return mixed
     */
    public function apply($builder)
    {
        $this->builder = $builder;

        foreach ($this->getFilters() as $filter => $value){
            if(method_exists($this,$filter)){  // 注:此處是 hasFilter() 方法的重構
                $this->$filter($value);
            }
        }

        return $this->builder;
    }

    public function getFilters()
    {
        return $this->request->only($this->filters);
    }
}

注:我們重構之後將hasFilter()方法去掉了,因為我們有更好的寫法。

執行測試:
file
發現有一個測試未通過,我們將$this->getFilters()的值列印出來:

.
.
public function getFilters()
{dd($this->request->only($this->filters));
    return $this->request->only($this->filters);
}
.
.

file
這種情況下是正常的。 但是,當我們訪問 http://forum.test/threads 這樣的地址時:
file
此時by的值為null,然後我們呼叫了null方法,於是丟擲異常。我們希望沒有by請求引數時,就不進行篩選。intersect 方法可以滿足我們的需求。改用 intersect 方法:

.
.
public function getFilters()
{dd($this->request->intersect($this->filters));
    return $this->request->intersect($this->filters);
}
.
.

重新整理頁面:
file
去掉dd()後再次執行測試:
file
重新整理頁面:
file
最後,我們將控制器程式碼重構:
forum\app\Http\Controllers\ThreadsController.php

.
.
public function index(Channel $channel,ThreadsFilters $filters)
{
    $threads = Thread::latest()->filter($filters);

    if ($channel->exists) {
        $threads->where('channel_id',$channel->id);
    }

    $threads = $threads->get();

    return view('threads.index',compact('threads'));
}
.
.

filter()使用的是 Laravel 的 本地作用域 ,作用域返回 查詢構建器。所以我們可以很方便地鏈式呼叫where()方法:

$threads->where('channel_id',$channel->id);

任然,我們可以繼續將獲取$threads的程式碼抽取成一個新的方法。繼續重構:

.
.
public function index(Channel $channel,ThreadsFilters $filters)
{
    $threads = $this->getThreads($channel, $filters);

    return view('threads.index',compact('threads'));
}
.
.
protected function getThreads(Channel $channel, ThreadsFilters $filters)
{
    $threads = Thread::latest()->filter($filters);

    if ($channel->exists) {
        $threads->where('channel_id', $channel->id);
    }

    $threads = $threads->get();
    return $threads;
}

最後,執行一下測試:
file
一切正常,Perfect!

3.寫在後面

  • 如有建議或意見,歡迎指出~
  • 如果覺得文章寫的不錯,請點贊鼓勵下哈,你的鼓勵將是我的動力!

相關文章