Meet the PHP

最後編輯於 2020-03-27

前言

2020-02-16 寫的內容因為受到當時公司的影響充滿了偏頗(完全是抱怨文),所以這次會盡可能以 PHP 這個語言給我的感覺來談談。

第一次接觸到 PHP 是在資策會,當時的結業專題因為種種原因我需要可以回應資料的 API ,但那時根本還沒教如何弄後端 API ,所以為了有資料可以在 Android 上呈現就找上了 PHP 。

直接說結論吧, PHP 跟當時所學的 Java 語法結構很不一樣而我那時也是超級菜雞,做出來的東西幾乎都是照抄網路上的範例,所以 PHP 之於我就像是被老師要求抄文言文的學生一樣,只想著『文言文他媽到底是三小,最好人討論是這樣子說話啦。』,但為了成績( API )還是不得不做。

我那時只覺得 PHP 真的是醜死了。

時間回到一月,我應徵上了自稱合法的博弈技術外包公司,職稱為 Java 開發工程師。

然後就開始學 PHP 了。

黑人問號

對,你沒看錯,不要懷疑。

當時面試有提到公司內有用 PHP 與 Java ,而且可能會寫一些 PHP 啦⋯⋯ 但我直到最近離職前寫的全都是 PHP 耶?

抱歉離題了。

我是一個不太喜歡動態語言的人,或者說我不太喜歡規範不明確。

我認為動態語言於單人快速開發很合適,多數情況下自己的思考邏輯不會有衝突,若是已知函式輸入與輸出型態,卻還得補上型別提示、強制轉型確實令人煩躁;於多人開發則有可能導致災難,畢竟每個人的思考邏輯很難不產生衝突,所以會變成需要在程式內進行某些檢查,但這樣反而囉唆。

1
2
3
4
5
6
# Python 的情況,為了確保其他人不誤用所以進行型態檢查
def doSomething(arg):
if not isinstance(arg, str):
raise ValueError("arg must be string")

# ...

還不如靜態語言直接註明輸入的型態:

1
2
3
4
// Go 的情況,直接標示輸入值須為 string
func doSomething(arg string) {
// ...
}

不過我也想過這可能是因為我沒參與過動態語言開發的大型專案,或許在那些專案中其實根本沒有這種問題存在。

總之,下面提到的點都是我不太喜歡或難以接受的部分。

語法

PHP 與我所熟悉的大多數語言寫法不同,當初看程式碼的時候感覺觀念被當面撕碎,加水攪拌後被潑了滿臉。

操作符號

PHP 字串串接使用 . 而屬性、函式存取用 ->

1
2
3
4
# Python 的情況
s = "Hello" + " World"

obj.something()
1
2
3
4
// PHP 的情況
$s = "Hello" . " World";

$obj->something();

一直以來碰過的語言 Java 、 Go 、 JavaScript 、 Dart 等都是用 + 來進行串接,而 . 則是用來存取屬性與函式,這讓我看程式碼的時候很痛苦,再加上公司的前輩們字串串接時都不喜歡加上空白,簡直跟天書一樣:

1
2
3
$url = $some_url.'?xxx_id='.$this->xxx_id;
$buffer = $params_array['no'].$params_array['amount'].$this->setting['key'].$this->setting['some_id'];
$postData['Sign'] = md5($postData['Amount'].'|'.$postData['MerId'].'|'.$postData['MerOrderNo'].'|'.$postData['MerOrderTime'].'|'.$this->setting['api_key']);

雖然這種情況 + 也很不好看,但因為我不會誤解 + 的語義所以情況或許會好點。

我想對於原本學習 PHP 或有相近語法語言的人大概不會有這個問題。

變數

動態語言的特性之一就是變數不受型態約束:

1
2
3
# Python 的情況
num = 1
num = "進化成字串"

PHP 只多了個規定,要求所有的變數需以 $ 開頭:

1
2
3
// PHP 的情況
$num = 123;
$num = "就是愛錢啦";

這對我來說感覺與操作符號一樣,都跟我原本的觀念衝突而導致觀看時的消耗。

$ 其實有專門的稱呼,稱為 Variable variables (可變的變數),這是什麼意思呢?

