首頁 >後端開發 >php教程 >[翻譯][php擴展開發與嵌入式]第11章-php5對象

[翻譯][php擴展開發與嵌入式]第11章-php5對象

黄舟
黄舟原創
2017-02-10 10:12:061519瀏覽


php5物件

將php5的物件和它的先輩php4物件進行比較實在有些不公平, 不過php5物件使用的API 如果遵循php4的API第10章"php4物件", 你將會對本章內容多少有些熟悉. 在開始本章之前, 可以像第10章開始時一樣, 重命名擴展為sample3並清理多餘的程式碼, 只保留擴展的骨架程式碼.

進化史

在php5物件變數中有兩個關鍵的組件. 第一個是一個數值的識別, 它和第9章"資源資料類型"中介紹的數值資源ID相似, 扮演了一個用來在對應表中查找物件實例的key的角色. 在這個實例表中的元素包含了到zend_class_entry的引用以及內部的屬性表.

第二個元素是物件變數的句柄表, 使用它可以自訂Zend引擎對實例的處理方式. 在本章後面你將看到這個句柄表.

zend_class_entry

類條目是你在用戶空間中定義的類的內部用戶空間定義的類的內部用戶空間定義條目表示. 如你在前一章所見, 這個結構透過呼叫INIT_CLASS_ENTRY()初始化, 參數為類別名稱和它的函數表. 接著在MINIT階段使用zend_register_internal_class()註冊.

zend_class_entry *php_sample3_sc_entry;
#define PHP_SAMPLE3_SC_NAME "Sample3_SecondClass"
static function_entry php_sample3_sc_functions[] = {
    { NULL, NULL, NULL }
};

PHP_MINIT_FUNCTION(sample3)
{
    zend_class_entry ce;
    INIT_CLASS_ENTRY(ce, PHP_SAMPLE3_SC_NAME,
                            php_sample3_sc_functions);
    php_sample3_sc_entry =
                zend_register_internal_class(&ce TSRMLS_CC);
    return SUCCESS;
}

方法。

PHP_METHOD(Sample3_SecondClass, helloWorld)
{
    php_printf("Hello World\n");
}

方法

如果你已經閱讀了上一章, 你可能就會想"到現在為止看起來幾乎一樣啊?", 到現在為止, 你是對的. 現在我們開始定義一些對象方法. 你將開始看到一些非常確定的並且大受歡迎的不同.

PHP_ME(Sample3_SecondClass,helloWorld,NULL,ZEND_ACC_PUBLIC)

在Zend引擎2中引入了PHP_METHOD()宏, 它是對PHP_FUNCTION()宏的封裝, 將類名和方法名聯合起來, 不用像php4中手動定義方法名稱了. 透過使用這個巨集, 在擴展中你的程式碼和其他維護者的程式碼的名字空間解析規範就保持一致了.

定義

定義一個方法的實現, 和
  • 定義一個方法的實現, 和

定義一個方法的實現, 和

  • 定義一個方法的實現, 和其他函數一樣, 只不過是將它連接到類別的函數表中. 除了用於實現的PHP_METHOD()宏, 還有一些新的宏可以用在函數列表的定義中.

PHP_ME( classname, methodname, arg_info, flags)

