全面解析PHP的糟糕設計

發表於2012-04-17

英文原文:veekun.com,翻譯:大東哥@oschina

前言

我的脾氣古怪. 我會抱怨很多東西. 這個星球上大多數技術我都不喜歡.

PHP不僅使用起來尷尬, 還有要嘛我想要的不適合, 要嘛不是最令人滿意, 要嘛違揹我的信仰. 我可以告訴你關於一門語言, 所有我想避免的好方式, 所有我喜歡的壞方式. 來吧, 問吧! 談話會很有趣!

php是唯一的例外. 幾乎php抽象的所有東西都是支離破碎的. 包括語言, 框架, 整個生態系統都一塌糊塗. 我幾乎不能單獨列出咒罵的事情, 因為它全身都壞了. 每次我打算編輯一堆雜亂如麻的php抱怨清單的時候, 我都被一些瑣事打亂, 越深入就越會發現其它令人震驚的事情.

php讓人難堪. 它是如此的破碎, 但那些被培訓的業餘愛好者, 卻對它稱讚不已. php在做一些徽不足道的挽回措施, 但我選擇忘記它.

不過我得讓我的系統擺脫這些東西, 也就這樣了, 這是最後一次嘗試.

打個比喻

我只是隨口和 Mel 抱怨下, 而她卻堅決讓我發表出來.

我甚至說不出來PHP到底怎麼了, 因為 — 還好. 想想你有一個, 嗯, 工具箱吧. 一堆工具. 看起來還好, 有標準的東西.

你拔除螺絲釘, 它怪異的有三個頭.  OK, 好吧, 這對你不太有用, 但你猜遲早有天會有用.

你拿出榔頭, 被震住了, 兩邊都有是尖爪. 但它仍然能用, 我的意思是, 你可以用兩頭的中部斜著敲.

你拿出老虎鉗, 但它們沒有鋸齒面. 表面平而光滑. 這沒多大用, 但依然能用, 沒什麼.

你可以繼續. 工具箱的東西都是怪異和琢磨不定的, 但又不能說毫無價值. 整體看沒什麼大問題; 它的工具都齊全.

現在, 想象有很多使用這些工具的木匠, 它們和你說:”這些工具有什麼問題呢? 我們都用過, 它們工作都很好啊!”. 工匠們給你展示他們建的房子,每個門都是五邊形的而屋頂是癲倒的. 你敲前門, 它向內倒榻了, 而他們卻抱怨你打破了他們的門.

這就是PHP的問題.

立場

我認為下面的特質對於一門語言的生產力和可用性是重要的, 而PHP在大範圍破壞它們. 如果你不同意這些, 好吧, 我無法想像, 我們永遠不會達成一致.

>>    一門語言必須是可預見的. 它是將人類的思想反映給計算機執行的媒介, 因此它的關鍵是, 人類對程式的理解實際要正確.

>>    語言必須一致. 相似的東西就要看起來相似, 不同的就是不同. 學習了語言的部分知識, 就應能很容易理解剩下的部分.

>>    語言必須簡潔. 新語言應該減少繼承舊語言的不好的形式. (我們也可以寫機器碼.) 新語言當然應努力避免織入新的特有的形式.

>>    語言必須是可靠的. 語言是解決問題的工具; 應儘量避免引入新問題. 任何”陷阱”都會大量的分散注意力.

>>    語言必須是可除錯的. 當出錯的時候, 程式設計師必須修正它, 我們需要獲得我們想要的幫助.

我的立場是:

>>    PHP到處處充滿驚奇: mysql_real_escape_string, E_ACTUALLY_ALL

>>    PHP不一致: strpos, str_rot13

>>    PHP需要特別形式: error-checking around C API calls, ===

>>    PHP古怪: ==. for($foo as &$bar)

>>    PHP晦澀: 預設無棧跟蹤或fatals, 複雜的錯誤報告

我不能就單個問題解釋為什麼它歸為這些類, 否則將會沒完沒了. 我相信讀者自己會思考.

不要再和我扯這些東西了

我知道很多有利的論點. 我也聽到很多反駁的論點. 這些都只能讓談話立即停止. 不要再跟我扯這些東西了, 求你了. :(

>>    不要和我說”好的開發者能用任何語言寫出好的程式碼”, 或者壞開發者.. 吧啦吧啦. 這毫無意義. 好的工匠可以用石頭或錘子駕馭釘子, 但你見過有多少工匠用石頭的? 成為一個好開發者的標準之一就是善於選擇工具.

>>    不要和我說熟記上千個例外和古怪行為是開發者的職責. 是的, 這在任何系統中都是必要的, 因為電腦是傻的. 這不意味著, 系統能瘋狂的接受而沒有上限. PHP有的只是異常, 這是不行的, 一旦和語言摔角決鬥, 你實際編寫程式就要花費更多的努力. 我的工具不能為我建立應用產生積極作用.

>>    不要和我說 “那就是C API 的工作方式”. 這星球上高階語言存在的目的是什麼, 它們能提供的一切僅僅是一些字串助手函式和一堆C的包裝器? 如果是這樣, 那就用C! 這裡, 甚至還有為它準備的CGI庫.

>>    不要和我扯 “搞出奇怪的事, 是你活該”. 如果存在兩個特性, 總有一天, 某些人會找到一起使用它們的理由. 再次強調, 這不是C; 這裡沒有規範, 這裡不需要 “未定義行為”.

>>    不要再和我扯 Facebook 和 Wikipedia 就用的PHP.  我早知道了! 它們也能用 Brainfuck 寫, 但只要他們足夠陪明, 不斷折騰這些事情, 他們總能克服平臺的問題. 眾所周知, 如果使用其它語言編寫, 開發時間可能會減少一半或加倍; 單獨拿出這些資料毫無意義.

上帝保佑, 不要再和我扯任何東西了!  如果列出的沒有傷害你的PHP的觀點, 無所謂, 因此請停止在網上做無意義的爭論, 繼續開發高帥富酷的站點來證明我是錯的 :).

偷偷告訴你: 我非常喜歡Python. 我也很樂意對它說些你不愛聽的話, 如果你真想的話. 我並不要求它完美; 我只是想揚長避短, 總結我想要的最佳東西.

PHP

語言核心

CPAN被稱為 “Perl的標準庫”. 這並沒有對Perl的標準庫做過多說明, 但它蘊含了健壯的核心可以構建強大的東西的思想.

