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

全部翻譯內容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

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");
}

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

定義

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

  • PHP_ME(classname, methodname, arg_info, flags)

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

PHP_ME(Sample3_SecondClass,helloWorld,NULL,ZEND_ACC_PUBLIC)
  • PHP_MALIAS(classname, name, alias, arg_info, flags)

和PHP_FALIAS()宏很像, 這個宏允許你給alias參數描述的方法(同一個類中的)實現提供一個name指定的新名字. 例如, 要複製你的helloWorld方法則可以如下定義

PHP_MALIAS(Sample3_SecondClass, sayHi, helloWorld,
                            NULL, ZEND_ACC_PUBLIC)
  • 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

可以在對象外任何作用域調用.這和php4方法的可見性是一樣的

ZEND_ACC_PROTECTED

只能在類中或者它的子類中調用

ZEND_ACC_PRIVATE

只能在類中調用


比如, 由於你前面定義的Sample3_SecondClass::helloWorld()方法不需要對象實例, 你就可以將它的定義從簡單的ZEND_ACC_PUBLIC修改爲ZEND_ACC_PUBLIC | ZEND_ACC_STATIC, 這樣引擎知道了就不會去提供(實例)了.

魔術方法

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


方法

用法

__construct(...)

可選的自動調用的對象構造器(之前定義的是和類名一致的方法).如果__construct()classname()兩種實現都存在,在實例化的過程中,將優先調用__construct()

__destruct()

當實例離開作用域,或者請求整個終止,都將導致隱式的調用實例的__destruct()方法去處理一些清理工作,比如關閉文件或網絡句柄.

__clone()

默認情況下,所有的實例都是真正的引用傳值.php5,要想真正的拷貝一個對象實例,就要使用clone關鍵字.當在一個對象實例上調用clone關鍵字時, __clone()方法就會隱含的被執行,它允許對象去複製一些需要的內部資源數據.

__toString()

在用文本表示一個對象時,比如當直接在對象上使用echoprint語句時, __toString()方法將自動的被引擎調用.類如果實現這個魔術方法,應該返回一個包含描述對象的當前狀態的字符串.

__get($var)

如果腳本中請求一個對象不可見的屬性(不存在或者由於訪問控制導致不可見), __get()魔術方法將被調用,唯一的參數是所請求的屬性名.實現可以使用它自己的內部邏輯去確定最合理的返回值返回.

__set($var, $value)

__get()很像, __set()提供了與之相反的能力,它用來處理賦值給對象的不可見屬性時的邏輯.__set()的實現可以選擇隱式的在標準屬性表中創建這些變量,以其他存儲機制設置值,或者直接拋出錯誤並丟棄值.

__call($fname, $args)

調用對象的未定義方法時可以通過使用__call()魔術方法實現漂亮的處理.這個方法接受兩個參數:被調用的方法名,包含調用時傳遞的所有實參的數值索引的數組.

__isset($varname)

php5.1.0之後, isset($obj->prop)的調用不僅是檢查$obj中是否有prop這個屬性,它還會調用$obj中定義的__isset()方法,動態的評估嘗試使用動態的__get()__set()方法是否能成功讀寫屬性

__unset($varname)

類似於__isset(), php 5.1.0unset()函數引入了一個簡單的OOP接口,它可以用於對象屬性,雖然這個屬性可能在對象的標準屬性表中並不存在,但它可能對於__get()__set()的動態屬性空間是有意義的,因此引入__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)
  • void write_dimension(zval *obj, zval *idx, zval *value TSRMLS_DC)

read_dimension()和write_dimension()類似於對應的read_property()和write_property();  不過它們在使用$obj['idx']方式將對象作爲數組訪問時被觸發. 如果對象的類沒有實現ArrayAccess接口, 默認的行爲是觸發一個錯誤; 否則它就會調用魔術方法offsetget($idx)或offsetset($idx, $value).

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

在設置或取回對象的值時, 則會在對象上調用get()或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)

當將對象看做數組調用isset()時(比如isset($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)
  • void unset_dimension(zval *obj, zval *idx TSRMLS_DC)

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

  • HashTable *get_properties(zval *obj 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_DC)

這個處理器在解析類的function_table中的對象方法時被調用. 如果在主的function_table中不存在方法, 則默認的處理器返回一個指向對對象的__call($name, $args)方法包裝的zend_function *指針.

  • int call_method(char *method, INTERNAL_FUNCTION_PARAMETERS)

定義爲ZEND_OVERLOADED_FUNCTION類型的函數將以call_method處理器的方式執行. 默認情況下, 這個處理器是未定義的.

  • union _zend_function *get_constructor(zval *obj TSRMLS_DC)

類似於get_method()處理器, 這個處理器返回一個對對應對象方法的引用. 類的zend_class_entry中構造器是特殊方式存儲的, 這使得它比較特殊. 對這個方法的重寫非常少見.

  • zend_class_entry *get_class_entry(zval *obj TSRMLS_DC)

和get_constructor()類似, 這個處理器也很少被重寫. 它的目的是將一個對象實例映射回它原來的類定義.

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

get_class_entry()就是get_class_name()其中的一步, 在得到對象的zend_object後, 它將對象的類名或它的父類名(這依賴於參數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設置, 定義常量, 以及向使用你擴展的用戶空間腳本提供超級全局變量.



目錄
上一章: php4對象
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章