在PHP4以前, PHP并不支持面向对象, 到PHP4的时候, PHP引入了一些OOP的关键字, 请注意我用的”关键字”, 因为在PHP4中的对象, 不过就是一个数组(属性)加上一个函数数组(方法), 没有访问权限控制, 没有析构函数(当然可以模拟), 等等.
到PHP5以后, 随着Zend Engine 2的发布:
- 1. 访问权限控制
- 2. 接口的引入
- 3. 魔术方法(PHP4中可以通过overload来有限模拟)
- 4. 接口的应用
- 5. 内置接口
- 等等.
PHP5终于可以算是较完美的支持面向对象了.
而这些看似复杂的实现, 在根本上, 还是没有脱离属性数组+方法数组的基本, 接下来我就为大家揭开隐藏在源代码中的秘密.
对象的结构
在PHP5中, 一个对象, 还是以一个zval做为载体的, 还记得什么是Zval么(深入理解PHP原理之变量).
typedef union _zvalue_value {
long lval;
double dval;
struct {
char *val;
int len;
} str;
HashTable *ht;
zend_object_value obj;
} zvalue_value;
如果, 一个zval是对象, 那么zvalue_value中的obj, 就指向一个zend_object_value的实例.
一个zend_object_value包含俩个成员, 一个是标识符(整形序号), 表明了当前对象存储在全局对象列表的位置, 另外还有一个zend_object_handlers指针, 指向当前对象所属类的handlers(标准操作集合).
真正的对象实体, zend_object中, 保存了如下的关键信息入口:
- ce, zend_class_entry 类入口
- properties, hashTable 普通属性集
对象的属性
如上所述, 普通属性是一个的hashTable, 在PHP5以后, 引入了访问权限控制, 而访问权限属性, 是通过属性名进行区分的(为此Zend引入了zend_mangle_property_name).
- public 属性名
- private \0类名\0属性名
- protected \0*\0属性名
PHP通过这种比较ugly但是简单高效的方法, 实现了对属性访问权限的标识.知道了这个, 我们就可以干一些不合常理的事请, 比如访问对象的私有/保护属性(见: Bug #44273 access to private and protected class variables allowed when casting to array):
class Foo {
private $_name = "laruence";
protected $_age = 28;
}
$foo = new Foo();
$arr = (array) $foo;
var_dump($arr["\0Foo\0_name"]);
var_dump($arr["\0*\0_age"]);
//output:
string(8) "laruence"
int(28)
既然我说到了普通属性, 那么也就有不普通的属性:Static/Constant, 因为静态属性和常量属性都是和类相关而不是和对象相关的, 那么想到然的, 这些属性也就应该保存在类的结构中, 也就是zend_class_entry中. 对象的方法同理也是和类绑定的.
在zend_class_entry, 有如下HashTable几个成员,
- 1. function_table, 方法集
- 2. default_static_members, 静态属性
- 3. default_properties, 默认的属性
- 4. constants_table, 常量表
属性如其名, 对象类型的属性, 存放在对象的成员中.
对象的方法
在zend_class_entry中的function_table是个hashTable, 这个函数表的结构和普通的函数表一样(深入理解PHP之函数), 也是以zend_op_array为最终载体, 所以和普通函数一样, 方法也是不区分大小写的, 也是可以附加arg_info的. 而不同的则是, 函数的访问权限则由zend_op_array的fn_flag属性表明.
我们知道, 在PHP内的函数, 都有统一的参数列表(遵守PHP函数约定开发的前提下),
#define INTERNAL_FUNCTION_PARAM_PASSTHRU ht, return_value, return_value_ptr, this_ptr,return_value_used TSRMLS_CC
在调用方法的时候, this关键字由函数参数中的this_ptr指明, 而对于用户定义的函数, this关键字则有Zend VM保证.
说到这里, 举个列子:
如下代码, 会得到Fatal
<?php
class Foo {
public function Say() {
$this = NULL;
}
}
?>
//output:
PHP Fatal error: Cannot re-assign $this in **
这是一个初级的保护措施, 防止this关键字被改写, 难道PHP就仅仅是做了这个保护? 不然, 让我们绕过这个保护措施看看, 如下:
<?php
class Foo {
public $id = "laruence";
function Say($arr) {
extract($arr, EXTR_OVERWRITE);
var_dump($this);
var_dump($this->id);
}
}
$a = new Foo();
$a->sAY(array("this" => NULL)); //只是未来说明方法名不区分大小写.
?>
//output:
NULL
string(8) "laruence"
可见this关键字, 并不是简单的符号表中的item. 是在语法分析阶段, 就由ZEND Engine保证其正确性的, 并Attach在函数的结构体中.
对象的标准操作
我们对对象的操作, 比如获取属性, 设置属性, 获取对象的类, 等等, 这些常用的方法, 都是实现在对象的标准方法中的,PHP5中, 提供了23个标准方法.
在zend_object_value中的handlers指针, 就指向类常用操作的方法集合, 默认的, 这个指针指向:
ZEND_API zend_object_handlers std_object_handlers = {
zend_objects_store_add_ref, /* add_ref */
zend_objects_store_del_ref, /* del_ref */
zend_objects_clone_obj, /* clone_obj */
zend_std_read_property, /* read_property */
zend_std_write_property, /* write_property */
zend_std_read_dimension, /* read_dimension */
zend_std_write_dimension, /* write_dimension */
zend_std_get_property_ptr_ptr, /* get_property_ptr_ptr */
NULL, /* get */
NULL, /* set */
zend_std_has_property, /* has_property */
zend_std_unset_property, /* unset_property */
zend_std_has_dimension, /* has_dimension */
zend_std_unset_dimension, /* unset_dimension */
zend_std_get_properties, /* get_properties */
zend_std_get_method, /* get_method */
NULL, /* call_method */
zend_std_get_constructor, /* get_constructor */
zend_std_object_get_class, /* get_class_entry */
zend_std_object_get_class_name, /* get_class_name */
zend_std_compare_objects, /* compare_objects */
zend_std_cast_object_tostring, /* cast_object */
NULL, /* count_elements */
};
默认的, 也是绝大多数的时候, 都是如上的这些标准方法.
需要指明的是, _dimension后缀的方法, 指的是通过数组($obj['name'])方式访问对象的属性.
魔术方法
魔术方法定义在class_entry中,
- union _zend_function *constructor;
- union _zend_function *destructor;
- union _zend_function *clone;
- union _zend_function *__get;
- union _zend_function *__set;
- union _zend_function *__unset;
- union _zend_function *__isset;
- union _zend_function *__call;
- union _zend_function *__tostring;
- union _zend_function *serialize_func;
- union _zend_function *unserialize_func;
魔术方法没有默认值, 在类定义的时刻指定.
对于一些魔术方法, 比如__get/__set是被标准操作发起调用的, 所以如果我们自己编写扩展中定义的类, 如果不使用标准方法, 那么也需要在适当的时机, 主动调用这些魔术方法.
[自鸟哥博客]
网友评论