基本原則

PHP最初很明確的是為非程式設計師設計的(言外之意,  非專業程式); 根源已經很難脫離. 從PHP 2.0 文件中挑選出來的對話:

    一旦你開始為每個型別區分不同的操作符, 你就開始使用語言變得複雜了. 例如, 你不能為strings使用 ‘==’, 你現在必須用 ‘eq’. 我沒看出這點來, 特別是那些類似PHP的指令碼語言, 它們大多數相當簡單而多數情況下, 作為非程式設計師, 只想要一門包含少量基本邏輯語法的語言, 而不想付出過多學習曲線.

>>    PHP 為保持前進不惜代價. 什麼都有比沒有好.

>>    這不是個正確的設計原則. 早期的PHP受Perl影響; 大量的標準庫參考C使用 “out” 引數; OO部分的設計像C++和Java.

>>    PHP從其它語言中引入大量的靈感, 但對那些熟知其它語言的人, 仍然難以理解. (int)看起來像 C, 但是 int 並不存在. 名稱空間使用 \. 新的陣列語法使用 [key => value], 不同於任何其它語言定義hash字面量的形式.

>>    弱型別(例如, 默默的自動在 strings/mumbers/等間轉換)是如此的複雜.

>>    少量的新特性以新語法實現; 大多數工作通過函式或者看起來像函式的東西完成. 除了類的支援, 這理所當然的需要新的操作符和關鍵字.

>>    本頁列出的問題都有官方解決方案 — 如果你想資助 Zend 修復它們的開源程式語言的話.

>>    路漫漫, 其修遠. 思考下面的程式碼, 從PHP文件的某地方挑出來的.

它將做什麼?

>>    如果PHP使用 –disable-url-fopen-wrapper編譯, 它將不工作. (文件沒有說, “不工作”是什麼意思; 返回 null, 丟擲異常?)

>>    注意這點已在 PHP 5.2.5 中移除.

>>    如果 allow_url_fopen 在 php.ini 中禁用, 也將不工作. (為什麼? 無從得知.)

>>    由於 @ , non-existent file 的警告將不列印.

>>    但如果在php.ini中設定了scream.enabled, 它又將列印.

>>    或者如果用 ini_set 手動設定 scream.enabled.

>>    但, 如果 error_reporting 級別沒設定, 又不同.

>>    如果列印出來了, 精確去向依賴於 display_errors , 再一次還是在 php.ini. 或者 ini_set中.

我無法告訴你這個函式呼叫的行為, 如果沒有檢視編譯時標誌 , 伺服器端配置, 和我的程式中的配置的話. 這些都是內建行為.

>>    該語言充滿了全域性和隱似狀態. mbstring 使用全域性字元編碼. func_get_arg 之類的看起來像正常的函式, 但是隻對當前正在執行的函式操作. Error/exception 處理預設是全域性的. register_tick_function 設定了一個全域性函式去執行每個 tick(鉤子?) —- 什麼?!

>>    沒有任何執行緒支援. (不奇怪, 因為上面已給出.) 加之缺乏內建的 fork (下面提到), 使得並行程式設計極其困難.

>>    PHP的某些部分在實踐中會產生錯誤程式碼.

>>    json_decode 對不正確的輸入返回 null,  儘管 null 也是一個 JSON 解碼的合法物件 — 該函式極不可靠, 除非你每次使用後都呼叫 json_last_error.

>>    如果在位置0處找到, array_search , strpos, 和其它類似的函式返回0, 但如果都沒有找到的話. 會返回 false

讓我們稍稍展開最後一部分.

在C中, 函式如 strpos 返回 -1, 如果未找到. 如果你沒檢查這種情況, 卻試著以下標使用它, 那將可能命中垃圾記憶體, 程式會崩潰. (也許吧, 這是C. 誰泥馬知道. 我確定至少有工具處理它)

話說, Python中, 等效的 .index 方法將丟擲一個異常, 如果元素沒找到的話. 如果你不檢查該情形, 程式將崩潰.

在PHP中, 該函式返回 false. 如果你把 FALSE 作為下標使用, 或者用它做其他事情, PHP會默默的將它轉成0, 但除了用於 === 比較. 程式是不會崩潰的; 它將執行錯誤的邏輯, 且無任何警告, 除非你記得在每個使用 strpos 和其它類似函式的地方包含正確的樣版處理程式碼.

這真是糟透了! 程式語言只是工具; 它們是為我服務的. 這裡, PHP給我佈下了陷阱, 等著我跳進去, 而我不得不時刻警惕這些無聊的字串操作和相等比較. PHP是個雷區.

我已經聽過很多關於PHP解析器的故事, 它的開發者來自世界各地. 有從事PHP核心開發工作的人, 有除錯PHP核心的人, 也有和核心開發者交流過的人. 沒有一個故事是讚賞的.

因此不得不在這裡插入一句, 因為它值得重複: PHP是個業餘愛好者的社群. 極少數人設計, 為它工作, 或極少有人知道他們在做什麼. (哦, 親愛的讀者, 你當然是個極品例外!) 那些成長了, 想轉投其它平臺的人, 使整個社群的平均水平下降. 這個, 就是這裡, 是PHP的最大問題: 絕對的盲目領導盲目.

好了, 回來面對現實吧.

操作符

== 不中用.

>>    “foo” == TRUE , 和 “foo” == 0… 但, 當然 TRUE != 0.

>>    == 會將兩邊轉成數字, 如果可能的話,  這意味著它將轉成 floats 如果可能. 所以大的16進位制字串(如, password hashes) 可能偶然會比較成 true , 儘管它們不一樣. 就連 JavaScript 都不會這樣做.

>>    由於某些原因, “6” == “6”, “4.2” == “4.20”, 和 “133” == “0133”. 但注意 133 != 0133, 因為 0133 是八進位制的.

>>    === 比較值和型別… 除了物件, 只有兩邊實際上是同一物件才為 true ! 對於物件, == 比較值(或每個屬性)和型別, 這又是 === 比較任何非物件型別的行為. 好玩嗎?

比較大小也好不到哪去.

>>    甚至行為都不一致: NULL < -1, 而 NULL == 0. 排序也因此不確定; 它依賴於在排序中比較元素的演算法的順序.

>>    比較操作符嘗試排序陣列, 以兩種不同的方式: 首先按長度, 然後按元素. 如果它們有相同數量的元素但不同的keys, 它們是不可比的.

