相對於其他語言,PHP 對於曾經寫過 C 的人來說比較容易上手,其中一個原因是 PHP 與 Standard C Library (libc) 有眾多函式名稱是一樣的,我相信 PHP 是刻意如此,有些函式連參數也一樣,只存在強弱型別的差異而已,檔案操作一系列的函式 fopenfreadfwritefclosefeoffgetsfgetcftellfseek...等等就是較經典的例子。

其中 feof 就是今天的主角,使用上有一些眉角要注意,feof 通常拿來確認有沒有完整讀取到檔尾,所以在 PHP 中如果要從 stdin 一行一行的讀出來做資料處理的話可以寫成下面這樣:

<?php
// feof.php
while (!feof(STDIN)) {
    $line = fgets(STDIN);
    var_dump($line);
}

執行結果輸出如下,你會發現多出了 false 這個預期外的結果...

shell> echo -e "1\n2\n3" | php feof.php
string(2) "1
"
string(2) "2
"
string(2) "3
"
bool(false)

原因是 feof 是被動方式確認 flag 是否包含 EOF,所以要有另外一個角色要主動來設 EOF flag,這個主動的方式就是透過讀取的行為觸發,這點在 C 裡行為也是一樣的,所以安全起見可以多加一行判斷改寫成這樣,多出來的那一行 false 就不見了。

<?php
// feof.php
while (!feof(STDIN)) {
    $line = fgets(STDIN);
    if (feof(STDIN)) {
        break;
    }
    var_dump($line);
}

上面會動,但是不夠,因為太醜了,fgets 回傳 false 代表兩個意思,一是沒有資料可以讀取,二是有錯誤產生,利用這個特性再改寫如下:

<?php
// feof.php
while (!feof(STDIN)) {
    $line = fgets(STDIN);
    if (false === $line) {
        break; // continue is bad.
    }
    var_dump($line);
}

上述程式碼變數 $line 遇到 falsebreak 跳出迴圈,如果 false 是代表一的狀況的話算是瞎貓碰上死耗子,如果是第二種狀況,那就有讀取不完整的問題。如果把 breakcontinue 取代,意味著 fgets 遇到錯誤重新嘗試 (retry),判斷檔案結束由 feof 把關,這樣同時解決了兩個的問題,但是如果碰上 fgets 一直失敗這種 edge case 就會造成無窮迴圈的產生,應該要儘量避免...

下面的版本充份利用 fgetsfeof 的特性:

<?php
// feof.php
while (true) {
    $line = fgets(STDIN);
    if (false === $line) {
        break;
    }
    var_dump($line);
}

if (!feof(STDIN)) {
    trigger_error("Failed to read", E_USER_WARNING);
}

大量的讀檔可以稍微快一點的版本,犧牲一點可讀性換來一點效率...

<?php
// feof.php
while (false !== ($line = fgets(STDIN))) {
    var_dump($line);
}

if (!feof(STDIN)) {
    trigger_error("Failed to read", E_USER_WARNING);
}

到這邊還沒結束,feof 參數是 stream resource,如果參數不對會有 warning 警告,但回傳值是 false,這很容易造成無窮迴圈,所以如果你沒有檢查回傳值是否正確的好習慣,預期 fopen 開檔都會成功回傳 file pointer,沒有檢查 fopen 開檔失敗,就很容易踩中這個地雷。

<?php
// infinite loop demonstration
$fp = fopen('/file/not_exist', 'r');
while (!feof($fp)) {
    var_dump($line);
}

另外 feof 拿來處理 fsockopen 或是 fopen HTTP 的 url 回傳的 stream 也很危險,官網上 workaround 1 的方法只能說醜到爆炸,所以如果有 HTTP 的操作請儘量用高階一點的 API 來取代。