你或许不知道PHP的这些坑

弱类型

==和===异同这种太过低级的坑就直接跳过了,先看一个稍微隐蔽点的坑

function translate($keyword)
{
    $trMap = [ 
        'baidu' => '百度',
        'sougou' => '搜狗',
        '360' => '360',
        'google' => '谷歌'
    ];  
    foreach ($trMap as $key => $value) {
        if (strpos($keyword, $key) !== false) {
            return $value;
        }
    }   
    return '其他';
}

echo translate("baidu") . "\n";
echo translate("360") . "\n";

期待的结果是

百度
360

实际运行结果是

百度
其他

仔细检查,没有string和int的混用,比较也都是用的 !== ,没有用==,为什么还会掉坑里?

问题出在了array上面,虽然你写的是

    $trMap = [ 
        'baidu' => '百度',
        'sougou' => '搜狗',
        '360' => '360',
        'google' => '谷歌'
    ];  

但是PHP给你处理成了

array(4) {
  ["baidu"]=>
  string(6) "百度"
  ["sougou"]=>
  string(6) "搜狗"
  [360]=>
  string(3) "360"
  ["google"]=>
  string(6) "谷歌"
}

360变成了int类型,这个时候strpos不该报错吗?不,当然是原谅它啦,它选择兼容int

If needle is not a string, it is converted to an integer and applied as the ordinal value of a character.

360的hex表示是0x168,所以当你这样调用时,它能匹配

translate("\x1\x68")

那么正确的写法是怎么样的呢?稍加改动即可

strpos($keyword, $key) //改为 strpos($keyword, (string) $key)

可怕之处在于

  • 自以为用了===就安全了,忽视了弱类型无处不在这个隐患
  • 你可能并没有仔细看每一个函数的说明,没有逐个核对每个参数的类型
  • 引发的bug不一定能重现,也有可能平时不会触发,但是留下了安全漏洞

如何100%的避免弱类型的坑?答案是换强类型语言。如果不能换呢?通过以下准则,虽然做不到100%避免,但是做到99.99%是有希望的。

  1. 能用===/!==的地方,绝不用==/!=,知道类型的情况下,先强转再用===比较
  2. 调用函数的时候,如果你知道参数类型,在调用时强制转换一下,不能嫌麻烦

我说的是弱类型,不是动态类型,两者不是一码事,不要误会。Python是动态类型强类型,PHP是动态类型弱类型,C语言是静态类型弱类型。如果可以选择,我宁可PHP放弃弱类型,因为弱类型带来的麻烦,已经超出它的便利了。提供一个strict运行模式也行,给足大家十年八年时间慢慢迁移。


空字典json序列化成了[]

随着APP的流行,PHP很多时候不是跟浏览器端的JS交互,而是跟Java和ObjC这样的静态类型语言交互,返回值的类型定义,就很重要了,举个栗子

$ret1 = [ 
    'choices' => ['鱼香肉丝', '宫保鸡丁'],
    'answers' => [
        '张三' => 0,
        '李四' => 1,
        '赵云' => 0,
    ],  
];

$ret2 = [ 
    'choices' => [], 
    'answers' => [], 
];

echo json_encode($ret1) . "\n";
echo json_encode($ret2) . "\n";

输出

{"choices":["\u9c7c\u9999\u8089\u4e1d","\u5bab\u4fdd\u9e21\u4e01"],"answers":{"\u5f20\u4e09":0,"\u674e\u56db":1,"\u8d75\u4e91":0}}
{"choices":[],"answers":[]}

客户端在定义这个model的时候,可能是这样定义的

class ResultDTO {
	lateinit var choices: List<String>
	lateinit var answers: Map<String, Int>
}

当返回ret1的时候,一切顺风顺水,皆大欢喜。如果返回ret2呢,客户端抗议了

com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.util.LinkedHashMap out of START_ARRAY token

原因是什么呢?PHP的json_encode面对一个空的array的时候,它很为难,它不知道应该当它是list还是map,所以它只能一刀切,认为它就是list,于是客户端就不高兴了。解决办法不是没有,依然是强制转换。

$ret2 = [
    'choices' => [],
    'answers' => (object) [],
];

但是这样就带来一个问题,如果answers不是写死的,而是某个API的返回值,你并不确定它是不是会返回空的,它也没有义务帮你cast成object,因为JSON序列化是跟前端交互的事情,不应该放到后端service层面解决。那么你只能自己动手了,手动把返回值中可能出现空map的地方,全部强制转换一遍。

PHP的关联数组的确很强大,算法设计的也不错,性能也很好,但是它不是没有代价的,上面的栗子算是其中一个。如果PHP也像其它语言一样,区分map和list,可能会省事一些,毕竟区分{}和[],对程序员来说并不会增加很多学习成本。


健忘的FPM