>>    物件比較比其它比較做得更多… 除了那些即不小於也不大於的物件.

>>    為了型別更安全的 == 比較, 我們有 ===. 為了型別更安全的 < 比較, 我們有… 什麼也沒有. “123” < “0124”, 通常, 不管你怎麼做. 型別轉換也無濟於事.

>>    儘管上面的舉動很瘋狂, 但卻明確拒絕Perl’s的字串 paris 和算術執行符, PHP沒有過載 +. + 就是通常的 +, 而 . 是通常的連線符.

>>    [] 下標操作符也可以拼寫成 {}.

>>    [] 可以用於任何變數, 不光是字串和陣列. 它返回 null , 無錯誤警告.

>>    [] 僅能獲取單個元素.

>>    foo()[0] 是個語法錯誤. (已在 PHP 5.4 中修復)

>>    不像(從字面上看)任何其它語言都有的類似的操作符, ?: 是左結合的. 因此:

列印 horse.

變數

>>    無法宣告變數. 當第一次使用時, 不存在的變數會被建立為 null 值.

>>    全域性變數在使用前, 需要 global 宣告. 這是根據上面得出的自然結果, 因此這是個完美的理由, 但, 如果沒有顯示的宣告, 全域性變數甚至無法讀取 — PHP 將悄悄的建立一個區域性同名變數取代它. 我還沒見過其它語言使用類似的方法處理範圍問題.

>>    沒有引用. PHP所謂的引用是個真正的別名; 這無疑是一種倒退, 不像 Perl 的引用, 也沒有像 Python 那樣的物件標識傳遞.

>>    沒有明顯的方式檢測和取消引用.

>>    “引用” 使變數在語言中與眾不同. PHP 是動態型別的, 因此變數通常無型別… 除了引用, 它修飾函式定義, 變數語法, 和賦值. 一旦變數被引用(可在任何地方發生), 它就一直是個引用. 沒有明顯的方法探測和解引用需要的變數值.

>>    好吧, 我說謊了. 有些”SPL types” 也作用於變數: $x = new SplBool(true); $x = “foo”; 將失敗. 這有點像靜態型別, 自己看看.

>>    A reference can be taken to a key that doesn’t exist within an undefined variable (which becomes an array). Using a non-existent array normally issues a notice, but this does not.

>>    通過函式定義的常量稱為 taking a string; 這之前, 它們不存在. (這可能實際上是複製 Perl 使用常量的行為.)

>>    變數名是大小寫敏感的. 函式和類名不是. 使得方法使用駝峰式命名會很奇怪.

結構

>>    array() 和幾個類似的結構不是函式.  $func = “array”; $func(); 不工作.

>>    陣列拆包可以使用 list($a,$b) = …. 操作完成. list() 是類函式語法, 就像陣列那樣. 我不知道為什麼不給一個真正的專用語法, 也不知道為什麼名字如些的讓人迷惑.

>>    (int) 很顯然的被設計成類似C, 但它不是單獨的標記; 在語言中, 沒有東西被稱為 int. 試試看: var_dump(int)不工作, 它會丟擲一個解析錯誤, 因為引數看起來像是強制轉操作符.

>>    (integer) 是 (int) 的別名. 也有 (bool)/(boolean)和(float)/(double)/(real).

>>    有個(array)操作符用來轉成陣列和 (object) 用來轉成物件. 這聽起來很貼心, 但常常有個用例: 你可以用 (array) 使得某個函式引數, 既可以是單個元素,也可以是列表, 相同對待. 但這樣做不可靠, 因為如果某人傳遞了單個物件,把它轉換成陣列將實際上生成了一個包含物件屬性的陣列. (轉換成物件執行了反轉操作.)

>>    include()這類的函式基本上就是C的#include: 他們將其它的檔案原始碼轉存到你的檔案中. 沒有模組系統, 甚至對 PHP 程式碼也一樣.

>>    沒有類似巢狀或者區域性範圍的函式或類. 它們都是全域性的. include 某檔案, 它的變數匯入到當前函式範圍中(給了檔案訪問你的變數的能力), 但是函式和類存入全域性範圍中.

>>    追加陣列使用 $foo[] = $bar.

>>    echo 不是函式.

>>    empty($var) 是如此極端, 對於任何其它東西不表現為函式, 除了變數, e.g. empty($var || $var2), 是個解析錯誤. 為什麼地球上有這種東西, 解析器為什麼需要了解 empty ?

>>    還有些冗餘的語法塊: if (…): … endif;, 等等.

錯誤處理

>>    PHP 的一個獨特操作符是 @ (實際上從DOS借用過來的), 它隱藏錯誤.

>>    PHP 錯誤不提供棧軌跡. 你不得不安裝一個處理器生成它們. (但 fatal errors不行 — 見下文.)

>>    PHP 的解析錯誤通常只丟擲解析的狀態, 沒其它東西了, 使得除錯很糟糕.

>>    PHP 的解析器所指的例如.  ::  內部作為 T_PAAMAYIM_NEKUDOTAYIM, 而 << 操作符作為 T_SL. 我說 “內部的”, 但像上面說的, 給程式設計師顯示的 :: 或 << 出現在了錯誤的位置.

>>    大多數錯誤處理列印給伺服器日誌列印一行錯誤日誌, 沒人看到而一直進行.

>>    E_STRICT看起來像那麼回事, 但它實際上沒多少保護, 沒有文件顯示它實際上是做什麼的.

>>    E_ALL包含了所有的錯誤類別 — 除了 E_STRICT.

>>    關於什麼允許而什麼不允許是古怪而不一致的. 我不知道 E_STRICT 是怎樣適用於這裡的, 但這些卻是正確的:

>>    試圖訪問不存在的物件屬性, 如, $foo->x. (warning)

>>    使用變數做為函式名, 或者變數名, 或者類名. (silent)

>>    試圖使用未定義常量. (notice)

>>    試圖訪問非物件型別的屬性.(notice)

>>    試圖使用不存在的變數名.(notice)

>>    2 < “foo” (隱藏)

>>    foreach (2 as $foo); (warning)

而下面這些不行:

>>    試圖訪問不存在的類常量, 如 $foo::x. (fatal error)

>>    使用字串常量作為函式名, 或變數名, 或類名. (parse error)