PHP_ME()相比於第5章"你的第一個擴展"中介紹的PHP_FE()宏, 增加了一個classname參數, 以及你的第一個擴展"中介紹的PHP_FE()宏, 增加了一個classname參數, 以及你末尾的一個flags參數(用來提供public, protected, private, static等存取控制, 以及abstract和其他一些選項). 例如要定義helloWorld方法, 就可以如下定義:

    PHP_MALIAS(Sample3_SecondClass, sayHi, helloWorld,
                                NULL, ZEND_ACC_PUBLIC)
  • aliaPHP_MALIAS(classname, name,

  • alia
PHP_MALIAS(classname, name, alia

arg_info, flags)

🎜🎜🎜🎜🎜和PHP_FALIAS()宏很像, 這個巨集允許你給alias參數描述的方法(同一個類別中的)實作提供一個name指定的新名字. 例如, 要複製你的helloWorld方法則可以如下定義🎜🎜🎜🎜🎜🎜
PHP_ME_MAPPING(hello, sample_hello_world, NULL)
🎜🎜🎜🎜PHP_ABSTRACT_ME(classname, methodname, arg_info)🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜🎜

内部类中的抽象方法很像用户空间的抽象方法. 在父类中它只是一个占位符, 期望它的子类提供真正的实现. 你将在接口一节中使用这个宏, 接口是一种特殊的class_entry.

  • PHP_ME_MAPPING(methodname, functionname, arg_info)

最后一种方法定义的宏是针对同时暴露OOP和非OOP接口的扩展(比如mysqli既有过程化的mysqli_query(), 也有面向对象的MySQLite::query(), 它们都使用了相同的实现.)的. 假定你已经有了一个过程化函数, 比如第5章写的sample_hello_world(), 你就可以使用这个宏以下面的方式将它附加为一个类的方法(要注意, 映射的方法总是public, 非static, 非final的):

PHP_ME_MAPPING(hello, sample_hello_world, NULL)

现在为止, 你看到的方法定义都使用了ZEND_ACC_PUBLIC作为它的flags参数. 实际上, 这个值可以是下面两张表的任意值的位域运算组合, 并且它还可以和本章后面"特殊方法"一节中要介绍的一个特殊方法标记使用位域运算组合.


方法不能被子類覆寫

类型标记

含义

ZEND_ACC_STATIC

方法可以静态调用.实际上,这就表示,方法如果通过实例调用, $this或者更确切的说this_ptr,并不会被设置到实例作用域中

ZEND_ACC_ABSTRACT

方法并不是真正的实现.当前方法应该在被直接调用之前被子类覆写.

ZEND_ACC_FINAL

方法不能被子類覆寫


方法不能被子類覆寫


只能在類別中或它的子類別中呼叫

可見性標記

意義

ZEND_ACC_PUBLIC

性是一樣的 ZEND_ACC_PROTECTED

_類別中呼叫


比如, 由於你前面定義的Sample3_SecondClass::helloWorld()方法不需要物件實例, 你就可以將它的定義從簡單的ZEND_ACC_PUBLIC 這樣修改為ZEND_ACC_PUBLIC | ZEND_ACCBLIC | ZEND_STATBLIC去提供(實例)了.

魔術方法

除了ZE1的魔術方法外, ZE2新增了很多魔術方法,如下表(或者可以在http://www.php.//cn中找到)


可選的自動呼叫的物件建構器(如果都會導致隱式的呼叫實例的__destruct()print語句時..__isset($varname)接口

方法

__construct(...)

之前定義的是和類別名稱一致的方法).

__construct() 在實例化的過程中,將優先調用__construct())當作用化de)。 ,或請求整個終止,

方法去處理一些清理工作,

__clone()

預設情況下, ,要真正的拷貝一個物件實例,就要使用clone關鍵字.當在一個物件實例上呼叫cl __clone()方法就會隱含的被執行,它允許物件複製一些需要的內部資源資料.用文字表示一個物件時,例如當直接在物件上使用echo

, __toString()方法將自動的被引擎呼叫

類別如果實作這個魔術方法,應該傳回一個包含描述物件的目前狀態的字串. __get($var)

如果腳本中請求一個物件不可見的屬性(不存在或由於存取控制導致不可見), __get()魔術方法會被呼叫,唯一的參數是所要求的屬性名稱.實作可以使用它自己的內部邏輯來決定最合理的回傳值將

__set($var, $value)

__get()__get() __set()提供了與之相反的能力,它用來處理賦值給對象的不可見屬性時的邏輯.__set()的實現可以選擇隱式的在標準屬性表中建立這些變數,以其他儲存機制設定值,

或直接拋出錯誤並丟棄值

)$

.

調用物件的未定義方法時可以透過使用__call()魔術方法實現漂亮的處理.這個方法接受兩個參數 包含呼叫時傳遞的所有實參的數值索引的數組.