近些年,Swoole和WorkerMan那样的CLI部署方式,慢慢被中国人知悉和应用,然而跟FPM或者mod_php相比,CLI方式还是太过非主流,在绝对垄断的FPM/mod_php面前,CLI在缓慢成长中。FPM的缺点很明显,每个请求结束的时候,你在PHP代码里创建的对象都被清理了,你执行过的代码,就跟没执行过一样,不留痕迹。

在hello world那样的微型应用中,好像问题不大,稍微大一点的项目,我们为了DRY,为了少做重复劳动,为了提高开发效率,不得不使用框架,然后问题就来了,用PHP写的PHP框架,由于FPM的健忘,框架从init开始,到读取配置文件,到初始化各个组件,这种工作在每个请求到来的时候,都要重复的做一次,如果你需要读一个100M的元数据,那么每个HTTP请求来时,你都要读一次并解析一次,当你HTTP请求结束返回时,你解析过的100M元数据,又被销毁了,下一个请求来时,你依然要重复做。

本来PHP 5.6已经可以吊打Python 3.6的性能了,PHP 7.1都不屑于跟Python比性能了,快几倍了。但是一旦引入同体量的框架,比如PHP用Laravel,Python用Django,剧情就反转了,Django竟然可以吊打PHP7加持的Laravel了。一个百米运动员就算跑的再快,每次枪响后都要先穿鞋带,穿好鞋带再穿鞋,然后再跑,跑完了把鞋脱下,再把鞋带抽出。就算它100米只要1秒就能跑完,光穿鞋的时间就够别的选手跑个来回了。

所以包括PHP之父本人在内,都对Laravel这样的封装深的厚框架表示质疑,在需要考虑性能的时候,主流人士往往推荐不用框架,或者用极简的框架,要么就是那些C写的框架,比如yaf和phalcon。框架性能问题算是曲线解决了,那么用户自己的逻辑呢?这个就比较麻烦了。分情况探讨,简单类型,如string,可以用yaconf这个扩展,可以做到不重复读取。如果是复杂的数据结构,比如树状结构,就没法用这种方式解决了。有没有解决办法呢?也不是没有,你可以写个脚本,把数据转换成PHP代码,然后通过opcache缓存起来,也能缓解一下问题。要彻底解决,只能写个C扩展让它常驻内存了,但这就超出一般PHP开发的能力范围了。

FPM这种方式并非PHP首创,在fastcgi出现之前,CGI都是这么干的,而且还是每个请求新开一个进程,比FPM还要开销大。然而到了21世纪,还在用FPM这种健忘型运行模式的,常见语言里就只剩PHP了。可能再过十年,FPM也渐渐被Swoole这样的不健忘的给取代了。


多线程支持

这里不讨论Apache的MPM是否支持多线程,也不讨论PHP的扩展是否支持多线程,更不讨论PHP到底能不能利用多线程或者多核,这里只讨论纯粹的PHP代码,能否创建和管理线程。前几年,PHP是完全不支持多线程,现在呢?据说有了pthreads,然后打开它的文档,发现

WarningThe pthreads extension cannot be used in a web server environment. Threading in PHP should therefore remain to CLI-based applications only.

WarningThe pthreads extension can only be used with PHP 7.2+. This is due to ZTS mode being unsafe in prior PHP versions.

两个限制

  1. 只能用在CLI下面
  2. 只支持PHP 7.2+

没用过多线程的人,自然不能体会多线程的便捷之处,跟多进程相比,数据共享在进程内部要容易的多。现代语言支持多线程是很自然的事情,跟PHP对比最多的Python,早就有了原生线程的支持,虽然因为GIL做不了CPU密集型应用,但是做个IO密集型还是很方便的。多线程只是锦上添花,不是雪中送炭,好在PHP的多进程支持还算OK,咱们就用多进程好了,最多共享数据结构的时候,想办法绕开便是。线程池 + 执行队列,变成进程池 + 执行队列。


32bit平台下,没有8字节的long类型

PHP的int是平台相关的,32位平台下是4字节,64位平台下是8字节,为了代码的健壮性和可移植性,我们只能假定int就是4字节的类型。但是我们很多时候需要8字节类型,因为

  1. 精确到毫秒的时间戳需要long
  2. 很多平台对接需要long,比如阿里巴巴

这个时候就需要GMP和BCMath这样的库了,比起语言直接支持8字节的long,麻烦了一些。


数组函数设计的太差,使用不便

PHP提供了一大堆array_xxxx函数,而没有把这些函数作为数组的方法,这种设计,乍看之下倒也没什么问题,但是有三个函数,在这种设计之下,实用性大打折扣。这三个函数是

array array_map ( callable $callback , array $array1 [, array $... ] )
mixed array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] )
array array_filter ( array $array [, callable $callback [, int $flag = 0 ]] )

举个栗子,把一个数组中的数求平方,并且把平方后大于100的数相加,用普通的写法是

$arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];

function foo_a($num_arr) {
    $sum = 0;

    foreach ($num_arr as $n) {
        $v = $n * $n;
        if ($v > 100) {
            $sum += $v;
        }
    }

    return $sum;
}
echo foo_a($arr) . "\n";

如果是简单的加减乘除,这种写法倒也OK,如果是比较复杂的逻辑,每一步的操作都会提出出来封装成相应的函数。我们来试试函数式写法,

function foo_b($num_arr) {
    return array_sum(
        array_filter(
            array_map(function ($v) { return $v * $v; }, $num_arr), function($v){
                return $v > 100;
            })
    );
}

echo foo_b($arr) . "\n";

看起来可读性比较差,也比较丑陋,这都拜PHP数组函数设计不合理所赐。假如可以这么写

function foo_c($num_arr) {
    return $num_arr.map(function ($v) { return $v * $v;})
        .filter(function ($v) {return $v > 100;})
        .sum()
}

可读性和实用性是不是提高了很多?只要把map/filter/reduce这3个定义成数组的方法,并且返回数组,就可以这么写了,能不能再简洁一点呢?我们继续

function foo_c($num_arr) {
    return $num_arr.map ($v -> $v * $v)
        .filter($v -> $v > 100)
        .sum()
}

有人可能要说我抄袭了,这不就是Java 8的lambda了嘛,对的,这就是Java 8的lambda。Java 8那点儿语法糖就吃饱了吗?显然不够,我们还可以再进一步简化下去。

function foo_c($num_arr) {
    return $num_arr.map {$it * $it}
        .filter {$it > 100}
        .sum()
}

给只接受一个参数的lambda一个默认的参数,名字叫it,是不是又简洁了一些?还可以再继续吗?当然可以

function foo_c($num_arr) = $num_arr.map {$it * $it}
        .filter {$it > 100}
        .sum()

看到这里,可能已经有人看出来这是哪门语言的语法了,对的,就是它。

顺便拿PHP最喜欢比较的Python对比一下,感受一下list comprehension的魅力

sum([y for y in [x * x for x in num_arr] if y > 100])

不懂Python的人是这么写Python的,求平方写成这样,哈哈

list(map(lambda x: x * x, num_arr))


函数命名风格太过不一致

PHP有nl2br这样的简写,还有htmlspecialchars_decode这样的长名字,据说当年PHP早期版本,用函数名字的长度作为hash,名字长度分布的均匀有助于减少hash冲突。听起来像是黑子们拿来喷PHP的,或者像PHP粉出来钓鱼的。但是看了这个

Re: Flexible function naming 我震惊了,PHP之父如是说

On 12/16/2013 07:30 PM, Rowan Collins wrote:

> The core functions which follow neither rule include C-style
> abbreviations like "strptime" which couldn't be automatically swapped to
> either format, and complete anomalies like "nl2br". If you named those
> functions as part of a consistent style, you would probably also follow
> stronger naming conventions than Rasmus did when he named
> "htmlspecialchars".

Well, there were other factors in play there. htmlspecialchars was a
very early function. Back when PHP had less than 100 functions and the
function hashing mechanism was strlen(). In order to get a nice hash
distribution of function names across the various function name lengths
names were picked specifically to make them fit into a specific length
bucket. This was circa late 1994 when PHP was a tool just for my own
personal use and I wasn't too worried about not being able to remember
the few function names.

-Rasmus

竟然是真的,太惊人了。据说后来到了PHP3的时候,替换掉了这个设计。而PHP在命名一致化的路上也一直在努力,但是考虑到兼容性,彻底解决可能还需要很多年的努力。


magic_quotes...

自动给你把GPC(GET/POST/COOKIE)变量中的特殊字符转义掉,幸好PHP 5.4已经删除这个特性了,不过有的比较传统的框架还保留着这个功能。我就想问问,你知道我要怎么用这些值吗?你知道哪些字符在我这边算特殊字符?自作主张一刀切,跟怕染HIV挥刀自宫的思路是一致的。再举个跟这个算是同类的栗子,配置对运行时行为影响过多过于复杂。

@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中.

最好的语言,隐藏了最多的黑魔法。要避开这个坑,只能尽量保证所有环境下面,编译参数一致,配置参数一致。


Error和Exception完全不同的机制

PHP 错误 (内部, 称为 trigger_error)不能被 try/catch 捕获。
同样, 异常不能通过 set_error_handler 安装的错误处理器触发错误。
作为替代, 有一个单独的 set_exception_handler 可以处理未捕获的异常。
Fatal 错误 (例如, new ClassDoesntExist()) 不能被任何东西捕获,大量的完全无害的操作会抛出 fatal 错误, 由 于一些有争议的原因被迫终结你的程序。

以上,一般框架层面会帮你解决,应用层面不需要操太多心。

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站点.


相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页