标签归档:PHP

PHP面向对象的历史

PHP面向对象的历史

PHP最开始的perl脚本,到C语言版的PHP/FI,再到PHP/FI 2.0、PHP3.0,直到PHP4,引入Zend Engine使PHP更加的强大,并在PHP5引入新的Zend Engine2,重写PHP的面向对象模型,使PHP不仅可以快速开发,同时也可以实现更加复杂的架构,甚至满足企业应用。

PHP最开始并没有面向对象,直到PHP4才有一些面向对象的影子,到PHP5才真正实现面向对象模型。大概来说,PHP面向对象历史包括两个阶段:

PHP4-Zend Engine阶段

此时并没有真正的面向对象,因为PHP根本没有实现面向对象的三大特性,所有的成员方法和成员函数都是公有的,成员变量通过var声明。

此时的构造函数和类名一样,序列和反序列化时能调用魔术函数_sleep 和 __wakeup。嗯,这是是叫魔术函数而不是魔术方法,因为它本来就是独立出来的函数,当执行序列化时,PHP会判断当前变量是什么类型,如果是IS_OBJECT,则会自动调用__sleep函数。

在4.0.2以后可以使用parent::调用父类的方法。这里的parent仅仅是函数调用时的一个特殊处理。

在PHP的内核实现中类和函数共用一个opcode(ZEND_DECLARE_FUNCTION_OR_CLASS),通过extended_value字段区分,类和函数的存储已经区分开。

总的来说,PHP4的面向对象有点脚手架的味道,各种定制后有了一些面向对象的形。

PHP5-Zend Engine2阶段

5.0.0引入Zend Engine2,至此PHP才真正引入了面向对象的机制。
Zend Engine2重写了PHP的面向对象模型,其中包括对构建器和析构器的定义,增加的私有成员变量、静态成员变量、接口、重载等面向对象特性以及新增加了魔术方法实现。除了面向对象特性外,Zend Engine2引入了异常处理控制流。具体见: http://www.zend.com/engine2/ZendEngine-2.0.pdf

5.1.0 新增:__isset 和 __unset 方法。

5.3.0 新增: __invoke 方法、 后期静态绑定、 heredoc 和 nowdoc 支持类的常量和属性的定义、__callStatic 方法等

5.4.0 增加Traits,Trait 和类相似,但它的目的是用细粒度和一致的方式来组合功能。Trait 不能实例化。它为传统继承增加了水平特性的组合;也就是说,应用类的成员不需要继承。

总的来说,PHP5已经实现了面向对象模型,可以基于PHP5实现企业级应用。但是一些新的功能和特性,在实际的开发过程中使用得并不多,如Traits、命名空间等。很多时候,业务决定技术,需求决定实现。

然,此篇仅为整理之作,只为理自己对于PHP面向对象的思路。
久不沾笔,些许生疏。

参考资料

  • http://php.net/manual/zh/history.php.php
  • http://www.php.net/ChangeLog-4.php
  • http://www.php.net/ChangeLog-5.php

使用Yii框架中遇到的三个问题

使用Yii框架中遇到的三个问题

1、main.php文件中欲引入全局变量的问题

还原一下此问题:在Yii框架中,main.php一般会作为整个应用的配置文件,保存Application的各种参数,直接return数组。在使用的过程中,因为main.php文件一定会被Yii提前加载,所以将一些全局性的操作也放在了此文件,加载一些类操作啥的没有什么问题,当有一次加了一个全局变量,并且在其它地方使用global获取全局变量时,发现无论我如何努力都得到的是NULL。各种尝试后,终于,把引入的位置放在入口文件index.php,得以解决。什么原因?我们重现一下Yii的main.php文件加载。如下代码

index.php文件:

 class CApp {
        public function __construct($config) {
            $config = require($config);
        }
    }
 
    $path = "main.php";
    $app = new CApp($path);
 
    global $global;
    var_dump($global);

main.php文件:

 <?php
    $global = array(1, 2, 3);
    return array();