>>    試圖呼叫一個示定義函式. (fatal error)

>>    Leaving off a semicolon on the last statement in a block or file. (parse error)

>>    使用 list 和其它準內建巨集作為方法名. (parse error)

>>    用下標訪問函式的返回值, 如: foo()[0]. (parse error; 已在 5.4 中修復)

在列表的其他地方也有幾個關於其它怪異解析錯誤的好例子

>>    __toString 方法不能丟擲異常. 如果你嘗試, PHP 將 … 呃, 丟擲一個異常. (實際上是個 fatal error, 可以被通過的, 除了…)

>>   PHP 錯誤和 PHP 異常是完全不同的物種. 它們不能相互作用.

>>    PHP 錯誤 (內部, 稱為 trigger_error)不能被 try/catch 捕獲.

>>    同樣, 異常不能通過 set_error_handler 安裝的錯誤處理器觸發錯誤.

>>    作為替代, 有一個單獨的 set_exception_handler 可以處理未捕獲的異常, 因為用 try 塊包裝你程式入口在         mod_pho 模組中是不可能的.

>>    Fatal 錯誤 (例如, new ClassDoesntExist()) 不能被任何東西捕獲. 大量的完全無害的操作會丟擲 fatal 錯誤, 由 於一些有爭議的原因被迫終結你的程式. 關閉函式仍然執行, 但它們無法獲取棧軌跡(它們執行在上層), 它們很難告知該程式是由一個錯誤還是程式的正常執行結束.

>>    沒有 finally 結構, 使得包裝程式碼 (註冊處理器, 執行程式碼, 登出處理器; monkeypatch, 執行測試, unmonkeypatch) 很難看, 很難寫. 儘管 OO 和異常大量的複製了Java的模式, 這是故意的, 因為 finally “在PHP上下文中, 只得其形不得其神”.Huh ?

函式

>>    函式呼叫似乎相當昂貴.

>>    一些內建函式與 reference-returning 函式互動, 呃, 一種奇怪的方式.

>>    正如在別處提到的, 很多看起來像函式或者看起來它們應該是函式的東西實際上是語言的構成部分, 因此無法像正常函式一樣的工作.

>>    函式引數可以具有 “型別提示”, 基本上只是靜態型別. 你不能要求某個引數是 int 或是 string 或是 物件 或其它 “核心” 型別, 即使每個內建函式使用這種型別, 可能因為 int 在PHP中不是個東西吧. (檢視上面關於 (int) 的討論). 你也不能使用特殊的被大量內建函式使用的偽型別裝飾: mixed, number, or callback.

>>    因此, 下面:

產生錯誤 the error:

PHP Catchable fatal error:  Argument 1 passed to foo() must be an instance of string, string given,         called in…

>>    你可能會注意到 “型別提示” 實際上並不存在; 在程式中沒有 string 類. 如果你試圖使用         ReflectionParameter::getClass() 動態測試型別提示, 將會得到型別不存在, 使得實際上不可能取得該型別名.

>>     函式的返回值不能被推斷

>>    將當前函式的引數傳給另一個函式 (分派, 不罕見) 通過 call_user_func_array(‘other_function’, func_get_args())完成. 但 func_get_args 在執行時丟擲一個 fatal 錯誤, 抱怨它不能作為函式引數. 為什麼為什麼這是個型別錯誤? ( 已在 PHP 5.3 中修復)

>>    閉包需要顯示的命名每個變數為 closed-over. 為什麼解析器不想辦法解決? (Okay, it’s because using a variable ever, at all, creates it unless explicitly told otherwise.)

>>    Closed-over 變數, 通過和其它函式引數相同的語義”傳遞”. 這樣的話, 陣列和字串等等, 將以傳值方式傳給閉包. 除非使用 &.

>>    因為閉包變數會自動傳遞引數, 沒有巢狀範圍, 閉包不能指向私有方法, 不管是否定義在類中. ( 可能在 5.4 中修復? 不清楚.)

>>    函式沒有命名引數. 實際上被 devs 顯示拒絕, 因為它 “會導致程式碼臭味”.

>>    Function arguments with defaults can appear before function arguments without, even though the documentation points out that this is both weird and useless. (So why allow it?)

>>    向函式傳遞額外的引數會被忽略 (除了內建函式, 會丟擲異常). 丟失的引數被假定為 null.

>>    “可變” 函式需要 func_num_args, func_get_arg, 和 func_get_args. 這類事情沒有語法.

OO

>>    PHP的函式部分被設計成類似C, 但物件導向 (ho ho) 被設計成類似 Java. 我不想過分強調這有多不合諧. 我還沒有發現一個有大寫字母的全域性函式, 重要的內建類使用駝峰式方法命名, 並有getFoo的Java風格的屬性訪問器. 這是門動態語言, 對嗎? Perl, Python, 和 Ruby 都有一些 通過程式碼訪問”屬性”的概念; PHP 僅僅有笨重的 __get 之類的東西. 型別系統圍繞著低層的 Java語言設計, Java 和PHP’s處一時代, Java 有意的做了更多限制, 照搬Java, 我百思不得其解.

>>    類不是物件. 超程式設計不得不通過字串名指向它們, 就像函式一樣.

>>    內建的型別不是物件, (不像Perl) 也無法使得看起來像物件.

>>    instanceof 是個操作符, 儘管很晚才增加進來, 而大多數語言都建有專門的函式和語法. 受Java影響嗎? 類不是第一類? (我不知道它們是不是.)

>>    但有一個 is_a 函式. 它有個可選引數指定是否允許物件實際是一個字串命名的類.

>>    get_class 是函式; 沒有 typeof 操作符. 同樣有 is_subclass_of.

>>    然而, 這對於內建型別無法工作, (再一次, int 不是個東西). 這樣, 你需要 is_int 等等.

>>    右值必須是變數或字面量; 不能是表示式. 不然會導致… 一個解析錯誤.

>>    clone 是一個操作符?!

>>    OO 的設計是一隻混合 Perl 和 Java 的怪物.

>>    物件屬性通過 $obj->foo, 但類屬性是 $obj::foo. 我沒見過任何其它語言這樣做, 或者這樣做有什麼用.

>>    而, 例項方法仍然能通過靜態的(Class::method)呼叫. 如果從其它方法中這麼呼叫, 會在當前 $this 上被看成常規的方法呼叫. 我認為吧.

