Phalcon的Mvc结构及启动流程(部分源码分析)
Phalcon本身有支持创建多种形式的Web应用项目以应对不同场景,包括迷你应用、单模块标准应用、以及较复杂的多模块应用
创建项目
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
分发服务,将路由命中的结果分发到对应的ControllermodelsManager
:Phalcon\Mvc\Model\Manager
Model管理modelsMetadata
:Phalcon\Mvc\Model\MetaData\Memory
ORM表结构response
:Phalcon\Http\Response
响应cookies
:Phalcon\Http\Response\Cookies
Cookiesrequest
: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(
'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
方法的过程并不复杂,下面按顺序介绍:
基础检查
首先检查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()
- 实例化App
- 启动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
- E 触发事件
- 分发
- 初始化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
- E 触发事件
- Action内如果有forward(),开始下一次分发
- E 触发事件
- 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()
结束缓冲区的接收
- E 触发事件
- 准备响应
- 将
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
找不到Actiondispatch:beforeException
抛出异常前dispatch:afterDispatch
单次分发结束dispatch:afterDispatchLoop
分发循环结束
application:afterHandleRequest
分发结束
- 渲染阶段
application:viewRender
渲染开始前
- 发送响应
application:beforeSendResponse
最终响应发送前
Phalcon的Mvc结构及启动流程(部分源码分析)的更多相关文章
- Phalcon Framework的MVC结构及启动流程分析
目前的项目中选择了Phalcon Framework作为未来一段时间的核心框架.技术选型的原因会单开一篇Blog另说,本次优先对Phalcon的MVC架构与启动流程进行分析说明,如有遗漏还望指出. P ...
- SpringMVC执行流程及源码分析
SpringMVC流程及源码分析 前言 学了一遍SpringMVC以后,想着做一个总结,复习一下.复习写下面的总结的时候才发现,其实自己学的并不彻底.牢固.也没有学全,视频跟书本是要结合起来一起, ...
- Flink的Job启动TaskManager端(源码分析)
前面说到了 Flink的JobManager启动(源码分析) 启动了TaskManager 然后 Flink的Job启动JobManager端(源码分析) 说到JobManager会将转化得到 ...
- Android应用层View绘制流程与源码分析
1 背景 还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原 ...
- django Rest Framework----APIView 执行流程 APIView 源码分析
在django—CBV源码分析中,我们是分析的from django.views import View下的执行流程,这篇博客我们介绍django Rest Framework下的APIView的源码 ...
- Struts2请求处理流程及源码分析
1.1 Struts2请求处理 1. 一个请求在Struts2框架中的处理步骤: a) 客户端初始化一个指向Servlet容器的请求: b) 根据Web.xml配置,请求首先经过ActionConte ...
- Phalcon Framework的Mvc结构及启动流程(部分源码分析)
创建项目 Phalcon环境配置安装后,可以通过命令行生成一个标准的Phalcon多模块应用 phalcon project eva --type modules入口文件为public/index.p ...
- spring mvc 启动过程及源码分析
由于公司开源框架选用的spring+spring mvc + mybatis.使用这些框架,网上都有现成的案例:需要那些配置文件.每种类型的配置文件的节点该如何书写等等.如果只是需要项目能够跑起来,只 ...
- 前端MVC框架Backbone 1.1.0源码分析(一)
前言 如何定义库与框架 前端的辅助工具太多太多了,那么我们是如何定义库与框架? jQuery是目前用的最广的库了,但是整体来讲jQuery目的性很也明确针对“DOM操作”,当然自己写一个原生态方法也能 ...
随机推荐
- charts & data visualization
charts & data visualization https://www.sitepoint.com/15-best-javascript-charting-libraries/ Can ...
- Java之反射举例
package reflection; import bean.User; public class ReflectionDemo { public static void main(String[] ...
- Essential Phone PH1官方刷机方法
Essential Phone官方有两种包 一种是ota包,即sideload线刷使用的包.但此刷机方法只能ota升级,不能降级. 另一种是Images包,即fastboot线刷使用的包.这种方法可以 ...
- iTunes 12.7 没有应用程序选项了,重新安装iTunes 12.6.3
iTunes 12.6.3 下载地址: Mac http://t.cn/RO5rIfE Win 32 http://t.cn/ROtta1T Win64 http://t.cn/ROtta1n 安装好 ...
- lvs逻辑卷详解
管理磁盘空间对系统管理员来说是一件重要的日常工作.一旦磁盘空间耗尽就需要进行一系列耗时而又复杂的任务,以提升磁盘分区中可用的磁盘空间.它也需要系统离线才能处理.通常这种任务会涉及到安装一个新的硬盘.引 ...
- BZOJ2124 等差子序列(树状数组+哈希)
容易想到一种暴力的做法:枚举中间的位置,设该位置权值为x,如果其两边存在权值关于x对称即合法. 问题是如何快速寻找这个东西是否存在.考虑仅将该位置左边出现的权值标1.那么若在值域上若关于x对称的两权值 ...
- ACM-ICPC 2018 焦作赛区网络预赛 E Jiu Yuan Wants to Eat (树链剖分+线段树)
题目链接:https://nanti.jisuanke.com/t/31714 题意:给你一棵树,初始全为0,有四种操作: 1.u-v乘x 2.u-v加x 3. u-v取反 4.询问u-v ...
- Cuba获取属性文件中的配置
最直接的办法是,使用AppContext.getProperty("cuba.trustedClientPassword"); 可以获取到系统中的web模块下的wep-app.pr ...
- java项目http变更https
1. 创建 keystore 文件 执行keytool -genkey -v -alias tomcat -keyalg RSA -validity 3650 -keystore c:\tomc ...
- 自学Python1.4-Centos内vim中文乱码问题
自学Python之路 自学Python1.4-Centos内vim中文乱码问题 1. 登陆的系统---区域语言设置 1.1查看安装中文包: 查看系统是否安装中文语言包 (列出所有可用的公共语言环境的名 ...