平常除草之後,都會在田埂上寫點 PHP 以防老年痴呆,然後一路上經歷了 Phalcon 0.9x, 1.0, 2.x, 3.0.x,雖然都不小心送了點 PR,但是這次遇到的是一個頗神奇的情形。
大概就像是記者說巴拉刈很毒只要 15 c.c. 就會致死...
那是用喝的!
那是用喝的!
那是用喝的!
老實說比起暗黑農會在推的什麼除草劑,巴拉刈還比較有效(以下不斷人財路就不多說惹...
PHP 4.0.1 以來的問題
array_merge_recursive
我查到最早是在 2001 年被提出,關於數字型態的鍵值所引發的問題,然後現在已經 PHP7 了,這個問題依舊存在著。
這大概不算 bug 所以不用修。
那我重現給你看看,邏輯上沒有問題,只是出來的結果跟我們想的不太一樣,
<?php
$a = [
1 => [
1 => 'A',
2 => 'B',
3 => 'C'
],
2 => [
1 => 'A',
2 => 'B',
3 => 'C'
],
3 =>
['step1' => [
1 => 'A'
]
],
4 => [
'step1' => [
2 => 'B'
]
],
5 => [
'step1' => [
3 => 'C'
]
]
];
$b = call_user_func_array("array_merge_recursive", $a);
print_r($b);
輸出的結果是,
Array
(
[0] => A
[1] => B
[2] => C
[3] => A
[4] => B
[5] => C
[step1] => Array
(
[1] => A,
[2] => B,
[3] => C
)
)
錯誤的預想結果
我們可能會想說,如果是 merge
的狀態的話,遇到數字類型的鍵值,應該是把數字鍵值放進去就好,所以我們可能會 想要 得到這樣,
Array
(
[2] => Array(
[1] => A
[2] => B
[3] => C
),
[step1] => Array(
[1] => A
[2] => B
[3] => C
)
)
但是事實上,array_merge_recursive
將第一層陣列拉出來,對於其子數值做 merge
,所以他的輸出並沒有錯,反而是 預想的結果 是錯的。
既然這樣,如果我不是從 1
開始,會如何呢?
<?php
$a = [
[
'step1' => [
4 => 'A'
]
],
[
'step1' => [
2 => 'B'
]
],
[
'step1' => [
3 => 'C'
]
]
];
$b = call_user_func_array("array_merge_recursive", $a);
print_r($b);
輸出結果為,
Array
(
[step1] => Array
(
[4] => A
[5] => B
[6] => C
)
)
欸?
Phalcon 3.0.x 出現的問題
在 Phalcon\Config\Adapter\Ini
這個工具當中,有使用到 array_merge_recursive
這個東西,有興趣的可以察看原始碼,裡面有,
https://github.com/phalcon/cphalcon/blob/master/phalcon/config/adapter/ini.zep#L95
問題出在於,如果你使用原生 PHP 重現這個地方,他的輸出會是正確的,以上述的例子來說,他確實會輸出成這樣,
Array
(
[step1] => Array
(
[1] => A
[2] => B
[3] => C
)
)
但是你如果使用 Phalcon\Config\Adapter\Ini
來輸出呢,首先你得先準備一個 ini 檔案,像是這樣,
[step1]
1 = 'A'
2 = 'B'
3 = 'C'
然後用 Phalcon\Config\Adapter\Ini
取出並輸出,他會輸出成這樣,
object(Phalcon\Config\Adapter\Ini)#1 (1) {
["step1"]=>
object(Phalcon\Config)#3 (3) {
["1"]=>
string(1) "B"
["2"]=>
string(1) "C"
}
}
欸欸?
我追蹤了 ini.zep
把過程印出來發現,再把資料餵給 array_merge_recursive
之前,資料格式是對的,但是餵給 array_merge_recursive
之後,資料就錯了。
// 餵入 array_merge_recursive 之前
Array
(
[step1] => Array
(
[1] => A
[2] => B
[3] => C
)
)
// 餵入 array_merge_recursive 之後
Array
(
[step1] => Array
(
[1] => A
[1] => B <-- 取了兩次 1
[2] => C
)
)
// 最終輸出結果
Array
(
[step1] => Array
(
[1] => B
[2] => C
)
)
合理懷疑 Phalcon 直接使用 Zend 來做 array_merge_recursive,或是另外自己做掉。
因為 PHP7 直接跑並不會出現這樣的錯誤。
接著如果改寫 ini 檔案,例如這樣,
[step1]
3 = 'A'
1 = 'B'
2 = 'C'
然後用 Phalcon\Config\Adapter\Ini
取出並輸出,他會輸出成這樣,
object(Phalcon\Config\Adapter\Ini)#1 (1) {
["step1"]=>
object(Phalcon\Config)#3 (3) {
["3"]=>
string(1) "A"
["4"]=>
string(1) "B"
["5"]=>
string(1) "C"
}
}
欸欸欸?
array_merge_recursive 的原罪
基本上這應該沒辦法從核心解決,所以 PHP 手冊的討論中有幾個解法,基本上是用這個,
http://www.php.net/manual/en/function.array-merge-recursive.php#96201
然後再改善一下,例如,
// 我不要讓他 push,指定 $key 給他,可以維持原有的數字型態序列
if(!in_array($value, $base)) $base[$key] = $value;
這樣的話,如果使用 Phalcon\Config\Adapter\Ini
然後呼叫這個作法,輸出的結果就會是,
object(Phalcon\Config\Adapter\Ini)#1 (1) {
["step1"]=>
object(Phalcon\Config)#3 (3) {
["1"]=>
string(1) "A"
["2"]=>
string(1) "B"
["3"]=>
string(1) "C"
}
}
這樣就會與 預期中的 ini 檔案內容 輸出的結果相同。為何要這樣改,我想,應該沒有人希望原本 ini 寫這這樣,
[step]
step1.1 = 'A'
step1.2 = 'B'
step1.3 = 'C'
step2.1 = 'D'
step2.3 = 'E'
輸出變成,
object(Phalcon\Config\Adapter\Ini)#1 (1) {
["step1"]=>
object(Phalcon\Config)#3 (3) {
["1"]=>
string(1) "B"
["2"]=>
string(1) "C"
}
["step2"]=>
object(Phalcon\Config)#3 (3) {
["1"]=>
string(1) "D"
["2"]=>
string(1) "E"
}
}
然後你用 $iniConfig->step1[3]
就直接 Fatel Error
惹~
然後你用 $iniConfig->step1[3]
就直接 Fatel Error
惹~
然後你用 $iniConfig->step1[3]
就直接 Fatel Error
惹~
小結
沒事不要送 PR,送了還被要求要 rebase!我最不會 rebase 了(誰來教教我 இ д இ;