>>    new, private, public, protected, static ,等等. 試圖虜獲 Java 開發者的芳心? 我知道這更多是個人的品位, 但我不知道為什麼這些東西在一門動態語言中是必要的 — 在 C++ 中, 它們中的大多數是有關彙編和編譯時的命名決議.

>>    子類不能覆蓋 private 方法. 子類覆蓋的公共方法也不可見, 單獨呼叫, 超類的私有方法. 會有問題, 如在測試mocks物件時.

>>    方法無法命名為, 例如 “list” , 因為 list() 是特殊的語法 (不是個函式) , 而解析器會被搞暈. 如此曖昧的原因無從得知, 而類工作得就很好. ($foo->list() 不是語法錯誤.)

>>    如果當解析建構函式引數時丟擲異常(如, new Foo(bar()) 而 bar() 丟擲), 建構函式不會被呼叫, 但解構函式會. (已在PHP 5.3 中修復)

>>    在 __autoload 和解析函式中的異常會導致 fatal 錯誤.

>>    沒有構造器或析構器. __construct 是個初始化函式, 像 Python 的 __init__. 無法通過呼叫類申請記憶體和建立物件.

>>    沒有預設的初始化函式. 呼叫 parent::__construct()的時候, 如果父類沒定義它自己的 __construct 方法會導致 fatal 錯誤.

>>    OO 帶來了個迭代器介面, 是語言規範的部分(如 … as …), 但該介面實際上沒有內建實現(如陣列) . 如果你想要個陣列迭代器,你必須用 ArrayIterator 包裝它. 沒有內建方式能夠讓迭代器將其作為第一類對像工作.

>>    類可以過載它們轉化成字串的方式,  但不能過載怎樣轉換成數字或任何其它內建型別的方式.

>>    字串, 數字, 和陣列都有字串轉換方式; 語言很依賴於此. 函式和類都是字串. 然而,如果沒定義 __toString , 試圖將換內建或自定義對像(甚至於一個閉包) 轉換成字串會導致錯誤, 甚至連 echo 都可能出錯.

>>    無法過載相等或比較操作.

>>    例項方法中的靜態變數是全域性的; 它們的值跨越該類的多個例項共享.

標準庫

Perl “某些需要彙編”. Python 是 “batteries included”. PHP 是 “廚房水槽, 它來自加拿大, 但所有的水龍頭用C貼牌”.

概括

>>    沒有型別系統. 你可以編譯PHP, 但必須通過 php.ini 指定要載入什麼, 選項因擴充套件部分存在(將它們的內容注入到全域性名稱空間中)或不存在.

>>    因為名稱空間是最近才有的特性, 標準庫一點沒被打亂. 在全域性名稱空間中有上千個函式.

>>    庫的某些部分很不一致.

>>    下劃線 對 無下劃線: strpos/str_rot13, php_uname/phpversion, base64_encode/urlencode, gettype/get_class

>>    “to” 對 2: ascii2ebcdic, bin2hex, deg2rad, strtolower, strtotime

>>    Object+verb 對 verb+object: base64_decode, str_shuffle, var_dump versus create_function,     recode_string

>>    引數順序: array_filter($input, $callback) versus array_map($callback, $input), strpos($haystack, $needle) versus array_search($needle, $haystack)

>>    字首混亂: usleep vs microtime

>>    Case insensitive functions vary on where the i goes in the name.

>>    大概一半的陣列函式以 array_ 開頭. 剩下的不是.

>>    廚房水槽. 庫包括:

>>    繫結 ImageMagick, 繫結 GraphicsMagick (ImageMagick的派生), 少量的幾個函式能檢測 EXIF 資料 (其中ImageMagick已經可以做到)

>>    解析 bbcode 的函式, 一些非常特殊的標記, 被幾個少量的論壇包使用.

>>    太多 XML 包. DOM (OO), DOM XML (not), libxml, SimpleXML, “XML Parser”, XMLReader/XMLWriter, 和一大砣我不能認出的東西就省略了. 當然會有些不同, 你可以自由的弄清晰它們的區別.

>>    繫結了兩個特別的信用卡處理器, SPPLUS 和 MCVE. 什麼?

>>    三種訪問 MySQL 資料庫的方式:  mysql, mysqli, 和 PDO 抽象的一些東西.

C 影響

它需要擁有的自己的符號. PHP 是個高層的, 動態型別的語言. 然後大量的標準庫的部分仍然只是圍繞 C APIS 的薄層封裝, 伴隨著下面的東西:

>>    “Out” 引數, 儘管 PHP 可以返回 ad-hoc 雜湊或毫不費力的返回多引數.

>>    至少一打的函式是為了獲取某子系統的最近一次錯誤(見下文), 儘管 PHP 已存存異常處理功能8年了.

>>     有個 mysql_real_escape_string, 儘管已有個具有相同引數的 mysql_escape_string, 僅僅因為它是 MySQL C API 的一部分.

>>    全域性行為卻是非全域性功能的(如 MySQL). 使用多個 MySQL 連線需要顯示的對每個函式呼叫傳遞連線控制程式碼.

>>    包裝器真的, 真的, 真的很薄. 例如, 呼叫了 dba_nextkey 而沒呼叫 dba_firstkey 將出現段錯誤.

>>    有一堆的 ctype_* 函式 (如 ctype_alnum) 對映類似名稱的 C 字元函式,  而不是如, isupper.

Genericism

如果函式相做兩件略有不同的事, PHP 就搞出兩個函式.

你怎樣反向排序? 在 Perl 中, 你可以用 { $b <=> $a}. 在 Python 中, 你可能用 .sort(reverse = True). 在 PHP 中, 有個特別的函式叫 rsort().

>>    那些看起來像 C error 的函式: curl_error, json_last_error, openssl_error_string, imap_errors, mysql_error, xml_get_error_code, bzerror, date_get_last_errors, 還有其它的嗎?

>>    排序函式: array_multisort, arsort, asort, ksort, krsort, natsort, natcasesort, sort, rsort, uasort, uksort, usort

>>    文字檢索函式: ereg, eregi, mb_ereg, mb_eregi, preg_match, strstr, strchr, stristr, strrchr, strpos, stripos, strrpos, strripos, mb_strpos, mb_strrpos, plus the variations that do replacements

>>    有大量的別名: strstr/strchr, is_int/is_integer/is_long, is_float/is_double, pos/current, sizeof/count, chop/rtrim, implode/join, die/exit, trigger_error/user_error…

