DouPHP DouView 模板引擎使用文档
本文档对应 DouPHP 自研编译式模板引擎 DouView 1.0(实现位于
core/web/template)。 引擎内置 DouPHP 专属的{url}、{list}、{category}数据标签,便于在模板中生成路由链接与按模块取数。
基本语法
定界符
默认左定界符 {、右定界符 },可在引擎实例上配置(leftDelimiter / rightDelimiter)。
模板变量输出
{$变量名}
示例:{$title}、{$user.name}、{$arr[0]}、{$article.content nofilter}
变量支持属性访问、数组下标、修饰器链、nofilter 标志与三元表达式,详见后续章节。
注释
{* 这是注释内容,编译后不输出 *}
注释可跨行,编译期整体丢弃(仅保留位置用于吞行首换行)。
保留定界符
{ldelim} - 输出左定界符 {
{rdelim} - 输出右定界符 }
当需要在模板中输出字面 { / }(如 JS 对象字面量)时使用;更推荐用 {literal} 包裹整段。
原样输出块 literal
{literal}
if (a > 0) { console.log("ok"); }
{/literal}
{literal}...{/literal} 块内内容原样输出,不做任何模板解析,用于保护 JavaScript / CSS 中含大量 { } 的代码段。
变量与表达式
属性 / 下标访问
| 写法 | 含义 |
|---|---|
{$user.name} |
等价 $user['name'](点号取键) |
{$user.$field} |
动态键名,等价 $user[$field] |
{$arr[0]} |
数字下标 |
{$arr[$i]} |
变量下标 |
{$arr[$i.j]} |
变量路径下标(编译为表达式) |
对象成员访问
->在模板中不允许(编译期报错),请控制器侧转数组后注入。
双引号字符串插值
双引号字符串中可直接嵌入变量,引擎自动展开为拼接:
{assign var="greeting" value="你好,$name"}
{* 或反引号形式 *}
{assign var="greeting" value="你好,`$user.name`"}
单引号字符串不做插值,按字面输出。
算术运算
变量基串支持顶层算术运算(+ - * / %):
{$price * $count}
{$total + 1}
{$discount % 10}
三元表达式
{$user ? '已登录' : '游客'}
{$count > 0 ? $count : '空'}
条件部分可为变量或简单比较(== != <> >= <= > <),真/假分支为值表达式。
nofilter 标志(关闭自动转义)
{$article.content nofilter}
当引擎开启全局自动 HTML 转义时,追加裸词 nofilter 可对该输出关闭转义,原样输出 HTML。{url} 标签同样支持 nofilter。
变量修饰器
修饰器用于对变量进行格式化处理,使用管道符 | 分隔,参数用冒号 : 传递。
DouView 内置 20 个标准修饰器(注册于 filter/StandardFilters.php):
1. truncate — 截断字符串(UTF-8 安全)
{$变量|truncate:长度:省略符:是否断词:是否中间截断}
- 长度:截断后最大字符数(默认 80,按 UTF-8 字符计)
- 省略符:默认
... - 是否断词:
false不在词中间断(默认),true允许 - 是否中间截断:
false从末尾截(默认),true从中间截
{$content|truncate:100}
{$content|truncate:50:"...更多":false}
2. escape — 转义输出
{$变量|escape:转义类型:字符集}
转义类型(默认 html):
html— HTML 转义(htmlspecialchars,ENT_QUOTES)htmlall— 所有 HTML 实体转义(htmlentities)url— URL 编码(rawurlencode)urlpathinfo— URL 路径编码(保留斜杠)quotes— 转义单引号hex/hexentity/decentity— 十六进制 / 十六进制实体 / 十进制实体编码javascript— JavaScript 字符串转义mail— 邮箱地址保护显示(@→[AT],.→[DOT])nonstd— 非标准字符(ASCII ≥ 126)转义为实体
{$user_input|escape:"html"}
{$url|escape:"url"}
若修饰器链中已含
|escape(或|escape:"html"),则该输出不再追加全局自动转义,避免双重转义。
3. nl2br — 换行符转 <br>
{$变量|nl2br}
4. strip_tags — 去除 HTML 标签
{$变量|strip_tags:是否替换为空格}
- 是否替换为空格:
true替换为空格(默认),false直接删除
{$html_content|strip_tags}
{$html_content|strip_tags:false}
5. capitalize — 首字母大写
{$变量|capitalize:是否数字大写}
- 是否数字大写:
true数字开头的词也大写(默认false)
{$title|capitalize}
6. cat — 字符串连接
{$变量|cat:"连接字符串"}
{$name|cat:"先生"}
7. count_characters — 统计字符数
{$变量|count_characters:是否包含空格}
- 是否包含空格:
true含空格(默认false)
8. count_paragraphs — 统计段落数
{$变量|count_paragraphs}
9. count_sentences — 统计句子数
{$变量|count_sentences}
10. count_words — 统计单词数
{$变量|count_words}
11. date_format — 日期格式化
{$日期变量|date_format:格式字符串:默认日期}
使用 strftime 占位符(PHP 8.1+ 自动映射为 date() 等价格式):
%Y四位年份 /%y两位年份%m月份(01-12)/%d日期(01-31)/%e日期(不补零)%H24 小时制 /%I12 小时制 /%M分钟 /%S秒%a星期缩写 /%A星期全称 /%b月份缩写 /%B月份全称%pAM/PM /%RH:i/%TH:i:s
{$timestamp|date_format:"%Y-%m-%d %H:%M:%S"}
{$create_time|date_format:"%Y年%m月%d日"}
输入可为时间戳、YYYYMMDDHHMMSS 串或 strtotime 可解析字符串。
12. default — 默认值
{$变量|default:"默认值"}
当变量未设置或为空字符串时回退默认值。
13. indent — 每行缩进
{$变量|indent:缩进数:缩进字符}
- 缩进数:每行缩进字符数(默认 4)
- 缩进字符:默认空格
{$code|indent:8:" "}
14. string_format — sprintf 格式化
{$变量|string_format:"格式"}
{$price|string_format:"%.2f"}
15. strip — 多空白压缩
{$变量|strip:替换字符}
把连续空白压缩为单个字符(默认空格)。
16. lower — 转小写
{$变量|lower}
17. upper — 转大写
{$变量|upper}
18. replace — 字符串替换
{$变量|replace:"查找字符串":"替换字符串"}
{$content|replace:"旧文本":"新文本"}
19. spacify — 字符间插入分隔
{$变量|spacify:分隔字符}
{$word|spacify:"-"}
20. wordwrap — 自动换行
{$变量|wordwrap:行长度:换行符:是否截断单词}
- 行长度:默认 80
- 换行符:默认
\n - 是否截断单词:
true在词中间换行(默认false)
修饰器链式组合
{$content|truncate:100|nl2br|strip_tags}
{$title|escape:"html"|default:"默认标题"}
控制结构
控制结构包含条件判断与循环。循环标签除通用的 {foreach} 外,还有 DouPHP 专属的 {list} / {category}——后两者自动从 Portal 取数后按与 {foreach} 相同的方式循环,共享 item / key / name / offset 参数、@property 循环属性、{break} / {continue} 与空态分支。
if 条件判断
{if $条件}
...
{elseif $其他条件}
...
{else}
...
{/if}
比较与逻辑运算符
| 符号形式 | 等价词 | 含义 |
|---|---|---|
== |
eq |
等于 |
!=、<> |
ne / neq |
不等于 |
=== |
— | 全等于 |
!== |
— | 不全等于 |
> |
gt |
大于 |
< |
lt |
小于 |
>= |
ge / gte |
大于等于 |
<= |
le / lte |
小于等于 |
&& |
and |
与 |
\|\| |
or |
或 |
! |
not |
非 |
% |
mod |
取模 |
同时支持位运算 & | ^ ~ << >> 和算术 + - * /,并允许括号分组。
{if $count > 0 && $user.logged_in}
{if $status eq 1}
{if ($a + $b) % 2 eq 0}
foreach 循环
{foreach from=$数组变量 item=值变量 [key=键变量] [name=循环名称] [limit=数量] [offset=起始位置]}
...
{foreachelse}
数组为空时显示
{/foreach}
参数说明
| 参数 | 必需 | 说明 |
|---|---|---|
from |
是 | 待遍历的数组或对象 |
item |
是 | 当前元素的值变量名(字面标识符) |
key |
否 | 当前元素的键变量名 |
name |
否 | 循环名称(用于 $douview.foreach.NAME.* 访问;省略时自动生成) |
limit |
否 | 最多循环次数(取数后切片,类似 array_slice) |
offset |
否 | 跳过的元素数量(从 0 开始) |
循环属性(@property 语法,推荐)
无需声明 name,直接在 item 变量后用 @ 前缀访问循环元数据:
| 属性 | 说明 | 示例输出 |
|---|---|---|
@iteration |
当前迭代序号(从 1 开始) | 1, 2, 3... |
@index |
当前索引(从 0 开始) | 0, 1, 2... |
@total |
循环总数 | 5 |
@first |
是否为第一个元素 | true / false |
@last |
是否为最后一个元素 | true / false |
@show |
循环是否有数据 | true / false |
{foreach from=$articles item=article}
{if $article@first}<strong>【最新】{/if}
{$article@iteration}. {$article.title}
{if $article@last}<em>(共{$article@total}篇){/if}
{/foreach}
limit / offset 分页
{* 只显示前 10 条 *}
{foreach from=$news item=news limit=10}
<li>{$news.title}</li>
{/foreach}
{* 跳过前 5 条,显示接下来的 10 条 *}
{foreach from=$news item=news limit=10 offset=5}
<li>{$news.title}</li>
{/foreach}
命名循环的 $douview.foreach.NAME.* 属性
声明 name 后可用 $douview.foreach.NAME.* 访问循环属性,支持 index / first / last / show:
{foreach from=$users item=user name=user_list}
{$douview.foreach.user_list.index}: {$user.name}
{/foreach}
推荐优先使用
@property语法({$user@index}等),更简洁且iteration/total仅在该语法下可靠取值。
— 模块内容列表
按模块白名单自动从 Portal 取数,循环输出。仅对在配置 module.column_module / module.single_module 中声明且可列表的模块生效;模块未启用或不可列表时返回空数组,走 {listelse} 分支。
{list module="模块名" item=值变量 [catId=分类ID] [limit=数量] [sort=排序] [excerpt=截字长度]
[key=键变量] [name=循环名称] [offset=跳过数]}
...
{listelse}
无数据时显示
{/list}
参数说明
| 参数 | 说明 |
|---|---|
module |
模块名(必须为字面标识符,禁止运行时变量,防注入) |
item |
当前元素值变量名 |
catId |
分类 ID(仅栏目型模块);ALL / 空 = 不按分类过滤;其它值含子孙分类 |
limit |
取数条数(走 SQL LIMIT) |
sort |
排序,仅接受 字段名 ASC\|DESC 逗号序列,否则忽略走默认 id DESC |
excerpt |
description 字段截字长度(默认 200,≤0 不截) |
key / name / offset |
同 {foreach};offset 在取数后切片 |
{* 文章列表,取最新 10 条 *}
{list module="article" limit=10 item=article}
<li>{$article@iteration}. {$article.title|truncate:50}</li>
{listelse}
<li>暂无文章</li>
{/list}
{* 指定分类下产品,跳过前 5 条取 12 条 *}
{list module="product" catId=$cat_id limit=12 offset=5 item=product}
<div class="product">{$product.name}</div>
{/list}
同一请求内相同
module + props的{list}块结果会被缓存,重复块不重复查询。
— 分类树
仅栏目型模块(module.column_module)可用,输出分类嵌套树。支持两种模式:
模式一:纯分类树
{category module="模块名" item=节点变量 [cur=当前分类ID] [key=键] [name=名称] [offset=跳过数]}
...
{categoryelse}
无分类时显示
{/category}
模式二:分类树 + 每类内容
{category module="模块名" with="items" [perCat=每类条数] [children=是否含子节点]
[excerpt=截字长度] item=节点变量}
{* 节点结构:category_id / name / list(内容行数组)/ child(子节点数组或空) *}
...
{categoryelse}
无分类时显示
{/category}
参数说明
| 参数 | 说明 |
|---|---|
module |
模块名(字面标识符) |
with |
仅接受字面量 "items",启用「分类树 + 每类内容」模式 |
perCat |
每个分类附带的内容条数(默认 5,仅 with="items" 时生效) |
children |
是否递归挂载子节点(仅 with="items" 时生效) |
cur |
当前激活分类 ID(仅纯树模式生效;引擎据此给 id 匹配的节点打 cur=true 标记,便于模板高亮当前分类。with="items" 模式忽略此参数) |
excerpt |
内容行 description 截字长度(默认 200) |
item / key / name / offset |
同 {foreach} |
{* 纯分类树:cur=$cat_id 让匹配节点带上 cur=true 标记,模板直接据此高亮 *}
{category module="article" cur=$cat_id item=cat}
<li{if $cat.cur} class="cur"{/if}>{$cat.cat_name}</li>
{categoryelse}
<li>暂无分类</li>
{/category}
{* 分类树 + 每类 5 条内容 *}
{category module="article" with="items" perCat=5 item=cat}
<h3>{$cat.cat_name}</h3>
<ul>
{foreach from=$cat.list item=row}
<li>{$row.title}</li>
{/foreach}
</ul>
{/category}
与
可在 {foreach} / {list} / {category} 循环内使用,控制循环流程:
{foreach from=$list item=item}
{if $item.status eq 0}
{continue} {* 跳过未发布项 *}
{/if}
{if $item@iteration > 10}
{break} {* 只显示前 10 条 *}
{/if}
<p>{$item.title}</p>
{/foreach}
内置标签
include — 包含子模板
{include file="模板文件名" [变量1="值1" 变量2=$值2] [assign="变量名"]}
file:要包含的模板资源名(必需)assign:将包含结果赋值给指定变量而非直接输出(可选)- 其他属性:作为局部变量传递给子模板
{include file="header.tpl" title=$page_title}
{include file="inc/pager.tpl"}
{include file="box.tpl" box=$box assign="box_html"}
模板文件扩展名白名单:
.tpl/.htm/.html/.dwt。资源名禁止包含..或绝对路径,防止路径穿越。
— 生成路由 URL
把点分路由键编译为 route() 调用,是 DouPHP 模板生成链接的标准方式。
{url link="路由键" [params=$数组] [page=$页码] [options=$选项] [nofilter] k1=$v1 k2=$v2 ...}
保留属性
| 属性 | 说明 |
|---|---|
link |
点分路由键,如 'article.show'、'admin.user.edit'(必需) |
params |
路由参数数组(可选,与内联属性合并) |
page |
分页页码,并入 options.page |
options |
路由选项数组(如 query、lang) |
nofilter |
裸词标志,关闭自动 HTML 转义 |
除上述保留属性外,其他内联属性自动并入 params,作为路由参数。
{* 基础用法 *}
<a href="{url link='article.show'}">文章</a>
{* 带路径/查询参数 *}
<a href="{url link='article.show' id=$article.id}">{$article.title}</a>
<a href="{url link='admin.book.rule_slot.edit' id=$item.id rule_id=$rule_id}">编辑</a>
{* 带分页 *}
<a href="{url link='admin.weixin.media.subscribe' id=$media.id page=$page}">订阅</a>
{* 裸词参数值(非变量)*}
<a href="{url link='admin.ai.daily_stats' date_range=today}">今日</a>
assign — 变量赋值
{assign var="变量名" value="变量值"}
{assign var="page_title" value="首页"}
{assign var="user_level" value=$user.level}
strip — 压缩空白
{strip}
<table>
<tr><td>内容</td></tr>
</table>
{/strip}
去除块内 HTML 标签间的空白字符,压缩输出。常用于优化页面体积。
php — 禁用
{php}...{/php} {* 编译期报错:php tags not permitted *}
DouView 禁止在模板中嵌入原生 PHP 代码。模板文本中出现的 <?、?>、language=php 也会被自动转义为字面输出,杜绝注入。
内置变量
$douview 系统变量
{$douview.now} 当前时间戳(time())
{$douview.template} 当前模板资源名
{$douview.version} 引擎版本号('1.0')
{$douview.ldelim} 左定界符
{$douview.rdelim} 右定界符
$douview.是 DouView 的规范命名,与应用变量命名空间(如模板中的$dou.auth、$dou.user)物理隔离,互不冲突;$smarty.作为兼容别名同等可用(如$smarty.now、$smarty.foreach.NAME.first),旧模板无需改动。
foreach 循环变量
推荐使用 @property 语法(见 foreach 循环);命名循环亦可用 $douview.foreach.NAME.*,支持 index / first / last / show。
预过滤器(Prefilter)
DouView 在编译前对模板源执行预过滤,前台 / 后台行为不同:
前台(FrontPrefilter)
- 主题静态资源相对路径(
images/、css/、js/)烘焙为绝对主题路径 - 移除旧式
<meta http-equiv="Content-Type">声明 - 还原 HTML 注释包裹的标签:
<!-- {tag} -->→{tag}
后台(AdminPrefilter)
- 后台静态资源(
css/、js/、images/)前缀{$admin_url}view/ - 裸
index.php?route=链接绝对化(仅href/action属性) - 同样还原
<!-- {tag} -->→{tag}
HTML 注释包裹标签
由于预过滤器会把 <!-- {标签} --> 还原为 {标签},模板中可用 HTML 注释包裹模板标签,使模板在未渲染时(如直接用浏览器打开)仍保持合法 HTML 结构:
<ul>
<!-- {foreach from=$list item=item} -->
<li>{$item.title}</li>
<!-- {/foreach} -->
</ul>
这是 DouPHP 模板的常见写法,注释内的标签会被正常编译执行。
自动转义与安全
全局自动 HTML 转义
引擎可开启 escapeHtml 开关(DouView::setEscapeHtml(true))。开启后:
- 所有
{$var}输出自动经htmlspecialchars(..., ENT_QUOTES, 'UTF-8')转义 - 已带
|escape修饰器的输出不重复转义 - 追加
nofilter标志的输出不转义(如{$html nofilter})
安全约束
{php}标签编译期拒绝;文本中的 PHP 起止符自动转义- 对象成员访问
->禁止 {list}/{category}的module必须为字面标识符,禁止运行时变量{list}的sort走白名单校验,仅接受字段 ASC|DESC序列,防 ORDER BY 注入include的模板资源名禁止..、绝对路径,扩展名须在白名单{if}条件中禁止函数调用与变量函数调用
注意事项
- 所有模板标签必须正确闭合(
{if}/{/if}、{foreach}/{/foreach}等)。 - 变量名区分大小写;
item/key/name/module必须为字面标识符,不可为表达式。 - 修饰器参数使用冒号
:分隔,字符串参数需加引号。 - 修改主题或升级引擎后,编译缓存会按
COMPILE_REVISION自动失效重编。 - 模板文件扩展名限
.tpl/.htm/.html/.dwt。
完整示例
<!DOCTYPE html>
<html>
<head>
<title>{$title|escape:"html"|default:"默认标题"}</title>
</head>
<body>
{include file="header.tpl" title=$page_title}
<h1>{$title|capitalize}</h1>
{if $user.logged_in}
<p>欢迎回来,{$user.name|escape:"html"}!</p>
{else}
<p>请先<a href="{url link='user.login'}">登录</a></p>
{/if}
<h2>文章列表</h2>
{list module="article" catId=$cat_id limit=10 item=article name=art}
<div class="article">
<h3>{$article@iteration}. {$article.title|truncate:50}</h3>
<p>{$article.description|truncate:200|nl2br}</p>
<p class="meta">
发布时间:{$article.publish_time|date_format:"%Y-%m-%d %H:%M"}
<a href="{url link='article.show' id=$article.id}">查看详情</a>
{if $article@last}<span>(共{$article@total}篇)</span>{/if}
</p>
</div>
{listelse}
<p>暂无文章</p>
{/list}
<h2>分类导航</h2>
<ul>
{category module="article" cur=$cat_id item=cat}
<li{if $cat.cat_id eq $cat_id} class="cur"{/if}>
<a href="{url link='article.category' category_id=$cat.cat_id}">{$cat.cat_name}</a>
</li>
{categoryelse}
<li>暂无分类</li>
{/category}
</ul>
<script>
{literal}
console.log("页面加载完成");
{/literal}
</script>
{include file="footer.tpl"}
</body>
</html> 