Template.php 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | ThinkPHP [ WE CAN DO IT JUST THINK ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
  8. // +----------------------------------------------------------------------
  9. // | Author: liu21st <liu21st@gmail.com>
  10. // +----------------------------------------------------------------------
  11. declare (strict_types = 1);
  12. namespace think;
  13. use Exception;
  14. use Psr\SimpleCache\CacheInterface;
  15. /**
  16. * ThinkPHP分离出来的模板引擎
  17. * 支持XML标签和普通标签的模板解析
  18. * 编译型模板引擎 支持动态缓存
  19. */
  20. class Template
  21. {
  22. /**
  23. * 模板变量
  24. * @var array
  25. */
  26. protected $data = [];
  27. /**
  28. * 模板配置参数
  29. * @var array
  30. */
  31. protected $config = [
  32. 'view_path' => '', // 模板路径
  33. 'view_suffix' => 'html', // 默认模板文件后缀
  34. 'view_depr' => DIRECTORY_SEPARATOR,
  35. 'cache_path' => '',
  36. 'cache_suffix' => 'php', // 默认模板缓存后缀
  37. 'tpl_deny_func_list' => 'echo,exit', // 模板引擎禁用函数
  38. 'tpl_deny_php' => false, // 默认模板引擎是否禁用PHP原生代码
  39. 'tpl_begin' => '{', // 模板引擎普通标签开始标记
  40. 'tpl_end' => '}', // 模板引擎普通标签结束标记
  41. 'strip_space' => false, // 是否去除模板文件里面的html空格与换行
  42. 'tpl_cache' => true, // 是否开启模板编译缓存,设为false则每次都会重新编译
  43. 'compile_type' => 'file', // 模板编译类型
  44. 'cache_prefix' => '', // 模板缓存前缀标识,可以动态改变
  45. 'cache_time' => 0, // 模板缓存有效期 0 为永久,(以数字为值,单位:秒)
  46. 'layout_on' => false, // 布局模板开关
  47. 'layout_name' => 'layout', // 布局模板入口文件
  48. 'layout_item' => '{__CONTENT__}', // 布局模板的内容替换标识
  49. 'taglib_begin' => '{', // 标签库标签开始标记
  50. 'taglib_end' => '}', // 标签库标签结束标记
  51. 'taglib_load' => true, // 是否使用内置标签库之外的其它标签库,默认自动检测
  52. 'taglib_build_in' => 'cx', // 内置标签库名称(标签使用不必指定标签库名称),以逗号分隔 注意解析顺序
  53. 'taglib_pre_load' => '', // 需要额外加载的标签库(须指定标签库名称),多个以逗号分隔
  54. 'display_cache' => false, // 模板渲染缓存
  55. 'cache_id' => '', // 模板缓存ID
  56. 'tpl_replace_string' => [],
  57. 'tpl_var_identify' => 'array', // .语法变量识别,array|object|'', 为空时自动识别
  58. 'default_filter' => 'htmlentities', // 默认过滤方法 用于普通标签输出
  59. ];
  60. /**
  61. * 保留内容信息
  62. * @var array
  63. */
  64. private $literal = [];
  65. /**
  66. * 扩展解析规则
  67. * @var array
  68. */
  69. private $extend = [];
  70. /**
  71. * 模板包含信息
  72. * @var array
  73. */
  74. private $includeFile = [];
  75. /**
  76. * 模板存储对象
  77. * @var object
  78. */
  79. protected $storage;
  80. /**
  81. * 查询缓存对象
  82. * @var CacheInterface
  83. */
  84. protected $cache;
  85. /**
  86. * 架构函数
  87. * @access public
  88. * @param array $config
  89. */
  90. public function __construct(array $config = [])
  91. {
  92. $this->config = array_merge($this->config, $config);
  93. $this->config['taglib_begin_origin'] = $this->config['taglib_begin'];
  94. $this->config['taglib_end_origin'] = $this->config['taglib_end'];
  95. $this->config['taglib_begin'] = preg_quote($this->config['taglib_begin'], '/');
  96. $this->config['taglib_end'] = preg_quote($this->config['taglib_end'], '/');
  97. $this->config['tpl_begin'] = preg_quote($this->config['tpl_begin'], '/');
  98. $this->config['tpl_end'] = preg_quote($this->config['tpl_end'], '/');
  99. // 初始化模板编译存储器
  100. $type = $this->config['compile_type'] ? $this->config['compile_type'] : 'File';
  101. $class = false !== strpos($type, '\\') ? $type : '\\think\\template\\driver\\' . ucwords($type);
  102. $this->storage = new $class();
  103. }
  104. /**
  105. * 模板变量赋值
  106. * @access public
  107. * @param array $vars 模板变量
  108. * @return $this
  109. */
  110. public function assign(array $vars = [])
  111. {
  112. $this->data = array_merge($this->data, $vars);
  113. return $this;
  114. }
  115. /**
  116. * 模板引擎参数赋值
  117. * @access public
  118. * @param string $name
  119. * @param mixed $value
  120. */
  121. public function __set($name, $value)
  122. {
  123. $this->config[$name] = $value;
  124. }
  125. /**
  126. * 设置缓存对象
  127. * @access public
  128. * @param CacheInterface $cache 缓存对象
  129. * @return void
  130. */
  131. public function setCache(CacheInterface $cache): void
  132. {
  133. $this->cache = $cache;
  134. }
  135. /**
  136. * 模板引擎配置
  137. * @access public
  138. * @param array $config
  139. * @return $this
  140. */
  141. public function config(array $config)
  142. {
  143. $this->config = array_merge($this->config, $config);
  144. return $this;
  145. }
  146. /**
  147. * 获取模板引擎配置项
  148. * @access public
  149. * @param string $name
  150. * @return mixed
  151. */
  152. public function getConfig(string $name)
  153. {
  154. return $this->config[$name] ?? null;
  155. }
  156. /**
  157. * 模板变量获取
  158. * @access public
  159. * @param string $name 变量名
  160. * @return mixed
  161. */
  162. public function get(string $name = '')
  163. {
  164. if ('' == $name) {
  165. return $this->data;
  166. }
  167. $data = $this->data;
  168. foreach (explode('.', $name) as $key => $val) {
  169. if (isset($data[$val])) {
  170. $data = $data[$val];
  171. } else {
  172. $data = null;
  173. break;
  174. }
  175. }
  176. return $data;
  177. }
  178. /**
  179. * 扩展模板解析规则
  180. * @access public
  181. * @param string $rule 解析规则
  182. * @param callable $callback 解析规则
  183. * @return void
  184. */
  185. public function extend(string $rule, callable $callback = null): void
  186. {
  187. $this->extend[$rule] = $callback;
  188. }
  189. /**
  190. * 渲染模板文件
  191. * @access public
  192. * @param string $template 模板文件
  193. * @param array $vars 模板变量
  194. * @return void
  195. */
  196. public function fetch(string $template, array $vars = []): void
  197. {
  198. if ($vars) {
  199. $this->data = array_merge($this->data, $vars);
  200. }
  201. if (!empty($this->config['cache_id']) && $this->config['display_cache'] && $this->cache) {
  202. // 读取渲染缓存
  203. if ($this->cache->has($this->config['cache_id'])) {
  204. echo $this->cache->get($this->config['cache_id']);
  205. return;
  206. }
  207. }
  208. $template = $this->parseTemplateFile($template);
  209. if ($template) {
  210. $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($this->config['layout_on'] . $this->config['layout_name'] . $template) . '.' . ltrim($this->config['cache_suffix'], '.');
  211. if (!$this->checkCache($cacheFile)) {
  212. // 缓存无效 重新模板编译
  213. $content = file_get_contents($template);
  214. $this->compiler($content, $cacheFile);
  215. }
  216. // 页面缓存
  217. ob_start();
  218. ob_implicit_flush(0);
  219. // 读取编译存储
  220. $this->storage->read($cacheFile, $this->data);
  221. // 获取并清空缓存
  222. $content = ob_get_clean();
  223. if (!empty($this->config['cache_id']) && $this->config['display_cache'] && $this->cache) {
  224. // 缓存页面输出
  225. $this->cache->set($this->config['cache_id'], $content, $this->config['cache_time']);
  226. }
  227. echo $content;
  228. }
  229. }
  230. /**
  231. * 检查编译缓存是否存在
  232. * @access public
  233. * @param string $cacheId 缓存的id
  234. * @return boolean
  235. */
  236. public function isCache(string $cacheId): bool
  237. {
  238. if ($cacheId && $this->cache && $this->config['display_cache']) {
  239. // 缓存页面输出
  240. return $this->cache->has($cacheId);
  241. }
  242. return false;
  243. }
  244. /**
  245. * 渲染模板内容
  246. * @access public
  247. * @param string $content 模板内容
  248. * @param array $vars 模板变量
  249. * @return void
  250. */
  251. public function display(string $content, array $vars = []): void
  252. {
  253. if ($vars) {
  254. $this->data = array_merge($this->data, $vars);
  255. }
  256. $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($content) . '.' . ltrim($this->config['cache_suffix'], '.');
  257. if (!$this->checkCache($cacheFile)) {
  258. // 缓存无效 模板编译
  259. $this->compiler($content, $cacheFile);
  260. }
  261. // 读取编译存储
  262. $this->storage->read($cacheFile, $this->data);
  263. }
  264. /**
  265. * 设置布局
  266. * @access public
  267. * @param mixed $name 布局模板名称 false 则关闭布局
  268. * @param string $replace 布局模板内容替换标识
  269. * @return $this
  270. */
  271. public function layout($name, string $replace = '')
  272. {
  273. if (false === $name) {
  274. // 关闭布局
  275. $this->config['layout_on'] = false;
  276. } else {
  277. // 开启布局
  278. $this->config['layout_on'] = true;
  279. // 名称必须为字符串
  280. if (is_string($name)) {
  281. $this->config['layout_name'] = $name;
  282. }
  283. if (!empty($replace)) {
  284. $this->config['layout_item'] = $replace;
  285. }
  286. }
  287. return $this;
  288. }
  289. /**
  290. * 检查编译缓存是否有效
  291. * 如果无效则需要重新编译
  292. * @access private
  293. * @param string $cacheFile 缓存文件名
  294. * @return bool
  295. */
  296. private function checkCache(string $cacheFile): bool
  297. {
  298. if (!$this->config['tpl_cache'] || !is_file($cacheFile) || !$handle = @fopen($cacheFile, "r")) {
  299. return false;
  300. }
  301. // 读取第一行
  302. preg_match('/\/\*(.+?)\*\//', fgets($handle), $matches);
  303. if (!isset($matches[1])) {
  304. return false;
  305. }
  306. $includeFile = unserialize($matches[1]);
  307. if (!is_array($includeFile)) {
  308. return false;
  309. }
  310. // 检查模板文件是否有更新
  311. foreach ($includeFile as $path => $time) {
  312. if (is_file($path) && filemtime($path) > $time) {
  313. // 模板文件如果有更新则缓存需要更新
  314. return false;
  315. }
  316. }
  317. // 检查编译存储是否有效
  318. return $this->storage->check($cacheFile, $this->config['cache_time']);
  319. }
  320. /**
  321. * 编译模板文件内容
  322. * @access private
  323. * @param string $content 模板内容
  324. * @param string $cacheFile 缓存文件名
  325. * @return void
  326. */
  327. private function compiler(string &$content, string $cacheFile): void
  328. {
  329. // 判断是否启用布局
  330. if ($this->config['layout_on']) {
  331. if (false !== strpos($content, '{__NOLAYOUT__}')) {
  332. // 可以单独定义不使用布局
  333. $content = str_replace('{__NOLAYOUT__}', '', $content);
  334. } else {
  335. // 读取布局模板
  336. $layoutFile = $this->parseTemplateFile($this->config['layout_name']);
  337. if ($layoutFile) {
  338. // 替换布局的主体内容
  339. $content = str_replace($this->config['layout_item'], $content, file_get_contents($layoutFile));
  340. }
  341. }
  342. } else {
  343. $content = str_replace('{__NOLAYOUT__}', '', $content);
  344. }
  345. // 模板解析
  346. $this->parse($content);
  347. if ($this->config['strip_space']) {
  348. /* 去除html空格与换行 */
  349. $find = ['~>\s+<~', '~>(\s+\n|\r)~'];
  350. $replace = ['><', '>'];
  351. $content = preg_replace($find, $replace, $content);
  352. }
  353. // 优化生成的php代码
  354. $content = preg_replace('/\?>\s*<\?php\s(?!echo\b|\bend)/s', '', $content);
  355. // 模板过滤输出
  356. $replace = $this->config['tpl_replace_string'];
  357. $content = str_replace(array_keys($replace), array_values($replace), $content);
  358. // 添加安全代码及模板引用记录
  359. $content = '<?php /*' . serialize($this->includeFile) . '*/ ?>' . "\n" . $content;
  360. // 编译存储
  361. $this->storage->write($cacheFile, $content);
  362. $this->includeFile = [];
  363. }
  364. /**
  365. * 模板解析入口
  366. * 支持普通标签和TagLib解析 支持自定义标签库
  367. * @access public
  368. * @param string $content 要解析的模板内容
  369. * @return void
  370. */
  371. public function parse(string &$content): void
  372. {
  373. // 内容为空不解析
  374. if (empty($content)) {
  375. return;
  376. }
  377. // 替换literal标签内容
  378. $this->parseLiteral($content);
  379. // 解析继承
  380. $this->parseExtend($content);
  381. // 解析布局
  382. $this->parseLayout($content);
  383. // 检查include语法
  384. $this->parseInclude($content);
  385. // 替换包含文件中literal标签内容
  386. $this->parseLiteral($content);
  387. // 检查PHP语法
  388. $this->parsePhp($content);
  389. // 获取需要引入的标签库列表
  390. // 标签库只需要定义一次,允许引入多个一次
  391. // 一般放在文件的最前面
  392. // 格式:<taglib name="html,mytag..." />
  393. // 当TAGLIB_LOAD配置为true时才会进行检测
  394. if ($this->config['taglib_load']) {
  395. $tagLibs = $this->getIncludeTagLib($content);
  396. if (!empty($tagLibs)) {
  397. // 对导入的TagLib进行解析
  398. foreach ($tagLibs as $tagLibName) {
  399. $this->parseTagLib($tagLibName, $content);
  400. }
  401. }
  402. }
  403. // 预先加载的标签库 无需在每个模板中使用taglib标签加载 但必须使用标签库XML前缀
  404. if ($this->config['taglib_pre_load']) {
  405. $tagLibs = explode(',', $this->config['taglib_pre_load']);
  406. foreach ($tagLibs as $tag) {
  407. $this->parseTagLib($tag, $content);
  408. }
  409. }
  410. // 内置标签库 无需使用taglib标签导入就可以使用 并且不需使用标签库XML前缀
  411. $tagLibs = explode(',', $this->config['taglib_build_in']);
  412. foreach ($tagLibs as $tag) {
  413. $this->parseTagLib($tag, $content, true);
  414. }
  415. // 解析普通模板标签 {$tagName}
  416. $this->parseTag($content);
  417. // 还原被替换的Literal标签
  418. $this->parseLiteral($content, true);
  419. }
  420. /**
  421. * 检查PHP语法
  422. * @access private
  423. * @param string $content 要解析的模板内容
  424. * @return void
  425. * @throws Exception
  426. */
  427. private function parsePhp(string &$content): void
  428. {
  429. // 短标签的情况要将<?标签用echo方式输出 否则无法正常输出xml标识
  430. $content = preg_replace('/(<\?(?!php|=|$))/i', '<?php echo \'\\1\'; ?>' . "\n", $content);
  431. // PHP语法检查
  432. if ($this->config['tpl_deny_php'] && false !== strpos($content, '<?php')) {
  433. throw new Exception('not allow php tag');
  434. }
  435. }
  436. /**
  437. * 解析模板中的布局标签
  438. * @access private
  439. * @param string $content 要解析的模板内容
  440. * @return void
  441. */
  442. private function parseLayout(string &$content): void
  443. {
  444. // 读取模板中的布局标签
  445. if (preg_match($this->getRegex('layout'), $content, $matches)) {
  446. // 替换Layout标签
  447. $content = str_replace($matches[0], '', $content);
  448. // 解析Layout标签
  449. $array = $this->parseAttr($matches[0]);
  450. if (!$this->config['layout_on'] || $this->config['layout_name'] != $array['name']) {
  451. // 读取布局模板
  452. $layoutFile = $this->parseTemplateFile($array['name']);
  453. if ($layoutFile) {
  454. $replace = isset($array['replace']) ? $array['replace'] : $this->config['layout_item'];
  455. // 替换布局的主体内容
  456. $content = str_replace($replace, $content, file_get_contents($layoutFile));
  457. }
  458. }
  459. } else {
  460. $content = str_replace('{__NOLAYOUT__}', '', $content);
  461. }
  462. }
  463. /**
  464. * 解析模板中的include标签
  465. * @access private
  466. * @param string $content 要解析的模板内容
  467. * @return void
  468. */
  469. private function parseInclude(string &$content): void
  470. {
  471. $regex = $this->getRegex('include');
  472. $func = function ($template) use (&$func, &$regex, &$content) {
  473. if (preg_match_all($regex, $template, $matches, PREG_SET_ORDER)) {
  474. foreach ($matches as $match) {
  475. $array = $this->parseAttr($match[0]);
  476. $file = $array['file'];
  477. unset($array['file']);
  478. // 分析模板文件名并读取内容
  479. $parseStr = $this->parseTemplateName($file);
  480. foreach ($array as $k => $v) {
  481. // 以$开头字符串转换成模板变量
  482. if (0 === strpos($v, '$')) {
  483. $v = $this->get(substr($v, 1));
  484. }
  485. $parseStr = str_replace('[' . $k . ']', $v, $parseStr);
  486. }
  487. $content = str_replace($match[0], $parseStr, $content);
  488. // 再次对包含文件进行模板分析
  489. $func($parseStr);
  490. }
  491. unset($matches);
  492. }
  493. };
  494. // 替换模板中的include标签
  495. $func($content);
  496. }
  497. /**
  498. * 解析模板中的extend标签
  499. * @access private
  500. * @param string $content 要解析的模板内容
  501. * @return void
  502. */
  503. private function parseExtend(string &$content): void
  504. {
  505. $regex = $this->getRegex('extend');
  506. $array = $blocks = $baseBlocks = [];
  507. $extend = '';
  508. $func = function ($template) use (&$func, &$regex, &$array, &$extend, &$blocks, &$baseBlocks) {
  509. if (preg_match($regex, $template, $matches)) {
  510. if (!isset($array[$matches['name']])) {
  511. $array[$matches['name']] = 1;
  512. // 读取继承模板
  513. $extend = $this->parseTemplateName($matches['name']);
  514. // 递归检查继承
  515. $func($extend);
  516. // 取得block标签内容
  517. $blocks = array_merge($blocks, $this->parseBlock($template));
  518. return;
  519. }
  520. } else {
  521. // 取得顶层模板block标签内容
  522. $baseBlocks = $this->parseBlock($template, true);
  523. if (empty($extend)) {
  524. // 无extend标签但有block标签的情况
  525. $extend = $template;
  526. }
  527. }
  528. };
  529. $func($content);
  530. if (!empty($extend)) {
  531. if ($baseBlocks) {
  532. $children = [];
  533. foreach ($baseBlocks as $name => $val) {
  534. $replace = $val['content'];
  535. if (!empty($children[$name])) {
  536. // 如果包含有子block标签
  537. foreach ($children[$name] as $key) {
  538. $replace = str_replace($baseBlocks[$key]['begin'] . $baseBlocks[$key]['content'] . $baseBlocks[$key]['end'], $blocks[$key]['content'], $replace);
  539. }
  540. }
  541. if (isset($blocks[$name])) {
  542. // 带有{__block__}表示与所继承模板的相应标签合并,而不是覆盖
  543. $replace = str_replace(['{__BLOCK__}', '{__block__}'], $replace, $blocks[$name]['content']);
  544. if (!empty($val['parent'])) {
  545. // 如果不是最顶层的block标签
  546. $parent = $val['parent'];
  547. if (isset($blocks[$parent])) {
  548. $blocks[$parent]['content'] = str_replace($blocks[$name]['begin'] . $blocks[$name]['content'] . $blocks[$name]['end'], $replace, $blocks[$parent]['content']);
  549. }
  550. $blocks[$name]['content'] = $replace;
  551. $children[$parent][] = $name;
  552. continue;
  553. }
  554. } elseif (!empty($val['parent'])) {
  555. // 如果子标签没有被继承则用原值
  556. $children[$val['parent']][] = $name;
  557. $blocks[$name] = $val;
  558. }
  559. if (!$val['parent']) {
  560. // 替换模板中的顶级block标签
  561. $extend = str_replace($val['begin'] . $val['content'] . $val['end'], $replace, $extend);
  562. }
  563. }
  564. }
  565. $content = $extend;
  566. unset($blocks, $baseBlocks);
  567. }
  568. }
  569. /**
  570. * 替换页面中的literal标签
  571. * @access private
  572. * @param string $content 模板内容
  573. * @param boolean $restore 是否为还原
  574. * @return void
  575. */
  576. private function parseLiteral(string &$content, bool $restore = false): void
  577. {
  578. $regex = $this->getRegex($restore ? 'restoreliteral' : 'literal');
  579. if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
  580. if (!$restore) {
  581. $count = count($this->literal);
  582. // 替换literal标签
  583. foreach ($matches as $match) {
  584. $this->literal[] = substr($match[0], strlen($match[1]), -strlen($match[2]));
  585. $content = str_replace($match[0], "<!--###literal{$count}###-->", $content);
  586. $count++;
  587. }
  588. } else {
  589. // 还原literal标签
  590. foreach ($matches as $match) {
  591. $content = str_replace($match[0], $this->literal[$match[1]], $content);
  592. }
  593. // 清空literal记录
  594. $this->literal = [];
  595. }
  596. unset($matches);
  597. }
  598. }
  599. /**
  600. * 获取模板中的block标签
  601. * @access private
  602. * @param string $content 模板内容
  603. * @param boolean $sort 是否排序
  604. * @return array
  605. */
  606. private function parseBlock(string &$content, bool $sort = false): array
  607. {
  608. $regex = $this->getRegex('block');
  609. $result = [];
  610. if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
  611. $right = $keys = [];
  612. foreach ($matches as $match) {
  613. if (empty($match['name'][0])) {
  614. if (count($right) > 0) {
  615. $tag = array_pop($right);
  616. $start = $tag['offset'] + strlen($tag['tag']);
  617. $length = $match[0][1] - $start;
  618. $result[$tag['name']] = [
  619. 'begin' => $tag['tag'],
  620. 'content' => substr($content, $start, $length),
  621. 'end' => $match[0][0],
  622. 'parent' => count($right) ? end($right)['name'] : '',
  623. ];
  624. $keys[$tag['name']] = $match[0][1];
  625. }
  626. } else {
  627. // 标签头压入栈
  628. $right[] = [
  629. 'name' => $match[2][0],
  630. 'offset' => $match[0][1],
  631. 'tag' => $match[0][0],
  632. ];
  633. }
  634. }
  635. unset($right, $matches);
  636. if ($sort) {
  637. // 按block标签结束符在模板中的位置排序
  638. array_multisort($keys, $result);
  639. }
  640. }
  641. return $result;
  642. }
  643. /**
  644. * 搜索模板页面中包含的TagLib库
  645. * 并返回列表
  646. * @access private
  647. * @param string $content 模板内容
  648. * @return array|null
  649. */
  650. private function getIncludeTagLib(string &$content)
  651. {
  652. // 搜索是否有TagLib标签
  653. if (preg_match($this->getRegex('taglib'), $content, $matches)) {
  654. // 替换TagLib标签
  655. $content = str_replace($matches[0], '', $content);
  656. return explode(',', $matches['name']);
  657. }
  658. }
  659. /**
  660. * TagLib库解析
  661. * @access public
  662. * @param string $tagLib 要解析的标签库
  663. * @param string $content 要解析的模板内容
  664. * @param boolean $hide 是否隐藏标签库前缀
  665. * @return void
  666. */
  667. public function parseTagLib(string $tagLib, string &$content, bool $hide = false): void
  668. {
  669. if (false !== strpos($tagLib, '\\')) {
  670. // 支持指定标签库的命名空间
  671. $className = $tagLib;
  672. $tagLib = substr($tagLib, strrpos($tagLib, '\\') + 1);
  673. } else {
  674. $className = '\\think\\template\\taglib\\' . ucwords($tagLib);
  675. }
  676. $tLib = new $className($this);
  677. $tLib->parseTag($content, $hide ? '' : $tagLib);
  678. }
  679. /**
  680. * 分析标签属性
  681. * @access public
  682. * @param string $str 属性字符串
  683. * @param string $name 不为空时返回指定的属性名
  684. * @return array
  685. */
  686. public function parseAttr(string $str, string $name = null): array
  687. {
  688. $regex = '/\s+(?>(?P<name>[\w-]+)\s*)=(?>\s*)([\"\'])(?P<value>(?:(?!\\2).)*)\\2/is';
  689. $array = [];
  690. if (preg_match_all($regex, $str, $matches, PREG_SET_ORDER)) {
  691. foreach ($matches as $match) {
  692. $array[$match['name']] = $match['value'];
  693. }
  694. unset($matches);
  695. }
  696. if (!empty($name) && isset($array[$name])) {
  697. return $array[$name];
  698. }
  699. return $array;
  700. }
  701. /**
  702. * 模板标签解析
  703. * 格式: {TagName:args [|content] }
  704. * @access private
  705. * @param string $content 要解析的模板内容
  706. * @return void
  707. */
  708. private function parseTag(string &$content): void
  709. {
  710. $regex = $this->getRegex('tag');
  711. if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
  712. foreach ($matches as $match) {
  713. $str = stripslashes($match[1]);
  714. $flag = substr($str, 0, 1);
  715. switch ($flag) {
  716. case '$':
  717. // 解析模板变量 格式 {$varName}
  718. // 是否带有?号
  719. if (false !== $pos = strpos($str, '?')) {
  720. $array = preg_split('/([!=]={1,2}|(?<!-)[><]={0,1})/', substr($str, 0, $pos), 2, PREG_SPLIT_DELIM_CAPTURE);
  721. $name = $array[0];
  722. $this->parseVar($name);
  723. //$this->parseVarFunction($name);
  724. $str = trim(substr($str, $pos + 1));
  725. $this->parseVar($str);
  726. $first = substr($str, 0, 1);
  727. if (strpos($name, ')')) {
  728. // $name为对象或是自动识别,或者含有函数
  729. if (isset($array[1])) {
  730. $this->parseVar($array[2]);
  731. $name .= $array[1] . $array[2];
  732. }
  733. switch ($first) {
  734. case '?':
  735. $this->parseVarFunction($name);
  736. $str = '<?php echo (' . $name . ') ? ' . $name . ' : ' . substr($str, 1) . '; ?>';
  737. break;
  738. case '=':
  739. $str = '<?php if(' . $name . ') echo ' . substr($str, 1) . '; ?>';
  740. break;
  741. default:
  742. $str = '<?php echo ' . $name . '?' . $str . '; ?>';
  743. }
  744. } else {
  745. if (isset($array[1])) {
  746. $express = true;
  747. $this->parseVar($array[2]);
  748. $express = $name . $array[1] . $array[2];
  749. } else {
  750. $express = false;
  751. }
  752. if (in_array($first, ['?', '=', ':'])) {
  753. $str = trim(substr($str, 1));
  754. if ('$' == substr($str, 0, 1)) {
  755. $str = $this->parseVarFunction($str);
  756. }
  757. }
  758. // $name为数组
  759. switch ($first) {
  760. case '?':
  761. // {$varname??'xxx'} $varname有定义则输出$varname,否则输出xxx
  762. $str = '<?php echo ' . ($express ?: 'isset(' . $name . ')') . ' ? ' . $this->parseVarFunction($name) . ' : ' . $str . '; ?>';
  763. break;
  764. case '=':
  765. // {$varname?='xxx'} $varname为真时才输出xxx
  766. $str = '<?php if(' . ($express ?: '!empty(' . $name . ')') . ') echo ' . $str . '; ?>';
  767. break;
  768. case ':':
  769. // {$varname?:'xxx'} $varname为真时输出$varname,否则输出xxx
  770. $str = '<?php echo ' . ($express ?: '!empty(' . $name . ')') . ' ? ' . $this->parseVarFunction($name) . ' : ' . $str . '; ?>';
  771. break;
  772. default:
  773. if (strpos($str, ':')) {
  774. // {$varname ? 'a' : 'b'} $varname为真时输出a,否则输出b
  775. $array = explode(':', $str, 2);
  776. $array[0] = '$' == substr(trim($array[0]), 0, 1) ? $this->parseVarFunction($array[0]) : $array[0];
  777. $array[1] = '$' == substr(trim($array[1]), 0, 1) ? $this->parseVarFunction($array[1]) : $array[1];
  778. $str = implode(' : ', $array);
  779. }
  780. $str = '<?php echo ' . ($express ?: '!empty(' . $name . ')') . ' ? ' . $str . '; ?>';
  781. }
  782. }
  783. } else {
  784. $this->parseVar($str);
  785. $this->parseVarFunction($str);
  786. $str = '<?php echo ' . $str . '; ?>';
  787. }
  788. break;
  789. case ':':
  790. // 输出某个函数的结果
  791. $str = substr($str, 1);
  792. $this->parseVar($str);
  793. $str = '<?php echo ' . $str . '; ?>';
  794. break;
  795. case '~':
  796. // 执行某个函数
  797. $str = substr($str, 1);
  798. $this->parseVar($str);
  799. $str = '<?php ' . $str . '; ?>';
  800. break;
  801. case '-':
  802. case '+':
  803. // 输出计算
  804. $this->parseVar($str);
  805. $str = '<?php echo ' . $str . '; ?>';
  806. break;
  807. case '/':
  808. // 注释标签
  809. $flag2 = substr($str, 1, 1);
  810. if ('/' == $flag2 || ('*' == $flag2 && substr(rtrim($str), -2) == '*/')) {
  811. $str = '';
  812. }
  813. break;
  814. default:
  815. // 未识别的标签直接返回
  816. $str = $this->config['tpl_begin'] . $str . $this->config['tpl_end'];
  817. break;
  818. }
  819. $content = str_replace($match[0], $str, $content);
  820. }
  821. unset($matches);
  822. }
  823. }
  824. /**
  825. * 模板变量解析,支持使用函数
  826. * 格式: {$varname|function1|function2=arg1,arg2}
  827. * @access public
  828. * @param string $varStr 变量数据
  829. * @return void
  830. */
  831. public function parseVar(string &$varStr): void
  832. {
  833. $varStr = trim($varStr);
  834. if (preg_match_all('/\$[a-zA-Z_](?>\w*)(?:[:\.][0-9a-zA-Z_](?>\w*))+/', $varStr, $matches, PREG_OFFSET_CAPTURE)) {
  835. static $_varParseList = [];
  836. while ($matches[0]) {
  837. $match = array_pop($matches[0]);
  838. //如果已经解析过该变量字串,则直接返回变量值
  839. if (isset($_varParseList[$match[0]])) {
  840. $parseStr = $_varParseList[$match[0]];
  841. } else {
  842. if (strpos($match[0], '.')) {
  843. $vars = explode('.', $match[0]);
  844. $first = array_shift($vars);
  845. if (isset($this->extend[$first])) {
  846. $callback = $this->extend[$first];
  847. $parseStr = $callback($vars);
  848. } elseif ('$Request' == $first) {
  849. // 输出请求变量
  850. $parseStr = $this->parseRequestVar($vars);
  851. } elseif ('$Think' == $first) {
  852. // 所有以Think.打头的以特殊变量对待 无需模板赋值就可以输出
  853. $parseStr = $this->parseThinkVar($vars);
  854. } else {
  855. switch ($this->config['tpl_var_identify']) {
  856. case 'array': // 识别为数组
  857. $parseStr = $first . '[\'' . implode('\'][\'', $vars) . '\']';
  858. break;
  859. case 'obj': // 识别为对象
  860. $parseStr = $first . '->' . implode('->', $vars);
  861. break;
  862. default: // 自动判断数组或对象
  863. $parseStr = '(is_array(' . $first . ')?' . $first . '[\'' . implode('\'][\'', $vars) . '\']:' . $first . '->' . implode('->', $vars) . ')';
  864. }
  865. }
  866. } else {
  867. $parseStr = str_replace(':', '->', $match[0]);
  868. }
  869. $_varParseList[$match[0]] = $parseStr;
  870. }
  871. $varStr = substr_replace($varStr, $parseStr, $match[1], strlen($match[0]));
  872. }
  873. unset($matches);
  874. }
  875. }
  876. /**
  877. * 对模板中使用了函数的变量进行解析
  878. * 格式 {$varname|function1|function2=arg1,arg2}
  879. * @access public
  880. * @param string $varStr 变量字符串
  881. * @param bool $autoescape 自动转义
  882. * @return string
  883. */
  884. public function parseVarFunction(string &$varStr, bool $autoescape = true): string
  885. {
  886. if (!$autoescape && false === strpos($varStr, '|')) {
  887. return $varStr;
  888. } elseif ($autoescape && !preg_match('/\|(\s)?raw(\||\s)?/i', $varStr)) {
  889. $varStr .= '|' . $this->config['default_filter'];
  890. }
  891. static $_varFunctionList = [];
  892. $_key = md5($varStr);
  893. //如果已经解析过该变量字串,则直接返回变量值
  894. if (isset($_varFunctionList[$_key])) {
  895. $varStr = $_varFunctionList[$_key];
  896. } else {
  897. $varArray = explode('|', $varStr);
  898. // 取得变量名称
  899. $name = trim(array_shift($varArray));
  900. // 对变量使用函数
  901. $length = count($varArray);
  902. // 取得模板禁止使用函数列表
  903. $template_deny_funs = explode(',', $this->config['tpl_deny_func_list']);
  904. for ($i = 0; $i < $length; $i++) {
  905. $args = explode('=', $varArray[$i], 2);
  906. // 模板函数过滤
  907. $fun = trim($args[0]);
  908. if (in_array($fun, $template_deny_funs)) {
  909. continue;
  910. }
  911. switch (strtolower($fun)) {
  912. case 'raw':
  913. break;
  914. case 'date':
  915. $name = 'date(' . $args[1] . ',!is_numeric(' . $name . ')? strtotime(' . $name . ') : ' . $name . ')';
  916. break;
  917. case 'first':
  918. $name = 'current(' . $name . ')';
  919. break;
  920. case 'last':
  921. $name = 'end(' . $name . ')';
  922. break;
  923. case 'upper':
  924. $name = 'strtoupper(' . $name . ')';
  925. break;
  926. case 'lower':
  927. $name = 'strtolower(' . $name . ')';
  928. break;
  929. case 'format':
  930. $name = 'sprintf(' . $args[1] . ',' . $name . ')';
  931. break;
  932. case 'default': // 特殊模板函数
  933. if (false === strpos($name, '(')) {
  934. $name = '(isset(' . $name . ') && (' . $name . ' !== \'\')?' . $name . ':' . $args[1] . ')';
  935. } else {
  936. $name = '(' . $name . ' ?: ' . $args[1] . ')';
  937. }
  938. break;
  939. default: // 通用模板函数
  940. if (isset($args[1])) {
  941. if (strstr($args[1], '###')) {
  942. $args[1] = str_replace('###', $name, $args[1]);
  943. $name = "$fun($args[1])";
  944. } else {
  945. $name = "$fun($name,$args[1])";
  946. }
  947. } else {
  948. if (!empty($args[0])) {
  949. $name = "$fun($name)";
  950. }
  951. }
  952. }
  953. }
  954. $_varFunctionList[$_key] = $name;
  955. $varStr = $name;
  956. }
  957. return $varStr;
  958. }
  959. /**
  960. * 请求变量解析
  961. * 格式 以 $Request. 打头的变量属于请求变量
  962. * @access public
  963. * @param array $vars 变量数组
  964. * @return string
  965. */
  966. public function parseRequestVar(array $vars): string
  967. {
  968. $type = strtoupper(trim(array_shift($vars)));
  969. $param = implode('.', $vars);
  970. switch ($type) {
  971. case 'SERVER':
  972. $parseStr = '$_SERVER[\'' . $param . '\']';
  973. break;
  974. case 'GET':
  975. $parseStr = '$_GET[\'' . $param . '\']';
  976. break;
  977. case 'POST':
  978. $parseStr = '$_POST[\'' . $param . '\']';
  979. break;
  980. case 'COOKIE':
  981. $parseStr = '$_COOKIE[\'' . $param . '\']';
  982. break;
  983. case 'SESSION':
  984. $parseStr = '$_SESSION[\'' . $param . '\']';
  985. break;
  986. case 'ENV':
  987. $parseStr = '$_ENV[\'' . $param . '\']';
  988. break;
  989. case 'REQUEST':
  990. $parseStr = '$_REQUEST[\'' . $param . '\']';
  991. break;
  992. default:
  993. $parseStr = '\'\'';
  994. }
  995. return $parseStr;
  996. }
  997. /**
  998. * 特殊模板变量解析
  999. * 格式 以 $Think. 打头的变量属于特殊模板变量
  1000. * @access public
  1001. * @param array $vars 变量数组
  1002. * @return string
  1003. */
  1004. public function parseThinkVar(array $vars): string
  1005. {
  1006. $type = strtoupper(trim(array_shift($vars)));
  1007. $param = implode('.', $vars);
  1008. switch ($type) {
  1009. case 'CONST':
  1010. $parseStr = strtoupper($param);
  1011. break;
  1012. case 'NOW':
  1013. $parseStr = "date('Y-m-d g:i a',time())";
  1014. break;
  1015. case 'LDELIM':
  1016. $parseStr = '\'' . ltrim($this->config['tpl_begin'], '\\') . '\'';
  1017. break;
  1018. case 'RDELIM':
  1019. $parseStr = '\'' . ltrim($this->config['tpl_end'], '\\') . '\'';
  1020. break;
  1021. default:
  1022. $parseStr = defined($type) ? $type : '\'\'';
  1023. }
  1024. return $parseStr;
  1025. }
  1026. /**
  1027. * 分析加载的模板文件并读取内容 支持多个模板文件读取
  1028. * @access private
  1029. * @param string $templateName 模板文件名
  1030. * @return string
  1031. */
  1032. private function parseTemplateName(string $templateName): string
  1033. {
  1034. $array = explode(',', $templateName);
  1035. $parseStr = '';
  1036. foreach ($array as $templateName) {
  1037. if (empty($templateName)) {
  1038. continue;
  1039. }
  1040. if (0 === strpos($templateName, '$')) {
  1041. //支持加载变量文件名
  1042. $templateName = $this->get(substr($templateName, 1));
  1043. }
  1044. $template = $this->parseTemplateFile($templateName);
  1045. if ($template) {
  1046. // 获取模板文件内容
  1047. $parseStr .= file_get_contents($template);
  1048. }
  1049. }
  1050. return $parseStr;
  1051. }
  1052. /**
  1053. * 解析模板文件名
  1054. * @access private
  1055. * @param string $template 文件名
  1056. * @return string
  1057. */
  1058. private function parseTemplateFile(string $template): string
  1059. {
  1060. if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
  1061. if (0 !== strpos($template, '/')) {
  1062. $template = str_replace(['/', ':'], $this->config['view_depr'], $template);
  1063. } else {
  1064. $template = str_replace(['/', ':'], $this->config['view_depr'], substr($template, 1));
  1065. }
  1066. $template = $this->config['view_path'] . $template . '.' . ltrim($this->config['view_suffix'], '.');
  1067. }
  1068. if (is_file($template)) {
  1069. // 记录模板文件的更新时间
  1070. $this->includeFile[$template] = filemtime($template);
  1071. return $template;
  1072. }
  1073. throw new Exception('template not exists:' . $template);
  1074. }
  1075. /**
  1076. * 按标签生成正则
  1077. * @access private
  1078. * @param string $tagName 标签名
  1079. * @return string
  1080. */
  1081. private function getRegex(string $tagName): string
  1082. {
  1083. $regex = '';
  1084. if ('tag' == $tagName) {
  1085. $begin = $this->config['tpl_begin'];
  1086. $end = $this->config['tpl_end'];
  1087. if (strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1) {
  1088. $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>[^' . $end . ']*))' . $end;
  1089. } else {
  1090. $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>(?:(?!' . $end . ').)*))' . $end;
  1091. }
  1092. } else {
  1093. $begin = $this->config['taglib_begin'];
  1094. $end = $this->config['taglib_end'];
  1095. $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false;
  1096. switch ($tagName) {
  1097. case 'block':
  1098. if ($single) {
  1099. $regex = $begin . '(?:' . $tagName . '\b\s+(?>(?:(?!name=).)*)\bname=([\'\"])(?P<name>[\$\w\-\/\.]+)\\1(?>[^' . $end . ']*)|\/' . $tagName . ')' . $end;
  1100. } else {
  1101. $regex = $begin . '(?:' . $tagName . '\b\s+(?>(?:(?!name=).)*)\bname=([\'\"])(?P<name>[\$\w\-\/\.]+)\\1(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end;
  1102. }
  1103. break;
  1104. case 'literal':
  1105. if ($single) {
  1106. $regex = '(' . $begin . $tagName . '\b(?>[^' . $end . ']*)' . $end . ')';
  1107. $regex .= '(?:(?>[^' . $begin . ']*)(?>(?!' . $begin . '(?>' . $tagName . '\b[^' . $end . ']*|\/' . $tagName . ')' . $end . ')' . $begin . '[^' . $begin . ']*)*)';
  1108. $regex .= '(' . $begin . '\/' . $tagName . $end . ')';
  1109. } else {
  1110. $regex = '(' . $begin . $tagName . '\b(?>(?:(?!' . $end . ').)*)' . $end . ')';
  1111. $regex .= '(?:(?>(?:(?!' . $begin . ').)*)(?>(?!' . $begin . '(?>' . $tagName . '\b(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end . ')' . $begin . '(?>(?:(?!' . $begin . ').)*))*)';
  1112. $regex .= '(' . $begin . '\/' . $tagName . $end . ')';
  1113. }
  1114. break;
  1115. case 'restoreliteral':
  1116. $regex = '<!--###literal(\d+)###-->';
  1117. break;
  1118. case 'include':
  1119. $name = 'file';
  1120. case 'taglib':
  1121. case 'layout':
  1122. case 'extend':
  1123. if (empty($name)) {
  1124. $name = 'name';
  1125. }
  1126. if ($single) {
  1127. $regex = $begin . $tagName . '\b\s+(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P<name>[\$\w\-\/\.\:@,\\\\]+)\\1(?>[^' . $end . ']*)' . $end;
  1128. } else {
  1129. $regex = $begin . $tagName . '\b\s+(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P<name>[\$\w\-\/\.\:@,\\\\]+)\\1(?>(?:(?!' . $end . ').)*)' . $end;
  1130. }
  1131. break;
  1132. }
  1133. }
  1134. return '/' . $regex . '/is';
  1135. }
  1136. public function __debugInfo()
  1137. {
  1138. $data = get_object_vars($this);
  1139. unset($data['storage']);
  1140. return $data;
  1141. }
  1142. }