>>    scandir 返回一個當前給出目錄的檔案列表. 而不是(可能有益)按返回目錄順序返回, 函式返回一個已排序的檔案列表. 有個可選的引數可以按字母逆順返回. 這些用於排序很顯然很不夠.

>>    str_split 將字串拆成等長的塊. chunk_split 將字串拆成等長的塊, 然後用個分隔符連線.

>>    讀取壓縮檔案需要一套單獨的函式, 取決於格式. 有六套函式, 它們的 API 都不同, 如 bzip2, LZF, phar, rar, zip, 和gzip/zlib

>>    因為使用引數陣列呼叫函式是如此的彆扭(call_user_func_array), 所以有些配套的像 printf/vprintf 和 sprintf/vsprintf. 它們做相同的事, 但一個帶多個引數, 另一個帶引數陣列.

文字

>>    preg_replace 帶 /e (eval) 標誌的將用待替換的字串替換匹配的部分, 然後 eval 它.

>>    strtok 的設計顯然是和 C 函式等效的, 由於很多原因, 已被認為是個壞注意. PHP 可以輕易的返回一個陣列(而這在C中彆扭), 很多的hack strtok(3) 用法 (修改字串某處), 在這裡不能使用.

>>    parse_str 解析查詢字串, 從函式名看不出任何跡象. 而它會 register_globals 並轉存查詢字串到本地範圍變數中, 除非你傳遞一個陣列來填充. (當然, 什麼也不返回)

>>    碰到空分隔符, explode 會拒絕分割. 每個其它的字串拆分實現採取這種作法的意思應該是把字串應拆分成字元; PHP有一個拆分函式, 令人迷惑的稱為 str_split 而卻描述為 “將字串轉成陣列”.

>>    格式化日期, 有 strftime, 像 C API 處理本地語言環境一樣. 當然也有 date, 完全不同的語法而僅用於 English.

>>    “gzgetss — 獲取 gz 檔案的行指標並去除 HTML 標記.” 知道了這一系列函式的概念, 讓我去死吧.

>>    mbstring

>>    都是關於 “multi-byte”, 解決字符集的問題.

>>    仍然處理的是普通字串. 有個單一的全域性”預設”的字符集. 一些函式允許指定字符集, 但它依賴於所有的引數和返回值.

>>    提供了 ereg_* 函式, 但這些都被廢棄了. preg_* 很幸運, 用一些 PCRE-specific 標記, 它們能理解 UTF-8.

系統和反射

>>    有一大堆的函式, 聚焦於文字和變數. 壓縮和提取僅是冰山一角.

>>    有幾種方式讓PHP動態, 咋一看沒有什麼明顯的不同或相對好處. 類工具不能修改自定義類; 執行時工具取代了它並能修改自定義的任何東西; Reflection* 類能反射語言的大部分東西; 有很多獨特的函式是為了報告函式和類的屬性的. 這些子系統是獨立, 相關, 多餘的嗎?

>>    get_class($obj) 返回物件的類名稱. get_class()返回被呼叫函式中的類的名稱. 撇開這些不說, 同一個函式會做完全不同的事情: get_class(null)… 行為象後者. 因此面對一個隨機的變數, 你不能信任它. 驚訝吧!

>>    stream_* 類允許實現自定義的流物件給fopen和其它的內建的類似檔案處理的東西使用. 由於幾個內部原因, “通知” 不能被實現.

>>    register_tick_function 能接受閉包物件. unregister_tick_function 不行; 相反, 它會丟擲錯誤, 抱怨閉包不能轉換成字串.

>>    php_uname 告知你當前作業系統相關東西.

>>    fork 和 exec 不是內建的. 它們來自 pcntl 擴充套件, 但預設不包含. popen 不提供 pid 檔案.

>>    session_decode 用於讀取任意的 PHP session 字串, 但僅當有個活躍的 session 時才工作. 它轉存結果到 $_SESSION 中, 而不是返回它的值.

雜項

>>    curl_multi_exec 不改變 curl_error 當出錯的時候, 但它改變 curl_error.

>>    mktime 的引數是有順序的: hour, minute, second, month, day, year

資料操縱

程式什麼都不是, 除了咀嚼和吐出資料以外. 大量的語言圍繞著資料操縱設計, 從 awk 到 Prolog 到 C. 如果語言無法操縱資料, 它就無法做任何事.

數字

>>    Integers 在32位平臺是是有符號32位數. 不像PHP的同時代者, 沒有自動 bigint 提升. 因此你的數學運算可能會由於CPU體系結構結果不一樣. 你唯一選擇大整數的方式是使用 GMP 或 BC 包裝函式. (開發者可能已經建義加入新的, 單獨的,64位型別. 這真是瘋了.)

>>    PHP支援八進位制數語法, 以0開頭, 因此如 012 是10. 然而, 08變成了0. 8(或9)和任何接下來的數字消失了. 01c是個語法錯誤.

>>    pi 是個函式. 或者有個常量, M_PI.

>>    沒有冪操作符, 只有 pow 函式.

文字

>>    無Unicode支援. 只有ASCII工作是可靠的, 真的. 有個 mbstring 擴充套件, 上面提過的, 但會稍被打擊.

>>    這意味著使用內建的string函式處理UTF-8文字會有風險.

>>    相似的, 在ASCII外, 也沒有什麼大小寫比較概念. 儘管有擴充套件版本的大小寫敏感的函式, 但它們不會認為 é 等於 É.

>>    你不能在變數中內插keys , 如, “$foo[‘key’]”是個語法錯誤. 你也不能 unquote it (這樣會產生警告, 無論什麼地方!), 或使用 ${…}/{$…}

>>    “${foo[0]}”是對的. “${foo[0][0]}”是個語法錯誤. 糟糕的拷貝類似 Perl 的語法 (兩個根本不同的語議)?

陣列

嘔, 騷年.

>>    這傢伙扮演list資料型別, 操作hash, 和排序set, 解析 list, 偶爾會有些奇怪的組合. 它是怎樣執行的? 以何種方式使用記憶體? 誰知道? 不喜歡, 反正我還有其它的選擇.

>>    => 不是操作符. 它是個特別的結構, 僅僅存在於 array(…) 和 foreach 結構中.

>>    負值索引不工作, 儘管 -1 也是個和0一樣的合法鍵值.

