全部翻译内容pdf文档下载地址: http://download.csdn.net/detail/lgg201/5107012
本书目前在github上由laruence(http://www.laruence.com)和walu(http://www.walu.cc)两位大牛组织翻译. 该翻译项目地址为: https://github.com/walu/phpbook
本书在github上的地址: https://github.com/goosman-lei/php-eae
未来本书将可能部分合并到phpbook项目中, 同时保留一份独立版本.
原书名: <Extending and Embedding PHP>
原作者: Sara Golemon
译者: goosman.lei(雷果国)
译者Email: [email protected]
译者Blog: http://blog.csdn.net/lgg201
php4的对象
曾几何时, 在很早的版本中, php还不支持任何的面向对象编程语法. 在php4中引入了Zend引擎(ZE1), 出现了几个新的特性, 其中就包括对象数据类型.
php对象类型的演化
第一次的面向对象编程(OOP)支持仅实现了对象关联的语义. 用一个php内核开发者的话来说就是"php4的对象只是将一个数组和一些方法绑定到了一起". 它就是现在你要研究的php对象.
Zend引擎(ZE2)的第二个大版本发布是在php5中, 在php的OOP实现中引入了一些新的特性. 例如, 属性和方法可以使用访问修饰符标记它们在你的类定义外面的可见性, 函数的重载可以用来定义内部语言结构的自定义行为, 在多个类的调用链之间可以使用接口实施API标准化. 在你学习到第11章"php5对象"时, 你将通过在php5的类定义中实现这些特性来建立对这些知识的认知.
实现类
在进入OOP的世界之前, 我们需要轻装上阵. 因此, 请将你的扩展恢复到第5章"你的第一个扩展"中刚刚搭建好的骨架形态.
为了和你原有的习作独立, 你可以将这个版本命名为sample2. 将下面的三个文件放入到你php源代码的ext/sample2目录下:
config.m4
PHP_ARG_ENABLE(sample2, [Whether to enable the "sample2" extension], [ enable-sample2 Enable "sample2" extension support])if test $PHP_SAMPLE2 != "no"; then PHP_SUBST(SAMPLE2_SHARED_LIBADD) PHP_NEW_EXTENSION(sample2, sample2.c, $ext_shared)fi
php_saple2.h
#ifndef PHP_SAMPLE2_H/* Prevent double inclusion */#define PHP_SAMPLE2_H/* Define Extension Properties */#define PHP_SAMPLE2_EXTNAME "sample2"#define PHP_SAMPLE2_EXTVER "1.0"/* Import configure options when building outside of the PHP source tree */#ifdef HAVE_CONFIG_H#include "config.h"#endif/* Include PHP Standard Header */#include "php.h"/* Define the entry point symbol * Zend will use when loading this module */extern zend_module_entry sample2_module_entry;#define phpext_sample2_ptr &sample2_module_entry#endif /* PHP_SAMPLE2_H */
sample2.c
#include "php_sample2.h"static function_entry php_sample2_functions[] = { { NULL, NULL, NULL }};PHP_MINIT_FUNCTION(sample2){ return SUCCESS;}zend_module_entry sample2_module_entry = {#if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER,#endif PHP_SAMPLE2_EXTNAME, php_sample2_functions, PHP_MINIT(sample2), NULL, /* MSHUTDOWN */ NULL, /* RINIT */ NULL, /* RSHUTDOWN */ NULL, /* MINFO */#if ZEND_MODULE_API_NO >= 20010901 PHP_SAMPLE2_EXTVER,#endif STANDARD_MODULE_PROPERTIES};#ifdef COMPILE_DL_SAMPLE2ZEND_GET_MODULE(sample2)#endif
现在, 就像在第5章时一样, 你可以执行phpize, ./configure, make去构建你的sample2.so扩展模块.
你之前的config.w32做与这里给出的config.m4一样的修改也可以正常工作.
定义类条目
在用户空间中, 定义一个类如下:
<?phpclass Sample2_FirstClass {}?>
毫无疑问, 你会猜到, 在扩展中实现它还是有一点难度的. 首先, 你需要在你的源代码文件中, 像上一章定义int le_sample_descriptor一样, 定义一个zend_class_entry指针:
zend_class_entry *php_sample2_firstclass_entry;
现在, 就可以在MINIT函数中初始化并注册类了.
PHP_MINIT_FUNCTION(sample2){ zend_class_entry ce; /* 临时变量 */ /* 注册类 */ INIT_CLASS_ENTRY(ce, "Sample2_FirstClass", NULL); php_sample2_firstclass_entry = zend_register_internal_class(&ce TSRMLS_CC); return SUCCESS;}
构建这个扩展, 测试get_declared_classes(), 将会看到Sample2_FirstClass现在在用户空间可用了.
定义方法的实现
此刻, 你实现的只是一个stdClass, 当然它是可用的. 但实际上你是希望你的类可以做一些事情的.
要达成这个目的, 你就需要回到第5章学到的另外一个知识点了. 将传递给INIT_CLASS_ENTRY()的NULL参数替换为php_sample2_firstclass_functions, 并直接在MINIT函数上面如下定义这个结构:
static function_entry php_sample2_firstclass_functions[] = { { NULL, NULL, NULL }};
看起来熟悉吗? 当然. 这和你原来定义过程函数的结构相同. 甚至, 设置这个结构的方式也很相似:
PHP_NAMED_FE(method1, PHP_FN(Sample2_FirstClass_method1), NULL)
当然, 你也可以选用PHP_FE(method1, NULL). 不过回顾一下第5章, 这样做期望找到的函数实现的名字是zif_method1, 它可能潜在的回合其他的method1()实现冲突. 为了函数的名字空间安全, 我们将类名作为方法名的前缀.
PHP_FALIAS(method1, Sample2_FirstClass_method1, NULL)的格式也是可以的; 但它有点不直观, 你以后回过头来看代码的时候可能会产生疑问"为什么当时没有使用PHP_FE()?"
现在, 你已经将一个函数列表附加到类的定义上了, 是时候定义一些方法了. 在php_sample2_firstclass_functions结构上面创建下面的函数:
PHP_FUNCTION(Sample2_FirstClass_countProps){ RETURN_LONG(zend_hash_num_elements(Z_OBJPROP_P(getThis())));}
相应的, 在它的函数列表中增加一条PHP_NAMED_FE()条目:
static function_entry php_sample2_firstclass_functions[] = { PHP_NAMED_FE(countprops, PHP_FN(Sample2_FirstClass_countProps), NULL) { NULL, NULL, NULL }};
要注意, 这里暴露给用户空间的函数名是全部小写的.为了确保方法和函数名都是大小写不敏感的, 就要求内部函数给出全部小写的名字.
这里唯一的新元素就是getThis(), 在所有的php版本中, 它都会被解析为一个宏, 展开是this_ptr. this_ptr从本质上来说就和用户空间对象方法中的$this含义相同. 如果没有可用的对象实例, 比如方法被静态化调用, 则getThis()返回NULL.
对象方法的数据返回语义和过程函数一致, 参数接受以及arg_info都是同一套东西.
PHP_FUNCTION(Sample2_FirstClass_sayHello){ char *name; int name_len; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &name, &name_len) == FAILURE) { RETURN_NULL(); } php_printf("Hello"); PHPWRITE(name, name_len); php_printf("!\nYou called an object method!\n"); RETURN_TRUE;}
构造器
你的类构造器可以和其他的普通类方法一样实现, 它的命名遵循也遵循相同的规则. 特别之处在于你需要将构造器命名为类名. 其他两个ZE1魔术方法__sleep()和__wakeup()也可以以这种方式实现.
继承
php4中, 内部对象之间的继承是不完善的, 最好避免使用. 如果你确实必须继承其他对象, 需要复制下面的ZE1代码:
void php_sample2_inherit_from_class(zend_class_entry *ce, zend_class_entry *parent_ce) { zend_hash_merge(&ce->function_table, &parent_ce->function_table, (void (*)(void *))function_add_ref, NULL, sizeof(zval*), 0); ce->parent = parent_ce; if (!ce->handle_property_get) { ce->handle_property_get = parent_ce->handle_property_get; } if (!ce->handle_property_set) { ce->handle_property_set = parent_ce->handle_property_set; } if (!ce->handle_function_call) { ce->handle_function_call = parent_ce->handle_function_call; } if (!zend_hash_exists(&ce->function_table, ce->name, ce->name_length + 1)) { zend_function *fe; if (zend_hash_find(&parent_ce->function_table, parent_ce->name, parent_ce->name_length + 1, (void**)fe) == SUCCESS) { zend_hash_update(&ce->function_table, ce->name, ce->name_length + 1, fe, sizeof(zend_function), NULL); function_add_ref(fe); } }}
定义这样一个函数, 你就可以在MINIT中zend_register_internal_class下面对其进行调用:
INIT_CLASS_ENTRY(ce, "Sample2_FirstClass", NULL);/* 假定php_saple2_ancestor是一个已经注册的zend_class_entry */php_sample2_firstclass_entry = zend_register_internal_class(&ce TSRMLS_CC);php_sample2_inherit_from_class(php_sample2_firstclass_entry ,php_sample2_ancestor);
尽管这种方式的继承可以工作, 但还是应该避免ZE1中的继承, 因为它并没有设计内部对象的继承处理. 对于php中的多数OOP实践, ZE2和它修订的对象模型是健壮的, 鼓励所有的OOP相关任务都直接使用它来处理.
使用实例工作
和其它用户空间变量一样, 对象存储在zval *容器中. 在ZE1中, zval *包含了一个HashTable *用于保存属性, 以及一个zend_class_entry *指针, 指向类的定义. 在ZE2中, 这些值被一个句柄表替代, 增加了一个数值的对象ID, 它和资源ID的用法类似.
很幸运, ZE1和ZE2的这些差异被第2章"变量的里里外外"中介绍的Z_*()族宏隐藏了, 因此在你的扩展中不需要关心这些. 下表10.1列出了两个ZE1的宏, 与非OOP的相关宏一致, 它们也有对应的_P和_PP版本, 用来处理一级或两级间访.
宏 | 含义 |
Z_OBJPROP(zv) | 获取内建属性的HashTable * |
Z_OBJCE(zv) | 获取关联的zend_class_entry * |
创建实例
大部分时间, 你的扩展都不需要自己创建实例. 而是用户空间调用new关键字创建实例并调用你的类构造器.
但你还是有可能需要创建实例, 比如在工厂方法中, ZEND_API中的object_init_ex(zval *val, zend_class_entry *ce)函数可以用于将对象实例初始化到变量中.
要注意, object_init_ex()函数并不会调用构造器. 当在内部函数中实例化对象时, 构造器必须手动调用. 下面的过程函数重演了new关键字的功能逻辑:
PHP_FUNCTION(sample2_new){ int argc = ZEND_NUM_ARGS(); zval ***argv = safe_emalloc(sizeof(zval**), argc, 0); zend_class_entry *ce; if (argc == 0 || zend_get_parameters_array_ex(argc, argv) == FAILURE) { efree(argv); WRONG_PARAM_COUNT; } /* 第一个参数是类名 */ SEPARATE_ZVAL(argv[0]); convert_to_string(*argv[0]); /* 类名存储为小写 */ php_strtolower(Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0])); if (zend_hash_find(EG(class_table), Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0]) + 1, (void**)&ce) == FAILURE) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Class %s does not exist.", Z_STRVAL_PP(argv[0])); zval_ptr_dtor(argv[0]); efree(argv); RETURN_FALSE; } object_init_ex(return_value, ce); /* 如果有构造器则调用, 额外的参数将传递给构造器 */ if (zend_hash_exists(&ce->function_table, Z_STRVAL_PP(argv[0]),Z_STRLEN_PP(argv[0]) + 1)) { /* 对象有构造器 */ zval *ctor, *dummy = NULL; /* 构造器名字是类名 */ MAKE_STD_ZVAL(ctor); array_init(ctor); zval_add_ref(argv[0]); add_next_index_zval(ctor, *argv[0]); zval_add_ref(argv[0]); add_next_index_zval(ctor, *argv[0]); if (call_user_function_ex(&ce->function_table, NULL, ctor, &dummy, /* 不关心返回值 */ argc - 1, argv + 1, /* 参数 */ 0, NULL TSRMLS_CC) == FAILURE) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to call constructor"); } if (dummy) { zval_ptr_dtor(&dummy); } zval_ptr_dtor(&ctor); } zval_ptr_dtor(argv[0]); efree(argv);}
不要忘了在php_sample2_functions中增加一个引用. 它是你的扩展的过程函数列表, 而不是类方法的列表. 为了使用php_strtolower()函数, 还需要增加#include "ext/standard/php_string.h".
这个函数是目前你实现的最复杂的一个, 其中有几个全新的特性. 首先就是SEPARATE_ZVAL(), 实际上它的功能你已经实现过很多次, 利用zval_copy_ctor()赋值值到一个临时的结构体, 避免修改原始的内容. 不过它是一个宏版本的封装.
php_strtolower()用于将类名转换为小写, 这样做是为了达到php类名和函数名不区分大小写的目的. 这只是附录B中列出的众多PHPAPI工具函数的其中一个.
EG(class_table)是一个全局变量, 所有的zend_class_entry定义都注册到它里面. 要注意的是在ZE1(php4)中这个HashTable存储了一级间访的zend_class_entry *结构体. 而在ZE2(php5)中, 它被存储为两级间访. 这应该不会是一个问题, 因为对这个HashTable的直接访问并不常见, 但知道这一点总归是有好处的.
call_user_function_ex()是你将在第20章"高级嵌入式"中看到的ZENDAPI调用的一部分. 这里你将从zend_get_parameters_ex()接收到的zval **参数栈第一个元素拿走, 这样做就是为了原封不动的将剩余的参数传递给构造器.
译注: 原著中的代码在译者的环境(php-5.4.9)中不能运行, 需要将zend_class_entry *ce修改为二级间访. 下面给出译者测试通过的代码.
PHP_FUNCTION(sample_new){ int argc = ZEND_NUM_ARGS(); zval ***argv = safe_emalloc(sizeof(zval **), argc, 0); zend_class_entry **ce; /* 译注: 这里在译者的环境(php-5.4.9)是二级间访 */ /* 数组方式读取所有传入参数 */ if ( argc == 0 || zend_get_parameters_array_ex(argc, argv) == FAILURE ) { efree(argv); WRONG_PARAM_COUNT; } /* 隔离第一个参数(隔离为了使下面的类型转换不影响原始数据) */ SEPARATE_ZVAL(argv[0]); /* 将第一个参数转换为字符串类型, 并转为小写(因为php的类名是不区分大小写的) */ convert_to_string(*argv[0]); php_strtolower(Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0])); /* 在类的HashTable中查找提供的类是否存在, 如果存在, ce中就得到了对应的zend_class_entry * */ if ( zend_hash_find(EG(class_table), Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0]) + 1, (void **)&ce) == FAILURE ) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Class %s does not exist.", Z_STRVAL_PP(argv[0])); zval_ptr_dtor(argv[0]); efree(argv); RETURN_FALSE; } /* 将返回值初始化为查找到的类的对象 */ object_init_ex(return_value, *ce); /* 检查类是否有构造器 */ if ( zend_hash_exists(&(*ce)->function_table, Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0]) + 1) ) { zval *ctor, *dummy = NULL; /* 将ctor构造为一个数组, 对应的用户空间形式为: array(argv[0], argv[0]), * 实际上对应于用户空间调用类的静态方法时$funcname的参数形式: * array(类名, 方法名) */ MAKE_STD_ZVAL(ctor); array_init(ctor); zval_add_ref(argv[0]); add_next_index_zval(ctor, *argv[0]); zval_add_ref(argv[0]); add_next_index_zval(ctor, *argv[0]); /* 调用函数 */ if ( call_user_function_ex(&(*ce)->function_table, NULL, ctor, &dummy, argc - 1, argv + 1, 0, NULL TSRMLS_CC) == FAILURE ) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to call constructor"); } /* 如果有返回值直接析构丢弃 */ if ( dummy ) { zval_ptr_dtor(&dummy); } /* 析构掉临时使用(用来描述所调用方法名)的数组 */ zval_ptr_dtor(&ctor); } /* 析构临时隔离出来的第一个参数(类名) */ zval_ptr_dtor(argv[0]); /* 释放实参列表空间 */ efree(argv);}
接受实例
有时你的函数或方法需要接受用户空间的对象参数. 对于这种目的, zend_parse_parameters()提供了两种格式的修饰符. 第一种是o(小写字母o), 它将验证传递的参数是否是对象, 并将它设置到传递的zval **中. 下面是这种方式的一个简单的用户空间函数示例, 它返回传入对象的类名.
PHP_FUNCTION(sample2_class_getname){ zval *objvar; zend_class_entry *objce; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "o", &objvar) == FAILURE) { RETURN_NULL(); } objce = Z_OBJCE_P(objvar); RETURN_STRINGL(objce->name, objce->name_length, 1);}
第二种修饰符是O(大写字母O), 它不仅允许zend_parse_parameters()验证zval *的类型, 还可以验证所传递对象的类. 要做到这一点, 就需要传递一个zval **容易以及一个zend_class_entry *用来验证, 比如下面的实现就期望传入的是Sample2_FirstClass类的实例:
PHP_FUNCTION(sample2_reload){ zval *objvar; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "O", &objvar, php_sample2_firstclass_entry) == FAILURE) { RETURN_NULL(); } /* 调用假想的"reload"函数 */ RETURN_BOOL(php_sample2_fc_reload(objvar TSRMLS_CC));}
访问属性
你已经看到了, 类方法可以通过getThis()获取到当前对象实例. 将这个宏的结果或其它包含对象实例的zval *与Z_OBJPROP_P()宏组合, 得到的HashTable *就包含了该对象的所有属性.
对象的属性列表是一个包含zval *的HashTable *, 它只是另外一种放在特殊位置的用户空间变量列表. 和使用zend_hash_find(EG(active_symbol_table), ...)从当前作用域获取变量一样, 你也可以使用第8章"在数组和HashTable上工作"中学习的zend_hash-API去获取或设置对象的属性.
例如, 假设在变量rcvdclass这个zval *中包含的是Sample2_FirstClass的实例, 下面的代码块就可以从它的标准属性HashTable中取到属性foo.
zval **fooval;if (zend_hash_find(Z_OBJPROP_P(rcvdclass), "foo", sizeof("foo"), (void**)&fooval) == FAILURE) { /* $rcvdclass->foo doesn't exist */ return;}
要向属性表中增加元素, 则是这个过程的逆向过程, 调用zend_hash_add()去增加元素, 或者也可以将第8章介绍数组时介绍的add_assoc_*()族函数的assoc替换为property来处理对象.
下面的构造器函数为Sample2_FirstClass的实例提供了一些预先设置的默认属性:
PHP_NAMED_FUNCTION(php_sample2_fc_ctor){ /* 为了简洁, 同时演示函数名可以是任意的, 这里实现的函数名并不是类名 */ zval *objvar = getThis(); if (!objvar) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Constructor called statically!"); RETURN_FALSE; } add_property_long(objvar, "life", 42); add_property_double(objvar, "pi", 3.1415926535); /* 构造器的返回值会被忽略(请回顾前面构造器的例子) */}
现在可以通过php_sample2_firstclass_functions列表将它连接到对象的构造器:
PHP_NAMED_FE(sample2_firstclass, php_sample2_fc_ctor, NULL)
译注: 由于前面的sample_new()工厂函数在call_user_function_ex()调用构造器时使用的是静态方法的调用格式, 因此, 如果是使用这个工厂函数触发的构造器调用, getThis()就不会有期望的结果. 因此译者对例子进行了相应的修改, 读者如果在这块遇到问题可以参考译者的代码.
PHP_FUNCTION(sample_new){ int argc = ZEND_NUM_ARGS(); zval ***argv = safe_emalloc(sizeof(zval **), argc, 0); zend_class_entry **ce; /* 译注: 这里在译者的环境(php-5.4.9)是二级间访 */ /* 数组方式读取所有传入参数 */ if ( argc == 0 || zend_get_parameters_array_ex(argc, argv) == FAILURE ) { efree(argv); WRONG_PARAM_COUNT; } /* 隔离第一个参数(隔离为了使下面的类型转换不影响原始数据) */ SEPARATE_ZVAL(argv[0]); /* 将第一个参数转换为字符串类型, 并转为小写(因为php的类名是不区分大小写的) */ convert_to_string(*argv[0]); php_strtolower(Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0])); /* 在类的HashTable中查找提供的类是否存在, 如果存在, ce中就得到了对应的zend_class_entry * */ if ( zend_hash_find(EG(class_table), Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0]) + 1, (void **)&ce) == FAILURE ) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Class %s does not exist.", Z_STRVAL_PP(argv[0])); zval_ptr_dtor(argv[0]); efree(argv); RETURN_FALSE; } /* 将返回值初始化为查找到的类的对象 */ object_init_ex(return_value, *ce); /* 检查类是否有构造器 */ if ( zend_hash_exists(&(*ce)->function_table, Z_STRVAL_PP(argv[0]), Z_STRLEN_PP(argv[0]) + 1) ) { #define DYNAMIC_CONSTRUCTOR#ifndef DYNAMIC_CONSTRUCTOR zval *ctor;#endif zval *dummy = NULL;#ifndef DYNAMIC_CONSTRUCTOR /* 将ctor构造为一个数组, 对应的用户空间形式为: array(argv[0], argv[0]), * 实际上对应于用户空间调用类的静态方法时$funcname的参数形式: * array(类名, 方法名) */ MAKE_STD_ZVAL(ctor); array_init(ctor); zval_add_ref(argv[0]); add_next_index_zval(ctor, *argv[0]); zval_add_ref(argv[0]); add_next_index_zval(ctor, *argv[0]);#endif /* 调用函数 */ if ( call_user_function_ex(&(*ce)->function_table,#ifndef DYNAMIC_CONSTRUCTOR NULL, ctor,#else &return_value, *argv[0],#endif &dummy, argc - 1, argv + 1, 0, NULL TSRMLS_CC) == FAILURE ) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to call constructor"); } /* 如果有返回值直接析构丢弃 */ if ( dummy ) { zval_ptr_dtor(&dummy); }#ifndef DYNAMIC_CONSTRUCTOR /* 析构掉临时使用(用来描述所调用方法名)的数组 */ zval_ptr_dtor(&ctor);#endif } /* 析构临时隔离出来的第一个参数(类名) */ zval_ptr_dtor(argv[0]); /* 释放实参列表空间 */ efree(argv);}
译注: 现在, 就可以用函数中是否定义DYNAMIC_CONSTRUCTOR这个宏来切换构造器的调用方式, 以方便读者理解.
小结
尽管ZE1/php4提供的类功能最好少用, 但是由于当前php4在产品环境下还是广泛使用的, 因此做这个兼容还是有好处的. 本章涉及的技术可以让你灵活的编写各种功能的代码, 它们现在可以编译运行, 并且未来也将继续可以工作.
下一章, 你将看到php5中真正的面向对象, 如果你想要OOP, 从中你就可以得到升级的理由, 并且, 升级后你肯定再也不愿回头.
上一章: 资源数据类型