php5.1.0之後, isset($obj->prop)的呼叫不只檢查$obj 它也會呼叫$obj中定義的__isset()方法,動態的評估嘗試使用動態的__成功讀寫屬性__unset($varname)類似於__isset(了一個簡單的

OOP
,

它可以用於對象屬性

,
雖然這個屬性可能在對象的標準屬性表中並不存在

__, )__set()的動態屬性空間是有意義的,因此引入__unset()來解決這個問題因此引入__unset()來解決這個問題


还有其他的魔术方法功能, 它们可以通过某些接口来使用, 比如ArrayAccess接口以及一些SPL接口.

在一个内部对象的实现中, 每个这样的"魔术方法"都可以和其他方法一样实现, 只要在对象的方法列表中正确的定义PHP_ME()以及PUBLIC访问修饰符即可.对于 __get(), __set(), __call(), __isset()以及__unset(), 它们要求传递参数, 你必须定义恰当的arg_info结构来指出方法需要一个或两个参数. 下面的代码片段展示了这些木梳函数的arg_info和它们对应的PHP_ME()条目:

static
    ZEND_BEGIN_ARG_INFO_EX(php_sample3_one_arg, 0, 0, 1)
    ZEND_END_ARG_INFO()
static
    ZEND_BEGIN_ARG_INFO_EX(php_sample3_two_args, 0, 0, 2)
    ZEND_END_ARG_INFO()
static function_entry php_sample3_sc_functions[] = {
    PHP_ME(Sample3_SecondClass, __construct, NULL,
                        ZEND_ACC_PUBLIC|ZEND_ACC_CTOR)
    PHP_ME(Sample3_SecondClass, __destruct, NULL,
                        ZEND_ACC_PUBLIC|ZEND_ACC_DTOR)
    PHP_ME(Sample3_SecondClass, __clone, NULL,
                        ZEND_ACC_PUBLIC|ZEND_ACC_CLONE)
    PHP_ME(Sample3_SecondClass, __toString, NULL,
                        ZEND_ACC_PUBLIC)
    PHP_ME(Sample3_SecondClass, __get, php_sample3_one_arg,
                        ZEND_ACC_PUBLIC)
    PHP_ME(Sample3_SecondClass, __set, php_sample3_two_args,
                        ZEND_ACC_PUBLIC)
    PHP_ME(Sample3_SecondClass, __call, php_sample3_two_args,
                        ZEND_ACC_PUBLIC)
    PHP_ME(Sample3_SecondClass, __isset, php_sample3_one_arg,
                        ZEND_ACC_PUBLIC)
    PHP_ME(Sample3_SecondClass, __unset, php_sample3_one_arg,
                        ZEND_ACC_PUBLIC)
    { NULL, NULL, NULL }
};

要注意__construct, __destruct, __clone使用位域运算符增加了额外的常量. 这三个访问修饰符对于方法而言是特殊的, 它们不能被用于其他地方.

属性

php5中对象属性的访问控制与方法的可见性有所不同. 在标准属性表中定义一个公开属性时, 就像你通常期望的, 你可以使用zend_hash_add()或add_property_*()族函数.

对于受保护的和私有的属性, 则需要使用新的ZEND_API函数:

void zend_mangle_property_name(char **dest, int *dest_length,
                        char *class, int class_length,
                        char *prop, int prop_length,
                        int persistent)

这个函数会分配一块新的内存, 构造一个"\0classname\0propname"格式的字符串. 如果类名是特定的类名, 比如Sample3_SecondClass, 则属性的可见性为private, 只能在Sample3_SecondClass对象实例内部可见.

如果类名指定为*, 则属性的可见性是protected, 它可以被对象实例所属类的所有祖先和后辈访问. 实际上, 属性可以以下面方式增加到对象上:

void php_sample3_addprops(zval *objvar)
{
    char *propname;
    int propname_len;
    /* public */
    add_property_long(objvar, "Chapter", 11);
    /* protected */
    zend_mangle_property_name(&propname, &propname_len,
        "*", 1, "Title", sizeof("Title")-1, 0);
    add_property_string_ex(objvar, propname, propname_len,
        "PHP5 Objects", 1 TSRMLS_CC);
    efree(propname);
    /* Private */
    zend_mangle_property_name(&propname, &propname_len,
        "Sample3_SecondClass",sizeof("Sample3_SecondClass")-1,
        "Section", sizeof("Section")-1, 0);
    add_property_string_ex(objvar, propname, propname_len,
        "Properties", 1 TSRMLS_CC);
    efree(propname);
}

