在開發 Web 應用過程中,條件搜尋是一個不可避免的話題。 特別是在後臺的開發中,會有很多根據條件來選篩選列表的場景。比如說使用者手機號、暱稱,產品的類別價格範圍等等,常規方法中,我們都是根據前臺不同的條件,然後進行相應的程式碼搜尋。但是這樣的擴充套件性很一般,特別在增加新條件的時候,需要去侵入業務程式碼,我們來看看傳統的程式碼中是怎麼實現的:
public function filter(Request $request, User $user) {
$user = (new User())->newQuery();
if ($request->has('name')) {
return $user->where('name', $request->input('name'))->get();
}
if ($request->has('company')) {
return $user->where('company', $request->input('company'));
}
return $user->get();
}
複製程式碼
這樣的程式碼沒有任何問題,也可以執行的很好,但是假如我想再增加一個手機號的篩選呢?又得要來改這一段的程式碼,如果有涉及到關聯查詢,程式碼可能會更加繁瑣,所以可以抽離一個單獨的搜尋元件出來。
基礎
我們定義一個單獨的使用者搜尋元件:
class UserSearch {
public static function apply(Request $filters) {
//TODO filter
}
}
複製程式碼
然後就可以在控制器中這樣來使用:
class SearchController extends Controller {
public function filter(Request $request) {
return UserSearch::apply($request);
}
}
複製程式碼
雖然我們只是把搜尋的程式碼移到一個單獨的類中了,但是已經比之前看起來好多了。然後再進一步抽象:
public static function apply(Request $filters){
$query = (new User)->newQuery();
$query = static::applyFiltersToQuery($filters, $query);
return $query->get();
}
複製程式碼
但是這樣還是需要判斷很多的條件,我們可以把篩選條件抽象成介面:
interface Filter{
public static function apply(Builder $builder, $value);
}
複製程式碼
然後定義一系列的搜尋規則:
class Name implements Filter{
public static function apply(Builder $builder, $value)
{
return $builder->where('name', $value);
}
}
複製程式碼
class City implements Filter
{
public static function apply(Builder $builder, $value)
{
return $builder->where('city', $value);
}
}
複製程式碼
這樣就可以和表單中的請求引數對應起來:
private static function applyFiltersToQuery(Request $filters, Builder $query) {
foreach ($filters->all() as $filterName => $value) {
$decorator =
__NAMESPACE__ . '\\Filters\\' . ucwords($filterName);
if (class_exists($decorator)) {
$query = $decorator::apply($query, $value);
}
}
return $query;
}
複製程式碼
進階
其實以上已經可以滿足大部分需求了,但是我們還可以再進一步的簡化。我們知道,搜尋一般都是搜尋特定的模型,並且會有一個獨立的搜尋介面,以上的程式碼每個模型都要寫一個搜尋元件,而且搜尋大部分都是等於和範圍查詢,所以我們可以把這一段再抽象一下。定義一個模型和字串的配置檔案:
return [
'user' => User::class
];
複製程式碼
然後定義路由,並且在控制器中例項化出模型,搜尋服務只要專注於查詢即可:
Route::get('{model}/search', 'SearchController@search');
複製程式碼
class SearchController{
public function search(Request $request, $model){
$model = config('search.'.$model);
$query = app($model)->newQuery();
Search::apply($request, $query);
}
}
複製程式碼
然後這是我們的搜尋服務:
class Search {
public static function apply(Request $filters, Builder $query) {
$query = static::applyFiltersToQuery($filters, $query);
return $query->get();
}
}
複製程式碼
還可以再精簡一下嗎?可以的,想想看我們的搜尋大部分都是等值搜尋和範圍搜尋,那麼我們就可定義幾個常用的過濾器:
class EqualFilter implements Filter {
public static function apply(Builder $builder, $key, $value) {
return $builder->where($key, $value);
}
}
複製程式碼
class RangeFilter implements Filter {
public static function apply(Builder $builder, $key, $value) {
return $builder->whereBetween($key, $value);
}
}
複製程式碼
然後定義好一些規則,那麼就可以這樣來:
public static function applyFiltersToQuery(array $filters, Builder $query) {
foreach ($filters as $filterName => $value) {
$decorator = static::createFilterDecorator($filterName, $value);
$query = $decorator::apply($query, $filterName, $value);
}
return $query;
}
private static function createFilterDecorator($filterName, $value) {
$stub = __NAMESPACE__ . '\Filters\%sFilter';
$filter = sprintf($stub, ucwords(str_replace('_', ' ', $filterName)));
if (class_exists($filter)){
return $filter;
}
if (is_array($value)){ //range
return RangeFilter::class;
}
return EqualFilter::class;
}
複製程式碼
這段程式碼的目的就是當有其他的搜尋條件時候,我們可以自定義一系列的過濾器,只要名稱對應上即可:
$stub = __NAMESPACE__ . '\Filters\%sFilter';
$filter = sprintf($stub, ucwords(str_replace('_', ' ', $filterName)));
複製程式碼
總結
我們從一個非常單一巨大控制器方法中,重構到了現在可以在不修改任何核心程式碼的情況下增加或者刪除一些過濾器。這是一個比較好的設計模式,一旦你理解了之後很多類似的問題都可以解決了。
歡迎關我的個人公眾號:左手程式碼(公眾號後臺傳送 jetbrains,你懂得~)