>>    儘管這是語言級的資料結構, 但沒有簡短語法; array(…)是簡短語法. (PHP 5.4 帶來了”literals”, […].)

>>    => 結構是基於 Perl , Perl允許 foo => 1 而不用引號.  在PHP中, 你這麼做會得到警告; 沒有無需引號建立 hash 字串鍵值的方式.

>>    陣列處理函式常常讓人迷惑或有不確定行為, 因為它們不得不對 lists, hashes, 或可能兩者的結合體做運算. 考慮 array 分組, “計算arrays的不同部分”.

這段程式碼將做什麼? 如果 array_diff 將引數以 hashes 看待, 它們明顯是不同的; 相同的keys有不同的值. 如果以list看待, 它們仍然是不同的; 值的順序不同.

事實上 array_diff 認為它們相等, 因為它以 sets 對待: 僅僅比較值, 忽略順序.

>>    同樣, array_rand 隨機選擇keys時, 也有奇怪的行為, 這對大多數需要從列表中挑出東西的用例沒什麼幫助.

儘管大量PHP程式碼依賴key的順序:

>>    如果兩個陣列混合的話, 會發生什麼? 我留給讀者自己弄清楚. (我不知道)

>>    array_fill 不能建立0長度的陣列; 相反它會發出警告並返回 false.

>>    所有的(很多的…) 排序函式就地操作而什麼都不返回. 想新建一個已排序陣列的拷貝, 沒門; 你不得不自己拷貝陣列, 然後排序, 然後再使用陣列.

>>    但 array_reverse 返回一個新陣列.

>>    一堆被排序的東西和一些鍵值對聽起來像是個某種強大的處理函式引數的方式, 但, 沒門.

非陣列

>>    標準庫包含 “快速雜湊”, “特定的強型別”的hash結構OO實現. 然, 深入它, 有4類, 每種處理不同的鍵值對型別組合. 不清楚為什麼內建的陣列實現不能優化這些極其普通情況, 也不清楚它相對的效能怎樣.

>>    有個 ArrayObject 類 (實現了4個不同的介面) , 它包裝陣列讓它看起來像物件. 自定義類可以實現同樣的介面. 但只有限的幾個方法, 其中有一半不像內建的陣列函式, 而內建的陣列函式不知道怎樣對ArrayObject或其它的類陣列的型別操作.

函式

>>    函式不是資料. 閉包實際上是物件, 但普通的函式不是. 你甚至不能通過它們裸名稱引用它們; var_dump(strstr) 會發出警告並猜測你的意思是字串字面量, “strstr”. 想辨別出字串還是”函式”引用, 沒門.

>>    create_function 基本上是個 eval 的包裝者. 它用普通的名字建立函式並在全域性範圍安裝它(因此永遠不會被垃圾回收—不要在迴圈中使用!). 它實際上對當前上下文一無所知, 因為它不是閉包. 名字包含一個 NUL 位元組, 因此永遠不會與普通函式衝突 (因為如果在檔案的任何地方有 NUL的話,  PHP 的解析器會失敗).

>>    Declaring a function named __lambda_func will break create_function—the actual implementation is to eval-create the function named __lambda_func, then internally rename it to the broken name. If __lambda_func already exists, the first part will throw a fatal error.

其它

>>    對 NULL 使用 (++) 生成 1. 對 NULL 用 (–) 生成 NULL.

>>    沒有生成器.

Web 框架

執行環境

>>    一個單一共享檔案 php.ini, 控制了 PHP 的大部分功能並織入了複雜的針對覆蓋什麼與何時覆蓋的規則. PHP軟體能部署在任意的機器上, 因此必須覆蓋一些設定使環境正常, 這在很大程式上會違背像 php.ini 這樣的機制的使用.

>>    PHP基本上以CGI執行. 每次頁面被點選, PHP 在執行前, 重編譯整個環境. 就連 Python 的玩具框架的開發環境都不會這樣.

>>    這就導致了整個 “PHP 加速器” 市場的形成, 僅僅編譯一次, 就能加速PHP, 就像其它的語言一樣. Zend, PHP的幕後公司, 將這個做為它們的商業模式.

>>    很長時間以來, PHP的錯誤預設輸出給客戶端 — 我猜是為開發環境提供幫助. 我不認為這是真相, 但我仍然看到偶爾會有mysql 錯誤出現在頁面的頂部.

>>    在 <?php … ?>標籤外的空白, 甚至在庫中, PHP以文字對待並解析給響應 (或者導致 “headers already sent” 錯誤). 一個流行的做法是忽略 ?>關閉標籤.

部署

部署方式常常被引述為PHP的最高階部分: 直接部署檔案就可以了. 是的, 這比需要啟動整個程式的 Python 或 Rury 或 Perl 要容易. 但 PHP 留下了許多待改進的地方.

我很樂意以應用伺服器的方式執行Web應用程式並反向代理它們. 這樣的代價最小, 而好處多多: 你可以單獨管理伺服器和應用程式, 你可以按機器的多或少執行執行多個或少量應用程式, 而不需要多個web伺服器,你可以用不同的使用者執行應用, 你可以選擇web伺服器, 你可以拆下應用而無需驚動web伺服器, 你可以無縫部署應用等等. 將應用與web伺服器直接焊接是荒謬的, 沒有什麼好的理由支援你這麼做.

>>    每個 PHP 應用程式都使用 php.ini . 但只有一個 php.ini 檔案, 它是全域性的; 如果你在一個共享的伺服器上, 需要修改它, 或者如果你執行兩個應用需要不同的設定, 你就不走運了; 你不得不向組織申請所有必須的設定並放在應用程式, 如使用 ini_set 或在 Apache 的配置檔案或在 .htaccess設定. 如果你能做的話. 可能 wow , 你有大量的地方需要檢查以找出怎樣獲取已設定的值.

>>    類似的, “隔離”PHP應用的方法也不容易, 它依賴於系統的其它部分. 想執行兩個應用程式,想要不同的庫版本, 或不同的PHP版本本身? 開始構建另一人Apache的拷貝吧.

>>    “一堆檔案”方案, 除了使路由像只病重的笨驢外, 還意味著你不得不小心處理白名單或黑名單, 以控制什麼東西可訪問, 這是因為你的 URL 層次也就是你的程式碼樹的層次. 配置檔案和其它的”區域性模組”需要C之類的東西守護以避免直接載入. 版本控制系統的檔案(如 .svn) 需要保護. 使用 mod_php , 使得檔案系統的所有東西都是潛在的入口; 使用應用伺服器, 僅有一個入口, 並且僅通過 URL 控制呼叫與否.