通过_ex()版的add_property_*()族函数, 可以明确标记属性名的长度. 这是需要的, 因为在protected和private属性名中会包含NULL字节, 而strlen()认为NULL字节是字符串终止标记, 这样将导致属性名被认为是空. 要注意的是_ex()版本的add_property_*()函数还要求显式的传递TSRMLS_CC. 而通常它是通过宏扩展隐式的传递的.

定义类常量和定义类属性非常相似. 两者的关键不同点在于它们的持久性, 因为属性的生命周期是伴随的实例的, 它发生在请求中, 而常量是和类定义在一起的, 只能在MINIT阶段定义.

由于标准的zval *维护宏的函数假定了非持久性, 所以你需要手动写不少代码. 考虑下面的函数:

void php_sample3_register_constants(zend_class_entry *ce)
{
    zval *constval;


    /* 基本的标量值可以使用Z_*()去设置它们的值 */
    constval = pemalloc(sizeof(zval), 1);
    INIT_PZVAL(constval);
    ZVAL_DOUBLE(constval, 2.7182818284);
    zend_hash_add(&ce->constants_table, "E", sizeof("E"),
                    (void*)&constval, sizeof(zval*), NULL);

    /* 字符串需要额外的空间分配 */
    constval = pemalloc(sizeof(zval), 1);
    INIT_PZVAL(constval);
    Z_TYPE_P(constval) = IS_STRING;
    Z_STRLEN_P(constval) = sizeof("Hello World") - 1;
    Z_STRVAL_P(constval) = pemalloc(Z_STRLEN_P(constval)+1, 1);
    memcpy(Z_STRVAL_P(constval), "Hello World",
                            Z_STRLEN_P(constval) + 1);
    zend_hash_add(&ce->constants_table,
                    "GREETING", sizeof("GREETING"),
                    (void*)&constval, sizeof(zval*), NULL);


    /* Objects, Arrays, and Resources can't be constants */
}
PHP_MINIT_FUNCTION(sample3)
{
    zend_class_entry ce;
    INIT_CLASS_ENTRY(ce, PHP_SAMPLE3_SC_NAME,
                            php_sample3_sc_functions);
    php_sample3_sc_entry =
                zend_register_internal_class(&ce TSRMLS_CC);
    php_sample3_register_constants(php_sample3_sc_entry);
    return SUCCESS;
}

在这之下, 这些类常量就可以访问了, 分别是: Sample3_SecondClass::E和Sample3_SecondClass::GREETING.

接口

接口的定义和类的定义除了几个差异外基本一致. 首先是所有的方法都定义为抽象的, 这可以通过PHP_ABSTRACT_ME()宏来完成.

static function_entry php_sample3_iface_methods[] = {
    PHP_ABSTRACT_ME(Sample3_Interface, workerOne, NULL)
    PHP_ABSTRACT_ME(Sample3_Interface, workerTwo, NULL)
    PHP_ABSTRACT_ME(Sample3_Interface, workerThree, NULL)
    { NULL, NULL, NULL }
};

由于这些方法是抽象的, 所以不需要实现. 接下来的第二个差异就是注册. 和一个实际的类注册类似, 首先调用INIT_CLASS_ENTRY和zend_register_internal_class.

当类(zend_class_entry)可用时, 最后一部就是标记这个类是接口, 实现方法如下:

zend_class_entry *php_sample3_iface_entry;
PHP_MINIT_FUNCTION(sample3)
{
    zend_class_entry ce;
    INIT_CLASS_ENTRY(ce, "Sample3_Interface",
                        php_sample3_iface_methods);
    php_sample3_iface_entry =
                zend_register_internal_class(&ce TSRMLS_CC);
    php_sample3_iface_entry->ce_flags|= ZEND_ACC_INTERFACE;

实现接口

假设你想让Sample3_SecondClass这个类实现Sample3_Interface这个接口, 就需要实现这个接口定义的所有抽象方法:

PHP_METHOD(Sample3_SecondClass,workerOne)
{
    php_printf("Working Hard.\n");
}
PHP_METHOD(Sample3_SecondClass,workerTwo)
{
    php_printf("Hardly Working.\n");
}
PHP_METHOD(Sample3_SecondClass,workerThree)
{
    php_printf("Going wee-wee-wee all the way home.\n");
}

接着在php_sample3_sc_functions列表中定义它们:

PHP_ME(Sample3_SecondClass,workerOne,NULL,ZEND_ACC_PUBLIC)
PHP_ME(Sample3_SecondClass,workerTwo,NULL,ZEND_ACC_PUBLIC)
PHP_ME(Sample3_SecondClass,workerThree,NULL,ZEND_ACC_PUBLIC)

最后, 定义你新注册的类实现php_sample3_iface_entry接口:

PHP_MINIT_FUNCTION(sample3)
{
    zend_class_entry ce;
    /* 注册接口 */
    INIT_CLASS_ENTRY(ce, "Sample3_Interface",
                        php_sample3_iface_methods);

    php_sample3_iface_entry =
                zend_register_internal_class(&ce TSRMLS_CC);
    php_sample3_iface_entry->ce_flags|= ZEND_ACC_INTERFACE;
    /* 注册实现接口的类 */
    INIT_CLASS_ENTRY(ce, PHP_SAMPLE3_SC_NAME,
                            php_sample3_sc_functions);
    php_sample3_sc_entry =
                zend_register_internal_class(&ce TSRMLS_CC);
    php_sample3_register_constants(php_sample3_sc_entry);
    /* 声明实现关系 */
    zend_class_implements(php_sample3_sc_entry TSRMLS_CC,
                1, php_sample3_iface_entry);
    return SUCCESS;
}

如果Sample3_SecondClass实现了其他接口, 比如ArrayAccess, 就需要将对应的类(zend_class_entry)作为附加参数增加到zend_class_implements()调用中, 并将现在传递为数字1的参数值相应的增大为2:

zend_class_implements(php_sample3_sc_entry TSRMLS_CC,
            2, php_sample3_iface_entry, php_other_interface_entry);

句柄

ZE2并没有把所有的对象实例看做是相同的, 它为每个对象实例关联了句柄表. 当在一个对象上执行特定的操作时, 引擎调用执行对象的句柄表中自定义的行为.

标准句柄

默认情况下, 每个对象都被赋予了std_object_handlers这个内建句柄表. std_object_handlers中对应的句柄方法以及它们的行为定义如下:

  • void add_ref(zval *object TSRMLS_DC)

当对象值的refcount增加时被调用, 比如, 当一个对象变量赋值给新的变量时. add_ref和del_ref函数的默认行为都是调整内部对象存储的refcount.

  • void del_ref(zval *object TSRMLS_DC)

和add_ref类似, 这个方法也在修改refcount时调用, 通常是在unset()对象变量时发生的.

  • zend_object_value clone_obj(zval *object TSRMLS_DC)

用于利用已有的对象实例创建一个新的实例. 默认行为是创建一个新的对象实例, 将它和原来的句柄表关联, 拷贝属性表, 如果该对象的类定义了__clone()方法, 则调用它让新的对象执行一些附加的复制工作.

  • zval *read_property(zval *obj, zval *prop, int type TSRMLS_DC)

  • void write_property(zval *obj, zval *prop, zval *value TSRMLS_DC)

在用户空间尝试以$obj->prop方式访问, 去读写对象的属性时, read_property/write_property对应的被调用. 默认的处理是首先在标准属性表中查找属性. 如果属性没有定义, 则检查是否存在__get()或__set()魔术方法, 如果有则调用该方法.

  • zval **get_property_ptr_ptr(zval *obj, zval *value TSRMLS_DC)

get_property_ptr_ptr()是read_property()的一個變種, 它的含義是允許調用作用域直接將當前的zval *替換為新的. 預設的行為是傳回標準屬性表中該屬性的指標位址. 如果不存在, 並且沒有__get()/__set()魔術方法, 則隱式創建並返回指針. 如果存在__get()或__set()方法, 則導致這個句柄失敗, 使得引擎轉而依靠單獨的read_property和write_property呼叫.

  • zval *read_dimension(zval *obj, zval *idx, int type TSRMLS_DC)

    RMLS_DC)
  • read_dimension()和write_dimension()類似於對應的read_property()和write_property();  不過它們在使用$obj['idx']方式將物件作為數組存取時被觸發. 如果物件的類別沒有觸發. 如果物件的類別沒有觸發. 如果物件的類別沒有觸發. 如果物件的類別沒有觸發. 如果物件的類別實作ArrayAccess介面, 預設的行為是觸發一個錯誤; 否則它就會呼叫魔術方法offsetget($idx)或offsetset($idx, $value).