直接看個例子:

1
2
3
4
5
$a = "hello";
$$a = "world";

echo "$a ${$a}"; // 輸出: hello world
echo $hello; // 輸出: world

稍微解釋一下演變過程:

  1. $a 取出指向的值 “hello”
  2. $ + “hello” 轉變為 $hello
  3. $hello 指向值 “world”

等等,蛤?

是個類似於 Pointer ,但又比它莫名其妙的玩意
我懷疑可能太容易激起同事的殺人慾望,尚未在專案中看人用過。

但你我都知道,總是有人喜歡自作聰明的使用這些功能。

萬能 Array

印象中的 Array (陣列)是種連續記憶體空間結構,也就是那種可以隨機存取的資料結構。

而在 PHP 內的 Array 實際上是 Sorted Hash Table (可排序雜湊表),也就是我印象中的 Map 或 Dictionary 結構。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 寫法有多種,大概是因為歷史包袱的關係
$arr = array("A", 100, 3.14159);
$arr = ["A", 100, 3.14159];

// 操作看起來起來很正常
$arr[0]; // A
$arr[1]; // 100
$arr[2]; // 3.14159

// 但實際上只是 Key 為數字的 Map
$arr = array(0 => "A", 1 => 100, 2 => 3.14159);
$arr = [
0 => "A",
1 => 100,
2 => 3.14159,
];

PHP 中 Array 很常使用,因為無論是多回傳值,多個參數都可以輕易地使用 Array 包裹起來傳遞。

要傳一堆變數?用 Array
回傳多個值?用 Array
臨時想弄個結構?用 Array

看起來很萬用對吧?因為你也沒其它選擇了。

PHP Array 還有個詭異的語法糖:

1
2
3
4
5
6
7
8
$arr = ['a' => "Apple", 'b' => "Banana"];

$arr[] = "Cat"; // [0 => "Cat", 'a' => "Apple", 'b' => "Banana"]
$arr[0]; // Cat

$arr[4] = "Four"; // [0 => "Cat", 4 => "Four", 'a' => "Apple", 'b' => "Banana"]
$arr[] = "Five"; // [0 => "Cat", 4 => "Four", 5 => "Five", 'a' => "Apple", 'b' => "Banana"]
$arr[5]; // Five

也就是當 Array 的 Key 不寫的時候會進行 Append (添加元素),這種語法糖我是第一次見到,老實說 PHP 有很多的工具函式,透過函式不是更具表達力嗎?

1
2
3
4
5
// 回傳新的陣列
$arr = append($arr, $new_elem);

// 直接修改原本的
append($arr, $new_elem);

PHP 的 Array 要是命名為 Map 或 Dict 之類的我都覺得還好,但是 Array 很容易誤會實際的結構

常數

常數早期只能透過 define(Name, Value, CaseSenstive?) 這個內建函式定義,直到 5.3 之後才可以使用 const 關鍵字。

PHP 內的常數是沒有 $ 開頭的,使用起來就跟其它語言的變數一樣,而這對我當然又是一種額外消耗,因為看到常數時會突然忘記 PHP 的變數要 $ ;看到變數時會突然看不懂常數是啥鬼。

1
2
3
4
5
define("SUCCESS", 1);

if ($status == SUCCESS) {
// ...
}

專案內與某些套件的程式碼都是看到 define() 比較多,可能原因大概是 define() 可以透過判斷式來追加定義:

1
2
3
4
5
6
7
8
9
// 無法這樣使用
if (defined("SUCCESS")) {
const SUCCESS = 1;
}

// 可以追加定義
if (defined("SUCCESS")) {
define("SUCCESS", 1);
}

另一個原因或許是 const 只能接受靜態數值,換句話說你沒辦法用函式之類的回傳值來設定常數。

老實說這點我覺得很正常啦,常數就應該是一種固定數值才對,如果要透過函式回傳值,那不就表示可能會變動嗎,這樣還能算是常數嗎?

需要修改 php.ini 來開啟函式庫

這個我不確定有沒有其它解決方法,是處理專案時碰到的一個問題。

當時碰到要串接的三方 API 是使用 SOAP 協議,而 PHP 可以透過 SoapClient 類別來處理。

