“中国要复兴、富强,必须在开源软件领域起到主导作用,为了国家安全和人类发展,责无旁贷,我们须为此而奋斗”——By:云客
上集重点介绍了twig模板的使用,供模板设计者阅读,下集供php开发者阅读,讲解如何调用和扩展twig,这一篇安装和采用版本将衔接上集内容。
Twig_Environment:
该类的实例是twig模板系统的中心枢纽,也叫环境对象,用于保存配置、twig扩展、加载模板等,在她内部调度twig系统的各个类,可以说她是系统的对外使用接口,代表整个twig系统,因此通常用$twig变量名来表示她(在本文中凡使用该变量名,均指环境对象),初始化时接收两个参数:
加载器:负责从不同的源加载模板的内容,比如文件系统、数据库等等,不同的源用不同的加载器
选项数组:在上集中已经讲述过各选项的含义,本篇仅介绍缓存选项相关的缓存对象,见后。
加载器:
twig提供了一些默认加载器,如有特殊需求需要自己实现,默认加载器如下:
Twig_Loader_Filesystem:
用于从文件系统加载模板文件,用法如下:
$loader = new Twig_Loader_Filesystem($templateDir , $rootPath);
参数$templateDir是模板目录的绝对路径,或者是相对路径(相对于第二个参数),也可以是由她们构成的数组,此时数组中的相对路径全部是相对于第二个参数的,可以通过第二个构造参数$rootPath指定相对的根目录,如果没有提供,那么将采用执行脚本的工作目录,也就是getcwd()函数的值,如下;
$loader = new Twig_Loader_Filesystem(array($templateDir1, $templateDir2));
此时将依次查找模板文件,直到找到一个为止,实例化后也可以添加路径:
$loader->addPath($templateDir3); //追加路径
$loader->prependPath($templateDir4); //添加路径到已添加路径的前面,以便首先查找
通过以上两方法添加路径时,也可以指定名字空间,这将不同模板分到不同的组中,如:
$loader->addPath($templateDir, 'admin');
当没有指定名字空间时,默认为“main”,名字空间相当于为其下的所有目录提供了别名,可以这样加载模板:
@namespace_name/template_path
如:
$twig->render('@admin/index.html', array());
这将在名字空间“admin”下的所有目录查找“index.html”模板,名字空间在分前台模板和后台管理模板时很有用
Twig_Loader_Array:
用于从数组加载模板,使用如下:
$loader = new Twig_Loader_Array(array(
'index.html' => 'Hello {{ name }}!',
));
键名是模板名,键值是模板内容,这通常用于单元测试,或者在一些将所有模板存放在php文件中的小项目中
Twig_Loader_Chain:
加载器链,用于将多个加载器整合到一起,当加载模板时,依次调用这些加载器,一旦找到模板即停止查找,使用方法如下:
$loader1 = new Twig_Loader_Array(array(
'base.html' => '{% block content %}{% endblock %}',
));
$loader2 = new Twig_Loader_Array(array(
'index.html' => '{% extends "base.html" %}{% block content %}Hello {{ name }}{% endblock %}',
'base.html' => 'Will never be loaded',
));
$loader = new Twig_Loader_Chain(array($loader1, $loader2));
$twig = new Twig_Environment($loader);
接收由多个加载器构成的数组,传递的加载器需要实现Twig_LoaderInterface,个数不限,实例化后还可以继续添加加载器,使用该方法:
addLoader(Twig_LoaderInterface $loader)
但须注意一旦加载器实例化后,无法调整加载器的顺序
自定义加载器:
全部加载器必须实现接口:
\Twig_LoaderInterface
该接口只有三个方法:
public function getSource($name);
得到模板内容
public function getCacheKey($name);
得到提供给缓存系统使用的模板缓存键,但在缓存系统中真实的缓存id还可以结合其他内容;在文件系统加载器中,该方法返回模板文件的相对路径
public function isFresh($name, $time);
判定模板的缓存是否还有效,第二个参数应该是缓存的模板的修改时间,也就是被编译后的模板的修改时间,在内部将和源模板文件进行时间对比,保证当源模板有变化时,被编译后的缓存即失效
为了让加载器链更加快速的工作,推荐加载器也实现接口:
Twig_ExistsLoaderInterface
该接口只有一个方法:
public function exists($name);
用于判定模板是否存在
缓存对象:
缓存对象用于储存、取回被系统编译后的模板文件,并决定缓存id(对于文件系统缓存对象而言,该id就是文件路径,含文件名)、返回缓存建立时间(该时间供加载器判断缓存是否已经失效)
在上集中已经解释了提供给环境对象的选项数组中cache选项的含义,如下:
可选值有储存编译后模板的目录,或者为false以禁用编译缓存,这是默认值,也可以是缓存对象(接口:Twig_CacheInterface的实例)
实际上在内部不管该选项提供的是哪一种值,都会被转化为缓存对象,也就是接口:Twig_CacheInterface的实例,
在选项是目录路径的时候,默认使用以下缓存对象:
\Twig_Cache_Filesystem
如果禁用缓存,那么默认使用以下缓存对象:
\Twig_Cache_Null
如果需要自定义缓存,如储存到数据库时,那么需要实现接口,并将自定义的缓存对象作为选项值传入环境变量。自定义缓存对象如下:
实现接口:\Twig_CacheInterface
其方法含义如下:
generateKey($name, $className);
返回缓存id,该接口的其他方法通过该id来加载和写入编译后的模板。第一个参数是原模板的文件名,第二个为编译后的模板php类名
write($key, $content);
保存编译后的模板到缓存,第一个参数为缓存id(generateKey方法返回的值),第二个为php代码
load($key);
加载编译后的模板文件实际上是加载一个可执行的php文件,因此使用include_once,并不是读取内容字符串
getTimestamp($key);
返回被编译后的模板文件的保存时间,以供加载器用来和原模板对比判断是否失效
twig扩展Extensions:
扩展用于给twig添加新功能,通过环境对象的addExtension方法添加即可,如:
$twig->addExtension(new Twig_Extension_Sandbox());
twig自带了一些扩展,介绍如下。
核心扩展Twig_Extension_Core:
定义Twig的核心功能,如标签、过滤器、函数、测试等,自动加载该扩展,不必手动添加
转义扩展Twig_Extension_Escaper:
该扩展提供模板中全部变量自动转义、autoescape标签对模板中一段局部内容变量转义、定义过滤器raw,在实例化环境对象时自动加载该扩展,不必手动添加;在已经初始化环境对象后,如果想更改通过选项数组传入的全局转义策略,可以在扩展初始化前调用以下代码:
$escaper = new Twig_Extension_Escaper('html');
$twig->addExtension($escaper);
沙盒扩展Twig_Extension_Sandbox:
提供沙盒模式,用于执行不可信的模板代码,将其隔离执行,示例如下:
$tags = array('if');
$filters = array('upper');
$methods = array(
'Article' => array('getTitle', 'getBody'),
);
$properties = array(
'Article' => array('title', 'body'),
);
$functions = array('range');
$policy = new Twig_Sandbox_SecurityPolicy($tags, $filters, $methods, $properties, $functions);
$sandbox = new Twig_Extension_Sandbox($policy);
$twig->addExtension($sandbox);
如你所见,沙盒扩展需要一个策略对象,twig自带了一个策略对象:Twig_Sandbox_SecurityPolicy,她配置一个白名单,名单以外的内容都不被允许,以上示例配置了如下内容:
允许的模板标签和过滤器、可访问的对象方法和属性、可使用的函数,格式如下:
标签、过滤器、函数的值均是对应元素构成的数组,大小写敏感
允许的方法($methods变量)是一个数组,键名为类名(应该是全限定类名,被直接用于$obj instanceof $class测试),键值为方法名或方法名构成的数组,方法名大小写不敏感
允许的属性($properties变量)和允许的方法一样,但大小写敏感
自定义策略对象只需要实现接口即可,可参照以上策略对象的实现,比较简单不多介绍。
以上代码还没有真正启用沙盒模式,还需要在模板中将不安全的内容放在沙盒标签内才行:
{% sandbox %}
{% include 'user.html' %}
{% endsandbox %}
如果需要对全部模板内容执行沙盒模式,那么在沙盒扩展的第二个参数传入true即可(她默认为false):
$sandbox = new Twig_Extension_Sandbox($policy,true);
如果沙盒内代码访问了不允许的内容,那么将抛出异常,该扩展没有被默认加载,如需使用需要手动加载
分析器扩展Twig_Extension_Profiler:
分析器扩展用于提供模板执行的时间、内存用量等等信息,没有默认加载,应该仅用于开发阶段,示例:
$profile = new Twig_Profiler_Profile();
$twig->addExtension(new Twig_Extension_Profiler($profile));
运行模板之后查看分析结果:
$dumper = new Twig_Profiler_Dumper_Text();
echo $dumper->dump($profile);
优化器扩展Twig_Extension_Optimizer:
用于在编译模板前优化节点树,默认加载并开启全部优化,开发者可用优化选项控制其行为,示例如下:
$optimizer = new Twig_Extension_Optimizer(Twig_NodeVisitor_Optimizer::OPTIMIZE_FOR);
$twig->addExtension($optimizer);
这会导致重新添加,覆写默认添加的选项,控制选项有如下类型:
Twig_NodeVisitor_Optimizer::OPTIMIZE_ALL:开启全部优化,默认值
Twig_NodeVisitor_Optimizer::OPTIMIZE_NONE:关闭优化,这降低模板编译时间,但加大执行时间和内存消耗 Twig_NodeVisitor_Optimizer::OPTIMIZE_FOR:优化for标签,尽可能减少变量循环
Twig_NodeVisitor_Optimizer::OPTIMIZE_RAW_FILTER:尽可能移除不必要的raw过滤器
Twig_NodeVisitor_Optimizer::OPTIMIZE_VAR_ACCESS:尽量简化模板中变量的访问和创建
自定义扩展:
twig提供强大的扩展能力,可以添加标签、过滤器、函数、全局变量、操作符、测试,甚至能扩展语法分析器本身,除扩展标签稍复杂外,其他都很简单,来看一看。
添加全局变量:
全局变量除在全部模板中(包括宏)有效外,和其他普通模板变量没什么区别,添加一个全局变量如下:
$twig = new Twig_Environment($loader);
$twig->addGlobal('text', new Text());
第一个参数是在模板中的全局变量名称,第二个为其值,可以是任意类型的值
全局变量只能在编译或渲染模板之前被添加,否则只能更新
添加过滤器:
示例如下:
$filter = new Twig_SimpleFilter('rot13', function ($string) {
return str_rot13($string);
});
$twig->addFilter($filter);
其中Twig_SimpleFilter构造函数的第一个参数为在模板中将要用到的过滤器名称,第二个为一个php回调$callable,她可以是如下形式:
匿名函数、已定义的php或用户自定义的函数名、类静态方法:
$filter = new Twig_SimpleFilter('rot13', array('SomeClass', 'rot13Filter'));
$filter = new Twig_SimpleFilter('rot13', 'SomeClass::rot13Filter');
对象方法:
$filter = new Twig_SimpleFilter('rot13', array($this, 'rot13Filter'));
在调用过滤器时管道符号“|”左侧的值作为第一个参数传入,若过滤器带圆括号参数,则里面的值按顺序传递给对应参数(并非合成数组传递给第二个参数),
类Twig_SimpleFilter构造函数还存在第三个参数,以指定附加选项,是一个数组,默认值为:
array(
'needs_environment' => false,
'needs_context' => false,
'is_variadic' => false,
'is_safe' => null,
'is_safe_callback' => null,
'pre_escape' => null,
'preserves_safety' => null,
'node_class' => 'Twig_Node_Expression_Filter',
'deprecated' => false,
'alternative' => null,
)
含义如下:
needs_environment:
如果过滤器需要访问环境变量,设置为true,那么第一个参数将传入环境变量,其他值依次后延
needs_context:
如果需要访问上下文,设置为true,那么第一个参数将传入上下文变量,其他值依次后延,如果同时还需要环境变量,那么上下文参数在环境变量参数后面
is_safe:
其值是一个转义策略构成的数组或为NULL,如果开启了自动转义,那么设置该值将避免返回值被指定的策略再次转义
pre_escape:
指定管道符前的变量在传入过滤器前需要先进行转义,字符串值,转义策略之一,不能使用数组指定多个,注意是管道符前的变量,如果是字面值将无效,过滤器圆括号中的额外参数不论是变量还是字面值都不会转义
is_variadic:
如果过滤器可接收任意多个参数时,将其设置为true,那么twig将额外参数合成数组后作为最后一个参数传入
deprecated:
标记这个过滤器已经被启用了
alternative:
当过滤器被标记为弃用时,可用该选项指定一个代替品
node_class:
默认值:Twig_Node_Expression_(Filter/ Function/ Test),指定一个类,用于处理节点的具体实现,比如奇偶数判断,核心并没有采用函数,而是给了一个类实现,在模板编译后的php代码中直接%2来判断
preserves_safety:
在系统中未见实质性使用,官方也未给出解释,可忽略
is_safe_callback:
定义一个回调,用于动态返回is_safe选项的内容,该回调接收Twig_Node类型的参数
动态名称过滤器:
指过滤器的名称不是固定的,twig可以实现不同过滤器名采用同一个过滤器函数,只需要在添加过滤器时动态部分用“*”号代替,如下示例:
$filter = new Twig_SimpleFilter('*_path_*', function ($name, $suffix, $arguments) {
// ...
});
此时假设模板代码如:'foo'|a_path_b() 那么传递参数时将是:('a', 'b', 'foo'),动态部分依次传入,如果有环境变量和上下文参数,那么他们在环境变量和上下文参数后面传入。
添加函数:
添加函数的方法和过滤器高度相似,示例如下:
$twig = new Twig_Environment($loader);
$function = new Twig_SimpleFunction ($name, $callable, $options);
$twig->addFunction($function);
除了选项pre_escape 和 preserves_safety外,函数和过滤器有相同的功能实现,函数也支持动态函数名
动态定义未定义的过滤器或函数:
当模板中使用一个没有定义过的过滤器或函数时,默认抛出异常,但在此之前twig会尝试访问动态定义,如果没有定义才抛出,否则使用动态定义结果,示例如下:
$twig->registerUndefinedFunctionCallback(function ($name) {
if (function_exists($name)) {
return new Twig_SimpleFunction($name, $name);
}
return false;
});
上列将使所有php函数暴露给模板;过滤器动态定义请使用:registerUndefinedFilterCallback()
如果依然不想定义,必须返回false
由于该功能是在编译阶段进行的,所以没有性能损失
注意这和动态名称过滤器和函数是不一样的概念
添加测试:
这里的测试不是指单元测试,也不是在调试代码,而是指模板中的如下运用:
{% if my_value is odd %}
添加一个测试和添加过滤器、函数是高度类似的,示例如下:
$test = new Twig_SimpleTest('red', function ($color) {
if ($color == 'red') {
return true;
}
return false;
});
$twig->addTest($test);
然后在模板中就可以这样用了:
{% if my_value is red %}
是红色
{% else %}
不是红色
{% endif %}
注意:测试用于条件判断,测试函数应该总是返回布尔值
添加标签:
是指自定义类似“if”、“for”等结构,比较复杂,需要知道twig内部原理,涉及编译原理知识,使用很罕见,内容大大超出本篇范围,所以忽略不讲,但这是一件激动人心的事情,在我国计算机专业均有开设专门课程讲解编译原理,但能够实质性接触编译原理实战的机会很少,如果你希望深入研究,那么twig是一个非常合适的实战项目,这也让你大致明白php、C、java等是如何变成机器码的,这里给出一些引导:
twig模板渲染分四个步骤:
加载loader:将模板源代码加载到程序
词法分析lexer:将模板源代码解析为有用的基本单元(token流)
语法分析parser:将token流转化为节点树(抽象语法树AST:the Abstract Syntax Tree)
编译compiler:将语法树转化为php代码
更多请见:https://twig.symfony.com/doc/1.x/internals.html
添加扩展:
以上添加全局变量、过滤器、函数、测试、标签等扩展行为都是单独进行的,一个项目往往需要添加很多东西,可以把这些操作集中到一起形成一个可重用的类,这个类就是扩展,扩展对象用于更加方便的统一进行上述操作,打包功能相关的组件。
扩展类需要实现以下接口:
Twig_ExtensionInterface
通常不需要直接实现,继承抽象类Twig_Extension即可
里面的方法都对应前文介绍的添加方法,具体实现可参考核心扩展类:
Twig_Extension_Core(位于文件Twig/Extension/Core.php中)
从该类也可以看出核心提供了哪些过滤器、函数等
如果需要修改(重载)已经存在的全局变量、过滤器、函数、测试、标签等只需要重新添加他们即可,如果是直接在环境变量上添加,那么她的优先级将高于任何扩展,即便扩展被后添加
自定义定界符:
可以通过向词法分析器传递选项数组改变默认的定界符,示例如下:
$twig = new Twig_Environment($loader);
$lexer = new Twig_Lexer($twig, array(
'tag_comment' => array('{#', '#}'),
'tag_block' => array('{%', '%}'),
'tag_variable' => array('{{', '}}'),
'whitespace_trim' => '-',
'interpolation' => array('#{', '}'),
));
$twig->setLexer($lexer);
但不推荐这样用,这将使模板不通用,但有时候是很有必要的。
对象的动态属性:
当使用article.title方式访问变量时,会检查article是否存在title属性,我们也可以定义php的魔术方法,以动态返回结果,如下:
class Article
{
public function __get($name)
{
if ('title' == $name) {
return 'The title';
}
// throw some kind of error
}
public function __isset($name)
{
if ('title' == $name) {
return true;
}
return false;
}
}
验证模板语法错误:
对第三方或调试阶段的模板可以进行语法验证,以便只有通过后才能保存,示例如下:
try {
$twig->parse($twig->tokenize(new Twig_Source($template)));
//模板有效
} catch (Twig_Error_Syntax $e) {
// 模板有语法错误
}
使用数据库储存模板:
在官网有一个列子,请见:
https://twig.symfony.com/doc/1.x/recipes.html
补充说明:
1、开发者需要注意被编译后的模板保鲜期只和源模板有关,而与调用程序无关,这可能会带来困惑,比如调用程序添加了新的扩展,调试选项也是打开的,但输出结果未变,这极可能就是该问题导致,并非程序bug
2、DSL,是Domain Specific Languages的缩写,可以翻译为“领域特定语言”,用于给贴近业务的人员来描述业务情况,可以算是程序员和业务需求制定人员的桥梁,让不懂技术的人通过她描述业务逻辑,自然语言应该算是DSL设计的极致目标了,计算机听懂自然语言然后自动产生程序去执行,twig提供的语法就是一种DSL的实现,她让设计师通过这种语法做自己的事情,然后twig将其翻译成php程序;SQL也是一种DSL,她单独提供语法,然后数据库软件解释执行她;正则表达式也是;总的来说DSL是一种语言,提供相比于底层技术高级抽象的描述能力,而对人类而言又是自然、亲切、简单的,她涉及语法定义、编译原理等。
交流互动