2015年7月31日 星期五

Laravel 搜尋與分頁功能製作的一些心得

之前自學 Laravel 4 的時候,分頁跟搜尋做的真是...亂七八糟,知道亂七八糟是因為,知道了有更好的寫法,不然就是會一直停留在自己的已知。

後來這兩個月接觸 Laravel 5 之後,發現有些以前的寫法,可以寫得更好,比方說... 有些現成的 method,我沒發現以前可以那樣用之類的情況,還去網路上找了一些沒有必要的方法,所以打算先一些做紀錄 XD,描述一下大概的邏輯,不會把整份 code 把拿出來講,如果有其他做法也歡迎讓我知道。

我這邊所指的分頁跟搜尋,是以一般後台所需要的功能為主,文章可能會牽涉 controller, model 跟 view,畢竟一個搜尋,除了 ORM 要處理帶入的搜尋條件之外,也要影響分頁的顯示,還有排序等等情況要做。改天有空,我會再分享一篇前端用 ajax 撈 json 的分頁方式。


我們先從 ORM 說起吧,在沒有分頁之前,撈資料,建 Model 都是稀鬆平常的事,最常見的撈法就是整個全撈出來,像這樣:
$articles = Article::all();
這個全部撈出來的 $articles 變數,可能會放在 ArticleController 某個顯示列表的 method,我來假設那個 method 叫做 getIndex 好了,那麼 ArticleController 的 getIndex 會將 $news 的變數拋到某個 view 去:
// ArticleController.php

public function getIndex() {
    
    // ORM...
    // $articles = ....

    return view('article.index', ['articles' => $articles]);

}

接下來,來做一點複雜的情況,假設在 article.view 加了一個搜尋用的 Form 表單,讓 user 可以搜尋文章的作者(Author),文章的標題(Title),文章的流水號(Id)。假設 user 送出了那個表單,應該發生什麼事情?

(1) 如果是用 GET  method 送出表單,網址應該呈現 localhost/article?author=winwu&title=&id=&sort=id&order=desc

(2) view 得到的資料要是正確的

(3) 資料超過某個數量後,要顯示分頁

