(一)Yaf是什麼
Yaf,全稱 Yet Another Framework,是一個C語言編寫的、基於PHP擴展開發的PHP框架,
相比於一般的PHP框架,它更快,快到被譽爲最快的PHP開發框架。
它提供了Bootstrap、路由、分發、視圖、插件功能。
Yaf由惠新宸(傳說中的鳥哥)開發,隆重介紹下,惠新宸,PHP開發組核心成員,
PECL開發者, Zend公司外聘顧問, 曾供職於雅虎,百度,現爲新浪微博架構師兼首席PHP技術顧問,是PHP5.4, 5.5的主要開發者。作爲PECL開發者貢獻了Yaf (Yet another framework),Yar(Yet another RPC framework)以及Yac(Yet another Cache)、Taint等多個優秀開源作品,同時也是APC,Opcache,Msgpack
等項目的維護者...此處略去1000字。總之是一位牛得掉渣的人,Yaf在百度和新浪微博得到廣泛使用。
在百度,絕大部分產品都是PHP開發的,其中絕大部分PHP程序都使用了Yaf框架;在新浪微博,Yaf也得到了廣泛使用。這些公司的產品都是高併發、超大流量的,Yaf經受住了考驗。
(二)Yaf的優點
- 用C語言開發的PHP框架,相比原生的PHP, 幾乎不會帶來額外的性能開銷。
- 所有的框架類,不需要編譯,在PHP啓動的時候加載, 並常駐內存。
- 更短的內存週轉週期,提高內存利用率,降低內存佔用率。
- 靈巧的自動加載。支持全局和局部兩種加載規則,方便類庫共享。
- 高度靈活可擴展的框架,支持自定義視圖引擎,支持插件,支持自定義路由等等。
- 內建多種路由,可以兼容目前常見的各種路由協議。
- 強大而又高度靈活的配置文件支持。並支持緩存配置文件,避免複雜的配置結構帶來的性能損失。
- 在框架本身,對危險的操作習慣做了禁止。
- 更快的執行速度, 更少的內存佔用。
以上內容引用鳥哥的官方介紹,當然,Yaf不是一個Full-Stack的web框架,它沒有對數據庫操作
的封裝,更不用說ORM;它不提供表單生成、驗證、分頁、國際化、JS和CSS壓縮等跟前端耦合
得比較緊密的功能;它不提供過濾器、緩存、組件這些web框架中常用的功能。很多人認爲這是
Yaf的不足,但我認爲這又是Yaf的優點,這代表着一種精神,就是追求簡單,追求高效,追求”簡單可依賴“, 所以Yaf專注於實現最核心的功能,提供最穩定的實現。Yaf不提供上面這些功能是有道理的,不提供ORM,
是因爲PHP已經提供了對DB的一個輕度封裝的PDO, 直接使用PDO, 會更加簡單, 更加高效。不提供和
前端有關的功能,是因爲在互聯網產品,前端和PHP後端往往分離得很清楚,PHP只專注於組裝數據給前端模板用,至於網頁的佈局、分頁、表單的驗證、JS和CSS的壓縮等,交給更加專業的前端工程師解決更合適。儘管Yaf的功能有限,但Yaf是可擴展的!它提供的插件機制,可以和其它類庫整合在一起。
總之Yaf非常適合互聯網產品的開發。比如整合Smarty模板引擎,甚至可以根據產品的業務特點,基於Yaf
再擴展一套適合自己的業務層框架。總之Yaf非常適合互聯網產品的開發。
(三)爲什麼要閱讀Yaf的源代碼
Yaf最大的不足在於缺少文檔,別擔心,Yaf的源代碼就是最好的文檔,Yaf的核心代碼才幾千行,
可讀性很強,通過閱讀代碼,你可以:
- 學會如何做PHP擴展開發
- 學會如何實現一個WEB框架
- 理解Yaf的內部實現,更有助於開發應用
何樂而不爲,一起來讀代碼吧!
(四)Yaf的啓動
Yaf的啓動包括配置的初始化和框架類的加載,Yaf是一個PHP擴展,理解Yaf得先理解PHP擴展的原理,
我們先從PHP程序的生命週期說起。
4.1 PHP程序的生命週期
一個PHP程序,依次經過Module init、Request init、Request shutdown、Module shutdown四個過程,
當然,之間還會執行腳本自身的代碼。在命令行模式下運行一個PHP程序的主要流程如圖4-1所示:
圖4-1 PHP生命週期
實現PHP擴展,即是在以上4個階段定義4個相應地函數,供PHP在執行的時候調用。
(1)Module init階段的函數
PHP_MINIT_FUNCTION(myext) { //註冊常量或者類等初始化操作 return SUCCESS; }
這個函數在擴展被載入時調用。
(2)Request test.php
請求test.php文件。當請求到達後,PHP會初始化執行腳本的基本環境,包括保存PHP運行過程中變量名稱
和變量值內容的符號表,以及當前所有的函數以及類等信息的符號表。
(3)Request init階段的函數
PHP_RINIT_FUNCTION(myext) { //例如記錄請求開始時間 //隨後在請求結束的時候記錄結束時間 //這樣就能夠記錄處理請求所花費的時間了 return SUCCESS; }然後PHP會調用所有模塊的RINIT函數。
(4)Execute test.php
執行test.php階段,主要是把PHP文件編譯成Opcodes,然後在PHP虛擬機下執行。
(5)Request shutdown階段的函數
PHP_RSHUTDOWN_FUNCTION(myext) { //例如記錄請求結束時間,並把 //相應地信息寫入到日誌文件中 return SUCCESS; }
請求處理完成後進入結束階段,一般腳本執行到末尾或者通過調用exit()或者die()函數,PHP都將進入結束
階段。和開始階段對應,結束階段也分爲兩個環節,一個在請求結束後(RSHUTDOWN),一個在SAPI生命週期結束時(MSHUTDOWN)。
(6)Module shutdown階段的函數:
PHP_MSHUTDOWN_FUNCTION(myext) { //註銷一些持久化的資源 return SUCCESS; }
在請求一個PHP頁面時,PHP基本上是按照這個流程執行的,Yaf擴展也是圍繞這個流程,插入自己的代碼,
進而使Yaf框架影響到PHP的請求中。
4.2 Yaf擴展配置的初始化
擴展可以在php.ini中寫自己的配置信息,或者在編譯PHP時--with-config-file-scan-dir指定目錄中的配置
文件比如yaf.ini中寫配置信息。Yaf擴展提供的配置項如表4-1所示
選項名稱 | 默認值 | 可修改範圍 | 說明 |
yaf.environ | product | PHP_INI_ALL | 環境名稱, 當用INI作爲Yaf的配置文件時, 這個指明瞭Yaf將要在INI配置中讀取的節的名字 |
yaf.library | NULL | PHP_INI_ALL | 全局類庫的目錄路徑 |
yaf.cache_config | 0 | PHP_INI_SYSTEM | 是否緩存配置文件(只針對INI配置文件生效), 打開此選項可在複雜配置的情況下提高性能 |
yaf.name_suffix | 1 | PHP_INI_ALL | 在處理Controller, Action, Plugin, Model的時候, 類名中關鍵信息是否是後綴式, 比如UserModel, 而在前綴模式下則是ModelUser |
yaf.name_separator | "" | PHP_INI_ALL | 在處理Controller, Action, Plugin, Model的時候, 前綴和名字之間的分隔符, 默認爲空, 也就是UserPlugin, 加入設置爲"_", 則判斷的依據就會變成:"User_Plugin", 這個主要是爲了兼容ST已有的命名規範 |
yaf.forward_limit | 5 | PHP_INI_ALL | forward最大嵌套深度 |
yaf.use_namespace | 0 | PHP_INI_ALL | 開啓的情況下, Yaf將會使用命名空間方式註冊自己的類, 比如Yaf_Application將會變成Yaf\Application |
yaf.use_spl_autoload | 0 | PHP_INI_SYSTEM | 開啓的情況下, Yaf在加載不成功的情況下, 會繼續讓PHP的自動加載函數加載, 從性能考慮, 除非特殊情況, 否則保持這個選項關閉 |
表4-1 Yaf的配置選項
那麼Yaf是如何讀取配置文件,並初始化這些參數呢?
讀取配置文件之前,得定義好參數,即聲明變量來保存參數的值:
PHP_INI_BEGIN() STD_PHP_INI_ENTRY("yaf.library", "", PHP_INI_ALL, OnUpdateString, global_library, zend_yaf_globals, yaf_globals) STD_PHP_INI_BOOLEAN("yaf.action_prefer", "0", PHP_INI_ALL, OnUpdateBool, action_prefer, zend_yaf_globals, yaf_globals) STD_PHP_INI_BOOLEAN("yaf.lowcase_path", "0", PHP_INI_ALL, OnUpdateBool, lowcase_path, zend_yaf_globals, yaf_globals) STD_PHP_INI_BOOLEAN("yaf.use_spl_autoload", "0", PHP_INI_ALL, OnUpdateBool, use_spl_autoload, zend_yaf_globals, yaf_globals) STD_PHP_INI_ENTRY("yaf.forward_limit", "5", PHP_INI_ALL, OnUpdateLongGEZero, forward_limit, zend_yaf_globals, yaf_globals) STD_PHP_INI_BOOLEAN("yaf.name_suffix", "1", PHP_INI_ALL, OnUpdateBool, name_suffix, zend_yaf_globals, yaf_globals) PHP_INI_ENTRY("yaf.name_separator", "", PHP_INI_ALL, OnUpdateSeparator) STD_PHP_INI_BOOLEAN("yaf.cache_config", "0", PHP_INI_SYSTEM, OnUpdateBool, cache_config, zend_yaf_globals, yaf_globals) /* {{{ This only effects internally */ STD_PHP_INI_BOOLEAN("yaf.st_compatible", "0", PHP_INI_ALL, OnUpdateBool, st_compatible, zend_yaf_globals, yaf_globals) /* }}} */ STD_PHP_INI_ENTRY("yaf.environ", "product", PHP_INI_SYSTEM, OnUpdateString, environ, zend_yaf_globals, yaf_globals) #ifdef YAF_HAVE_NAMESPACE STD_PHP_INI_BOOLEAN("yaf.use_namespace", "0", PHP_INI_SYSTEM, OnUpdateBool, use_namespace, zend_yaf_globals, yaf_globals) #endif PHP_INI_END();
定義參數時,使用宏 PHP_INI_BEGIN() 來標識的開始,並用 PHP_INI_END() 表示該配置節已經結束。然後在兩者之間我們用 PHP_INI_ENTRY() 來創建具體的配置項。PHP_INI_ENTRY 這個宏裏面設置的前面的兩個參數,分別代表着INI設置的名稱和它的默認值。第二個參數決定設置是否允許被修改,以及它能被修改的作用域。最後一個參數是一個回調函數,當INI的值被修改時候觸發此回調函數。你將會在某些修改事件的地方詳細的瞭解這個參數。最後定義好的參數結構體如下:
static zend_ini_entry ini_entries[] = { // BEGIN 的定義 { 0, PHP_INI_ALL, "yaf.library", sizeof("yaf.library"), NULL, NULL, NULL, NULL, NULL, 0, NULL, 0, 0, NULL}, ... { 0, 0, NULL, 0, NULL, NULL, NULL, NULL, NULL, 0, NULL, 0, 0, NULL } }; // END的定義
接下來在 Module init階段讀取配置文件,並把參數值填充到 init_entries 結構體中,由REGISTER_INI_ENTRIES函數完成:
PHP_MINIT_FUNCTION(yaf) { REGISTER_INI_ENTRIES(); ...... }
4.3 application配置的初始化
4.3.1 application的配置選項
與yaf的全局配置不一樣,application的配置是針對單個應用的,配置項如表4-2所示:
名稱 | 值類型 | 默認值 | 說明 |
application.directory | String | 應用的絕對目錄路徑 | |
application.ext | String | php | PHP腳本的擴展名 |
application.bootstrap | String | Bootstrapplication.php | Bootstrap路徑(絕對路徑) |
application.library | String | application.directory + "/library" | 本地(自身)類庫的絕對目錄地址 |
application.baseUri | String | NULL | 在路由中, 需要忽略的路徑前綴, 一般不需要設置, Yaf會自動判斷. |
application.dispatcher.defaultModule | String | index | 默認的模塊 |
application.dispatcher.throwException | Bool | TRUE | 在出錯的時候, 是否拋出異常 |
application.dispatcher.catchException | Bool | FALSE | 是否使用默認的異常捕獲Controller, 如果開啓, 在有未捕獲的異常的時候, 控制權會交給ErrorController的errorAction方法, 可以通過$request->getException()獲得此異常對象 |
application.dispatcher.defaultController | String | index | 默認的控制器 |
application.dispatcher.defaultAction | String | index | 默認的動作 |
application.view.ext | String | phtml | 視圖模板擴展名 |
application.modules | String | Index | 聲明存在的模塊名, 請注意, 如果你要定義這個值, 一定要定義Index Module |
application.system.* | String | * | 通過這個屬性, 可以修改yaf的runtime configure, 比如application.system.lowcase_path, 但是請注意只有PHP_INI_ALL的配置項纔可以在這裏被修改, 此選項從2.2.0開始引入 |
表4-2 application的配置選項
4.3.2 全局變量的定義
application的配置保存在Yaf的全局變量中,全局變量的定義方式爲:
php_yaf.h
ZEND_BEGIN_MODULE_GLOBALS(yaf) char *ext; char *base_uri; char *environ; char *directory; char *local_library; ... ZEND_END_MODULE_GLOBALS(yaf)展開後變成這樣:
typedef struct _zend_yaf_globals { unsigned long counter; } zend_yaf_globals;然後在 yaf.c 中調用 ZEND_DECLARE_MODULE_GLOBALS 來實例化此結構體:
ZEND_DECLARE_MODULE_GLOBALS(yaf);展開後變成:
zend_yaf_globals yaf_globals;
這樣就可以通過yaf_globals.base_uri來訪問base_uri這個全局變量了,以上說的是線程非安全的情況,
在線程安全中,全局變量的定義又是另一種情況,這裏不再展開。總之在線程安全和非安全中,對全局變量
的定義和訪問均不一樣,對於全局變量的訪問,PHP的擴展定義一個宏,用來訪問全局變量:
php_yaf.h
#ifdef ZTS #define YAF_G(v) TSRMG(yaf_globals_id, zend_yaf_globals *, v) #else #define YAF_G(v) (yaf_globals.v) #endif4.3.3 全局變量的初始化
Yaf在Request init階段初始化全局變量:
PHP_RINIT_FUNCTION(yaf) { php_printf("PHP_RINIT_FUNCTION...\n"); YAF_G(running) = 0; YAF_G(in_exception) = 0; YAF_G(throw_exception) = 1; YAF_G(catch_exception) = 0; YAF_G(directory) = NULL; YAF_G(bootstrap) = NULL; YAF_G(local_library) = NULL; YAF_G(local_namespaces) = NULL; YAF_G(modules) = NULL; YAF_G(base_uri) = NULL; YAF_G(view_directory) = NULL; #if ((PHP_MAJOR_VERSION == 5) && (PHP_MINOR_VERSION < 4)) YAF_G(buffer) = NULL; YAF_G(owrite_handler) = NULL; YAF_G(buf_nesting) = 0; #endif return SUCCESS; }
4.3.4 application配置的初始化
創建application實例的時候,必須傳入一個配置選項的array。
$app_conf['directory'] = '/home/work/yaf/sample_app
$app_conf['baseUri'] = '/';
$config = array(
"application" => $app_conf,
);
// 生成yaf實例
$app = new Yaf_Application($config);
所以,application配置的初始化在 Yaf_Application的構造函數中完成,yaf_application.c定義的構造函數
會調用yaf_application_parse_option函數來解析傳過來的參數,並完成初始化:
yaf_application.c
static int yaf_application_parse_option(zval *options TSRMLS_DC) { HashTable *conf; zval **ppzval, **ppsval, *app; conf = HASH_OF(options); if (zend_hash_find(conf, ZEND_STRS("application"), (void **)&ppzval) == FAILURE) { /* For back compatibilty */ if (zend_hash_find(conf, ZEND_STRS("yaf"), (void **)&ppzval) == FAILURE) { yaf_trigger_error(YAF_ERR_TYPE_ERROR TSRMLS_CC, "%s", "Expected an array of application configure"); return FAILURE; } } app = *ppzval; if (Z_TYPE_P(app) != IS_ARRAY) { yaf_trigger_error(YAF_ERR_TYPE_ERROR TSRMLS_CC, "%s", "Expected an array of application configure"); return FAILURE; } if (zend_hash_find(Z_ARRVAL_P(app), ZEND_STRS("directory"), (void **)&ppzval) == FAILURE || Z_TYPE_PP(ppzval) != IS_STRING) { yaf_trigger_error(YAF_ERR_STARTUP_FAILED TSRMLS_CC, "%s", "Expected a directory entry in application configures"); return FAILURE; } //解析出application.directory的配置項,使用YAF_G初始化對應的全局變量 if (*(Z_STRVAL_PP(ppzval) + Z_STRLEN_PP(ppzval) - 1) == DEFAULT_SLASH) { YAF_G(directory) = estrndup(Z_STRVAL_PP(ppzval), Z_STRLEN_PP(ppzval) - 1); } else { YAF_G(directory) = estrndup(Z_STRVAL_PP(ppzval), Z_STRLEN_PP(ppzval)); } 其它配置選項的初始化省略... }
4.4 加載框架類
Yaf的一個優點是所有的框架類,不需要編譯,在PHP啓動的時候加載, 並常駐內存。如何做到這點呢?
Yaf定義了一個YAF_STARTUP宏來加載類,加載類在 Module init階段完成:
PHP_MINIT_FUNCTION(yaf) { ... /* startup components */ YAF_STARTUP(application); YAF_STARTUP(bootstrap); YAF_STARTUP(dispatcher); YAF_STARTUP(loader); YAF_STARTUP(request); YAF_STARTUP(response); YAF_STARTUP(controller); YAF_STARTUP(action); YAF_STARTUP(config); YAF_STARTUP(view); YAF_STARTUP(router); YAF_STARTUP(plugin); YAF_STARTUP(registry); YAF_STARTUP(session); YAF_STARTUP(exception); return SUCCESS; }
YAF_STARTUP接收一個參數,可以理解爲類名,然後加載這個類。具體是怎麼實現的呢?
php_yaf.h
#define YAF_STARTUP_FUNCTION(module) ZEND_MINIT_FUNCTION(yaf_##module) #define YAF_STARTUP(module) ZEND_MINIT(yaf_##module)(INIT_FUNC_ARGS_PASSTHRU)
可以看到YAF_STARTUP_FUNCTION這個宏用來定義一個啓動函數,YAF_STARTUP調用此啓動函數完成類的加載。然後,每個類都會使用YAF_STARTUP_FUNCTION定義其啓動函數,例如,我們來看router類的啓動函數。
router.c
/* * router類的啓動函數 */ YAF_STARTUP_FUNCTION(router) { zend_class_entry ce; (void)yaf_route_route_arginfo; /* tricky, supress warning "defined but not used" */ //加載router類 YAF_INIT_CLASS_ENTRY(ce, "Yaf_Router", "Yaf\\Router", yaf_router_methods); yaf_router_ce = zend_register_internal_class_ex(&ce, NULL, NULL TSRMLS_CC); yaf_router_ce->ce_flags |= ZEND_ACC_FINAL_CLASS; //聲明router類的屬性 zend_declare_property_null(yaf_router_ce, ZEND_STRL(YAF_ROUTER_PROPERTY_NAME_ROUTERS), ZEND_ACC_PROTECTED TSRMLS_CC); zend_declare_property_null(yaf_router_ce, ZEND_STRL(YAF_ROUTER_PROPERTY_NAME_CURRENT_ROUTE), ZEND_ACC_PROTECTED TSRMLS_CC); //繼續加載路由協議相關的類 YAF_STARTUP(route); YAF_STARTUP(route_static); YAF_STARTUP(route_simple); YAF_STARTUP(route_supervar); YAF_STARTUP(route_rewrite); YAF_STARTUP(route_regex); YAF_STARTUP(route_map); return SUCCESS; }
當調用 YAF_STARTUP(router) 時,展開後等於調用
ZEND_MINIT(yaf_router)(INIT_FUNC_ARGS_PASSTHRU),
繼續展開,ZEND_MINIT(yaf_router)得到的是一個函數指針,剛好指向ZEND_MINIT_FUNCTION(yaf_router)
定義的函數,即是YAF_STARTUP_FUNCTION(router)定義的函數,所以
ZEND_MINIT(yaf_router)(INIT_FUNC_ARGS_PASSTHRU)相當於調用router.c中YAF_STARTUP_FUNCTION(router)定義的函數。
注意在加載router類的時候,還會加載router類依賴的其它類,Yaf通過這種方式完成所有框架類的加載。
下一篇文章 會分析Yaf完成一起請求處理的整個過程。