番外篇:原生twig模板引擎详解(下集:开发者篇)

原生twig模板引擎详解(下集:开发者篇)

 

上集重点介绍了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);

保存编译后的模板到缓存,第一个参数为缓存idgenerateKey方法返回的值),第二个为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是一个非常合适的实战项目,这也让你大致明白phpCjava等是如何变成机器码的,这里给出一些引导:

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

 

2DSL,是Domain Specific Languages的缩写,可以翻译为“领域特定语言”,用于给贴近业务的人员来描述业务情况,可以算是程序员和业务需求制定人员的桥梁,让不懂技术的人通过她描述业务逻辑,自然语言应该算是DSL设计的极致目标了,计算机听懂自然语言然后自动产生程序去执行,twig提供的语法就是一种DSL的实现,她让设计师通过这种语法做自己的事情,然后twig将其翻译成php程序;SQL也是一种DSL,她单独提供语法,然后数据库软件解释执行她;正则表达式也是;总的来说DSL是一种语言,提供相比于底层技术高级抽象的描述能力,而对人类而言又是自然、亲切、简单的,她涉及语法定义、编译原理等。

本书共94小节:

评论 (写第一个评论)