zval *get(zval *obj TSRMLS_DC)
  • zval *get(zval *objTSRMLS_DC)

  • void set(zval *obj, zval *value TSRMLS_DC)

在設定或取回物件的值時, 則會在物件上呼叫方法()或set().作為第一個參數被傳遞. 對於set, 新的值作為第二個參數傳遞; 實際上, 這些方法被用於算數運算中. 這些操作沒有默認處理器.

  • int has_property(zval *obj, zval *prop, int chk_type TSRMLS_DC)

當在一個物件屬性上呼叫isset()時, 這個句柄被呼叫. 預設情況下標準的處理器會檢查prop指定的屬性名, 在php 5.1.0中如果沒有找到這個屬性, 並且定義了__isset()方法, 則會調用這個方法. chk_type參數的值如果是2則僅需要屬性存在, 如果chk_type值為0, 則必須存在並且不能是IS_NULL的值, 如果chk_type值為1, 則屬性必須存在且必須是非FALSE的值. 注意: 在php 5.0.x中, chk_type的意思和has_dimension的chk_type一致.

  • int has_dimension(zval *obj, zval *idx, int chk_type TSRMLS_DC)

    , zval *idx, int chk_type TSRMLS_DC)

  • (5)當物件做數字($obj['idx'])), 使用這個處理器. 預設的標準處理器會檢查物件是否實作了ArrayAccess介面, 如果實作了, 則呼叫offsetexists($idx)方法. 如果沒有找到(指呼叫offsetexists ()), 則和沒有實作offsetexists()方法一樣, 回傳0. 否則, 如果chk_type為0, 直接回傳true(1). chk_type為1標識它必須呼叫物件的offsetget($idx)方法並測試回傳值, 檢查值為非FALSE才回傳TRUE(1).

    void unset_property(zval *obj, zval *prop TSRMLS_DC)
  • 市「RM」

  • 這兩個方法在嘗試卸載物件屬性時(或將物件以數組方式應用呼叫unset()時)被呼叫.unset_property()處理器要麼從標準屬性表刪除屬性(如果存在), 要麼就嘗試呼叫實作的__unset($prop)方法(php 5.1.0中), unset_dimension()則在類別實作了ArrayAccess時, 呼叫offsetunset($idx)方法.

HashTable *get_properties(zval; TSRMLS_DC)

  • 當內部函數使用Z_OBJPROP()巨集從標準屬性表中讀取屬性時, 實際上是調用了這個處理器.php物件的預設處理器是解開並傳回Z_OBJ_P(object) ->properties, 它是真正的標準屬性表.

union _zend_function *get_method(zval **obj_ptr char *method_name, int methodname_len TSRMLS_obj_ptr char *method_name, int methodname_len TSRMLS_DC)

這個處理器在解析類別的function_table中的物件方法時被呼叫. 如果在主的function_table中不存在方法, 則預設的處理器傳回一個指向對物件的__call($name, $args)方法包裝的zend_function *指標.

  • int call_method(char *method, INTERNAL_FUNCTION_PARAMETERS)

_D.處理器是未定義的.

  • union _zend_function *get_constructor(zval *obj TSRMLS_DC)

