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.本節內容
本節讓我們來對之前不那麼優雅的程式碼進行重構。很多人都不喜歡進行重構,一個很重要的原因就是害怕重構了程式碼之後,其他地方會發生錯誤。可是對於我們來說,在重構的時候完全不用擔心這一點。因為我們有測試來幫我們保證已有功能不會被破壞: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;
}
執行測試,功能任然完整:
但是我們不準備這麼做。因為在後面我們可能會根據不同的條件進行篩選,僅僅像上面那樣,功能太單一,無法滿足我們的需求。
首先新建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);
}
}
執行測試:
測試全部通過,這意味著一切正常。重新整理頁面:
讓我們繼續重構: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);
}
}
執行測試:
一切正常,但是我們任然可以繼續重構: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;
}
.
.
執行測試:
如果我們想在後面根據其他的條件進行篩選,那我們幾乎需要重寫一遍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
十分簡潔。執行測試:
依然一切正常。現在訪問 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()
方法去掉了,因為我們有更好的寫法。
執行測試:
發現有一個測試未通過,我們將$this->getFilters()
的值列印出來:
.
.
public function getFilters()
{dd($this->request->only($this->filters));
return $this->request->only($this->filters);
}
.
.
這種情況下是正常的。 但是,當我們訪問 http://forum.test/threads 這樣的地址時:
此時by
的值為null
,然後我們呼叫了null
方法,於是丟擲異常。我們希望沒有by
請求引數時,就不進行篩選。intersect 方法可以滿足我們的需求。改用 intersect 方法:
.
.
public function getFilters()
{dd($this->request->intersect($this->filters));
return $this->request->intersect($this->filters);
}
.
.
重新整理頁面:
去掉dd()
後再次執行測試:
重新整理頁面:
最後,我們將控制器程式碼重構: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;
}
最後,執行一下測試:
一切正常,Perfect!
3.寫在後面
- 如有建議或意見,歡迎指出~
- 如果覺得文章寫的不錯,請點贊鼓勵下哈,你的鼓勵將是我的動力!