作者:mckee
这篇文章主要介绍了PHP模板解析类,涉及php针对模板文件的解析与字符串处理的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
1 <?php
2 class template {
3 private $vars = array();
4 private $conf = '';
5 private $tpl_name = 'index';
6 //如果模板不存在 会查找当前 controller默认index模板
7 private $tpl_suffix = '.html';//如果CONFIG没配置默认后缀 则显示
8 private $tpl_compile_suffix= '.tpl.php';//编译模板路径
9 private $template_tag_left = '<{';//模板左标签
10 private $template_tag_right = '}>';//模板右标签
11 private $template_c = '';//编译目录
12 private $template_path = '';//模板完整路径
13 private $template_name = '';//模板名称 index.html
14 //定义每个模板的标签的元素
15 private $tag_foreach = array('from', 'item', 'key');
16 private $tag_include = array('file');//目前只支持读取模板默认路径
17 public function __construct($conf) {
18 $this->conf = &$conf;
19 $this->template_c = $this->conf['template_config']['template_c'];//编译目录
20 $this->_tpl_suffix = $this->tpl_suffix();
21 }
22 private function str_replace($search, $replace, $content) {
23 if(empty($search) || empty($replace) || empty($content)) return false;
24 return str_replace($search, $replace, $content);
25 }
26 /**
27 * preg_match_all
28 * @param $pattern 正则
29 * @param $content 内容
30 * @return array
31 */
32 private function preg_match_all($pattern, $content) {
33 if(empty($pattern) || empty($content)) core::show_error('查找模板标签失败!');
34 preg_match_all("/".$this->template_tag_left.$pattern.$this->template_tag_right."/is", $content, $match);
35 return $match;
36 }
37 /**
38 * 模板文件后缀
39 */
40 public function tpl_suffix() {
41 $tpl_suffix = empty($this->conf['template_config']['template_suffix']) ?
42 $this->tpl_suffix :
43 $this->conf['template_config']['template_suffix'] ;
44 return $tpl_suffix;
45 }
46 /**
47 * 此处不解释了
48 * @return
49 */
50 public function assign($key, $value) {
51 $this->vars[$key] = $value;
52 }
53 /**
54 * 渲染页面
55 * @param
56 * 使用方法 1
57 * $this->view->display('error', 'comm/');
58 * 默认是指向TPL模版的跟目录,所以comm/就是 tpl/comm/error.html
59 * 使用方法 2
60 * $this->view->display('errorfile');
61 * 默认指向控制器固定的文件夹
62 * 例如你的域名是 http://heartphp/admin/index, 那么正确路径就是tpl/admin/index/errorfile.html
63 * @return
64 */
65 public function display($filename = '', $view_path = '') {
66 $tpl_path_arr = $this->get_tpl($filename, $view_path);//获取TPL完整路径 并且向指针传送路径以及名称
67 if(!$tpl_path_arr) core::show_error($filename.$this->_tpl_suffix.'模板不存在');
68 //编译开始
69 $this->view_path_param = $view_path;//用户传递过来的模版跟目录
70 $this->compile();
71 }
72 /**
73 * 编译控制器
74 * @param
75 * @return
76 */
77 private function compile() {
78 $filepath = $this->template_path.$this->template_name;
79 $compile_dirpath = $this->check_temp_compile();
80 $vars_template_c_name = str_replace($this->_tpl_suffix, '', $this->template_name);
81 $include_file = $this->template_replace($this->read_file($filepath), $compile_dirpath, $vars_template_c_name);//解析
82 if($include_file) {
83 $this->read_config() && $config = $this->read_config();
84 extract($this->vars, EXTR_SKIP);
85 [url=home.php?mod=space&uid=48608]@include[/url] $include_file;
86 }
87 }
88 /**
89 * 读取当前项目配置文件
90 */
91 protected function read_config() {
92 if(file_exists(SYSTEM_PATH.'conf/config.php')) {
93 @include SYSTEM_PATH.'conf/config.php';
94 return $config;
95 }
96 return false;
97 }
98 /**
99 * 解析模板语法
100 * @param $str 内容
101 * @param $compile_dirpath 模版编译目录
102 * @param $vars_template_c_name 模版编译文件名
103 * @return 编译过的PHP模板文件名
104 */
105 private function template_replace($str, $compile_dirpath, $vars_template_c_name) {
106 if(empty($str)) core::show_error('模板内容为空!');
107 //处理编译头部
108 $compile_path = $compile_dirpath.$vars_template_c_name.$this->tpl_compile_suffix;//编译文件
109 if(is_file($compile_path)) {
110 //$header_content = $this->get_compile_header($compile_path);
111 //$compile_date = $this->get_compile_header_comment($header_content);
112 $tpl_filemtime = filemtime($this->template_path.$this->template_name);
113 $compile_filemtime = filemtime($compile_path);
114 //echo $tpl_filemtime.'=='.date('Y-m-d H:i:s', $tpl_filemtime).'<br/>';
115 //echo $compile_filemtime.'=='.date('Y-m-d H:i:s', $compile_filemtime);
116 //如果文件过期编译 当模板标签有include并且有修改时 也重新编译
117 //<{include file="public/left.html"}> 当修改include里的文件,非DEBUG模式时 如果不更改主文件
目前是不重新编译include里的文件,我在考虑是否也要更改,没想好,暂时这样,所以在开发阶段一定要开启DEBUG=1模式
要不然修改include文件无效 。 有点罗嗦,不知道表述清楚没
118 if($tpl_filemtime > $compile_filemtime || DEBUG) {
119 $ret_file = $this->compile_file($vars_template_c_name, $str, $compile_dirpath);
120 } else {
121 $ret_file = $compile_path;
122 }
123 } else {//编译文件不存在 创建他
124 $ret_file = $this->compile_file($vars_template_c_name, $str, $compile_dirpath);
125 }
126 return $ret_file;
127 }
128 /**
129 * 模板文件主体
130 * @param string $str 内容
131 * @return html
132 */
133 private function body_content($str) {
134 //解析
135 $str = $this->parse($str);
136 $header_comment = "Create On##".time()."|Compiled from##".$this->template_path.$this->template_name;
137 $content = "<? if(!defined('IS_HEARTPHP')) exit('Access Denied');/*{$header_comment}*/?>
$str";
138 return $content;
139 }
140 /**
141 * 开始解析相关模板标签
142 * @param $content 模板内容
143 */
144 private function parse($content) {
145 //foreach
146 $content = $this->parse_foreach($content);
147 //include
148 $content = $this->parse_include($content);
149 //if
150 $content = $this->parse_if($content);
151 //elseif
152 $content = $this->parse_elseif($content);
153 //模板标签公用部分
154 $content = $this->parse_comm($content);
155 //转为PHP代码
156 $content = $this->parse_php($content);
157 return $content;
158 }
159 /**
160 * echo 如果默认直接<{$config['domain']}> 转成 <?php echo $config['domain']?>
161 */
162 private function parse_echo($content) {
163 }
164 /**
165 * 转换为PHP
166 * @param $content html 模板内容
167 * @return html 替换好的HTML
168 */
169 private function parse_php($content){
170 if(empty($content)) return false;
171 $content = preg_replace("/".$this->template_tag_left."(.+?)".$this->template_tag_right."/is", "<?php $1 ?>", $content);
172 return $content;
173 }
174 /**
175 * if判断语句
176 * <{if empty($zhang)}>
177 * zhang
178 * <{elseif empty($liang)}>
179 * liang
180 * <{else}>
181 * zhangliang
182 * <{/if}>
183 */
184 private function parse_if($content) {
185 if(empty($content)) return false;
186 //preg_match_all("/".$this->template_tag_left."ifs+(.*?)".$this->template_tag_right."/is", $content, $match);
187 $match = $this->preg_match_all("ifs+(.*?)", $content);
188 if(!isset($match[1]) || !is_array($match[1])) return $content;
189 foreach($match[1] as $k => $v) {
190 //$s = preg_split("/s+/is", $v);
191 //$s = array_filter($s);
192 $content = str_replace($match[0][$k], "<?php if({$v}) { ?>", $content);
193 }
194 return $content;
195 }
196 private function parse_elseif($content) {
197 if(empty($content)) return false;
198 //preg_match_all("/".$this->template_tag_left."elseifs+(.*?)".$this->template_tag_right."/is", $content, $match);
199 $match = $this->preg_match_all("elseifs+(.*?)", $content);
200 if(!isset($match[1]) || !is_array($match[1])) return $content;
201 foreach($match[1] as $k => $v) {
202 //$s = preg_split("/s+/is", $v);
203 //$s = array_filter($s);
204 $content = str_replace($match[0][$k], "<?php } elseif ({$v}) { ?>", $content);
205 }
206 return $content;
207 }
208 /**
209 * 解析 include include标签不是实时更新的 当主体文件更新的时候 才更新标签内容,所以想include生效 请修改一下主体文件
210 * 记录一下 有时间开发一个当DEBUG模式的时候 每次执行删除模版编译文件
211 * 使用方法 <{include file="www.phpddt.com"}>
212 * @param $content 模板内容
213 * @return html
214 */
215 private function parse_include($content) {
216 if(empty($content)) return false;
217 //preg_match_all("/".$this->template_tag_left."includes+(.*?)".$this->template_tag_right."/is", $content, $match);
218 $match = $this->preg_match_all("includes+(.*?)", $content);
219 if(!isset($match[1]) || !is_array($match[1])) return $content;
220 foreach($match[1] as $match_key => $match_value) {
221 $a = preg_split("/s+/is", $match_value);
222 $new_tag = array();
223 //分析元素
224 foreach($a as $t) {
225 $b = explode('=', $t);
226 if(in_array($b[0], $this->tag_include)) {
227 if(!empty($b[1])) {
228 $new_tag[$b[0]] = str_replace(""", "", $b[1]);
229 } else {
230 core::show_error('模板路径不存在!');
231 }
232 }
233 }
234 extract($new_tag);
235 //查询模板文件
236 foreach($this->conf['view_path'] as $v){
237 $conf_view_tpl = $v.$file;//include 模板文件
238 if(is_file($conf_view_tpl)) {
239 $c = $this->read_file($conf_view_tpl);
240 $inc_file = str_replace($this->_tpl_suffix, '', basename($file));
241 $this->view_path_param = dirname($file).'/';
242 $compile_dirpath = $this->check_temp_compile();
243 $include_file = $this->template_replace($c, $compile_dirpath, $inc_file);//解析
244 break;
245 } else {
246 core::show_error('模板文件不存在,请仔细检查 文件:'. $conf_view_tpl);
247 }
248 }
249 $content = str_replace($match[0][$match_key], '<?php include("'.$include_file.'")?>', $content);
250 }
251 return $content;
252 }
253 /**
254 * 解析 foreach
255 * 使用方法 <{foreach from=$lists item=value key=kk}>
256 * @param $content 模板内容
257 * @return html 解析后的内容
258 */
259 private function parse_foreach($content) {
260 if(empty($content)) return false;
261 //preg_match_all("/".$this->template_tag_left."foreachs+(.*?)".$this->template_tag_right."/is", $content, $match);
262 $match = $this->preg_match_all("foreachs+(.*?)", $content);
263 if(!isset($match[1]) || !is_array($match[1])) return $content;
264 foreach($match[1] as $match_key => $value) {
265 $split = preg_split("/s+/is", $value);
266 $split = array_filter($split);
267 $new_tag = array();
268 foreach($split as $v) {
269 $a = explode("=", $v);
270 if(in_array($a[0], $this->tag_foreach)) {//此处过滤标签 不存在过滤
271 $new_tag[$a[0]] = $a[1];
272 }
273 }
274 $key = '';
275 extract($new_tag);
276 $key = ($key) ? '$'.$key.' =>' : '' ;
277 $s = '<?php foreach('.$from.' as '.$key.' $'.$item.') { ?>';
278 $content = $this->str_replace($match[0][$match_key], $s, $content);
279 }
280 return $content;
281 }
282 /**
283 * 匹配结束 字符串
284 */
285 private function parse_comm($content) {
286 $search = array(
287 "/".$this->template_tag_left."/foreach".$this->template_tag_right."/is",
288 "/".$this->template_tag_left."/if".$this->template_tag_right."/is",
289 "/".$this->template_tag_left."else".$this->template_tag_right."/is",
290 );
291 $replace = array(
292 "<?php } ?>",
293 "<?php } ?>",
294 "<?php } else { ?>"
295 );
296 $content = preg_replace($search, $replace, $content);
297 return $content;
298 }
299 /**
300 * 检查编译目录 如果没有创建 则递归创建目录
301 * @param string $path 文件完整路径
302 * @return 模板内容
303 */
304 private function check_temp_compile() {
305 //$paht = $this->template_c.
306 $tpl_path = ($this->view_path_param) ? $this->view_path_param : $this->get_tpl_path() ;
307 $all_tpl_apth = $this->template_c.$tpl_path;
308 if(!is_dir($all_tpl_apth)) {
309 $this->create_dir($tpl_path);
310 }
311 return $all_tpl_apth;
312 }
313 /**
314 * 读文件
315 * @param string $path 文件完整路径
316 * @return 模板内容
317 */
318 private function read_file($path) {
319 //$this->check_file_limits($path, 'r');
320 if(($r = @fopen($path, 'r')) === false) {
321 core::show_error('模版文件没有读取或执行权限,请检查!');
322 }
323 $content = fread($r, filesize($path));
324 fclose($r);
325 return $content;
326 }
327 /**
328 * 写文件
329 * @param string $filename 文件名
330 * @param string $content 模板内容
331 * @return 文件名
332 */
333 private function compile_file($filename, $content, $dir) {
334 if(empty($filename)) core::show_error("{$filename} Creation failed");
335 $content = $this->body_content($content);//对文件内容操作
336 //echo '开始编译了=====';
337 $f = $dir.$filename.$this->tpl_compile_suffix;
338 //$this->check_file_limits($f, 'w');
339 if(($fp = @fopen($f, 'wb')) === false) {
340 core::show_error($f.'<br/>编译文件失败,请检查文件权限.');
341 }
342 //开启flock
343 flock($fp, LOCK_EX + LOCK_NB);
344 fwrite($fp, $content, strlen($content));
345 flock($fp, LOCK_UN + LOCK_NB);
346 fclose($fp);
347 return $f;
348 }
349 /**
350 * 这个检查文件权限函数 暂时废弃了
351 * @param [$path] [路径]
352 * @param [status] [w=write, r=read]
353 */
354 public function check_file_limits($path , $status = 'rw') {
355 clearstatcache();
356 if(!is_writable($path) && $status == 'w') {
357 core::show_error("{$path}<br/>没有写入权限,请检查.");
358 } elseif(!is_readable($path) && $status == 'r') {
359 core::show_error("{$path}<br/>没有读取权限,请检查.");
360 } elseif($status == 'rw') {//check wirte and read
361 if(!is_writable($path) || !is_readable($path)) {
362 core::show_error("{$path}<br/>没有写入或读取权限,请检查");
363 }
364 }
365 }
366 /**
367 * 读取编译后模板的第一行 并分析成数组
368 * @param string $filepath 文件路径
369 * @param number $line 行数
370 * @return 返回指定行数的字符串
371 */
372 /*
373 private function get_compile_header($filepath, $line = 0) {
374 if(($file_arr = @file($filepath)) === false) {
375 core::show_error($filepath.'<br/>读取文件失败,请检查文件权限!');
376 }
377 return $file_arr[0];
378 }
379 */
380 /**
381 * 分析头部注释的日期
382 * @param string $cotnent 编译文件头部第一行
383 * @return 返回上一次日期
384 */
385 /*
386 private function get_compile_header_comment($content) {
387 preg_match("//*(.*?)*//", $content, $match);
388 if(!isset($match[1]) || empty($match[1])) core::show_error('编译错误!');
389 $arr = explode('|', $match[1]);
390 $arr_date = explode('##', $arr[0]);
391 return $arr_date[1];
392 }
393 */
394 /**
395 * 获取模板完整路径 并返回已存在文件
396 * @param string $filename 文件名
397 * @param string $view_path 模板路径
398 * @return
399 */
400 private function get_tpl($filename, $view_path) {
401 empty($filename) && $filename = $this->tpl_name;
402 //遍历模板路径
403 foreach($this->conf['view_path'] as $path) {
404 if($view_path) {//直接从tpl跟目录找文件
405 $tpl_path = $path.$view_path;
406 $view_file_path = $tpl_path.$filename.$this->_tpl_suffix;
407 } else {//根据目录,控制器,方法开始找文件
408 $view_file_path = ($tpl_path = $this->get_tpl_path($path)) ? $tpl_path.$filename.$this->_tpl_suffix : exit(0);
409 }
410 if(is_file($view_file_path)) {
411 //向指针传送模板路径和模板名称
412 $this->template_path = $tpl_path;//
413 $this->template_name = $filename.$this->_tpl_suffix;
414 return true;
415 } else {
416 core::show_error($filename.$this->_tpl_suffix.'模板不存在');
417 }
418 }
419 }
420 /**
421 * 获取模板路径
422 * @param string $path 主目录
423 * @return URL D和M的拼接路径
424 */
425 private function get_tpl_path($path = '') {
426 core::get_directory_name() && $path_arr[0] = core::get_directory_name();
427 core::get_controller_name() && $path_arr[1] = core::get_controller_name();
428 (is_array($path_arr)) ? $newpath = implode('/', $path_arr) : core::show_error('获取模板路径失败!') ;
429 return $path.$newpath.'/';
430 }
431 /**
432 * 创建目录
433 * @param string $path 目录
434 * @return
435 */
436 private function create_dir($path, $mode = 0777){
437 if(is_dir($path)) return false;
438 $dir_arr = explode('/', $path);
439 $dir_arr = array_filter($dir_arr);
440 $allpath = '';
441 $newdir = $this->template_c;
442 foreach($dir_arr as $dir) {
443 $allpath = $newdir.'/'.$dir;
444 if(!is_dir($allpath)) {
445 $newdir = $allpath;
446 if(!@mkdir($allpath, $mode)) {
447 core::show_error( $allpath.'<br/>创建目录失败,请检查是否有可都写权限!');
448 }
449 chmod($allpath, $mode);
450 } else {
451 $newdir = $allpath;
452 }
453 }
454 return true;
455 }
456 public function __destruct(){
457 $this->vars = null;
458 $this->view_path_param = null;
459 }
460 }