處理器對處理方法的對應中構造器是特殊方式儲存的, 這使得它比較特殊. 對這個方法的重寫非常少見.

  • zend_class_entry *get_class_entry(zval *obj TSRMLS_DC)

    ( , 這個處理器也很少被重寫. 它的目的是將一個物件實例映射回它原來的類別定義.

int get_class_name(zval *object, char **name zend_uint *len, int parent TSRMLS_DC)

  • get_class_entry()就是get_class_name()其中的一步, 在得到物件的zend_object後, 它將物件的類別名稱或它的父類別名稱(這依賴於參數parent的值)複製一份參數parent的值)回傳. 傳回的類別名稱拷貝必須使用非持久化儲存(emalloc()).

int compare_objects(zval *obj1, zval *obj2 TSRMLS_DC) ==, !=, , >=)用在兩個物件上時, 在操作數(參與比較的兩個物件)上呼叫compare_objects()就是這個工作的第一部分. 它的返回值通常是1, 0, -1, 分別代表大於, 等於, 小於.預設, 物件是基於它們的標準屬性表比較的, 使用的比較規則和第8章"在數組和HashTable上工作"中學習的陣列比較規則一樣.

  • int cast_object(zval *src, zval *dst, int type, int should_free TSRMLS_DC)

這個處理器. 如果將should_free設為非0值, zval_dtor()將會在dst上調用, 首先釋放內部的資源. 總之, 處理器應該嘗試將src中的物件表示為dst給出的zval *的類型中. 這個處理器預設是未定義的, 但當有它的時候, 應該返回SUCCESS或FAILURE.

  • int count_elements(zval *obj, long *count TSRMLS_DC)

    數組存取的物件應該定義這個處理器, 它將設定當前的元素數量到count中並返回SUCCESS. 如果當前實例沒有實現數組存取, 則它應該返回FAILURE, 以使引擎回頭去檢查標準屬性表.

譯註: 上面的句柄表和譯者使用的php-5.4.9中已經不完全一致, 讀者在學習這一部分的時候, 可以參考Zend/zend_object_handlers.c中最下面的標準處理器句柄表.

魔術方法第二部

使用前面看到的对象句柄表的自定义版本, 可以让内部类提供与在用户空间基于对象或类的__xxx()魔术方法相比, 相同或更多的能力.将这些自定义的句柄设置到对象实例上首先要求创建一个新的句柄表. 因为你通常不会覆写所有的句柄, 因此首先将标准句柄表拷贝到你的自定义句柄表中再去覆写你想要修改的句柄就很有意义了:

static zend_object_handlers php_sample3_obj_handlers;
int php_sample3_has_dimension(zval *obj, zval *idx,
                        int chk_type TSRMLS_DC)
{
    /* 仅在php版本>=1.0时使用 */
    if (chk_type == 0) {
       /* 重新映射chk_type的值 */
       chk_type = 2;
    }
    /* 当chk_type值为1时保持不变. 接着使用标准的hash_property方法执行逻辑 */
    return php_sample3_obj_handlers.has_property(obj,
                            idx, chk_type TSRMLS_CC);
}
PHP_MINIT_FUNCTION(sample3)
{
    zend_class_entry ce;
    zend_object_handlers *h = &php_sample3_obj_handlers;


    /* 注册接口 */
    INIT_CLASS_ENTRY(ce, "Sample3_Interface",
                        php_sample3_iface_methods);
    php_sample3_iface_entry =
                zend_register_internal_class(&ce TSRMLS_CC);
    php_sample3_iface_entry->ce_flags = ZEND_ACC_INTERFACE;
    /* 注册SecondClass类 */
    INIT_CLASS_ENTRY(ce, PHP_SAMPLE3_SC_NAME,
                            php_sample3_sc_functions);
    php_sample3_sc_entry =
                zend_register_internal_class(&ce TSRMLS_CC);
    php_sample3_register_constants(php_sample3_sc_entry);


    /* 实现AbstractClass接口 */
    zend_class_implements(php_sample3_sc_entry TSRMLS_CC,
                1, php_sample3_iface_entry);


    /* 创建自定义句柄表 */
    php_sample3_obj_handlers = *zend_get_std_object_handlers();


    /* 这个句柄表的目的是让$obj['foo']的行为等价于$obj->foo */
    h->read_dimension = h->read_property;
    h->write_dimension = h->write_property;
    h->unset_dimension = h->unset_property;
#if PHP_MAJOR_VERSION > 5 || \
            (PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION > 0)
    /* php-5.1.0中, has_property和has_dimension的chk_type含义不同, 为使它们行为一致, 自己包装一个函数 */
    h->has_dimension = php_sample3_has_dimension;

#else
    /* php 5.0.x的has_property和has_dimension行为一致 */
    h->has_dimension = h->has_property;
#endif


    return SUCCESS;
}

