武汉PHP培训
达内武汉民大中心

15271940953

热门课程

五大受损, 全面解析PHP的糟糕设计

  • 时间:2017-10-17 14:41
  • 发布:php设计
  • 来源:武汉达内

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 on-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 抽象的一些东西.

武汉达内php10月免费训练营开课啦!

时间:10月23日-10月27日,为期5

地点:武汉达内培训PHP课程教学中心

正式开班时间:10月31日

上一篇:「PHP 是最好的语言」这个梗是怎么来的?
下一篇:作为PHP开发者一定要了解 Composer
选择城市和中心
贵州省

广西省

海南省