1
2
3
4
5
// WSDL 是 XML 格式的服務描述檔案
$client = new SoapClient("...?wsdl");

// 指定要使用的 SOAP 服務名稱
$res = $client->__soapCall('METHOD', $params);

但測試時出現錯誤說找不到 SoapClient 類別。

拜請了 Google 大神後發現要使用這個類別竟然得修改 php.ini 來啟用,我實在難以相信一個內建的函式庫竟然必須修改 PHP 設定檔才能開啟。

大小寫敏感不敏感?

PHP 中只有變數、常數是大小寫敏感的,而像是:

  • 類別
  • 函式
  • 關鍵字

換句話說,你可以像這樣寫:

1
2
3
4
5
FuncTion justdoit($flag = tRuE) {
// ...
}

JustDoIt();

當然真的這麼做的人很少,可這真的是個匪夷所思的設計,完全不能理解。

有篇文章比較詳細的說了此設計的原因:PHP黑系列之一:PHP 为什么大小写规则是如此不规则?

多樣化的關鍵字

各種各樣的關鍵字出現在 PHP 中,讓我感到困惑。

好比說 includerequire 都可以將指定的文件給引入,類似一種複製貼上。

那麼為什麼需要兩個?因為如果是 include 即使對象不存在也不會出現錯誤, require 則會報錯,所以多數情況下都建議使用 require ,這就很讓人懷疑 include 的存在價值。

還有特殊的實體關鍵字,比如可以透過:

1
2
3
4
5
// 不論如何都會實例化當前這個類別
$real_me = new self();

// 會實例化執行期的類別,好比說 Son 繼承後執行此段會實例化 Son
$runtime_me = new static();

其它像是為了不寫花括號而增加的 end 系列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 原本的寫法
if ($x == true) {
echo "X is true";
echo ", real true";
} else {
echo "X is not true";
echo ", real false";
}

// 改用冒號與 end 結尾
if ($x == true):
echo "X is true";
echo ", real true";
else:
echo "X is not true";
echo ", real false";
endif;

不太清楚這是為了照顧多種風格的設計師還是怎樣,老樣子太多選擇容易導致困惑。

官方無規範

官方理應具有一致性規範,這才不容易造成開發者的負擔。

但是 PHP 的內建函式命名你可以發現多種風格:

  • strptime : C 語言風格
  • nl2br :縮寫風格
  • htmlspecialchars :全小寫風格
  • json_encode :底線區隔

這來自一篇詳細解釋的文章:PHP黑系列之二:PHP 为什么函数命名是如此不一致?

PHP 內建的函式多的誇張,所以 PHPer 最常做的事情就是去查看官方手冊,聽起來雖然很正常,但如前述 PHP 根本沒有命名脈絡可循,所以你根本不知道字串操作、加密解密都提供了哪些函式給你,還曾發生過版更替時將舊有的函式給移除的情況,再加上參數順序延續此傳統各種混亂,導致難以想像在沒手冊的狀況下寫 PHP 。

就連型態都有好幾種寫法,以下幾種都是一樣型態:

  • int/integer
  • bool/boolean
  • float/double/real

結論

最早我是因為不喜歡動態語言的關係不太喜歡 PHP ,但是這次學習部分知識與實際撰寫後我想我可以認真的說:「我不喜歡 PHP 。」

這個語言散發著濃厚 “ 走一步算一步 “ 的氣息,也很明顯有許多致敬其它語言的部分,但重點是太多設計理念被包含在 PHP 中卻沒有被整理,變成東拼西湊弄出一個看上去還可以的成品,不過當你仔細觀察後就會發現其糟糕的內在。

當然我也不否認 PHP 在快速開發上真的很強,前公司之所以使用也是看上了 PHP 的某些特性,而我不喜歡的只是它的設計風格,如果能自己選的話 PHP 目前不會在選項內,但假使未來 PHP 狠下心決定處理掉某些糟糕的設計,那我可能就會考慮使用了。

畢竟你看 PHP 可是唯一一個寫完會變得更開心的、不容你質疑的、世界上最好的程式語言啊!

最棒的語言!

額外閱讀

「PHP 是最好的语言」这个梗是怎么来的?