摘要:將修改如下直接包含重復包含包含不存在的文件結果結論成功,返回值重復包含,返回,并忽略此次包含總結成功時,都會返回,差別在于包含失敗重復包含的處理嘗試加載未定義的類。
寫在前邊
我們將介紹 include* require* 的一些使用細節,
以及從 PHP 應用 和 zend 源碼角度,來分別分析 __autoload spl_autoload_register 的實現和調用過程。
分析的目的更多的是讓自己對這些細節加深認識,并進一步深入了解 Zend 源碼。
PHP 版本:`php-5.6` 核心方法:` spl_autoload_register`
手動加載
__autoload
spl_autoload_register
手動加載包含:include include_once requice requice_one
include以下文檔也適用于 require。
被包含文件先按參數給出的路徑尋找,如果沒有給出目錄(只有文件名)時則按照 include_path 指定的目錄尋找。如果在 include_path 下沒找到該文件則 include 最后才在調用腳本文件所在的目錄和當前工作目錄下尋找。如果最后仍未找到文件則 include 結構會發出一條警告;這一點和 require 不同,后者會發出一個致命錯誤。如果定義了路徑——不管是絕對路徑(在 Windows 下以盤符或者 開頭,在 Unix/Linux 下以 / 開頭)還是當前目錄的相對路徑(以 . 或者 .. 開頭)——include_path 都會被完全忽略。例如一個文件以 ../ 開頭,則解析器會在當前目錄的父目錄下尋找該文件。
代碼示例:
FILE: run.php
FILE: class.php
結果:
結論:
include 成功返回值:1,失敗返回值:false
include 失敗會有 warning,不會中斷進程
include_onceinclude_once 行為和 include 語句類似,唯一區別是如果該文件中已經被包含過,則不會再次包含。
將 run.php 修改如下:
結果:
結論:
include_once 重復包含時,會直接返回 1,并忽略此次包含操作,繼續執行
requirerequire 和 include 幾乎完全一樣,但 require 在出錯時產生 E_COMPILE_ERROR 級別的錯誤。(腳本將會中止運行)
將 run.php 修改如下:
結果:
結論:
require 包含成功,同 include 一樣,返回值:1
require 包含失敗,直接拋出 Fatal error,進程中止
require_oncerequire_once 語句和 require 語句完全相同,唯一區別是 PHP 會檢查該文件是否已經被包含過,如果是則不會再次包含。
將 run.php 修改如下:
ini_set("display_errors", "1"); // 直接包含 $ret = require_once "class.php"; echo sprintf("include ret-value:%d,ret-type:%s ", $ret, gettype($ret)); Foo::getFoo(); // 重復包含 $ret2 = require_once "./class.php"; echo sprintf("include ret-value:%d,ret-type:%s ", $ret, gettype($ret)); Foo::getFoo(); // 包含不存在的文件 $ret1 = require_once "./class1.php"; echo sprintf("include ret-value:%d,ret-type:%s ", $ret1, gettype($ret1));結果:
結論:
成功,返回值:1
重復包含,返回 1,并忽略此次包含
總結include include_once requice requice_one 成功時,都會返回 1,差別在于 包含失敗、重復包含 的處理
__autoloadPHP 代碼解釋嘗試加載未定義的類。此函數將會在 PHP 7.2.0 中棄用。
使用示例:
FILE:foo.phpFILE:run.php
結果:
? load git:(master) ? php run.php I am foo!結論:
Zend 代碼解釋
遇到未包含的類,會觸發 __autoload 進行加載,如果所有加載規則中沒有此類,則 Fatal error。下面,我們來看一下 Zend 引擎是如何觸發 __autoload 調用的。
利用 vld 來查看剛才執行過程中產生的 opcode,結果如下:我們看到,PHP 運行到第 10 行時,所生成的 opcode 為:INIT_STATIC_METHOD_CALL,兩個操作數都為常量(CONST)。
根據 opcode 的處理函數對應規則,我們利用 命名法 可以確定,
處理函數為:ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST_HANDLER
源碼位置為:vim Zend/zend_vm_execute.h +3819
源碼如下:static int ZEND_FASTCALL ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE zval *function_name; zend_class_entry *ce; call_slot *call = EX(call_slots) + opline->result.num; SAVE_OPLINE(); if (IS_CONST == IS_CONST) { /* no function found. try a static method in class */ if (CACHED_PTR(opline->op1.literal->cache_slot)) { ce = CACHED_PTR(opline->op1.literal->cache_slot); } else { ce = zend_fetch_class_by_name(Z_STRVAL_P(opline->op1.zv), Z_STRLEN_P(opline->op1.zv), opline->op1.literal + 1, opline->extended_value TSRMLS_CC); if (UNEXPECTED(EG(exception) != NULL)) { HANDLE_EXCEPTION(); } if (UNEXPECTED(ce == NULL)) { zend_error_noreturn(E_ERROR, "Class "%s" not found", Z_STRVAL_P(opline->op1.zv)); } CACHE_PTR(opline->op1.literal->cache_slot, ce); } call->called_scope = ce; } else { ce = EX_T(opline->op1.var).class_entry; if (opline->extended_value == ZEND_FETCH_CLASS_PARENT || opline->extended_value == ZEND_FETCH_CLASS_SELF) { call->called_scope = EG(called_scope); } else { call->called_scope = ce; } } if (IS_CONST == IS_CONST && IS_CONST == IS_CONST && CACHED_PTR(opline->op2.literal->cache_slot)) { call->fbc = CACHED_PTR(opline->op2.literal->cache_slot); } else if (IS_CONST != IS_CONST && IS_CONST == IS_CONST && (call->fbc = CACHED_POLYMORPHIC_PTR(opline->op2.literal->cache_slot, ce))) { /* do nothing */ } else if (IS_CONST != IS_UNUSED) { char *function_name_strval = NULL; int function_name_strlen = 0; if (IS_CONST == IS_CONST) { function_name_strval = Z_STRVAL_P(opline->op2.zv); function_name_strlen = Z_STRLEN_P(opline->op2.zv); } else { function_name = opline->op2.zv; if (UNEXPECTED(Z_TYPE_P(function_name) != IS_STRING)) { if (UNEXPECTED(EG(exception) != NULL)) { HANDLE_EXCEPTION(); } zend_error_noreturn(E_ERROR, "Function name must be a string"); } else { function_name_strval = Z_STRVAL_P(function_name); function_name_strlen = Z_STRLEN_P(function_name); } } if (function_name_strval) { if (ce->get_static_method) { call->fbc = ce->get_static_method(ce, function_name_strval, function_name_strlen TSRMLS_CC); } else { call->fbc = zend_std_get_static_method(ce, function_name_strval, function_name_strlen, ((IS_CONST == IS_CONST) ? (opline->op2.literal + 1) : NULL) TSRMLS_CC); } if (UNEXPECTED(call->fbc == NULL)) { zend_error_noreturn(E_ERROR, "Call to undefined method %s::%s()", ce->name, function_name_strval); } if (IS_CONST == IS_CONST && EXPECTED(call->fbc->type <= ZEND_USER_FUNCTION) && EXPECTED((call->fbc->common.fn_flags & (ZEND_ACC_CALL_VIA_HANDLER|ZEND_ACC_NEVER_CACHE)) == 0)) { if (IS_CONST == IS_CONST) { CACHE_PTR(opline->op2.literal->cache_slot, call->fbc); } else { CACHE_POLYMORPHIC_PTR(opline->op2.literal->cache_slot, ce, call->fbc); } } } if (IS_CONST != IS_CONST) { } } else { if (UNEXPECTED(ce->constructor == NULL)) { zend_error_noreturn(E_ERROR, "Cannot call constructor"); } if (EG(This) && Z_OBJCE_P(EG(This)) != ce->constructor->common.scope && (ce->constructor->common.fn_flags & ZEND_ACC_PRIVATE)) { zend_error_noreturn(E_ERROR, "Cannot call private %s::__construct()", ce->name); } call->fbc = ce->constructor; } if (call->fbc->common.fn_flags & ZEND_ACC_STATIC) { call->object = NULL; } else { if (EG(This) && Z_OBJ_HT_P(EG(This))->get_class_entry && !instanceof_function(Z_OBJCE_P(EG(This)), ce TSRMLS_CC)) { /* We are calling method of the other (incompatible) class, but passing $this. This is done for compatibility with php-4. */ if (call->fbc->common.fn_flags & ZEND_ACC_ALLOW_STATIC) { zend_error(E_DEPRECATED, "Non-static method %s::%s() should not be called statically, assuming $this from incompatible context", call->fbc->common.scope->name, call->fbc->common.function_name); } else { /* An internal function assumes $this is present and won"t check that. So PHP would crash by allowing the call. */ zend_error_noreturn(E_ERROR, "Non-static method %s::%s() cannot be called statically, assuming $this from incompatible context", call->fbc->common.scope->name, call->fbc->common.function_name); } } if ((call->object = EG(This))) { Z_ADDREF_P(call->object); call->called_scope = Z_OBJCE_P(call->object); } } call->num_additional_args = 0; call->is_ctor_call = 0; EX(call) = call; CHECK_EXCEPTION(); ZEND_VM_NEXT_OPCODE(); }通過以上源碼,我們發現關鍵方法為 zend_fetch_class_by_name,跟進此方法:
zend_class_entry *zend_fetch_class_by_name(const char *class_name, uint class_name_len, const zend_literal *key, int fetch_type TSRMLS_DC) /* {{{ */ { zend_class_entry **pce; int use_autoload = (fetch_type & ZEND_FETCH_CLASS_NO_AUTOLOAD) == 0; if (zend_lookup_class_ex(class_name, class_name_len, key, use_autoload, &pce TSRMLS_CC) == FAILURE) { if (use_autoload) { if ((fetch_type & ZEND_FETCH_CLASS_SILENT) == 0 && !EG(exception)) { if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_INTERFACE) { zend_error(E_ERROR, "Interface "%s" not found", class_name); } else if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TRAIT) { zend_error(E_ERROR, "Trait "%s" not found", class_name); } else { zend_error(E_ERROR, "Class "%s" not found", class_name); } } } return NULL; } return *pce; }我們發現是通過 zend_lookup_class_ex 來獲取類,繼續跟進:
ZEND_API int zend_lookup_class_ex(const char *name, int name_length, const zend_literal *key, int use_autoload, zend_class_entry ***ce TSRMLS_DC) /* {{{ */ { ... /* 注意:在 類的符號表 中沒有找到示例中調用的類 foo */ if (zend_hash_quick_find(EG(class_table), lc_name, lc_length, hash, (void **) ce) == SUCCESS) { if (!key) { free_alloca(lc_free, use_heap); } return SUCCESS; } ... /* * ZVAL_STRINGL 為 zval (即 PHP 類型的實現基礎 zvalue_value)賦值宏, * 此處實現了 把 ZEND_AUTOLOAD_FUNC_NAME 值 賦給 autoload_function * #define ZEND_AUTOLOAD_FUNC_NAME "__autoload" */ ZVAL_STRINGL(&autoload_function, ZEND_AUTOLOAD_FUNC_NAME, sizeof(ZEND_AUTOLOAD_FUNC_NAME) - 1, 0); ... fcall_info.size = sizeof(fcall_info); fcall_info.function_table = EG(function_table); fcall_info.function_name = &autoload_function; fcall_info.symbol_table = NULL; fcall_info.retval_ptr_ptr = &retval_ptr; fcall_info.param_count = 1; fcall_info.params = args; fcall_info.object_ptr = NULL; fcall_info.no_separation = 1; fcall_cache.initialized = EG(autoload_func) ? 1 : 0; fcall_cache.function_handler = EG(autoload_func); /* 留意此處 */ fcall_cache.calling_scope = NULL; fcall_cache.called_scope = NULL; fcall_cache.object_ptr = NULL; ... retval = zend_call_function(&fcall_info, &fcall_cache TSRMLS_CC); /* 調用自動加載函數 */ ... EG(autoload_func) = fcall_cache.function_handler; zval_ptr_dtor(&class_name_ptr); zend_hash_quick_del(EG(in_autoload), lc_name, lc_length, hash); ... }我們發現是通過 zend_call_function 出發了自動加載函數,而且看到了加載方法的名字 __autoload (宏:ZEND_AUTOLOAD_FUNC_NAME)
zend_call_function 中會做一下檢測并調用等,而且我們看到 zend_lookup_class_ex 的返回結果即為 zend_call_function 的返回結果。
接下來我們逐步退出函數調用棧:
結論
假設 zend_call_function 調用失敗,返回 FALSE,
則 zend_lookup_class_ex 返回 FALSE;
則 zend_fetch_class_by_name 返回 NULL;
則 ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST_HANDLER 拋出異常 Class ** not found,如下圖所示:至此,我們通過 PHP 代碼 Zend 源碼 了解了 __autoload 的調用過程。
spl_autoload_register
我們知道 __autoload 現在已并不推薦使用,
它的缺點也很明顯,不支持多個自動加載函數。PHP 代碼解釋將函數注冊到SPL __autoload函數隊列中。如果該隊列中的函數尚未激活,則激活它們。如果在你的程序中已經實現了__autoload()函數,它必須顯式注冊到__autoload()隊列中。
使用示例:
FILE:foo.php
(同上 __autoload)FILE:foo2.class.php
FILE:run.php
結果如下:
我們看到,調用 getFoo2 時,會先調用第一個注冊的 autoload 方法,如果沒找到對應的類,會產生 warning 并繼續調用后邊注冊的 autoload 方法。
Zend 源碼解釋
說明了 PHP 內核中為通過 spl_autoload_register 注冊的 autoload 方法維護了一個隊列,當前文件為包含調用類,便會觸發此隊列,并依次調用,直到隊列結束 或者 找到對應類。首先,我們看一下 PHP 文件生成的 opcode
我們發現,其方法調用所生成的 opcode 跟 __autoload 一樣,
但是我們之前調用了 `spl_autoload_register,
那么,看一下 spl_autoload_register 的源碼:
FILE: ext/spl/php_spl.c*PHP_FUNCTION(spl_autoload_register) { char *func_name, *error = NULL; int func_name_len; char *lc_name = NULL; zval *zcallable = NULL; zend_bool do_throw = 1; zend_bool prepend = 0; zend_function *spl_func_ptr; autoload_func_info alfi; zval *obj_ptr; zend_fcall_info_cache fcc; if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS() TSRMLS_CC, "|zbb", &zcallable, &do_throw, &prepend) == FAILURE) { return; } if (ZEND_NUM_ARGS()) { if (Z_TYPE_P(zcallable) == IS_STRING) { if (Z_STRLEN_P(zcallable) == sizeof("spl_autoload_call") - 1) { if (!zend_binary_strcasecmp(Z_STRVAL_P(zcallable), sizeof("spl_autoload_call"), "spl_autoload_call", sizeof("spl_autoload_call"))) { if (do_throw) { zend_throw_exception_ex(spl_ce_LogicException, 0 TSRMLS_CC, "Function spl_autoload_call() cannot be registered"); } RETURN_FALSE; } } } if (!zend_is_callable_ex(zcallable, NULL, IS_CALLABLE_STRICT, &func_name, &func_name_len, &fcc, &error TSRMLS_CC)) { alfi.ce = fcc.calling_scope; alfi.func_ptr = fcc.function_handler; obj_ptr = fcc.object_ptr; if (Z_TYPE_P(zcallable) == IS_ARRAY) { if (!obj_ptr && alfi.func_ptr && !(alfi.func_ptr->common.fn_flags & ZEND_ACC_STATIC)) { if (do_throw) { zend_throw_exception_ex(spl_ce_LogicException, 0 TSRMLS_CC, "Passed array specifies a non static method but no object (%s)", error); } if (error) { efree(error); } efree(func_name); RETURN_FALSE; } else if (do_throw) { zend_throw_exception_ex(spl_ce_LogicException, 0 TSRMLS_CC, "Passed array does not specify %s %smethod (%s)", alfi.func_ptr ? "a callable" : "an existing", !obj_ptr ? "static " : "", error); } if (error) { efree(error); } efree(func_name); RETURN_FALSE; } else if (Z_TYPE_P(zcallable) == IS_STRING) { if (do_throw) { zend_throw_exception_ex(spl_ce_LogicException, 0 TSRMLS_CC, "Function "%s" not %s (%s)", func_name, alfi.func_ptr ? "callable" : "found", error); } if (error) { efree(error); } efree(func_name); RETURN_FALSE; } else { if (do_throw) { zend_throw_exception_ex(spl_ce_LogicException, 0 TSRMLS_CC, "Illegal value passed (%s)", error); } if (error) { efree(error); } efree(func_name); RETURN_FALSE; } } alfi.closure = NULL; alfi.ce = fcc.calling_scope; alfi.func_ptr = fcc.function_handler; obj_ptr = fcc.object_ptr; if (error) { efree(error); } lc_name = safe_emalloc(func_name_len, 1, sizeof(long) + 1); zend_str_tolower_copy(lc_name, func_name, func_name_len); efree(func_name); if (Z_TYPE_P(zcallable) == IS_OBJECT) { alfi.closure = zcallable; Z_ADDREF_P(zcallable); lc_name = erealloc(lc_name, func_name_len + 2 + sizeof(zend_object_handle)); memcpy(lc_name + func_name_len, &Z_OBJ_HANDLE_P(zcallable), sizeof(zend_object_handle)); func_name_len += sizeof(zend_object_handle); lc_name[func_name_len] = "