自已曾經碰過,今天被問到的問題,覺得解釋的不太好,所以事後把它寫下來。

問題是這樣子:

某個 class 裡面的 method 呼叫一段在其他檔案定義的 function,回傳值卻不如預期。

實際把問題簡化成 sample code 如下:

假設 app.phplib.php 都在同一層目錄

lib.php

<?php

$exchange_rates = array(
    'TW' => 30,
);

function currency_convert($currency, $to)
{
    global $exchange_rates;
    return $currency * $exchange_rates[$to];
}

app.php

<?php

require __DIR__ . '/lib.php';

echo currency_convert(10, 'TW') . PHP_EOL;

這邊為了簡單呈現問題,不做 sanity check,上面這一段輸出預期會是 300,但是在特地的條件下 currency_convert() 的回傳的會是讓你驚訝的 0,我們把 app.php 修改為下面的內容:

<?php

call_user_func(function() {
    require __DIR__ . '/lib.php';
});

echo currency_convert(10) . PHP_EOL;

這邊簡單用 anonymous function 來 require lib.php 來重製問題,故意讓 currency_convert() 壞掉,結果輸出就會是 0,知道問題了嗎?

與 C 一樣,PHP 在 function 裡的變數都是區域變數,唯一不同的地方在全域變數在 PHP function 裡必需要用 global 宣告才能看的到。

所以在上面的例子中, $exchange_rates 因為 anonymous function 的關係,他從全域變數變成了區域變域,只在 anonymous function 裡才存取的到。

千萬別認為神經病才用 anonymous function 來 require library!市面上所見到 autoload 機制的 class loader 1,大多都是在 method (或 function) 裡來 require 檔案,然後就引爆地雷了。

workaround 的方法就是在看的到變數的地方做手腳,所以有幾種方法:

  1. 在 require 前宣告 global $var
  2. 用 superglobal 變數 $GLOBALS
  3. 你有權限 touch 的到 lib.php 的話,在外面的 $exchange_rate 也可以宣告成 global,global 並沒有限制一定要在 function 裡使用,在 function 外也可以使用,因為檔案在 function 裡被引用的話,此時的 global 就發揮用處了

後話

有一定年紀的 PHP Application,如果持續維護至今的話,應該都會面臨 programming paradigm 從 precedural 轉換至 object-oriented 的過程,在轉換過程之中就很容易遇到這類的問題。

這個問題是義大利麵與定食共享的結果。