建立專案
Phalcon環境配置安裝後,可以通過命令列生成一個標準的Phalcon多模組應用
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的所有元件服務都是通過DI(依賴注入)進行組織的,這也是目前大部分主流框架所使用的方法。通過DI,可以靈活的控制框架中的服務:哪些需要啟用,哪些不啟用,元件的內部細節等等,因此Phalcon是一個鬆耦合可替換的框架,完全可以通過DI替換MVC中任何一個元件。
require __DIR__ . '/../config/services.php';
這個檔案中預設註冊了Phalcon\Mvc\Router(路由)、Phalcon\Mvc\Url(Url)、Phalcon\Session\Adapter\Files(Session)三個最基本的元件。同時當MVC啟動後,DI中預設註冊的服務還有很多,可以通過DI得到所有當前已經註冊的服務:
$services = $application->getDI()->getServices();
foreach($services as $key => $service) {
var_dump($key);
var_dump(get_class($application->getDI()->get($key)));
}
列印看到Phalcon還註冊了以下服務:
dispatcher : Phalcon\Mvc\Dispatcher 分發服務,將路由命中的結果分發到對應的Controller
modelsManager : Phalcon\Mvc\Model\Manager Model管理
modelsMetadata : Phalcon\Mvc\Model\MetaData\Memory ORM表結構
response : Phalcon\Http\Response 響應
cookies : Phalcon\Http\Response\Cookies Cookies
request : Phalcon\Http\Request 請求
filter : Phalcon\Filter 可對使用者提交資料進行過濾
escaper : Phalcon\Escaper 轉義工具
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
而每一個服務都可以通過DI進行替換。接下來例項化一個標準的MVC應用,然後將我們定義好的DI注入進去
$application = new Phalcon\Mvc\Application();
$application->setDI($di);
模組註冊階段
與DI一樣,Phalcon建議通過引入一個獨立檔案的方式註冊所有需要的模組:
require __DIR__ . '/../config/modules.php';
這個檔案的內容如下
$application->registerModules(array(
'frontend' => array(
'className' => 'Eva\Frontend\Module',
'path' => __DIR__ . '/../apps/frontend/Module.php'
)
));
可以看到Phalcon所謂的模組註冊,其實只是告訴框架MVC模組的引導檔案Module.php所在位置及類名是什麼。
MVC階段
$application->handle()是整個MVC的核心,這個函式中處理了路由、模組、分發等MVC的全部流程,處理過程中在關鍵位置會通過事件驅動觸發一系列application:事件,方便外部注入邏輯,最終返回一個Phalcon\Http\Response。整個handle方法的過程並不複雜,下面按順序介紹:
基礎檢查
首先檢查DI,如果沒有任何DI注入,會丟擲錯誤
A dependency injection object is required to access internal services
然後從DI啟動EventsManager,並且通過EventsManager觸發事件application:boot
路由階段
接下來進入路由階段,從DI中獲得路由服務router,將uri傳入路由並呼叫路由的handle()方法。
路由的handle方法負責將一個uri根據路由配置,轉換為相應的Module、Controller、Action等,這一階段接下來會檢查路由是否命中了某個模組,並通過Router->getModuleName()獲得模組名。
如果模組存在,則進入模組啟動階段,否則直接進入分發階段。
注意到了麼,在Phalcon中,模組啟動是後於路由的,這意味著Phalcon的模組功能比較弱,我們無法在某個未啟動的模組中註冊全域性服務,甚至無法簡單的在當前模組中呼叫另一個未啟動模組。這可能是Phalcon模組功能設計中最大的問題,解決方法暫時不在本文的討論範圍內,以後會另開文章介紹。
模組啟動
模組啟動時首先會觸發application:beforeStartModule事件。事件觸發後檢查模組的正確性,根據modules.php中定義的className、path等,將模組引導檔案載入進來,並呼叫模組引導檔案中必須存在的方法
Phalcon\Mvc\ModuleDefinitionInterface->registerAutoloaders ()
Phalcon\Mvc\ModuleDefinitionInterface->registerServices (Phalcon\DiInterface $dependencyInjector)
registerAutoloaders()用於註冊模組內的名稱空間實現自動載入。registerServices ()用於註冊模組內服務,在官方示例中registerServices ()註冊並定義了view服務以及模板的路徑,並且註冊了資料庫連線服務db並設定資料庫的連線資訊。
模組啟動完成後觸發 application:afterStartModule事件,進入分發階段
分發階段(Dispatch)
分發過程由Phalcon\Mvc\Dispatcher(分發器)來完成,所謂分發,在Phalcon裡本質上是分發器根據路由命中的結果,呼叫對應的Controller/Action,最終獲得Action返回的結果。
分發開始前首先會準備View,雖然View理論上位於MVC的最後一環,但是如果在分發過程中出現任何問題,通常都需要將問題顯示出來,因此View必須在這個環節就提前啟動。Phalcon沒有準備預設的View服務,需要從外部注入,在多模組demo中,View的注入官方推薦在模組啟動階段完成的。如果是單模組應用,則可以在最開始的DI階段注入。
如果始終沒有View注入,會丟擲錯誤
Service 'view' was not found in the dependency injection container
導致分發過程直接中斷。
分發需要Dispatcher,Dispatcher同樣從DI中取得。然後將router中得到的引數(NamespaceName / ModuleName / ControllerName / ActionName / Params),全部複製到Dispatcher中。
分發開始前,會呼叫View的start()方法。具體可以參考View相關文件,其實Phalcon\Mvc\View->start()就是PHP的輸出緩衝函式ob_start的一個簡單封裝,分發過程中所有輸出都會被暫存到緩衝區。
分發開始前還會觸發事件application:beforeHandleRequest。
正式開始分發會呼叫Phalcon\Mvc\Dispatcher->dispatch()。
Dispatcher內的分發處理
進入Dispatcher後會發現Dispatcher對整個分發過程進行了進一步細分,並且在分發的過程中會按順序觸發非常多的分發事件,可以通過這些分發事件進行更加細緻的流程控制。部分事件提供了可中斷的機制,只要返回false就可以跳過Dispatcher的分發過程。
由於分發中可以使用Phalcon\Mvc\Dispatcher->forward()來實現Action的複用,因此分發在內部會通過迴圈實現,通過檢測一個全域性的finished標記來決定是否繼續分發。當以下幾種情況時,分發才會結束:
Controller丟擲異常
forward層數達到最大(256次)
所有的Action呼叫完畢
渲染階段 View Render
分發結束後會觸發application:afterHandleRequest,接下來通過Phalcon\Mvc\Dispatcher->getReturnedValue()取得分發過程返回的結果並進行處理。
由於Action的邏輯在框架外,Action的返回值是無法預期的,因此這裡根據返回值是否實現Phalcon\Http\ResponseInterface介面進行區分處理。
當Action返回一個非Phalcon\Http\ResponseInterface型別
此時認為返回值無效,由View自己重新排程Render過程,會觸發application:viewRender事件,同時從Dispatcher中取得ControllerName / ActionName / Params作為Phalcon\Mvc\View->render()的入口引數。
Render完畢後呼叫Phalcon\Mvc\View->finish()結束緩衝區的接收。
接下來從DI獲得resonse服務,將Phalcon\Mvc\View->getContent()獲得的內容置入response。
當Action返回一個Phalcon\Http\ResponseInterface型別
此時會將Action返回的Response作為最終的響應,不會重新構建新的Response。
返回響應
通過前面的流程,無論中間經歷了多少分支,最終都會彙總為唯一的響應。此時會觸發application:beforeSendResponse,並呼叫
Phalcon\Http\Response->sendHeaders()
Phalcon\Http\Response->sendCookies()
將http的頭部資訊先行傳送。至此,Application->handle()對於請求的處理過程全部結束,對外返回一個Phalcon\Http\Response響應。
傳送響應
HTTP頭部傳送後一般把響應的內容也傳送出去:
echo $application->handle()->getContent();
這就是Phalcon Framework的完整MVC流程。
流程控制
分析MVC的啟動流程,無疑是希望對流程有更好的把握和控制,方法有兩種:
自定義啟動
按照上面的流程,我們其實完全可以自己實現$application->handle()->getContent()這一流程,下面就是一個簡單的替代方案,程式碼中暫時沒有考慮事件的觸發。
//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();
流程清單
為了方便查詢,將整個流程整理為一個樹形清單如下:
初始化DI (config/services.php) $di = new FactoryDefault();
設定路由 $di['router'] = function () {}
設定URL $di['url'] = function () {}
設定Session $di['session'] = function () {}
初始化Application (public/index.php)
例項化App $application = new Application();
注入DI $application->setDI($di);
註冊模組 (config/modules.php) $application->registerModules()
啟動Application (ext/mvc/application.c) $application->handle()
檢查DI
E 觸發事件 application:boot
路由啟動 $di['router']->handle()
獲得模組名 $moduleName = $di['router']->getModuleName(),如果沒有則從 $application->getDefaultModule獲取
模組啟動 (如果路由命中)
E 觸發事件 application:beforeStartModule
呼叫模組初始化方法 (Module.php) registerAutoloaders() 以及 registerServices()
E 觸發事件 application:afterStartModule
分發
初始化View
初始化Dispatcher,將Router中的引數複製到Dispatcher
呼叫View View->start()開啟緩衝區
E 觸發事件 application:beforeHandleRequest
開始分發 (etc/dispatcher.c) Dispatcher->dispatch()
E 觸發事件 dispatch:beforeDispatchLoop
迴圈開始單次分發
E 觸發事件 dispatch:beforeDispatch
根據Dispatcher攜帶的Module、Namespace、Controller、Action獲得完整的類與方法名,如果找不到則觸發事件 E dispatch:beforeException
E 觸發事件 dispatch:beforeExecuteRoute
呼叫Controller->beforeExecuteRoute()
呼叫Controller->initialize()
E 觸發事件 dispatch:afterInitialize
呼叫Action方法
E 觸發事件 dispatch:afterExecuteRoute
E 觸發事件 dispatch:afterDispatch
Action內如果有forward(),開始下一次分發
E 全部分發結束,觸發事件 dispatch:afterDispatchLoop
Application獲得分發後的輸出 $dispatcher->getReturnedValue()
E 觸發事件 application:afterHandleRequest 分發結束
渲染,Appliction如果從分發拿到Phalcon\Http\ResponseInterface型別的返回,則渲染直接結束
E 觸發事件 application:viewRender 分發結束
呼叫 Phalcon\Mvc\View->render(),入口引數為Dispatcher的 ControllerName / ActionName / Params
呼叫 Phalcon\Mvc\View->finish()結束緩衝區的接收
準備響應
將Phalcon\Mvc\View->getContent()通過Phalcon\Http\Response->setContent()放入Response
E 觸發事件 application:beforeSendResponse
呼叫Phalcon\Http\Response->sendHeaders()傳送頭部
呼叫Phalcon\Http\Response->sendCookies()傳送Cookie
將準備好的響應作為$application->handle()的返回值返回
傳送響應
echo $application->handle()->getContent();
MVC事件
Phalcon作為C擴充套件型的框架,其優勢就在於高效能,雖然我們可以通過上一種方法自己實現整個啟動,但更好的方式仍然是避免替換框架本身的內容,而使用事件驅動。
下面梳理了整個MVC流程中所涉及的可被監聽的事件,可以根據不同需求選擇對應事件作為切入點:
DI注入
application:boot 應用啟動
路由階段
模組啟動
application:beforeStartModule 模組啟動前
application:afterStartModule 模組啟動後
分發階段
application:beforeHandleRequest進入分發器前
開始分發
dispatch:beforeDispatchLoop 分發迴圈開始前
dispatch:beforeDispatch 單次分發開始前
dispatch:beforeExecuteRoute Action執行前
dispatch:afterExecuteRoute Action執行後
dispatch:beforeNotFoundAction 找不到Action
dispatch:beforeException 丟擲異常前
dispatch:afterDispatch 單次分發結束
dispatch:afterDispatchLoop 分發迴圈結束
application:afterHandleRequest 分發結束
渲染階段
application:viewRender 渲染開始前
傳送響應
application:beforeSendResponse 最終響應傳送前