2014/11/25

PHP array_merge() 在資料型別上行為的差異

array_merge() 可合併多個陣列,遇到相同的陣列索引,將由後方陣列的值取代前一個值。

如:
$d = array(
  'a' => 111,
  'b' => 222,
);
 
$e = array(
  'a' => 123,
  'c' => 333,
);
var_dump(array_merge($d, $e));

執行結果為:
array(3) {
  ["a"]=>
  int(123)
  ["b"]=>
  int(222)
  ["c"]=>
  int(333)
}

此特性常被用來做設定選項合併,如:
function__construct(array $config){
  $defaultsStyle = array(
    'display' => true,
    'color' => 'red',
    'size' => '14px',
  );

  $settings = array_merge($defaultStyle, $config);

  // initialize by $settings
}

上面的寫法可以讓預設的三項索引「display」、「color」、「size」一定存在,不需特別在做 array_key_exists() 檢查,且外部傳入的數值,會將內定的預設值覆蓋掉。

不過只有字串型態的陣列索引,經過 array_merge() 以後可以保留原有的索引名稱,若為數字型態的陣列索引,array_merge() 會將原有索引打掉重新排列。

如:
$d = array(
  0 => 111,
  1 => 222,
);
 
$e = array(
  9 => 123,
  7 => 333,
);
var_dump(array_merge($d, $e));

執行的結果會是:
array(4) {
  [0]=>
  int(111)
  [1]=>
  int(222)
  [2]=>
  int(123)
  [3]=>
  int(333)
}


另外 array_merge() 有個很奇怪的行為,假設陣列的索引是「類似數字」的字串 (不曉得判斷規則為何),像是 "100"、"200",經過 array_merge() 時會被當成數字型索引處理。

如:
$d = array(
  '100' => 111,
  '200' => 222,
);
 
$e = array(
  '100' => 100,
  '300' => 333,
);
var_dump(array_merge($d, $e));

執行後為:
array(4) {
  [0] =>
  int(111)
  [1] =>
  int(222)
  [2] =>
  int(100)
  [3] =>
  int(333)
}

若索引名稱為 "001"、"002",經過 array_merge() 才會被當成字串,不會有上述情況發生。

要避免索引被吃掉,可以改用 array unioin operator:
$d = array(
  '100' => 111,
  '200' => 222,
);
 
$e = array(
  '100' => 100,
  '300' => 333,
);
var_dump($d + $e));

陣列索引會被保留下來:
array(3) {
  [100] =>
  int(111)
  [200] =>
  int(222)
  [300] =>
  int(333)
}

不過要注意的是,array union operator 遇到相同索引時,陣列值並不會被後方的陣列取代 ($merged['100'] 會是 111 而非 100),所以使用時要注意順序問題。

總之,'100'、'200' 被 array_merge() 當成數字讓我極度不爽。

2014/09/29

Vim Arrow Key Alias



每次用 autocomplepop 都會覺得右有要移動到方向鍵才能選字實在麻煩。

於是開使加了一些 short keys 讓操作簡單一底,二隻手也可以盡量不宜開主鍵盤區域。





在 .vimrc 中加入 maping,來取帶 arrow 按鍵:
imap <C-J> <Down>
imap <C-K> <Up>

這樣編輯時跳出選字方塊時,變可以使用 Ctrl + J or K 來上下選字。

2014/09/14

Post/Redirect/Get Pattern

Post/Redirect/Get (PRG) pattern 是一種處理表單資料的流程,可以防止使用者回上一頁、或是使用重新整理的方式重複送出表單資料。

再拿留言板舉例 (萬年題材 XD),假設在 form.html 填寫完表單,送出到資料到 post.php,而 post.php 處理資料後直接將結果顯示出來 (傳回 HTML),這個時候再瀏覽器按下「重新整理」按鈕,變會看到提示訊息,詢問是否要重送表單內容。


此時若重送表單資料,則會重新送出一模一樣的內容,若 post.php 沒有特別檢查,就會重複處理。很久以前開心農場就是用這種方法洗禮物的。

為了避免瀏覽器可以重新整理頁面,將資料處理的流程稍微做個調整:
  1. form.html 設定 submit 後,表單資料送給 post.php 處理
  2. post.php 處理資料後,不直接顯示 HTML,只送出 HTTP 3xx 做重新導向,跳到 done.php 顯示處理結果
  3. done.php 透過 URL 參數,顯示相對應的訊息

post.php 寫法大致如下:
if(saveData($_POST)){
   $status = 'ok';
}else{
   $status = 'error';
}

header('Location: done.php?status=' . $status);

