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

2015年7月30日 星期四

Laravel phpunit 報錯 Cannot redeclare .. previously declared ...

今天在 laravel 的專案下跑 phpunit 突然報了一堆 [Symfony\Component\Debug\Exception\FatalErrorException] 的錯誤。

可能的原因有很多。

以我的問題來說,一追才知道原來是因為我在 blade.php 的 view 下,寫了兩隻現成的 function,但是 phpunit 不能這樣做,測試被執行之後,重複執行相同的函數,就等於函數被重新定義,所以好方法就是,還是得乖乖的把這兩個 function 封裝到某個 class 下。

原本我的 xx.blade.php 是這樣:
function myViewHelper() {
      return ...;
}

<a href="{{ myViewHelper() }}">Go!</a>

最後我寫了一支類似 helper 或是 service 的 php,把 class use 進來,再 call 那個 function,得救 :P 。

類似這樣:
use XXX\XXX\ myViewHelper as myViewHelper;

<a href="{{ myViewHelper::getSortLinks()  }}">Go!</a>


應該還有更好的方式,可能 use 不應該放在 view(xx.blade.php) 的檔案裡,不過重點就是... 不能直接定義 function 在 view 裡。



2015年7月21日 星期二

homebrew 升級 PHP56

1. 先 tap hombrew-php
brew tap homebrew/homebrew-php

2. 安裝 php56 (我沒有刻意移除過去的存在的 php55版本)(安裝需要一點時間)
brew install php56

3. 修改你的檔案 .profile, .zshrc, .bashrc 或者 .bash_profile,看你用哪一種,加入以下的 export $PATH:
export PATH="$(brew --prefix homebrew/php/php56)/bin:$PATH”

4. 重新 source  .profile, .zshrc, .bashrc 或者 .bash_profile:
像我是用 .zshrc:
source ~/.zshrc

2015年7月15日 星期三

[chai]The ChromeDriver could not be found on the current PATH

在使用 chai 的 webdriver 時出現了一點問題,其 demo code 如下,基本上同官方網站:
// test/index.js
var sw = require('selenium-webdriver');
var chai = require('chai');
var chaiWebdriver = require('chai-webdriver');

// Start with a webdriver instance:
var driver = new sw.Builder()
  .withCapabilities(sw.Capabilities.chrome())
  .build();

// And then...
chai.use(chaiWebdriver(driver));

// And you're good to go!
driver.get('http://github.com');
chai
  .expect('#site-container h1.heading')
  .dom.to.not.contain.text("I'm a kitty!");

然而在執行的時候出現這個問題:
node test/index.js
XXXX/node_modules/selenium-webdriver/chrome.js:54
        'http://chromedriver.storage.googleapis.com/index.html and ensure ' +
                                                                            ^
Error: The ChromeDriver could not be found on the current PATH. Please download the latest version of the ChromeDriver from http://chromedriver.storage.googleapis.com/index.html and ensure it can be found on your PATH.
    at Error (native)
而且其實我的 chromeDriver 已經是最新版了...

後來在 stackoverflow 找到了這個解法,還蠻有用的:
http://stackoverflow.com/questions/27733731/passing-requirechromedriver-path-directly-to-selenium-webdriver

修改結果如下:
var sw = require('selenium-webdriver');
var chai = require('chai');
var chaiWebdriver = require('chai-webdriver');

// Start with a webdriver instance:
var chrome = require('selenium-webdriver/chrome');
var path = require('chromedriver').path;

var service = new chrome.ServiceBuilder(path).build();
chrome.setDefaultService(service);

var driver = new sw.Builder()
    .withCapabilities(sw.Capabilities.chrome())
    .build();

// And then...
chai.use(chaiWebdriver(driver));

// And you're good to go!
driver.get('http://github.com');
chai
  .expect('#site-container h1.heading')
  .dom.to.not.contain.text("I'm a kitty!");

FYI.


2015年7月13日 星期一

用 safari 照相一直 crash 的一種解法

http://stackoverflow.com/questions/27763729/iphone-ios-8-buffer-limit-on-html5-media-capture

2015/07/31 補充:
這個問題後續有些想法,因為手機照相,我跟我朋友有個結論是,iphone 6 以下都沒有問題,初步猜測,也許是因為 iphone6 拍出來得照片太大,在轉換 base64 之後太大,導致 safari crash...

Vue multiselect set autofocus and tinymce set autofocus

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