本文主要讨论 Phalcon 视图层的渲染。
原理
把 action 中传过来的数据定义成变量。
加载视图文件,此时视图里的变量已经有值了 (如果是volt的话,把volt语法编译成原生php,加载编译后的文件),它是一个 php 和 html 混写的文件,php 的代码会执行,最后只剩下 html。
把执行完后的内容返回出去。
控制器传值给视图
使用方法:
// 直接设置变量,会调用 __set() 的魔术方法
$this->view->products = $products;
// setParamToView()
$this->view->setParamToView('products', $products);
// setVar()
$this->view->setVar('products', $products);
// setVars()
$this->view->setVars(array('products' => $products));
下面我们分别看一下源码中这些方法是如何实现的
/**
* Magic method to pass variables to the views
*
* @param string key
* @param mixed value
*/
public function __set(string! key, value)
{
let this->_viewParams[key] = value;
}
/**
* Adds parameters to views (alias of setVar)
*
* @param string key
* @param mixed value
* @return \Phalcon\Mvc\View
*/
public function setParamToView(string! key, value) -> <View>
{
let this->_viewParams[key] = value;
return this;
}
/**
* Set all the render params
*
* @param array params
* @param boolean merge
* @return \Phalcon\Mvc\View
*/
public function setVars(array! params, boolean merge = true) -> <View>
{
var viewParams;
if merge {
let viewParams = this->_viewParams;
if typeof viewParams == "array" {
let this->_viewParams = array_merge(viewParams, params);
} else {
let this->_viewParams = params;
}
} else {
let this->_viewParams = params;
}
return this;
}
/**
* Set a single view param
*
* @param string key
* @param mixed value
* @return \Palcon\Mvc\View
*/
public function setVar(string! key, value) -> <View>
{
let this->_viewParams[key] = value;
return this;
}
这几个方法都是把值设置给了 view
实例的一个属性 _viewParams
,在调用 _engineRender()
方法时,它取出 _viewParams
的值作为参数传给了具体的渲染引擎。
视图
在 index.php 中,实例化出一个 application 对象,执行 application->handle() 方法,在这个方法里,实例化了 view,调 view->render() 方法,我们追踪下去,就可以找出 view 层是如何处理渲染的。
视图渲染逻辑
/**
* Executes render process from dispatching data
*
* @param string controllerName
* @param string actionName
* @param array params
*/
public function render(string! controllerName, string! actionName, params = null) -> <View>|boolean
{
}
下面我以主要的业务逻辑过一下源码
_disabled
检查视图是否禁用,如果禁用,输出内容, return falselayout
检查是否设置了layout_loadTemplateEngines()
加载模板引擎pickView
检查用户是否指定了一个不同于当前url对应的 controller/action 视图_cacheLevel
检查缓存create_symbol_table()
创建符号表view:beforeRender
抛出事件Inserts view related to action
Inserts templates before layout
Inserts controller layout
Inserts templates after layout
Inserts main view
Store the data in the cache
view:afterRender
抛出事件
关于多次插入视图,他们之间的关系可以参考我的另一篇博文:http://segmentfault.com/a/1190000004363452
在插入视图时,都是调用的这个方法this->_engineRender(engines, renderView, silence, mustClean, cache);
_engineRender
在 Phalcon 框架中,所有的模板引擎适配器都必须继承Engine
类,这个类提供了引擎和视图组件的基本接口,每一个引擎都必须实现自己的render
方法。
当插入视图调用 _engineRender()
方法时,Phalcon 又做了如下处理
注:本文仅是示例代码
/**
* Checks whether view exists on registered extensions and render it
*
* @param array engines
* @param string viewPath
* @param boolean silence
* @param boolean mustClean
* @param \Phalcon\Cache\BackendInterface $cache
*/
protected function _engineRender(engines, string viewPath, boolean silence, boolean mustClean, <BackendInterface> cache = null)
{
let viewsDir = this->_viewDir,
basePath = this->_basePath,
viewsDirPath = basePath . viewsDir . viewPath;
let viewParams = this->_viewParams;
for extension, engine in engines {
let viewEnginePath = viewsDirPath . extension;
if file_exists(viewEnginePath) {
if typeof eventsManager == "object" {
let this->_activeRenderPath = viewEnginePath;
if (eventsManager->fire("view:beforeRenderView", this, viewEnginePath) === false) {
continue;
}
}
engine->render(viewEnginePath, viewParams, mustClean);
let notExists = false;
if typeof eventsManager == "object" {
eventsManager->fire("view:afterRenderView", this);
}
break;
}
}
}
它在调用具体模板引擎渲染的前后分别抛出两个事件,便于开发者使用。
此时的 engine 可能是 php engine,也可以是 volt engine,或者其他任何你注册的渲染引擎。
下面是这些参数的含义。
viewEnginePath
: 视图文件的路径。viewParams
: action 传给 view 的数据。mustClean
: 是否必须清除输出缓冲区。
这里有几个要点需要注意:
如果侦听
view:beforeRenderView
事件,处理后返回false
,将不进入渲染阶段。engine->render()
方法调用后,任何输出都将忽略,原因参考下面模板引擎渲染的代码。view:afterRenderView
事件不可中止。
php engine
以 php 默认渲染引擎为例,当上面调用engine->render()
方法时,会执行以下几步
清空输出缓冲区。
把 action 传给 view 的数据,以变量的形式声明出来。
加载视图文件,这时候在视图中php代码会把变量的值输出。
-
使用
setContent()
方法把输出缓冲区的内容放入 view 对象_content
属性中。这样在 视图文件里就可以使用getContent()
方法取出。/** * Externally sets the view content * *<code> * $this->view->setContent("<h1>hello</h1>"); *</code> */ public function setContent(string content) -> <View> { let this->_content = content; return this; } /** * Returns cached output from another view stage */ public function getContent() -> string { return this->_content; }
php engine render 源码分析
/**
* Adapter to use PHP itself as templating engine
*/
class Php Engine implements EngineInterface
{
/**
* Renders a view using the template engine
*/
public function render(string! path, var params, boolean mustClean = false)
{
var key, value;
if mustClean === true {
ob_clean();
}
/**
* Create the variables in local symbol table
*/
for key, value in params {
let {key} = value;
}
/**
* Require the file
*/
require path;
if mustClean == true {
$this->_view->setContent(ob_get_contents());
}
}
}
参数解析:
path
:视图文件 .phtml 或 .volt 文件的路径mustClean
:是否必须清除输出缓冲区params
: array 类型,存储的是 action 传给 view 的变量。
如:在 action 中
$this->view->test = 123;
$this->view->haha = "hello world";
则 params
array (size=2)
'test' => int 123
'haha' => string 'hello world'
volt
volt 是 Phalcon 自带的模板渲染引擎。如果我们注册的是 volt 引擎,那么_engineRender
调的是 volt 引擎,我们看一下它是如何处理的。
render
/**
* Renders a view using the template engine
*/
public function render(string! templatePath, var params, boolean mustClean = false)
{
var compiler, compiledTemplatePath, key, value;
if mustClean {
ob_clean();
}
/**
* The compilation process is done by Phalcon\Mvc\View\Engine\Volt\Compiler
*/
let compiler = this->getCompiler();
compiler->compile(templatePath);
let compiledTemplatePath = compiler->getCompiledTemplatePath();
/**
* Export the variables the current symbol table
*/
if typeof params == "array" {
for key, value in params {
let {key} = value;
}
}
require compiledTemplatePath;
if mustClean {
this->_view->setContent(ob_get_contents());
}
}
volt 和 php engine 处理差不多,只是中间多了一步编译,而编译所做的工作就是把 volt 语法转换成原生 php 语法。
我们可以看出,它首先得到一个 compiler 的实例,然后调用compiler的compile()
方法来编译 volt 文件,最后是得到编译后文件的路径。
下面我们通过源代码验证我们的设想
首先看 getCompiler()
/**
* Returns the Volt's compiler
*/
public function getCompiler() -> <Compiler>
{
var compiler, dependencyInjector, options;
let compiler = this->_compiler;
if typeof compiler != "object" {
let compiler = new Compiler(this->_view);
/**
* Pass the IoC to the compiler only of it's an object
*/
let dependencyInjector = <DiInterface> this->_dependencyInjector;
if typeof dependencyInjector == "object" {
compiler->setDi(dependencyInjector);
}
/**
* Pass the options to the compiler only if they're an array
*/
let options = this->_options;
if typeof options == "array" {
compiler->setOptions(options);
}
let this->_compiler = compiler;
}
return compiler;
}
然后看compile()
,这个方法内容很多,我们忽略大部分细节,只看关键部分。
/**
* Compiles a template into a file applying the compiler options
* This method does not return the compiled path if the template was not compiled
*
*<code>
* $compiler->compile('views/layouts/main.volt');
* require $compiler->getCompiledTemplatePath();
*</code>
*/
public function compile(string! templatePath, boolean extendsMode = false)
{
let compilation = this->compileFile(templatePath, realCompiledPath, extendsMode);
let this->_compiledTemplatePath = realCompiledPath;
return compilation;
}
最后我们看getCompiledTemplatePath()
/**
* Returns the path to the last compiled template
*/
public function getCompiledTemplatePath() -> string
{
return this->_compiledTemplatePath;
}
compileFile
从 compile()
,我们就可以看出 compileFile()
方法是下一个突破口.
/**
* Compiles a template into a file forcing the destination path
*
*<code>
* $compiler->compile('views/layouts/main.volt', 'views/layouts/main.volt.php');
*</code>
*
* @param string path
* @param string compiledPath
* @param boolean extendsMode
* @return string|array
*/
public function compileFile(string! path, string! compiledPath, boolean extendsMode = false)
{
var viewCode, compilation, finalCompilation;
/**
* Always use file_get_contents instead of read the file directly, this respect the open_basedir directive
*/
let viewCode = file_get_contents(path);
let this->_currentPath = path;
let compilation = this->_compileSource(viewCode, extendsMode);
/**
* We store the file serialized if it's an array of blocks
*/
if typeof compilation == "array" {
let finalCompilation = serialize(compilation);
} else {
let finalCompilation = compilation;
}
/**
* Always use file_put_contents to write files instead of write the file directly, this respect the open_basedir directive
*/
if file_put_contents(compiledPath, finalCompilation) === false {
throw new Exception("Volt directory can't be written");
}
return compilation;
}
从这里我们又找到线索_compileSource()
,继续前行。
_compileSource
/**
* Compiles a Volt source code returning a PHP plain version
*/
protected function _compileSource(string! viewCode, boolean extendsMode = false) -> string
{
let currentPath = this->_currentPath;
// Check for compilation options
// ......
let intermediate = phvolt_parse_view(viewCode, currentPath);
let compilation = this->_statementList(intermediate, extendsMode);
// Check if the template is extending another
// ......
return compilation;
}
看到方法注释的时候,我们以为终于找到正主,看完却发现并没有。
_statementList
/**
* Traverses a statement list compiling each of its nodes
*/
final protected function _statementList(array! statements, boolean extendsMode = false) -> string
{
// ......
}
这个方法有246行,我把关键部分截图了,方便查看。
它通过 switch case,匹配不同表达式的类型,调用不同的方法具体编译某段代码。
我以 compileEcho()
和 compileIf()
为例给大家看一下.
进行到这里,我们终于找出了 volt 模板引擎的大致渲染思路,它比php engine 多了一步就是编译,把它自身语法翻译成原生php,然后就和 php engine 处理是一样的了。
volt 总结
volt 比 phtml 在处理渲染时多了一步编译,虽然它已编译成机器码在服务器端运行,但多了一步编译,肯定比 phtml 慢一些的;而且前期使用 volt 必然需要阅读官方文档,了解其规则,这些都是时间成本。
但是熟练使用它的话,在开发过程中能够简化代码,提高可读性和开发效率。
总结
我们在处理视图渲染时,先把控制器传过来的数据,通过变量声明出来,这样视图文件的变量就有值了,随着 php 代码的执行,最后只剩下一个 html 的内容返回出去。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。