前言
严格说,Smarty 里并没有一个叫「预保留变量」的官方分类。这个说法指的是一组以$smarty为前缀、由 Smarty 自己在模板里提供的内置数据:常量、配置项、循环信息、请求参数镜像、当前模板信息等等。它们的共同点是:不需要你assign(),模板里直接就能读到。
这里要纠正两个常见误解。其一,有人以为{$smarty.now}之类的变量在 PHP 代码里也能直接用——不能,它们只在模板作用域里存在,PHP 侧要走对应的 API(比如用getTemplateVars()看已赋值的变量)。其二,有人以为这些内置变量是「安全的、系统给的」,可以随便输出。事实恰恰相反:$smarty.get、$smarty.post这一组是把未经校验的用户输入原样搬进了模板,直接输出就是 XSS。
本文按用途把这组内置变量分四类梳理,并把真正会出问题的地方点出来。示例基于 Smarty 4.x 与 PHP 8.1;Smarty 3 也提供同一组变量,个别项在新版本中已被建议改用别的写法,遇到这类情况本文会标注。文中示例无法在本机执行验证,请结合本地实测阅读。
一、常量、配置、时间与捕获
这一组是日常用得最多的四个。
{$smarty.now}返回当前的 Unix 时间戳(整数),通常配合date_format修饰器使用:
{* 现在是:{$smarty.now|date_format:'Y-m-d H:i:s'} *}注意一个版本坑:date_format修饰器的格式串如果含有%(如'%Y-%m-%d'),走的是 PHP 的strftime()风格;而PHP 8.1 起strftime()已被废弃,会触发 deprecation 提示。新代码请用date()风格的格式串('Y-m-d H:i:s'这种,不含%),或者干脆在 PHP 侧date()好再 assign。
{$smarty.const.XXX}读取 PHP 常量,等价于模板里访问XXX:
<p>站点:{$smarty.const.APP_NAME}</p>
<p>PHP 版本:{$smarty.const.PHP_VERSION}</p>常量必须在使用前define()过,否则模板里取到的是空值。这个入口适合放「部署环境相关、不随请求变化」的值,比如站点名、静态资源版本号。
{$smarty.config.XXX}读取 Smarty 配置文件里的变量。配置文件默认放在配置目录下,用{config_load file="site.conf"}加载,或者由 PHP 侧加载:
site_name = 演示站点
page_size = 20{config_load file="site.conf"}
<p>站点:{$smarty.config.site_name}</p>也可以用更老的花括号井号语法直接读:{#site_name#}。这种写法在 Smarty 2 时代更常见,新项目一般不需要配置文件这层,直接assign()更直白。
{$smarty.capture.NAME}读取{capture}块捕获的内容,用途是「先渲染一段,稍后再用」:
{capture name="sidebar"}
<aside>这一段被存起来了</aside>
{/capture}
<div class="main">
{$smarty.capture.sidebar}
</div>{capture}里的内容不会输出到当前位置,而是存进$smarty.capture。写布局时需要「先定义后使用」的场景(比如把某个区块放在页面底部渲染、但内容在头部生成)就靠它。
二、循环信息:foreach 与 section
循环属性有两种写法,必须先分清版本:
| 写法 | 形式 | 版本 |
|---|
| 元素属性法 | {$item@index}、{$item@first} | Smarty 3 起推荐 |
| 全局属性法 | {$smarty.foreach.名字.index}、{$smarty.section.名字.index} | Smarty 2 风格,需给循环起name属性 |
这里只讲$smarty.系列这一种,因为它正是「内置变量」的一部分。要让{$smarty.foreach.名称.属性}可用,{foreach}必须带name:
{foreach from=$items item=row name=itemLoop}
<li>
{$smarty.foreach.itemLoop.iteration} / {$smarty.foreach.itemLoop.total}
{$row.name}
</li>
{/foreach}{section}的信息挂在$smarty.section下面,属性略有不同:
| 属性 | 含义 |
|---|
index | 当前下标,从 0 开始 |
iteration | 当前是第几轮,从 1 开始 |
first/last | 是否第一轮 / 最后一轮(布尔) |
rownum | 已循环的次数(从 1 开始,与iteration相近) |
total | 循环总次数 |
loop | 被遍历的数组本身 |
show | 当前这一轮是否应该输出(配合show属性做条件跳过) |
{section name=i loop=$items}
{if $smarty.section.i.first}<hr>{/if}
<li>{$items[i]}</li>
{/section}新代码建议直接用{$item@iteration}这一类元素属性法,它不依赖name,也不会和其他循环的属性互相干扰。
三、模板自身信息与定界符
{$smarty.template}是当前处理的模板名,调试时很有用;{$smarty.current_dir}是当前模板所在目录,配合{include}写相对路径时会用到。
{$smarty.version}是 Smarty 的版本号字符串,排查「这台机器上装的是哪一版」时最直接。
{$smarty.ldelim}与{$smarty.rdelim}分别输出左、右定界符(默认是{和})。要在页面上显示一对花括号、又不想被 Smarty 当成标签解析时,用它比写{literal}更轻便:
要在模板里输出一个花括号,可以写 {$smarty.ldelim}{$smarty.rdelim}{$smarty.block.parent}与{$smarty.block.child}属于模板继承体系:用{extends}继承父模板、用{block}定义可覆盖区块时,父模板里写{$smarty.block.child}表示「把子模板覆写的内容插到这里」,子模板里写{$smarty.block.parent}表示「保留父模板原内容的基础上追加」。没用到模板继承的话,这两个变量不会出现。
四、请求镜像变量:好用但危险
这一组是把 PHP 的超全局变量搬进模板的镜像:
| 模板变量 | 对应 PHP | 说明 |
|---|
{$smarty.get.x} | $_GET['x'] | URL 查询参数 |
{$smarty.post.x} | $_POST['x'] | 表单提交 |
{$smarty.cookies.x} | $_COOKIE['x'] | Cookie |
{$smarty.server.x} | $_SERVER['x'] | 服务器与请求环境 |
{$smarty.env.x} | $_ENV['x'] | 环境变量 |
{$smarty.session.x} | $_SESSION['x'] | 会话数据 |
{$smarty.request.x} | $_REQUEST['x'] | GET/POST/Cookie 的合集,部分版本已不再提供,不要依赖 |
用法很直观:
<p>搜索关键词:{$smarty.get.kw}</p>但这正是本文要强调的地方:这些值全是原始的用户输入,没有经过任何过滤。下面两件事必须做到:
- 输出必须转义。要么在 PHP 侧打开
$smarty->escape_html = true,要么逐处写{$smarty.get.kw|escape:'html'}。 - 不要直接参与业务判断。
{if $smarty.get.admin == 1}这种写法把权限判断建立在请求参数上,是典型的设计缺陷。
更稳妥的做法是根本不用这一组:在 PHP 侧接收参数、校验、清洗,再以明确的变量名assign()进去。这样模板里出现的数据都是「已经处理过的」,评审时一眼能看出哪些是用户输入。
常见坑点
- ❌ 以为
{$smarty.get.kw}自带安全处理
✅ 它就是$_GET['kw']的镜像,原样输出等于把用户输入直接写进页面。全局打开escape_html,或逐处|escape:'html'。
- ❌ 在 PHP 代码里写
$smarty.now想拿时间戳
✅ 这类变量只存在于模板作用域。PHP 侧直接用time()。
- ❌ 用
{$smarty.now|date_format:'%Y-%m-%d'},在 PHP 8.1 上报 deprecated
✅ 改用不含%的格式串:{$smarty.now|date_format:'Y-m-d'};格式串含%时会走已被 PHP 8.1 废弃的strftime()。
- ❌ 用
{$smarty.const.FOO}时忘了先define('FOO', ...)
✅ 常量必须已定义。未定义常量在 PHP 8 里是Error,模板里则表现为取到空值,容易误判成「Smarty 坏了」。
- ❌ 用
{$smarty.foreach.x.index}却没给{foreach}起name
✅ 这种全局写法要求循环有name属性。新代码直接用{$item@index},不依赖 name。
- ❌ 直接用
{$smarty.capture.name}而对应的{capture name="name"}还没执行
✅{capture}必须先于读取执行。模板是从上往下渲染的,顺序反了就是空值。
- ❌ 依赖
{$smarty.request.x}
✅ 这个变量在不同版本中的可用性不一致,且合并来源不清晰。要么显式用$smarty.get/$smarty.post,要么在 PHP 侧决定取哪个数组。
- ❌ 把
{$smarty.server.PATH}之类的环境信息展示给用户
✅ 服务器路径、PHP 版本、软件版本都属于敏感信息,暴露出去等于给攻击者省事。这类信息只应写进服务端日志。
总结
| 内置变量 | 读什么 | 主要注意点 |
|---|
{$smarty.now} | 当前时间戳 | 格式串含%会走已废弃的strftime() |
{$smarty.const.X} | PHP 常量 | 常量需已定义 |
{$smarty.config.X} | 配置文件项 | 需先加载配置文件 |
{$smarty.capture.X} | {capture}捕获的内容 | 必须先捕获后读取 |
{$smarty.foreach.X.Y} | 循环信息(Smarty 2 风格) | 循环需要name属性 |
{$smarty.section.X.Y} | section 循环信息 | 属性名与 foreach 不同 |
{$smarty.template}/{$smarty.current_dir} | 当前模板名 / 目录 | 适合调试 |
{$smarty.version} | Smarty 版本号 | 排查环境差异时最有用 |
{$smarty.ldelim}/{$smarty.rdelim} | 定界符 | 输出花括号时用它 |
{$smarty.block.parent}/child | 模板继承内容 | 只在继承体系中使用 |
{$smarty.get/post/...} | 请求参数镜像 | 原始用户输入,必须转义 |
一句话结论:这组以$smarty为前缀的内置变量,价值在于「省一次 assign」,代价是把数据来源藏了起来。常量和时间这类只读信息尽可以放心用;循环信息建议改用{$item@index}这类新写法;而$smarty.get/$smarty.post这一组,除非只是做调试输出,否则都应该换成「PHP 侧校验后再 assign」的方式。模板的数据来源越明确,安全审查就越省力。