两个文件放在同一目录,直接运行index.php,输出的$global为NULL,如果我们在CApp的构造函数中直接输出$global,则会有结果输出。什么原因?作用域的问题!

当我们在main.php文件中定义了一个变量,虽然是想将其作为全局变量使用,但是当我们在局部的作用域中require时,其仅仅作为一个局部作用域的变量存在。我们在TIPI中有说到函数调用是嵌套的,每个嵌套都会有一个作用域,在这个作用域中的变量仅在当前有效,嵌套结束,变量生命周期结束。

因此,我们如果想把main.php中的全局变量真的作为整个应用的全局变量使用,则需要在入口文件的作用域中require main.php文件。

2、引入第三方扩展时的class_exists问题

Yii框架Yii基于PHP5的autoload机制来提供类的自动加载功能,自动加载器为YiiBase类的静态方法autoload()。当程序中用new创建对象或访问到类的静态成员,PHP将类名传递给类加载器,由类加载器完成类文件的include。但是如果我们引入了第三方扩展,而第三方扩展的命名规则和Yii的不一样,于是我们会经常看到报错说 require XXX 文件失败。如果你在google中搜索“yii framework class_exists”,你会发现Yii框架的作用Xue Qiang有回答使用者可以通过使用类似于: class_exists(‘MyClass’, false)的方式。

class_exists函数检查类是否已定义,如果由 class_name 所指的类已经定义,此函数返回 TRUE,否则返回 FALSE。在PHP内核中,此函数会查找当前类表中由 class_name 所指的类是否存在,在查找之前会全部转化为小写,所以不会区分大小写。其第二个参数是指是否使用autoload,默认为使用,此时class_exists函数会先执行autoload,然后再查找执行了autoload后类表中由 class_name 所指的类是否存在。因此我们可以通过设置第二个参数其为FALSE来绕过自动加载。

这可以解决问题,但是如果我们使用的是无法修改的第三方代码呢?怎么办?我自己是简单的hack了下,在调用第三方的操作之就将需要的类给加载了。

后来又采用了另一种解决方案:直接使用Yii:import的第二个参数,强制加载整个目录。

3、Yii的错误日志

问题就不细述了,只是将生产环境的配置整到了开发环境,于是错误看不到了。调整了下日志的规则,就OK了。

Yii对错误日志的处理依赖于PHP的set_error_handler函数和set_exception_handler函数。在CApplication的initSystemHandlers方法中有对这两个函数的处理。

PHP中的前缀自增(++i) 和后缀自增 (i++)

当我们学第一门语言时,比如大学课程中的C语言程序设计,也许曾经被前缀自增(++i) 和后缀自增 (i++)纠结过。 曾经以为我们懂了:

  • i++ :先引用后增加,先在i所在的表达式中使用i的当前值,后让i加1
  • ++i :先增加后引用,让i先加1,然后在i所在的表达式中使用i的新值

这个表达基本没错,只能说不够精确。在《Expert C Programming》这本书中的附录中,有这样一段说明: ++i表示取i的地址,增加它的内容,然后把值放在寄存器中;i++表示取i的地址,把它的值装入寄存器中,然后增加内存中的i的值。 这里的寄存器存放的就是我们在表达式中使用的值。

在PHP中也有++$i和$i++,那么Zend内核是如何实现这两种自增方式的呢? 看下面一个例子,在不运行这段代码的情况下,你认为会输出什么呢?

$i = 0;
$i = $i++;
echo $i;

咱们先不论答案是什么?我们直接从Zend内核查看这种自增操作的实现。

使用VLD查看包含了$i++和++$i的PHP代码生成的中间代码:

$i = 0;
$i++;
++$i;

使用VLD命令(php -dvld.active=1 -dvld.verbosity=3 t.php)查看详细参数:

