ホームページ >バックエンド開発 >PHPチュートリアル >[翻訳][php拡張機能の開発と組み込み] 第10章 - php4のオブジェクト
php4 のオブジェクト
かつて、非常に初期のバージョンでは、php はオブジェクト指向プログラミング構文をサポートしていませんでした。Zend エンジン (ZE1) が php4 に導入され、オブジェクト データ Type を含むいくつかの新機能が登場しました。
PHP オブジェクト タイプの進化 最初のオブジェクト指向プログラミング (OOP) サポートは、オブジェクトの関連付けのセマンティクスを実装しただけでした。PHP カーネル開発者の言葉を借りれば、「php4 オブジェクトは単なる配列であり、いくつかのメソッドは結合されています」。これは、これから学習する php オブジェクトです Zend エンジン (ZE2) の 2 番目のメジャー リリースは、php の OOP 実装にいくつかの新機能を導入しました。たとえば、プロパティとメソッドでアクセス修飾子を使用できます。第 11 章「php5 オブジェクト」を学習すると、クラス定義の外部での可視性をマークしたり、内部言語構造のカスタム動作を定義するために関数のオーバーロードを使用したり、複数のクラスの呼び出しチェーン間にインターフェイスを実装したりできます。これらの機能を php5 のクラス定義に実装することで、これらの知識を認識できるようになります。クラスの実装 OOP の世界に入る前に、この章で構築したばかりのスケルトン フォームに拡張機能を復元する必要があります。 5 「最初の拡張機能」。元の演習から独立させるために、このバージョンに「sample2」という名前を付けることができます。次の 3 つのファイルを 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) fiphp_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さて、第 5 章と同じように、第 1 章と同様に、phpize、./configure、make を実行して、sample2.so 拡張モジュールを構築できます。以前の設定ここで指定されている config.m4 と同じ変更を加えれば、.w32 は正常に動作します。
クラス エントリを定義します ユーザー空間で、次のようにクラスを定義します:
<?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_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_FE(method1, NULL) を使用することもできます。ただし、次のように第 5 章を確認してください。見つかると予想される関数実装の名前は zif_method1 ですが、関数の名前空間の安全性のために、これは他のメソッド 1() 実装と競合する可能性があります。クラス名の先頭にメソッド名を付けることもできます。 PHP_FALIAS(method1, Sample2_FirstClass_method1, NULL ) 形式も可能ですが、後でコードを見返すと、「なぜ PHP_FE( ではなかったのか)」と疑問に思うかもしれません。 ) 使用しますか?" これで、クラスに関数リストが追加されました。定義が完了したので、いくつかのメソッドを定義します。php_sample2_firstclass_functions 構造体に次の関数を作成します:
PHP_NAMED_FE(method1, PHP_FN(Sample2_FirstClass_method1), NULL)同様に、その関数に PHP_NAMED_FE() エントリを追加します。リスト:
PHP_FUNCTION(Sample2_FirstClass_countProps) { RETURN_LONG(zend_hash_num_elements(Z_OBJPROP_P(getThis()))); }これはユーザー空間の関数名がすべて小文字であることに注意してください。メソッド名と関数名が大文字と小文字を区別しないようにするために、内部関数はすべて小文字の名前を付ける必要があります。ここの要素は getThis() であり、PHP バージョンではマクロとして解析され、利用可能なオブジェクト インスタンスがない場合の This_ptr はユーザー空間オブジェクト メソッドの $this と本質的に同じです。メソッドが静的に呼び出されると、getThis() は NULL を返します。 オブジェクト メソッドのデータ戻りセマンティクスは、パラメーターの受け入れと arg_info のセマンティクスと同じです。
static function_entry php_sample2_firstclass_functions[] = { PHP_NAMED_FE(countprops, PHP_FN(Sample2_FirstClass_countProps), NULL) { NULL, NULL, NULL } };Constructorクラス コンストラクターは、他の通常のクラス メソッドと同様に実装できます。その命名方法も同じルールに従います。特別なのは、他の 2 つの ZE1 マジック メソッド __sleep() と __wakeup() は、コンストラクターにクラス名を付ける必要があることです。 継承 php4 では、内部オブジェクト間の継承は不完全であるため、本当に他のオブジェクトを継承する必要がある場合は、次の ZE1 コードをコピーする必要があります:
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; }そのような関数を定義します。 、MINIT で使用できます。 zend_register_internal_class は以下のように呼び出します:
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); } } }この方法での継承は機能しますが、ZE1 での継承は避けるべきです。PHP や ZE2 のほとんどの OOP プラクティスでは、ZE1 は内部オブジェクトの継承を処理するように設計されていないからです。改訂されたオブジェクト モデルは堅牢であり、すべての OOP 関連タスクをこれを使用して直接処理することを奨励しますサンプルを使用してください
。
和其它用户空间变量一样, 对象存储在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)!