其他特性

魔术方法

魔术方法是 PHP 独有的特性。魔术方法的实现方式与一般的方法几乎无异。区别就是部分魔术方法不只存在于 zend_class_entry 中的 function_table,在 zend_class_entry 中也会直接存一份,所以会有如图6-6所示的结构图(请注意 __get 方法的存储位置)。

image 2024 06 09 16 10 24 756
Figure 1. 图6-6 __get函数在类中的位置

自动加载

自动加载是依据用户实现的规则来加载 PHP 文件,实现类的加载。比如现在新建一个 PHP 7 类型的对象,如果还未加载这个类,且用户没有实现自动加载方法,或是实现的自动加载方法没能把 PHP 7 这个类加载进来,则会报语法错误。

根据这个逻辑,能迅速猜测到自动加载的实现原理:当用到一个类时,如果在 EG(class_table) 中没有找到这个类,则去调用用户实现的自动加载方法。调用后到 EG(class_table) 中查找此类,如果此时仍然没有,则抛出语法错误。

PHP 7 中提供了两种自动加载方式:__autoload()spl_autoload_register()

  1. __autoload:这个自动加载方法比较简单,在 PHP 中实现了此方法后,会存储在 EG(autoload_func) 中。当需要加载新的类时,内核会调用此方法加载类。此外,内核自己实现了 __autoload 的默认版本 PHP_FUNCTION(spl_autoload)

  2. spl_autoload_register:这种加载方法比较高级。除了可以实现多个加载逻辑之外,还可以设置优先级(把加载逻辑放在最高优先级或者最低优先级)。知名管理工具 Composer 便主要是靠 spl_autoload_register 来实现的。

而且,还可以通过 spl_autoload_unregister 来删除某个加载逻辑。

现在我们知道,通过 spl_autoload_register 来进行自动加载,大体有以下几个特征:多个、优先级、可查找(查找后删除)。那么之前学到的内核数据结构有没有可以满足这个需求的?答案是有,内核强大的 HashTable 完全可以满足这些需求。

事实上,内核确实就是用 HashTable 来实现的。内核使用 HashTable 存储在全局变量 SPL_G(autoload_functions) 中。SPL_G(autoload_functions) 初始化为 NULL,当程序第一次调用 spl_autoload_register() 方法来增加自动加载逻辑时,内核会对其进行初始化。

由于 spl_autoload_register() 可以通过方法、类的普通方法甚至是闭包来实现自动加载,所以这个 HashTable 的 valuezend_function 或用 zval 都不能满足需求,为了满足需求,内核定义了一个结构体 autoload_func_info,用于存储用户实现的自动加载逻辑。

typedef struct {
    zend_function *func_ptr;
    zval obj;
    zval closure;
    zend_class_entry *ce;
} autoload_func_info;

可以看出,此 value 类型并非联合体,实现起来很简单。

此外,需要说明的是,无论实现了哪种自动加载,都是在 PHP_FUNCTION(spl_autoload_call) 中对自动加载方法进行调用。如果全局变量 SPL_G(autoload_functions) 已初始化,则按顺序调用加载逻辑,直到此类正确加载。否则直接调用 __autoload 实现。

if (SPL_G(autoload_functions)) {
    . . .
    while  (zend_hash_get_current_key_ex(SPL_G(autoload_functions),  &func_name,
        &num_idx, &pos) == HASH_KEY_IS_STRING) {
        alfi = zend_hash_get_current_data_ptr_ex(SPL_G(autoload_functions), &pos);
        . . .
        if (zend_hash_exists(EG(class_table), lc_name)) {
            break;
        }
        zend_hash_move_forward_ex(SPL_G(autoload_functions), &pos);
    }
    zend_exception_restore();
    zend_string_free(lc_name);
    SPL_G(autoload_running) = l_autoload_running;
} else {
    /* do not use or overwrite &EG(autoload_func) here */
    zend_call_method_with_1_params(NULL, NULL, NULL, "spl_autoload", NULL, class_
        name);
}