DouPHP模板语法概述

发布时间:2013-11-16点击次数:15709

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 日期(不补零)
  • %H 24 小时制 / %I 12 小时制 / %M 分钟 / %S
  • %a 星期缩写 / %A 星期全称 / %b 月份缩写 / %B 月份全称
  • %p AM/PM / %R H:i / %T H: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 路由选项数组(如 querylang
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} 条件中禁止函数调用与变量函数调用

注意事项

  1. 所有模板标签必须正确闭合({if}/{/if}{foreach}/{/foreach} 等)。
  2. 变量名区分大小写;item / key / name / module 必须为字面标识符,不可为表达式。
  3. 修饰器参数使用冒号 : 分隔,字符串参数需加引号。
  4. 修改主题或升级引擎后,编译缓存会按 COMPILE_REVISION 自动失效重编。
  5. 模板文件扩展名限 .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>