number of ops:  8
compiled vars:  !0 = $i
line     # *  op                           fetch          ext  return  operands
--------------------------------------------------------------------------------
-
   2     0  >   EXT_STMT                                          RES[  IS_UNUSED  ]         OP1[  IS_UNUSED  ] OP2[  IS_UNUSED  ]
         1      ASSIGN                                                    OP1[IS_CV !0 ] OP2[ ,  IS_CONST (0) 0 ]
   3     2      EXT_STMT                                          RES[  IS_UNUSED  ]         OP1[  IS_UNUSED  ] OP2[  IS_UNUSED  ]
         3      POST_INC                                          RES[  IS_TMP_VAR ~1 ]       OP1[  IS_CV !0 ]
         4      FREE                                                      OP1[IS_TMP_VAR ~1 ]
   4     5      EXT_STMT                                          RES[  IS_UNUSED  ]         OP1[  IS_UNUSED  ] OP2[  IS_UNUSED  ]
         6      PRE_INC                                                   OP1[IS_CV !0 ]
   5     7    > RETURN                                                    OP1[IS_CONST (0) 1 ]

branch: #  0; line:     2-    5; sop:     0; eop:     7
path #1: 0,

从VLD扩展的输出信息可以知道,前缀自增(++$i)对应的opcode为PRE_INC,后缀自增($i++)对应的opcode为POST_INC。 首先我们看前缀自增(++$i),++$i没有返回值或者说它的返回值为空。 根据中间代码和VLD显示的OP1的参数类型, 我们可以知道++$i的中间代码在执行是最终调用的是Zend/zend_vm_execute.h文件中的ZEND_PRE_INC_SPEC_CV_HANDLER函数。 在ZEND_PRE_INC_SPEC_CV_HANDLER函数中有几个关键点:

  • CV类型变量的获取,它是调用_get_zval_ptr_ptr_cv获取CV类型变量。 这里的CV类型的变量是PHP编译期间的类似于缓存的作用,主要作用是提高某些变量的存储速度。
  • increment_function函数,不管是实例变量,类变量或者常规的变量,最终都是调用increment_function函数实现变量的增加操作。 在这个函数中,程序会根据变量的类型做出不同的处理,在PHP5.3.1这个版本中,PHP支持IS_LONG、IS_DOUBLE、IS_NULL和IS_STRING四种类型。 如果变量的类型是IS_NULL,程序会将变量的值赋值为1。如果变量类型是字符串,程序会将其转化成整形或浮点型进行计算。
  • 使用RETURN_VALUE_UNUSED宏清除返回结果,这个宏的作用是将result变量的类型设置为EXT_TYPE_UNUSED类型。

前缀自增(++$i)操作在Zend内核中本质上是操作变量本身,而且在表达式中使用的也是这个变量本身。

了解了++$i的实现,我们来看下可能使用得更多的$i++操作的实现。 同样,从中间代码POST_INC和OP1的类型是IS_CV,我们可以在Zend/zend_vm_execute.h文件中找到其实现为ZEND_POST_INC_SPEC_CV_HANDLER。 与前面的ZEND_PRE_INC_SPEC_CV_HANDLER相比,它们都有一个取CV类型变量的过程,也有一个increment_function函数增加变量值的过程, 但是除此之外它多了一个操作,同时也少了一个操作。 它多的一个操作是:

EX_T(opline->result.u.var).tmp_var = **var_ptr;
zendi_zval_copy_ctor(EX_T(opline->result.u.var).tmp_var);

这两行代码的作用是初始化返回值到临时变量,并且将原始的$i的值存储在这,这就是我们在前面使用VLD查看生成的中间代码其结果为RES[ IS_TMP_VAR ~1 ]的原因。 在这个初始化完成后,程序会继续执行增加操作,在增加操作完成后,它就结束了,而之前的++$i操作则会将result设置为UNUSED类型,这就是它少的那个操作。

后缀自增($i++)在表达式中使用的是存放在临时变量中原先的变量值,而变量本身的值已经增加了。 在PHP中这种变量的分离是通过临时变量+返回值解决。

到这里,我们可以回答最开始的问题了,它会输出0。因为在表达式中$i++的返回值是一个临时变量,也就是$i原来的值,也就是0。