先來解決 (1),當搜尋的條件已經進到頁面時,ORM 就需要做一些改變,首先我會在 controller 決定我願意接受的搜尋條件參數: (簡單來說其實就是接 $_GET 參數啦! 只是我現在 focus 在 Laravel 上。)
$queryString =  Request::only([ 'id', 'author', 'title', 'order', 'sort]); 
接到 GET 參數後,接下來,需要把條件,帶到 ORM 的搜尋表達上,在這邊,我個人比較偏好 where 帶 $query 方式去做搜尋,因為比較彈性,也可以在 function 下一般的 if else 或是一些商業邏輯的判斷, 資料轉換:
// default desc
$order = (isset($queryString['order']) && ($queryString['order'] === 'asc')) ? 'asc' : 'desc';

// defaul sort
$sort = (isset($queryString['sort'])) ? $queryString['sort'] : 'id';

$articles = Article::where(function($query) use ($queryString) {


if (isset($queryString['id']) && $queryString['id'] !== "" ) {
$query->where('id', '=',  $queryString['id'] );
}

if (isset($queryString['author']) && $queryString['author'] !== "" ) {
$query->where('author', 'LIKE', '%' . $queryString['author'] . '%');
}
        // do other things 
        
}) // 如果你需要 join 其他表,可以接在這裡: 
   /*
      -> join('other_table', function ($join) {
          $join->on('article.id', '=', 'other_table.reference')
      })     
   */;

// 處理order, sort 以及分頁數量設定 10 筆為一頁
$articles =  $articles->orderBy($sort, $order)->paginate(10);
分頁筆數的部分,建議也可以拆出去用變數傳入。
(1) 的部分,算是到一個段落了。


接下來,處理 (2)。
view 就只有兩件事,兩件事都可以簡單,也可以複雜。
(2-1) 顯示分頁
(2-2) 列表的表格 tr th 上,要有排序的功能可以點按。

(2-1) 相對單純,pagination 只要看官方文件即可,顯示方式也跟 orm 那邊的設定有關,如果你是用 ->simplePaginate(10) 那就只會呈現只有上一頁, 下一頁的分頁顯示,如果是 ->paginate(10),就會顯示比較完整的分頁。

顯示分頁,要在 article.view 加上 {!! $articles->render() !!} ,就會顯示出分頁了,這就是 (3) 。

不過這裡有個問題要注意,當我已經有搜尋某個條件之後,點到分頁的第二頁,原本的 query string 並不會帶到第二頁去,網址只會變成  localhost/article?page=2  而不會是  localhost/article?author=ssss&title=&id=&sort=id&order=desc&page=2

這怎麼辦呢,有個好用的 method 叫做 appends(),可以把當下頁面上的搜尋條件參數,帶到分頁 link 的 url 上,你可以參考文件的 Appending To Pagination Links,或是像我一樣操作在 ORM 上。

我加上了 appends 到 articles 上。
$articles =  $articles->orderBy($sort, $order)
                                ->paginate(10)

                                ->appends($querystringArray);
因為我把 appends 這個項目加在 ORM 上,所以我的 view 的  {!! $articles->render() !!}  完全不用調整。你可以挑一個做法來用。

(2-2) 的話,排序 icon 大部份都是採用 bootstrap 的 icon 或是 font-awesome,麻煩就在這個排序的按鈕跟分頁其實是有些類似的,他一樣要帶入當下的搜尋條件,除了 order 跟 sort 要改變。

這個部分我至今也沒有很好的解法,但一直寫類似的 code,有些麻煩,後來我把這塊的 html 拆出去寫成兩個 function (這兩個 function 可以寫在 article.index 的 view 的最上方,好一點的做法就是拆出去一個 class 去包裝他 )

function getArticleSortLinks($sortField) {
  $sortLinks = route('article.index',
array(
     'sort'      => $sortField,
     'order'    => (isset($_GET['order']) && $_GET['order'] == 'desc') 
                                  ? 'asc' : 'desc',
     'id'          => isset($_GET['id']) ? $_GET['id'] : '',
     'author'  =>  isset($_GET['author']) ? $_GET['author'] : '',
     'title'       =>  isset($_GET['title']) ? $_GET['title'] : ''
     ));
     return $sortLinks;
}

function getSortIcons($sortField) {
  /*
     這個寫法的 else 會有一些問題,但就簡單參考一下。
   
     如果當下的排序是遞減,那個 icon 就要切換成遞增,反之。

     然後預設我是擺 asc。
   */

  if (isset($_GET['sort']) && ($_GET['sort'] == $sortField)) {
    $sortLinkIcons = ''
    . 'fa fa-sort-amount-'
    . ( (isset($_GET['order']) && ($_GET['order'] == 'desc')) ? 'asc' : 'desc');
  } else {
    $sortLinkIcons = ''
    . 'fa fa-sort-amount-asc';
  }
  
  return  $sortLinkIcons;
}

接著在排序的按鈕使用這個 function: (只舉某個欄位當作例子)
<th>文章標題
    <a href="{{ getArticleSortLinks('title')}}">
          <span class="{{ getSortIcons('title')}}"></span>
    </a>
</th>

大概是這樣,有想到什麼我會再補充。
另外根據經驗,如果 ORM 需要 join 到三張表以上,會變得蠻慢的... QQ。

下週還會跟公司的架構師做一些 code 的調整,有什麼我覺得一定會修正的部分,我會再補上來 :P

2 則留言:

  1. 沒有 (3) ?
    @@
    是太簡單了不用寫
    還是忘了寫XD

    回覆刪除
    回覆
    1. 抱歉, (3) 其實已經包含在 (2) 裡面了,只是我忘記提及。

      刪除

若你看的文章,時間太久遠的問題就別問了,因為我應該也忘了... XD

Vue multiselect set autofocus and tinymce set autofocus

要在畫面一進來 focus multiselect 的方式: 參考: https://jsfiddle.net/shentao/mnphdt2g/ 主要就是在 multiselect 的 tag 加上 ref (例如: my_multiselect), 另外在 mounted...