要将这个句柄表应用到对象上, 你有两种选择. 最简单也是最具代表性的就是实现一个构造器方法, 并在其中重新赋值变量的句柄表.

PHP_METHOD(Sample3_SecondClass,__construct)
{
    zval *objptr = getThis();


    if (!objptr) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING,
                    "Constructor called statically!");
        RETURN_FALSE;
    }
    /* 执行正常的构造器任务... */
    /* 覆写句柄表 */
    Z_OBJ_HT_P(objptr) = &php_sample3_obj_handlers;
}

当构造器返回时, 对象就有了新的句柄表以及对应的自定义行为. 还有一种更加受欢迎的方法是覆写类的对象创建函数.

zend_object_value php_sample3_sc_create(zend_class_entry *ce
                                        TSRMLS_DC)
{
    zend_object *object;
    zend_object_value retval;


    /* 返回Zend创建的对象 */
    retval = zend_objects_new(&object, ce TSRMLS_CC);
    /* 覆写create_object时, 属性表必须手动初始化 */
    ALLOC_HASHTABLE(object->properties);
    zend_hash_init(object->properties, 0, NULL,
                                    ZVAL_PTR_DTOR, 0);
    /* 覆写默认句柄表 */
    retval.handlers = &php_sample3_obj_handlers;
    /* 这里可能会执行其他对象初始化工作 */
    return retval;
}

这样就可以在MINIT阶段注册类(zend_class_entry)之后直接将自定义句柄表附加上去.

INIT_CLASS_ENTRY(ce, PHP_SAMPLE3_SC_NAME,
                        php_sample3_sc_functions);
php_sample3_sc_entry =
            zend_register_internal_class(&ce TSRMLS_CC);
php_sample3_sc_entry->create_object= php_sample3_sc_create;
php_sample3_register_constants(php_sample3_sc_entry);
zend_class_implements(php_sample3_sc_entry TSRMLS_CC,
            1, php_sample3_iface_entry);

这两种方法唯一可预见的不同是它们发生的时机不同. 引擎在碰到new Sample3_SecondClass后会在处理构造器及它的参数之前调用create_object. 通常, 你计划覆盖的各个点使用的方法(create_object Vs. __construct)应该一致.

译注: php-5.4.9中, xxx_property/xxx_dimension这一组句柄的原型是不一致的, 因此, 按照原著中的示例, 直接将xxx_property/xxx_dimension进行映射已经不能工作, 要完成上面的功能, 需要对4个句柄均包装一个函数去映射. 由于译者没有详细跟踪具体在哪一个版本发生了这些改变, 因此这里不给出译者测试的示例(没有做兼容性处理检查), 如果读者碰到这个问题, 请检查自己所使用php版本中两组句柄原型的差异并进行相应修正.

小结

毋庸置疑, php5/ZE2的对象模型比它的前辈php4/ZE1中的对象模型更加复杂. 在看完本章中介绍的所有特性和实现细节后, 你可能已经被它的所包含的信息量搞得手足无措. 幸运的是, php中在OOP之上有一层可以让你选择你的任务所需的部分而不关心其他部分. 找到复杂性之上一个舒适的层级开始工作, 剩下的都会顺起来的.

现在已经看完了所有的php内部数据类型, 是时候回到之前的主题了: 请求生命周期. 接下来的两章, 将在你的扩展中使用线程安全全局变量增加内部状态, 定义自定义的ini设置, 定义常量, 以及向使用你扩展的用户空间脚本提供超级全局变量.

以上就是[翻译][php扩展开发和嵌入式]第11章-php5对象的内容,更多相关内容请关注PHP中文网(www.php.cn)!


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn