Phalcon Framework的MVC結構及啟動流程分析

Allo發表於2014-06-11

目前的專案中選擇了Phalcon Framework作為未來一段時間的核心框架。技術選型的原因會單開一篇Blog另說,本次優先對Phalcon的MVC架構與啟動流程進行分析說明,如有遺漏還望指出。

Phalcon本身支援建立多種形式的Web應用專案以應對不同場景,包括迷你應用單模組標準應用、以及較複雜的多模組應用

本次以最複雜的多模組應用為例,Phalcon版本為1.3.2,用一個Phalcon所建立的標準專案來分析。

建立專案

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中定義的classNamepath等,將模組引導檔案載入進來,並呼叫模組引導檔案中必須存在的方法。

  • 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();

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 最終響應傳送前

相關文章