摘要:最近,我需要在開發的事件管理系統中實現搜索功能。今天,我會介紹整個過程以及如何構建靈活且可擴展的搜索系統。這將是個挑戰前端的條件過濾的截圖。像剛剛的情況下搜索用戶時加上一個過濾器再返回搜索結果。
最近,我需要在開發的事件管理系統中實現搜索功能。 一開始只是簡單的幾個選項 (通過名稱,郵箱等搜索),到后面參數變得越來越多。
今天,我會介紹整個過程以及如何構建靈活且可擴展的搜索系統。如果你想查看代碼,請訪問?Git 倉庫?。
我們將創造什么我們公司需要一種跟蹤我們與世界各地客戶舉辦的各種活動和會議的方式。我們目前的唯一方法是讓每位員工在 Outlook 日程表上存儲會議的詳細信息。可拓展性較差!
我們需要公司的每個人都可以訪問,來查看我們客戶的被邀請的詳細信息以及他們的RSVP(國際縮用語:請回復)狀態。
這樣,我們可以通過上次與他們互動的數據來確定哪些用戶可以邀請來參加未來的活動。
使用高級搜索過濾器查找的截圖
查找用戶常用過濾用戶的方法:
通過姓名,電子郵件,位置
通過用戶工作的公司
被邀請參加特定活動的用戶
參加過特定活動的用戶
邀請及已參加活動的用戶
邀請但尚未回復的用戶
答應參加但未出席的用戶
分配給銷售經理的用戶
雖然這個列表不算完整,但可以讓我們知道需要多少個過濾器。這將是個挑戰!
前端的條件過濾的截圖。
模型及模型關聯在這個例子中我們回用到很多模型:
User?---? 代表被邀請參加活動的用戶。一個用戶可以參加很多活動。
Event?--- 代表我公司舉辦的活動?;顒涌梢杂卸鄠€。
Rsvp?---? 代表用戶對活動邀請的回復。一個用戶對一個活動的回復是一對一的。
Manager?---? 一個用戶可以對應多個我公司的銷售經理.
搜索的需求在開始代碼之前,我想先把搜索的需求明確一下。也就是說我要很清楚地知道我要對哪些數據做搜索功能。
下面就是一個例子:
{ "name": "Billy", "company": "Google", "city": "London", "event": "key-note-presentation-new-york-05-2016", "responded": true, "response": "I will be attending", "managers": [ "Tom Jones", "Joe Bloggs" ], }
總結一下上面數據想表達的搜索的條件:
客人的名字是 "Billy",來自 "Google" 公司,目前居住在 "London",已經對 "key-note-presentation-new-york-05--2016" 的活動邀請做出了回復,并且回復的內容是 "I will be attending",負責跟進這位客人的銷售經理是 "Tom Jones" 或者 "Joe Bloggs"。開始 --- 最佳實踐
我是一個堅定不移的極簡主義者,我堅信少即是多。下面就讓我們以最簡單的方式探索出解決這個需求的最佳實踐。
首先,在 ?routes.php 文件中添加如下代碼:
Route::post("/search", "SearchController@filter");
接下來,創建 ?SearchController.
php artisan make:controller SearchController
添加前面路由中明確的?filter()? 方法:
由于我們需要在 filter 方法中處理請求提交的數據,所以我把 Request 類做了依賴注入。Laravel 的服務容器 會解析這個依賴,我們可以在方法中直接使用 Request 的實例,也就是 $request。User 類也是同樣道理,我們需要從中檢索一些數據。
這個搜索需求有一點比較麻煩的是,每個參數都是可選的。所以我們要先寫一系列的條件語句來判斷每個參數是否存在:
這是我初步寫出來的代碼:
public function filter(Request $request, User $user) { // 根據姓名查找用戶 if ($request->has("name")) { return $user->where("name", $request->input("name"))->get(); } // 根據公司名查找用戶 if ($request->has("company")) { return $user->where("company", $request->input("company")) ->get(); } // 根據城市查找用戶 if ($request->has("city")) { return $user->where("city", $request->input("city"))->get(); } // 繼續根據其他條件查找 // 再無其他條件, // 返回所有符合條件的用戶。 // 在實際項目中需要做分頁處理。 return User::all(); }很明顯,上面的代碼邏輯是錯誤的。
首先,它只會根據一個條件去檢索用戶表,然后就返回了。所以,通過上面的代碼邏輯,我們根本無法獲得姓名為 "Billy", 而且住在 "London" 的用戶。
實現這種目的的一種方式是嵌套條件:
// 根據用戶名搜索用戶 if ($request->has("name")) { // 是否還提供了 "city" 搜索參數 if ($request->has("city")) { // 基于用戶名及城市搜索用戶 return $user->where("name", $request->input("name")) ->where("city", $request->input("city")) ->get(); } return $user->where("name", $request->input("name"))->get(); }我確信你可以看到這在兩個或者三個參數的時候起作用,但是一旦我們添加更多選項,這將會難以管理。
改進我們的搜索 api所以我們如何讓這個生效,而同時不會因為嵌套條件而變得瘋狂?
我們可以使用 User 模型繼續重構,來使用?builder?而不是直接返回模型。
public function filter(Request $request, User $user) { $user = $user->newQuery(); // 根據用戶名搜索用戶 if ($request->has("name")) { $user->where("name", $request->input("name")); } // 根據用戶公司信息搜索用戶 if ($request->has("company")) { $user->where("company", $request->input("company")); } // 根據用戶城市信息搜索用戶 if ($request->has("city")) { $user->where("city", $request->input("city")); } // 繼續執行其他過濾 // 獲得并返回結果 return $user->get(); }好多了!我們現在可以將每個搜索參數做為修飾符添加到從 ?$user->newQuery() 返回的查詢實例中。
我們現在可以根據所有的參數來做搜索了, 再多參數都不怕.
一起來實踐吧:
$user = $user->newQuery(); // 根據姓名查找用戶 if ($request->has("name")) { $user->where("name", $request->input("name")); } // 根據公司名查找用戶 if ($request->has("company")) { $user->where("company", $request->input("company")); } // 根據城市查找用戶 if ($request->has("city")) { $user->where("city", $request->input("city")); } // 只查找有對接我公司銷售經理的用戶 if ($request->has("managers")) { $user->whereHas("managers", function ($query) use ($request) { $query->whereIn("managers.name", $request->input("managers")); }); } // 如果有 "event" 參數 if ($request->has("event")) { // 只查找被邀請的用戶 $user->whereHas("rsvp.event", function ($query) use ($request) { $query->where("event.slug", $request->input("event")); }); // 只查找回復邀請的用戶( 以任何形式回復都可以 ) if ($request->has("responded")) { $user->whereHas("rsvp", function ($query) use ($request) { $query->whereNotNull("responded_at"); }); } // 只查找回復邀請的用戶( 限制回復的具體內容 ) if ($request->has("response")) { $user->whereHas("rsvp", function ($query) use ($request) { $query->where("response", "I will be attending"); }); } } // 最終獲取對象并返回 return $user->get();搞定,棒極了!
是否還需要重構?通過上面的代碼我們實現了業務需求,可以根據搜索條件返回正確的用戶信息。但是我們能說這是最佳實踐嗎?顯然是不能。
現在是通過一系列條件判斷的嵌套來實現業務邏輯,而且所有的邏輯都在控制器里,這樣做真的合適嗎?
這可能是一個見仁見智的問題,最好還是結合自己的項目,具體問題具體分析。如果你的項目比較小,邏輯相對簡單,而且只是一個短期需求的項目,那么就不必糾結這個問題了,直接照上面的邏輯寫就好了?!?/p>
然而,如果你是在構建一個比較復雜的項目,那么我們還是需要更加優雅且擴展性好的解決方案。
編寫新的搜索?api當我要寫一個功能接口的時候,我不會立刻去寫核心代碼,我通常會先想想我要怎么用這個接口。這可能就是俗稱的「面向結果編程」(或者說是「結果導向思維」)。
「在你寫一個組件之前,建議你先寫一些要用這個組件的測試代碼。通過這種方式,你會更加清晰地知道你究竟要寫哪些函數,以及傳哪些必要的參數,這樣你才能寫出真正好用的接口。因為寫接口的目的是簡化使用組件的代碼,而不是簡化接口自身的代碼?!?( 摘自:?c2.com)
根據我的經驗,這個方法能幫助我寫出可讀性更強,更加優雅的程序。還有一個很大的額外收獲就是,通過這種階段性的驗收測試,我能更好地抓住商業需求。因此,我可以很自信地說我寫的程序可以很好地滿足市場的需求,具有很高商業價值。
以下添加到搜索功能的代碼中,我希望我的搜索 api 是這樣寫的:
return UserSearch::apply($filters);這樣有著很好的可讀性。 根據經驗, 如果我查閱代碼能想看文章的句子一樣,那會非常美妙。像剛剛的情況下:
搜索用戶時加上一個過濾器再返回搜索結果。這對技術人員和非技術人員都有意義。
我想我需要新建一個 UserSearch 類,還需要一個靜態的 apply 函數來接收過濾條件。讓我開始吧:
最簡單的方式,讓我們把控制器中的代碼復制到 apply 函數中:
newQuery(); // 基于用戶名搜索 if ($filters->has("name")) { $user->where("name", $filters->input("name")); } // 基于用戶的公司名搜索 if ($filters->has("company")) { $user->where("company", $filters->input("company")); } // 基于用戶的城市名搜索 if ($filters->has("city")) { $user->where("city", $filters->input("city")); } // 只返回分配了銷售經理的用戶 if ($filters->has("managers")) { $user->whereHas("managers", function ($query) use ($filters) { $query->whereIn("managers.name", $filters->input("managers")); }); } // 搜索條件中是否包含 "event’ ? if ($filters->has("event")) { // 只返回被邀請參加了活動的用戶 $user->whereHas("rsvp.event", function ($query) use ($filters) { $query->where("event.slug", $filters->input("event")); }); // 只返回以任何形式答復了邀請的用戶 if ($filters->has("responded")) { $user->whereHas("rsvp", function ($query) use ($filters) { $query->whereNotNull("responded_at"); }); } // 只返回以某種方式答復了邀請的用戶 if ($filters->has("response")) { $user->whereHas("rsvp", function ($query) use ($filters) { $query->where("response", "I will be attending"); }); } } // 返回搜索結果 return $user->get(); } }我們做了一系列的改變。 首先, 我們將在控制器中的 $request 變量更名為 filters 來提高可讀性。
其次,由于 newQuery() 方法不是靜態方法,無法通過 User 類靜態調用,所以我們需要先創建一個 User 對象,再調用這個方法:
$user = (new User)->newQuery();調用上面的 UserSearch 接口,對控制器的代碼進行重構:
好多了,是不是?把一系列的條件判斷交給專門的類處理,使控制器的代碼簡介清新。
下面進入見證奇跡的時刻在這篇文章的例子中,一共有 7 個過濾條件,但是現實的情況是更多更多。所以在這種情況下,只用一個文件來處理所有的過濾邏輯,就顯得差強人意了。擴展性不好,而且也不符合 S.O.L.I.D. principles 原則。目前,apply()? 方法需要處理這些邏輯:
檢查參數是否存在
把參數轉成查詢條件
執行查詢
如果我想增加一個新的過濾條件,或者修改一下現有的某個過濾條件的邏輯,我都要不停地修改 UserSearch 類,因為所有過濾條件的處理都在這一個類里,隨著業務邏輯的增加,會有點尾大不掉的感覺。所以對每個過濾條件多帶帶建個類文件是非常有必要的。
先從 Name 條件開始吧。但是,就像我們前面講的,還是想一下我們需要怎樣使用這種單一條件過濾的接口。
我希望可以這樣調用這個接口:
$user = (new User)->newQuery(); $user = static::applyFiltersToQuery($filters, $user); return $user->get();不過這里再使用 $user 這個變量名就不合適了,應該用 $query 更有意義。
public static function apply(Request $filters) { $query = (new User)->newQuery(); $query = static::applyFiltersToQuery($filters, $query); return $query->get(); }然后把所有條件過濾的邏輯都放到 applyFiltersToQuery() 這個新接口里。
下面開始創建第一個條件過濾類:Name.
where("name", $value); } }在這個類里定義一個靜態方法 apply(),這個方法接收兩個參數,一個是 Builder 實例,另一個是過濾條件的值( 在這個例子中,這個值是 "Billy" )。然后帶著這個過濾條件返回一個新的 Builder 實例。
接下來是 City 類:
where("city", $value); } }如你所見,City 類的代碼邏輯跟 Name 類相同,只是過濾條件變成了 "city"。讓每個條件過濾類都只有一個簡單的 apply() 方法,而且方法接收的參數和處理的邏輯都相同,我們可以把這看成一個協議,這一點很重要,下面我會具體說明。
為了確保每個條件過濾類都能遵循這個協議,我決定寫一個接口,讓每個類都實現這個接口。
我為這個接口的方法寫了詳細的注釋,這樣做的好處是,對于每一個實現這個接口的類,我都可以利用我的 IDE ( PHPStorm ) 自動生成同樣的注釋。
下面,分別在 Name 和 City 類中實現這個 Filter 接口:
where("name", $value); } }以及
where("city", $value); } }完美?,F在已經有兩個條件過濾類完美地遵循了這個協議。把我的目錄結構附在下面給大家參考一下:
這是到目前為止關于搜索的文件結構。
我把所有的條件過濾類的文件放在一個多帶帶的文件夾里,這讓我對已有的過濾條件一目了然。
使用新的過濾器現在回過頭來看 UserSearch 類的 applyFiltersToQuery() 方法,發現我們可以再做一些優化了。
首先,把每個條件判斷里構建查詢語句的工作,交給對應的過濾類去做。
// 根據姓名搜索用戶 if ($filters->has("name")) { $query = Name::apply($query, $filters->input("name")); } // 根據城市搜索用戶 if ($filters->has("city")) { $query = City::apply($query, $filters->input("city")); }現在根據過濾條件構建查詢語句的工作已經轉給各個相應的過濾類了,但是判斷每個過濾條件是否存在的工作,還是通過一系列的條件判斷語句完成的。而且條件判斷的參數都是寫死的,一個參數對應一個過濾類。這樣我每增加一個新的過濾條件,我都要重新修改 UserSearch 類的代碼。這顯然是一個需要解決的問題。
其實,根據我們前面介紹的命名規則, 我們很容易把這段條件判斷的代碼改成動態的:
AppUserSearchFiltersNameAppUserSearchFiltersCity
就是結合命名空間和過濾條件的名稱,動態地創建過濾類(當然,要對接收到的過濾條件參數做適當的處理)。
大概就是這個思路,下面是具體實現:
private static function applyFiltersToQuery( Request $filters, Builder $query) { foreach ($filters->all() as $filterName => $value) { $decorator = __NAMESPACE__ . "Filters" . str_replace(" ", "", ucwords( str_replace("_", " ", $filterName))); if (class_exists($decorator)) { $query = $decorator::apply($query, $value); } } return $query; }下面逐行分析這段代碼:
foreach ($filters->all() as $filterName => $value) {遍歷所有的過濾參數,把參數名(比如 city)賦值給變量 $filterName,參數值(比如 London)復制給變量 $value。
$decorator = __NAMESPACE__ . "Filters" . str_replace(" ", "", ucwords( str_replace("_", " ", $filterName)));這里是對參數名進行處理,將下劃線改成空格,讓每個單詞都首字母大寫,然后去掉空格,如下例子:
"name" => AppUserSearchFiltersName, "company" => AppUserSearchFiltersCompany, "city" => AppUserSearchFiltersCity, "event" => AppUserSearchFiltersEvent, "responded" => AppUserSearchFiltersResponded, "response" => AppUserSearchFiltersResponse, "managers" => AppUserSearchFiltersManagers如果有參數名是帶下劃線的,比如 has_responded,根據上面的規則,它將被處理成 HasResponded,因此,其相應的過濾類的名字也要是這個。
if (class_exists($decorator)) {這里就是要先確定這個過濾類是存在的,再執行下面的操作,否則在客戶端報錯就尷尬了。
$query = $decorator::apply($query, $value);這里就是神器的地方了,PHP 允許把變量 $decorator 作為類,并調用其方法(在這里就是 apply() 方法了)?,F在再看這個接口的代碼,發現我們再次實力證明了磨刀不誤砍柴工。現在我們可以確保每個過濾類對外響應一致,內部又可以分別處理各自的邏輯。
最后的優化現在 UserSearch 類的代碼應該已經比之前好多了,但是,我覺得還可以更好,所以我又做了些改動,這是最終版本:
newQuery() ); return static::getResults($query); } private static function applyDecoratorsFromRequest(Request $request, Builder $query) { foreach ($request->all() as $filterName => $value) { $decorator = static::createFilterDecorator($filterName); if (static::isValidDecorator($decorator)) { $query = $decorator::apply($query, $value); } } return $query; } private static function createFilterDecorator($name) { return return __NAMESPACE__ . "Filters" . str_replace(" ", "", ucwords(str_replace("_", " ", $name))); } private static function isValidDecorator($decorator) { return class_exists($decorator); } private static function getResults(Builder $query) { return $query->get(); } }我最后決定去掉 applyFiltersToQuery() 方法,是因為感覺跟接口的主要方法名 apply() 有點沖突了。
而且,為了貫徹執行單一職責原則,我把原來 applyFiltersToQuery() 方法里比較復雜的邏輯又做了拆分,為動態創建過濾類名稱,和確認過濾類是否存在的判斷,都寫了多帶帶的方法。
這樣,即便要擴展搜索接口,我也不需要再去反復修改 UserSearch 類里的代碼了。需要增加新的過濾條件嗎?簡單,只要在 AppUserSearchFilters 目錄下創建一個過濾類,并使之實現 Filter 接口就 OK 了。
結論我們已經把一個擁有所有搜索邏輯的巨大控制器方法保存成一個允許打開過濾器的模塊化過濾系統,而不需要添加修改核心代碼。 像評論里?@rockroxx所建議的,另一個重構的方案是把所有方法提取到?trait 并將?User? 設置成 ?const? 然后由?Interface 實現。
class UserSearch implements Searchable { const MODEL = AppUser; use SearchableTrait; }如果你很好的理解了這個設計模式,你可以?利用多態代替多條件。
代碼會提交到?GitHub?你可以 fork,測試和實驗。
如何解決多條件高級搜索,我希望你能留下你的想法、建議和評論。
文章轉自:https://learnku.com/laravel/t...
更多文章:https://learnku.com/laravel/c...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/31090.html
摘要:安全生成安全的隨機數,加密數據,掃描漏洞的庫一個兼容標準的過濾器一個生成隨機數和字符串的庫使用生成隨機數的庫一個安全庫一個純安全通信庫一個簡單的鍵值加密存儲庫一個結構化的安全層一個試驗的面向對象的包裝庫一個掃描文件安全的庫 Security 安全 生成安全的隨機數,加密數據,掃描漏洞的庫 HTML Purifier-一個兼容標準的HTML過濾器 RandomLib-一個生成隨機數和字...
摘要:安全生成安全的隨機數,加密數據,掃描漏洞的庫一個兼容標準的過濾器一個生成隨機數和字符串的庫使用生成隨機數的庫一個安全庫一個純安全通信庫一個簡單的鍵值加密存儲庫一個結構化的安全層一個試驗的面向對象的包裝庫一個掃描文件安全的庫 Security 安全 生成安全的隨機數,加密數據,掃描漏洞的庫 HTML Purifier-一個兼容標準的HTML過濾器 RandomLib-一個生成隨機數和字...
摘要:是文檔的一種表示結構。這些任務大部分都是基于它。這個實踐的重點是把你在前端練級攻略第部分中學到的一些東西和結合起來。一旦你進入框架部分,你將更好地理解并使用它們。到目前為止,你一直在使用進行操作。它是在前端系統像今天這樣復雜之前編寫的。 本文是 前端練級攻略 第二部分,第一部分請看下面: 前端練級攻略(第一部分) 在第二部分,我們將重點學習 JavaScript 作為一種獨立的語言,如...
摘要:別堵塞了傳輸層大多數事件處理器被當作傳輸層組件。解耦事件處理器開始本命題前,我們來使用一個示例。假想下把隊列處理器用來發送消息給用戶。盡量避免在事件處理器中摻雜太多的業務邏輯。 聲明:本文并非博主原創,而是來自對《Laravel 4 From Apprentice to Artisan》閱讀的翻譯和理解,當然也不是原汁原味的翻譯,能保證90%的原汁性,另外因為是理解翻譯,肯定會有錯誤的...
閱讀 834·2023-04-26 00:13
閱讀 2836·2021-11-23 10:08
閱讀 2455·2021-09-01 10:41
閱讀 2121·2021-08-27 16:25
閱讀 4205·2021-07-30 15:14
閱讀 2367·2019-08-30 15:54
閱讀 867·2019-08-29 16:22
閱讀 2744·2019-08-26 12:13