譯者注:這篇文章很長,而且可能讀起來很亂,很難懂
前言
我的脾氣古怪. 我會抱怨很多東西. 這個星球上大多數技術我都不喜歡.
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文檔的某地方挑出來的.
- @fopen('http://example.com/not-existing-file', 'r');
它將做什麼?
>> 如果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 中修復)
>> 不像(從字面上看)任何其它語言都有的類似的操作符, ?: 是左結合的. 因此:
- $arg = 'T';
- $vehicle = ( ( $arg == 'B' ) ? 'bus' :
- ( $arg == 'A' ) ? 'airplane' :
- ( $arg == 'T' ) ? 'train' :
- ( $arg == 'C' ) ? 'car' :
- ( $arg == 'H' ) ? 'horse' :
- 'feet' );
- echo $vehicle;
打印 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.
>> 因此, 下面:
- function foo(string $s) {}
- foo("hello world");
產生錯誤 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的不同部分".
- $first = array("foo" => 123, "bar" => 456);
- $second = array("foo" => 456, "bar" => 123);
- echo var_dump(array_diff($first, $second));
這段代碼將做什麼? 如果 array_diff 將參數以 hashes 看待, 它們明顯是不同的; 相同的keys有不同的值. 如果以list看待, 它們仍然是不同的; 值的順序不同.
事實上 array_diff 認為它們相等, 因為它以 sets 對待: 僅僅比較值, 忽略順序.
>> 同樣, array_rand 隨機選擇keys時, 也有奇怪的行為, 這對大多數需要從列表中挑出東西的用例沒什麼幫助.
盡管大量PHP代碼依賴key的順序:
- array("foo", "bar") != array("bar", "foo")
- array("foo" => 1, "bar" => 2) == array("bar" => 2, "foo" => 1)
>> 如果兩個數組混合的話, 會發生什麼? 我留給讀者自己弄清楚. (我不知道)
>> 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站點.
英文: