[PHP] array_merge_recursive 的神奇狀況

平常除草之後,都會在田埂上寫點 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 了(誰來教教我 இ д இ;

Hina Chen
偏執與強迫症的患者,算不上是無可救藥,只是我已經遇上我的良醫了。
Taipei