Phalcon的Mvc結構及啓動流程(部分源碼分析)

Phalcon自己有支持建立多種形式的Web應用項目以應對不一樣場景,包括迷你應用單模塊標準應用、以及較複雜的多模塊應用php

建立項目

Phalcon環境配置安裝後,能夠經過命令行生成一個標準的Phalcon多模塊應用html

phalcon project eva --type modules 

入口文件爲public/index.php,簡化後一共5行,包含了整個Phalcon的啓動流程,如下將按順序說明git

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中任何一個組件。github

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還註冊瞭如下服務:sql

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

   $application->setDI($di);api

模塊註冊階段

  與DI同樣,Phalcon建議經過引入一個獨立文件的方式註冊全部須要的模塊:cookie

 require __DIR__ . '/../config/modules.php'; 

 這個文件的內容以下

$application->registerModules(array(
'base' => array(
'className' => 'Cn\Liuxue\Site\Base\Module',
'path' => __DIR__ . '/../apps/base/Module.php'
),
//前臺
'front' => array(
'className' => 'Cn\Liuxue\Site\Front\Module',
'path' => __DIR__ . '/../apps/www/Module.php'
),
//後臺
'backend' => array(
'className' => 'Cn\Liuxue\Site\Backend\Module',
'path' => __DIR__ . '/../apps/admin/Module.php'
),
//CMS
'cms' => array(
'className' => 'Cn\Liuxue\Site\Cms\Module',
'path' => __DIR__ . '/../apps/cms/Module.php'
),
));

能夠看到Phalcon所謂的模塊註冊,其實只是告訴框架MVC模塊的引導文件Module.php所在位置及類名是什麼。

MVC階段

$application->handle()是整個MVC的核心,這個函數中處理了路由、模塊、分發等MVC的所有流程,處理過程當中在關鍵位置會經過事件驅動觸發一系列application:事件,方便外部注入邏輯,最終返回一個Phalcon\Http\Response。整個handle方法的過程並不複雜,下面按順序介紹:$application->handle()application:Phalcon\Http\Responsehandle
 

基礎檢查

首先檢查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(); 

流程清單

爲了方便查找,將整個流程整理爲一個樹形清單以下:

  • 初始化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得到完整的類與方法名,若是找不到則觸發事件 Edispatch: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 最終響應發送前
相關文章
相關標籤/搜索