>>    你不能無縫的升級那堆以 CGI-style 執行的檔案, 除非你想要應用崩潰和出現未定義行為, 當使用者在升級的間歇期點選你的站點時.

>>    儘管配置 Apache 執行 PHP 很”簡單”, 仍然會有一些陷阱. 而 PHP 文件建議使用 SetHandler 使得 .php 檔案以 PHP方式執行, AddHandler 看起來執行良好, 然而事實上會有問題.

當你使用 AddHandler, 你在告知 Apache “以 php 執行它” , 這是一個可能的處理 .php 檔案的方式. 但! Apache 對檔案的副檔名不這樣認為. 它被設計為能支援如, index.html.en 這樣的檔案. 對於 Apache , 檔案可以同時具有任意數量的副檔名.

猜想, 你有個檔案上傳的表單, 儲存一些檔案到公共目錄中. 確保沒人能上傳 PHP 檔案, 你僅僅檢查檔案不能有.php 副檔名. 所有的攻擊需要做的只是上傳以 foo.php.txt 命名的檔案; 你的上傳工具不會看出問題,  Apache 會認為它是個 PHP, 它會很高興的執行.

這裡不是 “使用原始檔名” 或 “沒有更好的驗證”導致的問題; 問題是你的web伺服器要被配置用來執行任何舊程式碼, 使得PHP “容易部署”.  這不是理論上的問題; 我已發現很多實際的站點有類似的問題了.

缺失的特性

我認為所有這些都是以構建一個Web應用為中心的. 對PHP看起來很合理, 是它的銷售賣點之一, 它是 “Web語言”, 理應有它們.

>>    無模組系統. PHP就是模版.

>>    無 XSS 過濾器. htmlspecialchars 不是 XSS 過濾器.

>>    無 CSRF 保護. 你必須自己做.

>>    無通用標準的資料庫API. 像PDO這類東西不得不包裝每個特定資料庫的API, 分別抽象不同部分.

>>    無路由系統. 你的站點結構就是你的檔案系統結構.

>>    無認證或授權.

>>    無開發伺服器.

>>    無互動除錯模式.

>>    無一致的部署機制; 僅僅”拷貝所有檔案到伺服器中”.

安全

語言邊界

PHP的蹩腳安全機制可能會放大, 因為它利用某語言拿出資料, 又把它轉存到另一箇中. 這是個壞注意. “<script>” 可能在SQL中意味著什麼都不是, 但在HTML中就很是了.

讓情況更糟糕的是通常有人哇哇喊到 “你的輸入要消毒”. 那完全錯誤; 你不可能有什麼魔法使塊資料完全”幹靜”. 你需要做的就是對語言說: SQL使用佔位符, 程式孵化使用引數列表, 等等.

>>    PHP公然鼓勵 “消毒”: 有個資料過濾擴充套件可以做到.

>>    所有的 addslashes, scripslashes, 和其它的 slashes相關的東西都是廢物, 毫無用處.

>>    我只能告訴你這麼多, 無法安全的孵化程式. 你僅能通過shell執行字串. 你的選擇是瘋狂的轉義, 並希望預設的shell使用正確的轉義, 或手動的 pcntl_fork_exec 和 pcntl_exec.

>>    所有的轉義命令和轉義引數存在大致相同的描述. 注意在Windows中, 轉義引數不工作 (因為它假設成 Bourne shell 語議), 轉義命令僅僅用空格替換一堆標點符號, 因為沒人能搞清楚 Windows 命令轉義行為 (它可能默默的破壞你試圖做的任何事情).

>>    原始的內建 MySQL 繫結, 仍然廣泛使用, 它無法建立 prepared statements.

直到今天, PHP 文件關於SQL隱碼攻擊的建議還是讓人抓狂的做如型別檢查, 使用sprintf 和 is_numeric, 在每個地方手動的使用mysql_real_escape_string , 或在每處手動使用 addslashes (這個”可能更有用”!) 這樣的實踐. 並沒有提到 PDO 或 引數化, 除了在使用者評論中有點線索.  至少在兩年以前, 我就有具體的向 PHP dev 抱怨過了 , 他被驚動了, 而頁面卻從未變過.

Insecure-by-default

>>    register_globals. 它被預設關閉的,而在5.4中去除了. 我不在乎.

>>    include 接受 HTTL URLS. 和上面一樣.

>>    Magic quotes. So close to secure-by-default, and yet so far from understanding the concept at all.

核心

PHP直譯器本身就有一些惱人的安全問題.

>>    2007年的時候, 解析器有個整數溢位漏洞. 修復始於 if(size > INT_MAX) return NULL; 從那以後就走下坡路了. (對於那些不需要使用C的人: 曾經, INT_MAX 是適合變數最大整數. 我希望你能從這裡搞清楚其餘的東西.)

>>    最近, PHP 5.3.7 包括了個 crypt() 函式, 有個漏洞讓任何人可以用任何密碼登入.

>>    PHP5.4是容易遭受拒絕服務攻擊,因為它需要Content-Length頭(任何人都可以設定),並試圖分配更多記憶體。這是一個壞主意。

我可以挖掘更多, 但重點不是這有很多X漏洞 — 是軟體就有bugs, 無論如何都有. 這些自然是令人咋舌. 我並沒有特意尋找這些; 但在過去的幾個月裡, 它們自己送上門來了.

總結

一些評論會理所當然的指出我沒得出任何結論. 好吧, 我是沒有結論. 如果你一路看到了這裡, 我假設一開始你就同意我了 :)

如果你僅瞭解PHP而對學習其它東西感興趣, 可以看看 Python 教程, 嘗試 Flask 這個為web準備的傢伙. (我不是它的模版語言的鐵桿粉絲, 但它確實很好的完成了這些工作.) 它將你的應用分成多個部分, 但它們看起來仍然是一致的. 我可能稍後會寫個關於這個的貼子; 旋風般的介紹整個語言和不同於這裡所說的web堆疊.

之後或對於更大的專案, 你可能需要 Pyramid, 一箇中等規模的框架, 或者是 Django, 一個構建站點的複雜的框架, 如 Django站點.

 

相關文章