# PHP throwing exceptions instead of returning false ###### tags: `PHP` [`file_get_contents`](https://www.php.net/manual/en/function.file-get-contents.php)、[`json_decode`](https://www.php.net/manual/en/function.json-decode.php)、[`strpos`](https://www.php.net/manual/en/function.strpos.php) 是 PHP 蠻常見的內建函式,不過現在到處引用套件的年代,很多套件都會自動幫我們處理錯誤,會發現這些內建函式 return 特別之處,是因為一個 code review 的事件,簡單做心得紀錄一下。 ## 前因 也許經過 `typescript` 跟 `golang` 強型別的洗禮,對於 function return 的型別會開始在意。 總之,我看到一段 code 大概是長這樣子 ```php const A_NAME = 'Name#1'; const B_NAME = 'Name#2'; public function getNameByOOO(string $ooo) { if ($ooo === 'xxx') { return A_NAME; } else if ($ooo === 'vvv') { return B_NAME; } return false; } if (!$name = getNameByOOO($ooo)) { // 處理 error } ``` 這案例就是藉由 ooo 去決定回傳的 name 是什麼,但是不符合條件的 name 要過濾掉。 第一次看到這 function 覺得很困惑,就 function name 會預期拿到一個 `string` type 的 name,但是卻又可能是 `boolean` type 的 false。 就我的想法會寫成: ```php const A_NAME = 'Name#1'; const B_NAME = 'Name#2'; public function getNameByOOO(string $ooo): string { $name = '' if ($ooo === 'xxx') { $name = 'Name#1'; } else if ($ooo === 'vvv') { $name = 'Name#2'; } return $name; } $name = getNameByOOO($ooo); if (!in_array($name, [A_NAME, B_NAME])) { // 處理 error } ``` 主要原因有三點 - function getNameByOOO 預期會拿到 `string` type 的 name,拿到 `boolean` type 的 false 會讓人困惑。 - function 職責儘量單一,原本的寫法 function 除了做 OOO 跟 name 的轉換之外,還幫忙做 name 是否合法的驗證。 - 假設使用 function 的人沒注意,沒有去處理 false 的情境,那他不就拿著 `$name = false` 的參數,去處理他自己的流程了? 後來作者回覆我 php 的 `strpos`、`file_get_contents`、`json_decode` 錯誤時也會回傳 false or NULL,要自行去做錯誤處理,於是我就去一探究竟... ## Function Returning False 後來爬一下文,有一篇文章 [THROWING EXCEPTIONS INSTEAD OF RETURNING FALSE](https://thecodingmachine.io/introducing-safe-php),裡頭有提到可能的歷史原因,在 PHP 支援 exception 之前,大部分的 function 發生 error 都會回復帶有 `E_ERROR` or `E_WARNING` 的 error message,自然會有 return false 的情況。 `strpos`、`file_get_contents`、`json_decode` 這三個內建函式,官方文件都有在 Return Values 的部分做描述,甚至會 Warning `This function may return Boolean FALSE`,要我們使用時要做 false 的驗證。 使用上將會變成 ```php $content = file_get_contents('test.json'); if ($content === false) { throw new Exception('Could not load file test.json'); } $test = json_decode($content); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('json decode error: ' . json_last_error()); } ``` 但可能不少開發者不會特別注意到,不一定會做 error 的驗證,所以才有上段提到的文章,應該要用 throw exception 代替 return false。 PHP 開發者也有試圖修正,例如在 PHP 7.3,`json_decode` & `json_encode` 多了一個 option [`JSON_THROW_ON_ERROR`](https://www.php.net/manual/en/class.jsonexception.php),讓使用者在執行時出錯會 throw exception。 另外也有 [Safe PHP library](https://github.com/thecodingmachine/safe),將原來 function 再包一層,return false 會被轉成 throw exception,避免使用者漏掉處理 return false 的情境。 再回到剛剛例子,如果要在 `getNameByOOO` 內做錯誤處理,也許改成下面方式會比較好 ```php const A_NAME = 'Name#1'; const B_NAME = 'Name#2'; public function getNameByOOO(string $ooo) { if ($ooo === 'xxx') { return A_NAME; } else if ($ooo === 'vvv') { return B_NAME; } throw new \Exception('invalid ooo'); } $name = getNameByOOO($ooo) ``` 如果真要 return false,function 註解需要標明,提醒使用者有 return false 的情境,例如: ```php const A_NAME = 'Name#1'; const B_NAME = 'Name#2'; /** * @param string $ooo * @return string|false return false if $ooo is vaild */ public function getNameByOOO(string $ooo) { if ($ooo === 'xxx') { return A_NAME; } else if ($ooo === 'vvv') { return B_NAME; } return false; } if (!$name = getNameByOOO($ooo)) { // 處理 error } ```