由於使用 HTTP 3xx 重新導向,瀏覽器不會將 post.php 紀錄到瀏覽紀錄中,按下「上一頁」按鈕也是回到 form.html 而非 post.php,且 done.php 使用 HTTP GET 方式取得內容,所以重新整理頁面也不會重新送出表單內容。此流程按照三個步驟的 HTTP request 方式稱為 Post/Redirect/Get pattern。

PRG pattern 寫起來至少會有三個頁面要實作,也是挺麻煩的。若還要簡單一點的方式,那大概就是改用 AJAX 處理表單了吧。


Reference:
Post/Redirect/Get - Wikipedia, the free encyclopedia
http://en.wikipedia.org/wiki/Post/Redirect/Get

2014/09/03

手腕受傷

最近手腕舊傷似乎又復發了,距離上一次了大約一年。

去年不明原因手腕腫起來就醫,診斷為肌腱發炎,做了接近一年的復健才康復,而最近又有僵硬、酸痛的症狀發生。

這二次的共通點大致都是工作較忙、打字數較多,最近注意了一下,左手在按組合按鍵時的姿勢特別覺得酸,注意了一下打字的習慣。

平常均使用左手按 Ctrl / Alt / Shift,另外的 key 則是看哪隻手位置比較順。另外也稍微注意一下主要的開發環境,vim 編輯程式時,會用到 Ctrl + w (switch window)、zR / zM (folding)、Ctrl + F (page down) 等,還有一個可能的原因是 PHP 的語法中,所有的變數名稱都是「$」開頭,讓左手的負擔加重。

嘗試了改用右手還按組合鍵,不過習慣難改,工作效率反而一直往下掉。看來就只有晚上多休息,先撐過專案以後,來考慮修改 vim key map,甚至換掉原有的鍵盤排列方式。

2014/08/22

Nginx upstream 的容錯設定方法

自己的 Redmine server 是使用 Thin 執行,在透過 Nginx 將 request 轉送給 Thin。

Nginx 中設定 Thin server pool:
upstream redmine_thin_servers {
  server unix:/home/redmine/run/thin.0.socket;
  server unix:/home/redmine/run/thin.1.socket;
  server unix:/home/redmine/run/thin.2.socket;
  server unix:/home/redmine/run/thin.3.socket;
}

Nginx server:
  location / {
    try_files $uri/index.html $uri/index.htm @redmine_thin_servers;
  }

  location @redmine_thin_servers {
    proxy_pass http://redmine_thin_servers;
  }

這樣設定,Nginx 會將 request 平均轉送給 4 個 Thin instance。

但若其中一個 Thin instance 處理叫複雜或是較花時間的工作,如 checkout repository 等,會長時間沒有回應,可能沒辦法再同時處理另一個 request。此時若 Nginx 將另一個 request 轉送到已經 pending 的 Thin instance,使用者就會一直等到 Nginx 預設的 proxy timeout 後,出現 HTTP 500 Internal Sever Error 錯誤訊息。但實際上可以讓 Nginx 暫時略過 pending 的 server,將 request 優先轉送給其他 3 個正在 stand by 的 server。

利用 ngx_http_upstream_module 中的 parameter,將 timeout 的 server 標記先移出 pool,讓 request 不會送到 pending 的 server,過一段時間以後再拉進 pool 繼續使用。

以下設定若該 server 超過 10 秒沒有回應,則移出 pool,過 5 分鐘在拉回 pool:
upstream redmine_thin_servers {
  server unix:/home/redmine/run/thin.0.socket fail_timeout=10s slow_start=300s;
  ....
}

另外也可讓 Nginx 主動去檢查 server status,不要在使用者送 request failed 後才將 server 移出 pool。health_check 會讓 Nginx 主動發送間單的 request,透過 HTTP status 來判斷 server 狀態:
  location / {
    try_files $uri/index.html $uri/index.htm @redmine_thin_servers;
  }

  location @redmine_thin_servers {
    proxy_pass http://redmine_thin_servers;
    health_check interval=3s 
  }


測試機上,不曉得是不是 Nginx 1.6.0 還不支援這幾個參數,以上的設定在 configtest 會出現錯誤訊息:
invalid parameter "slow_start=300s"
unknown directive "health_check"


後來是繞路換一個方法。Nginx 的 ngx_http_proxy_module 中,可以設定當 server 多久沒有收到回應 (proxy_read_timeout) 時,直接當作失效並從 pool 中找下一台 server 送 request:
  location / {
    try_files $uri/index.html $uri/index.htm @redmine_thin_servers;
  }

  location @redmine_thin_servers {
    proxy_pass http://redmine_thin_servers;

    # 超過 3 秒沒有收到 response 則當作 timeout
    proxy_read_timeout 3s;

    # 若 response 是 HTTP 50x 也當作 server 失效
    proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
  }