前言

我的脾气古怪. 我会抱怨很多东西. 这个星球上大多数技术我都不喜欢.

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文档的某地方挑出来的.

1 @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 中修复)

>>    不像(从字面上看)任何其它语言都有的类似的操作符, ?: 是左结合的. 因此:

01 $arg = 'T';
02
03 $vehicle = ( ( $arg == 'B' ) ? 'bus' :
04
05              ( $arg == 'A' ) ? 'airplane' :
06
07              ( $arg == 'T' ) ? 'train' :
08
09              ( $arg == 'C' ) ? 'car' :
10
11              ( $arg == 'H' ) ? 'horse' :
12
13              'feet' );
14
15 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.

>>    因此, 下面:

1 function foo(string $s) {}
2
3 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的不同部分”.

1 $first  = array("foo" => 123, "bar" => 456);
2
3 $second = array("foo" => 456, "bar" => 123);
4
5 echo var_dump(array_diff($first, $second));

这段代码將做什么? 如果 array_diff 將参数以 hashes 看待, 它们明显是不同的; 相同的keys有不同的值. 如果以list看待, 它们仍然是不同的; 值的顺序不同.

事实上 array_diff 认为它们相等, 因为它以 sets 对待: 仅仅比较值, 忽略顺序.

>>    同样, array_rand 随机选择keys时, 也有奇怪的行为, 这对大多数需要从列表中挑出东西的用例没什么帮助.

尽管大量PHP代码依赖key的顺序:

1 array("foo", "bar") != array("bar", "foo")
2
3 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站点.

五大受损, 全面解析PHP的糟糕设计 – 讨论区 – 开源中国社区.

- EOF -