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_SAMPLE2 ZEND_GET_MODULE(sample2) #endif
現在,像第55章時一樣, 你可以執行phpize, ./configure, make去構建你的sample2.so擴展模組.
你之前的config.w32做與這裡給出的config.m4一樣的修改也可以正常工作.
定義類別條目
在用戶空間中, 定義一個類別如下:
<?php class 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_deced_ 測試get_deced_ 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_FUNCTION(Sample2_FirstClass_countProps) { RETURN_LONG(zend_hash_num_elements(Z_OBJPROP_P(getThis()))); }
static function_entry php_sample2_firstclass_functions[] = { PHP_NAMED_FE(countprops, PHP_FN(Sample2_FirstClass_countProps), NULL) { NULL, NULL, NULL } };
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; }
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); } } }
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);
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); }
和其它用户空间变量一样, 对象存储在zval *容器中. 在ZE1中, zval *包含了一个HashTable *用于保存属性, 以及一个zend_class_entry *指针, 指向类的定义. 在ZE2中, 这些值被一个句柄表替代, 增加了一个数值的对象ID, 它和资源ID的用法类似.
很幸运, ZE1和ZE2的这些差异被第2章"变量的里里外外"中介绍的Z_*()族宏隐藏了, 因此在你的扩展中不需要关心这些. 下表10.1列出了两个ZE1的宏, 与非OOP的相关宏一致, 它们也有对应的_P和_PP版本, 用来处理一级或两级间访.
创建实例
大部分时间, 你的扩展都不需要自己创建实例. 而是用户空间调用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, 从中你就可以得到升级的理由, 并且, 升级后你肯定再也不愿回头.
以上就是 [翻译][php扩展开发和嵌入式]第10章-php4的对象的内容,更多相关内容请关注PHP中文网(www.php.cn)!