目前的項目中選擇了Phalcon Framework作為未來一段時間的核心框架。技術(shù)選型的原因會單開一篇Blog另說,本次優(yōu)先對Phalcon的MVC架構(gòu)與啟動流程進行分析說明,如有遺漏還望指出。
Phalcon本身支持創(chuàng)建多種形式的Web應(yīng)用項目以應(yīng)對不同場景,包括迷你應(yīng)用、單模塊標(biāo)準(zhǔn)應(yīng)用、以及較復(fù)雜的多模塊應(yīng)用。
本次以最復(fù)雜的多模塊應(yīng)用為例,Phalcon版本為1.3.2,用一個Phalcon所創(chuàng)建的標(biāo)準(zhǔn)項目來分析。
創(chuàng)建項目
Phalcon環(huán)境配置安裝后,可以通過命令行生成一個標(biāo)準(zhǔn)的Phalcon多模塊應(yīng)用:
phalcon project eva --type modules
入口文件為public/index.php
,簡化后一共5行,包含了整個Phalcon的啟動流程,以下將按順序說明。
require __DIR__ . '/../config/services.php';
$application = new Phalcon\Mvc\Application();
$application->setDI($di);
require __DIR__ . '/../config/modules.php';
echo $application->handle()->getContent();
DI注冊階段
Phalcon的所有組件服務(wù)都是通過DI(依賴注入)進行組織的,這也是目前大部分主流框架所使用的方法。通過DI,可以靈活的控制框架中的服務(wù):哪些需要啟用,哪些不啟用,組件的內(nèi)部細(xì)節(jié)等等。因此Phalcon是一個松耦合可替換的框架,完全可以通過DI替換MVC中任何一個組件。
require __DIR__ . '/../config/services.php';
這個文件中默認(rèn)注冊了Phalcon\Mvc\Router
(路由)、Phalcon\Mvc\Url
(Url)、Phalcon\Session\Adapter\Files
(Session)三個最基本的組件。同時當(dāng)MVC啟動后,DI中默認(rèn)注冊的服務(wù)還有很多,可以通過DI得到所有當(dāng)前已經(jīng)注冊的服務(wù):
$services = $application->getDI()->getServices();
foreach($services as $key => $service) {
var_dump($key);
var_dump(get_class($application->getDI()->get($key)));
}
打印看到Phalcon還注冊了以下服務(wù):
-
dispatcher
:Phalcon\Mvc\Dispatcher
分發(fā)服務(wù),將路由命中的結(jié)果分發(fā)到對應(yīng)的Controller -
modelsManager
:Phalcon\Mvc\Model\Manager
Model管理 -
modelsMetadata
:Phalcon\Mvc\Model\MetaData\Memory
ORM表結(jié)構(gòu) -
response
:Phalcon\Http\Response
響應(yīng) -
cookies
:Phalcon\Http\Response\Cookies
Cookies -
request
:Phalcon\Http\Request
請求 -
filter
:Phalcon\Filter
可對用戶提交數(shù)據(jù)進行過濾 -
escaper
:Phalcon\Escaper
轉(zhuǎn)義工具 -
security
:Phalcon\Security
密碼Hash、防止CSRF等 -
crypt
:Phalcon\Crypt
加密算法 -
annotations
:Phalcon\Annotations\Adapter\Memory
注解分析 -
flash
:Phalcon\Flash\Direct
提示信息輸出 -
flashSession
:Phalcon\Flash\Session
提示信息通過Session延遲輸出 -
tag
:Phalcon\Tag
View的常用Helper
而每一個服務(wù)都可以通過DI進行替換。接下來實例化一個標(biāo)準(zhǔn)的MVC應(yīng)用,然后將我們定義好的DI注入進去。
$application = new Phalcon\Mvc\Application();
$application->setDI($di);
模塊注冊階段
與DI一樣,Phalcon建議通過引入一個獨立文件的方式注冊所有需要的模塊:
require __DIR__ . '/../config/modules.php';
這個文件的內(nèi)容如下:
$application->registerModules(array(
'frontend' => array(
'className' => 'Eva\Frontend\Module',
'path' => __DIR__ . '/../apps/frontend/Module.php'
)
));
可以看到Phalcon所謂的模塊注冊,其實只是告訴框架MVC模塊的引導(dǎo)文件Module.php
所在位置及類名是什么。
MVC階段
$application->handle()
是整個MVC的核心,這個函數(shù)中處理了路由、模塊、分發(fā)等MVC的全部流程,處理過程中在關(guān)鍵位置會通過事件驅(qū)動觸發(fā)一系列application:
事件,方便外部注入邏輯,最終返回一個Phalcon\Http\Response
。整個handle
方法的過程并不復(fù)雜,下面按順序介紹:
基礎(chǔ)檢查
首先檢查DI,如果沒有任何DI注入,會拋出錯誤:
A dependency injection object is required to access internal services
然后從DI啟動EventsManager,并且通過EventsManager觸發(fā)事件application:boot
。
路由階段
接下來進入路由階段,從DI中獲得路由服務(wù)router
,將uri傳入路由并調(diào)用路由的handle()
方法。
路由的handle方法負(fù)責(zé)將一個uri根據(jù)路由配置,轉(zhuǎn)換為相應(yīng)的Module、Controller、Action等,這一階段接下來會檢查路由是否命中了某個模塊,并通過Router->getModuleName()
獲得模塊名。
如果模塊存在,則進入模塊啟動階段,否則直接進入分發(fā)階段。
注意到了么,在Phalcon中,模塊啟動是后于路由的,這意味著Phalcon的模塊功能比較弱,我們無法在某個未啟動的模塊中注冊全局服務(wù),甚至無法簡單的在當(dāng)前模塊中調(diào)用另一個未啟動模塊。這可能是Phalcon模塊功能設(shè)計中最大的問題,解決方法暫時不在本文的討論范圍內(nèi),以后會另開文章介紹。
模塊啟動
模塊啟動時首先會觸發(fā)application:beforeStartModule
事件。事件觸發(fā)后檢查模塊的正確性,根據(jù)modules.php
中定義的className
、path
等,將模塊引導(dǎo)文件加載進來,并調(diào)用模塊引導(dǎo)文件中必須存在的方法。
Phalcon\Mvc\ModuleDefinitionInterface->registerAutoloaders ()
Phalcon\Mvc\ModuleDefinitionInterface->registerServices (Phalcon\DiInterface $dependencyInjector)
registerAutoloaders()
用于注冊模塊內(nèi)的命名空間實現(xiàn)自動加載。registerServices ()
用于注冊模塊內(nèi)服務(wù),在官方示例中registerServices ()
注冊并定義了view
服務(wù)以及模板的路徑,并且注冊了數(shù)據(jù)庫連接服務(wù)db
并設(shè)置數(shù)據(jù)庫的連接信息。
模塊啟動完成后觸發(fā) application:afterStartModule
事件,進入分發(fā)階段。
分發(fā)階段(Dispatch)
分發(fā)過程由Phalcon\Mvc\Dispatcher
(分發(fā)器)來完成,所謂分發(fā),在Phalcon里本質(zhì)上是分發(fā)器根據(jù)路由命中的結(jié)果,調(diào)用對應(yīng)的Controller/Action,最終獲得Action返回的結(jié)果。
分發(fā)開始前首先會準(zhǔn)備View,雖然View理論上位于MVC的最后一環(huán),但是如果在分發(fā)過程中出現(xiàn)任何問題,通常都需要將問題顯示出來,因此View必須在這個環(huán)節(jié)就提前啟動。Phalcon沒有準(zhǔn)備默認(rèn)的View服務(wù),需要從外部注入,在多模塊demo中,View的注入官方推薦在模塊啟動階段完成的。如果是單模塊應(yīng)用,則可以在最開始的DI階段注入。
如果始終沒有View注入,會拋出錯誤:
Service 'view' was not found in the dependency injection container
導(dǎo)致分發(fā)過程直接中斷。
分發(fā)需要Dispatcher,Dispatcher同樣從DI中取得。然后將router中得到的參數(shù)(NamespaceName / ModuleName / ControllerName / ActionName / Params),全部復(fù)制到Dispatcher中。
分發(fā)開始前,會調(diào)用View的start()
方法。具體可以參考View相關(guān)文檔,其實Phalcon\Mvc\View->start()
就是PHP的輸出緩沖函數(shù)ob_start
的一個簡單封裝,分發(fā)過程中所有輸出都會被暫存到緩沖區(qū)。
分發(fā)開始前還會觸發(fā)事件application:beforeHandleRequest
。
正式開始分發(fā)會調(diào)用Phalcon\Mvc\Dispatcher->dispatch()
。
Dispatcher內(nèi)的分發(fā)處理
進入Dispatcher后會發(fā)現(xiàn)Dispatcher對整個分發(fā)過程進行了進一步細(xì)分,并且在分發(fā)的過程中會按順序觸發(fā)非常多的分發(fā)事件,可以通過這些分發(fā)事件進行更加細(xì)致的流程控制。部分事件提供了可中斷的機制,只要返回false
就可以跳過Dispatcher的分發(fā)過程。
由于分發(fā)中可以使用Phalcon\Mvc\Dispatcher->forward()
來實現(xiàn)Action的復(fù)用,因此分發(fā)在內(nèi)部會通過循環(huán)實現(xiàn),通過檢測一個全局的finished
標(biāo)記來決定是否繼續(xù)分發(fā)。當(dāng)以下幾種情況時,分發(fā)才會結(jié)束:
- Controller拋出異常
-
forward
層數(shù)達到最大(256次) - 所有的Action調(diào)用完畢
渲染階段 View Render
分發(fā)結(jié)束后會觸發(fā)application:afterHandleRequest
,接下來通過Phalcon\Mvc\Dispatcher->getReturnedValue()
取得分發(fā)過程返回的結(jié)果并進行處理。
由于Action的邏輯在框架外,Action的返回值是無法預(yù)期的,因此這里根據(jù)返回值是否實現(xiàn)Phalcon\Http\ResponseInterface
接口進行區(qū)分處理。
當(dāng)Action返回一個非Phalcon\Http\ResponseInterface
類型
此時認(rèn)為返回值無效,由View自己重新調(diào)度Render過程,會觸發(fā)application:viewRender
事件,同時從Dispatcher中取得ControllerName / ActionName / Params作為Phalcon\Mvc\View->render()
的入口參數(shù)。
Render完畢后調(diào)用Phalcon\Mvc\View->finish()
結(jié)束緩沖區(qū)的接收。
接下來從DI獲得resonse服務(wù),將Phalcon\Mvc\View->getContent()
獲得的內(nèi)容置入response。
當(dāng)Action返回一個Phalcon\Http\ResponseInterface
類型
此時會將Action返回的Response作為最終的響應(yīng),不會重新構(gòu)建新的Response。
返回響應(yīng)
通過前面的流程,無論中間經(jīng)歷了多少分支,最終都會匯總為唯一的響應(yīng)。此時會觸發(fā)application:beforeSendResponse
,并調(diào)用
Phalcon\Http\Response->sendHeaders()
Phalcon\Http\Response->sendCookies()
將http的頭部信息先行發(fā)送。至此,Application->handle()
對于請求的處理過程全部結(jié)束,對外返回一個Phalcon\Http\Response
響應(yīng)。
發(fā)送響應(yīng)
HTTP頭部發(fā)送后一般把響應(yīng)的內(nèi)容也發(fā)送出去:
echo $application->handle()->getContent();
這就是Phalcon Framework的完整MVC流程。
流程控制
分析MVC的啟動流程,無疑是希望對流程有更好的把握和控制,方法有兩種:
自定義啟動
按照上面的流程,我們其實完全可以自己實現(xiàn)$application->handle()->getContent()
這一流程,下面就是一個簡單的替代方案,代碼中暫時沒有考慮事件的觸發(fā)。
//Roter
$router = $di['router'];
$router->handle();
//Module handle
$modules = $application->getModules();
$routeModule = $router->getModuleName();
if (isset($modules[$routeModule])) {
$moduleClass = new $modules[$routeModule]['className']();
$moduleClass->registerAutoloaders();
$moduleClass->registerServices($di);
}
//dispatch
$dispatcher = $di['dispatcher'];
$dispatcher->setModuleName($router->getModuleName());
$dispatcher->setControllerName($router->getControllerName());
$dispatcher->setActionName($router->getActionName());
$dispatcher->setParams($router->getParams());
//view
$view = $di['view'];
$view->start();
$controller = $dispatcher->dispatch();
//Not able to call render in controller or else will repeat output
$view->render(
$dispatcher->getControllerName(),
$dispatcher->getActionName(),
$dispatcher->getParams()
);
$view->finish();
$response = $di['response'];
$response->setContent($view->getContent());
$response->sendHeaders();
echo $response->getContent();
MVC事件
Phalcon作為C擴展型的框架,其優(yōu)勢就在于高性能,雖然我們可以通過上一種方法自己實現(xiàn)整個啟動,但更好的方式仍然是避免替換框架本身的內(nèi)容,而使用事件驅(qū)動。
下面梳理了整個MVC流程中所涉及的可被監(jiān)聽的事件,可以根據(jù)不同需求選擇對應(yīng)事件作為切入點:
- DI注入
-
application:boot
應(yīng)用啟動
-
- 路由階段
- 模塊啟動
-
application:beforeStartModule
模塊啟動前 -
application:afterStartModule
模塊啟動后
- 分發(fā)階段
-
application:beforeHandleRequest
進入分發(fā)器前 - 開始分發(fā)
-
dispatch:beforeDispatchLoop
分發(fā)循環(huán)開始前 -
dispatch:beforeDispatch
單次分發(fā)開始前 -
dispatch:beforeExecuteRoute
Action執(zhí)行前 -
dispatch:afterExecuteRoute
Action執(zhí)行后 -
dispatch:beforeNotFoundAction
找不到Action -
dispatch:beforeException
拋出異常前 -
dispatch:afterDispatch
單次分發(fā)結(jié)束 -
dispatch:afterDispatchLoop
分發(fā)循環(huán)結(jié)束
-
-
application:afterHandleRequest
分發(fā)結(jié)束
-
- 渲染階段
-
application:viewRender
渲染開始前
-
- 發(fā)送響應(yīng)
-
application:beforeSendResponse
最終響應(yīng)發(fā)送前
-