第二篇 提高与应用

第5章 使用模板引擎,改善服务性能

第6章 安全机制

第7章 实例:简易文章管理系统

第8章 实例:简易会员(用户)管理系统

第9章 实例:基于AJAX的文件管理系统

第5章 使用模板引擎,改善服务性能

作为一名开发人员,需要不断编写代码并总结经验才能获得水平的提高,不过如果总是重复性的劳动似乎工作本身就在不知不觉中降低技术含量了,比如结合HTML技术生成不同的页面就是一个比较典型的例子。在科技飞速发展的今天,如何使用PHP来快速编写代码,模板似乎成了唯一的选择。

PHP模板引擎可以让你的代码脉络更加清晰,结构更加合理化。但是,PHP模板引擎的发展总是会比PHP应用的迅速发展缓慢许多,在这种情况下,反而会影响到PHP应用的开发。对于每一个PHPer来说,没有一个PHP模板引擎对他是最合适、最完美的。因为所谓的PHP模板引擎就是大众化的东西,并不是针对个人的。所以,如果能在对PHP特点、应用有清楚的认识的基础上充分认识到模板的优劣势和该PHP应用原理,PHPer就能得到自己需要的适合自己开发风格的PHP模板引擎。这也是PHP最重要的思想之一,PHP提供的只是方法、途径,而不是最终解决方案。所以模板和PHP应用、产品一样都可以改造为自己的PHP模板引擎。因为这里面寄托着创造者的思想。

限于各种不同的条件限制,比如时间、经验,所以大家可能认为做一个自己的PHP模板引擎非常困难。其实,你需要的不是重新构造一个PHP模板引擎,而是选择一个最贴近自己的PHP模板引擎加以改造。因为PHP需要继承、创新。当然,能够一步一步地实现一个自己的PHP模板引擎,并及时融入最新的思想和理念更好。个人PHPer可以从细节做起,从自己最需要的地方、自己的开发习惯做起。PHP团队可以分工协作进行PHP模板引擎本地化,尤其对于公司而言尤为实用。就算你只是修改了PHP模板引擎里面的一个符号,仅仅是一个符号也能说明你对它加以了改造,它就是属于你的。最重要的是,自己惯于使用的PHP模板引擎永远不是固定不变的,它将伴随你一直成长,也会成为你的PHP学习历程的见证。

未使用模板引擎时,PHP程序的工作流程如图5-1所示。

图5-1 未使用模板引擎时PHP程序的工作流程

使用模板引擎后的工作流程如图5-2所示。

图5-2 使用模板引擎后PHP程序的工作流程

常见的PHP模板引擎有PHPLIB、IT、Flexy、Smarty等,这些模板引擎各有所长,根据个人使用感受来看,Smarty有以下特点:

● 模板支持丰富的语法,方便程序员在模板中实现丰富灵活的逻辑。

● 使用“预编译模板”的概念,使性能得到提升。

● 支持Cache(缓存)功能。

● 支持自定义插件,插件实际就是一些自定义的函数。

本章将介绍如何基于Smarty模板引擎编写PHP程序,以及在服务器端改善程序性能的一些做法。

5.1 使用Smarty建立模板机制

Smarty(http://www.smarty.net/)是一套PHP模板引擎,更准确地说,它分开了逻辑程序和外在的内容,提供了一种易于管理的方法。Smarty为程序员和页面设计师分配了不同的角色:编写程序逻辑和设计页面呈现的内容,例如,你正在创建一个用于浏览新闻的网页,其中新闻标题、标签栏、作者和新闻内容等都是内容要素,程序员并不考虑应该怎样去呈现这些要素;而是由页面设计师们编辑模板、组合使用HTML标签和模板标签去格式化这些要素的输出(表格、背景色、字体大小、样式表等)。当程序员打算改变文章检索的方式(即程序逻辑的改变)时,并不影响页面设计师,内容仍将准确地输出到模板并显示为正确的页面;当然,如果页面设计师想要完全重做界面,也不会影响到程序逻辑。事实上应该让程序逻辑远离模板,页面表现逻辑远离程序逻辑,这将在以后使内容更容易管理,程序更容易升级。

5.1.1 安装和配置

将Smarty解压到Web应用的根目录下,例如,这里是Web服务器根目录下的smarty子目录,其中包括demo和libs两个目录,如图5-3所示。

图5-3 解压Smarty并放置到合适的目录

然后打开浏览器并定位到demo目录,将会出现演示页面的运行结果,这表示Smarty已经正确安装并运行了,如图5-4所示。

图5-4 Smarty自带的演示程序运行结果

此时,demo目录中的templates_c子目录下会生成如图5-5所示的文件,这些文件就是经过Smarty“编译”后的模板文件,而原始模板文件则存放在templates目录下。

图5-5 Smarty“编译”后的模板文件

细心的读者一定会发现,在Smarty自带的默认演示程序运行的过程中会自动打开一个页面,如图5-6所示。

图5-6 Smarty默认演示程序自动打开的调试信息页面

此页面显示了Smarty运行过程中的各类基本调试信息,能为开发人员提供必要的帮助,若不需要显示此调试信息(例如,程序已经全部调试成功,站点准备上线),则可以关闭其显示。在Smarty默认的演示程序中控制是否显示调试信息的是位于index.php中的一条赋值语句:

$smarty->debugging = true;

关闭显示调试信息只需将此变量的值改为“false”即可。

在实际使用中并不需要demo目录,只保留libs目录即可,当然,libs目录也可以重命名为其他名称,例如include。然后在include中新建文件夹templates和templates_c。templates中存放页面的原始模板文件,文件格式为.tpl或者.html都可以。templates_c中存放“编译”后的模板文件。至此,Smarty的基本配置已经完成。为简便起见,还需要一个配置文件,在其中引入Smarty.class.php类文件,然后配置路径信息等。在include同级的目录下建立config.php,在应用到的所有程序文件中引入即可。config.php的基本内容如例5-1所示。

实例演练

例5-1 Smarty配置文件config.php的基本内容

<?php
define("BASE_PATH", dirname(__FILE__));
define("SMARTY_PATH", BASE_PATH."/include/");
include SMARTY_PATH."Smarty.class.php";
$sma = new Smarty;
//设置Smarty原始模板文件目录
$sma->template_dir = SMARTY_PATH."templates/";
//设置Smarty"编译"后模板文件目录
$sma->compile_dir = SMARTY_PATH."templates_c/";
//设置Smarty开始定界符
$sma->left_delimiter = "{{";
//设置Smarty结束定界符
$sma->right_delimiter = "}}";
//设置是否使用Smarty的缓存机制
//$sma->caching = false;
//设置是否使用Smarty的调试机制
//$sma->debugging = false;
?>

在程序文件test.php中引入config.php,使用以下语句

$sma->display('test.html');

即可实现Smarty程序代码与模板页面分开的功能。

5.1.2 基本语法

Smarty的基本思想是在模板文件中添加一些成对的标签,用来指明其中的内容为Smarty的语法控制内容,例如:

{* 这里是注释内容 *}

这就是Smarty中用来表示注释的语法,其中“{*”为开始标签,“*}”是结束标签,其中的内容就是具体的注释内容。

Smarty默认的定界符(Delimiter,即开始和结束标签)是一对花括号:“{”和“}”,有时需要在HTML代码里输出花括号,如果在模板里直接写出来,会被Smarty的解析器认为是定界符并报错,这种情况下有两种解决办法:

1.如果是输出很少的几个花括号,可以使用Smarty的内置变量ldelim和rdelim。

2.如果是在HTML代码中书写JavaScript代码等较长的内容,那么可以使用Smarty的文本转义标签,将代码包围起来,例如:

{literal}
<script type="text/javascript">
function sayHello() {alert('Hello World!')}
</script>
{/literal}

一般在使用Smarty的时候,往往是将PHP程序文件中的变量替换模板文件中相应的内容,生成“编译”后的模板文件,并在响应客户端请求时执行,例如,模板文件里面有一个标签$user_name(注意:标签里面的变量必须带$),则在PHP文件中可以这样做:

$new_name = "Joan";
//注意:引号中的user_name不带"$"
$smarty->assign("user_name", $new_name);
//载入index模板文件并显示
$smarty->display("index.html");

上例所使用的Smarty语法可总结如下:

$smarty->assign("标签名", "值");
$smarty->display("模板文件名");

在Smarty中,各种语法结构如分支判断、循环等,是作为内部函数来处理的,相对的,用户可以自定义一些Smarty函数来扩充相应的功能,即被称为“插件机制”。

Smarty中的“{if}”语法结构与PHP语言本身相比,具有相同的灵活性,并且在其基础上添加了一些适应模板引擎的新特性。每一个“{if}”必须存在成对的“{/if}”与其对应,同时也可以在其中使用else或elseif。所有的PHP操作符或函数都可以正常使用,例如“||”、“or”、“&&”、“and”、“is_array()”等。例5-2是一段使用“{if}”语法结构的Smarty模板文件代码。

实例演练

例5-2 使用“{if}”语法结构的Smarty模板文件代码示例

{if isset($name) && $name == 'Blog'}
    {* 逻辑代码段1 *}
{elseif $name == $foo}
    {* 逻辑代码段2 *}
{/if}
{if is_array($foo) && count($foo) > 0)
    {* 执行foreach循环 *}
{/if}

与PHP语言中的foreach循环语法相似,在Smarty中,{foreach}…{/foreach}结构用来遍历一个关联数组或数字索引数组,{foreach}标签可以使用的属性如表5-1所示。

表5-1 {foreach}标签可以使用的属性

{foreach}标签的元属性是在循环过程中的一些变量,用于方便地取得循环过程的一些信息,元属性共有6个,如表5-2所示。

表5-2 {foreach}标签的元属性

访问一个未定义的元属性不会导致产生一个错误,但可能会导致不可预见的结果。

例5-3是使用{foreach}标签的一个例子,作用是对于第一个条目显示“最新”而不是id号。

实例演练

例5-3 使用{foreach}标签的例子

<table>
{foreach from=$items key=Id item=i name=foo}
<tr>
  <td>{if $smarty.foreach.foo.first}最新{else}{$Id}{/if}</td>
  <td>{$i.label}</td>
</tr>
{/foreach}
</table>

Smarty支持使用配置文件预先存放一些字符串或值,并在模板文件中使用{config_load}标签读取并使用其中的内容,一个简单的例子如例5-4所示。

实例演练

例5-4 使用{config_load}标签的例子

配置文件config.conf的内容如下。

#this is config file comment
# global variables
pageTitle = "Main Menu"
bodyBgColor = #000000
tableBgColor = #000000
rowBgColor = #00ff00
#customer variables section
[Customer]
pageTitle = "Customer Info"

模板文件内容如下:

{config_load file="config.conf"}
<html>
<title>{#pageTitle#|default:"No title"}</title>
<body bgcolor="{#bodyBgColor#}">
<table border="{#tableBorderSize#}" bgcolor="{#tableBgColor#}">
   <tr bgcolor="{#rowBgColor#}">
      <td>First</td>
      <td>Last</td>
      <td>Address</td>
   </tr>
</table>
</body>
</html>

{config_load}标签支持配置文件的内容分段,如将例5-4中的

{config_load file="config.conf"}

更换为

{config_load file="config.conf" section="Customer"}

与PHP中的引入文件机制相似,Smarty也支持模板布局拆分,即将一个完整的页面内容拆分为功能与作用不同的若干部分。在程序中灵活控制,根据逻辑需要来组合成目标页面,需要使用{include}标签,例子如例5-5所示。

实例演练

例5-5 使用{include}标签引入其他模板文件

<html>
<head>
  <title>{$title}</title>
</head>
<body>
{include file='page_header.tpl'}
{* ……这里是模板文件具体内容…… *}
{include file="$tpl_name.tpl"}
{include file='page_footer.tpl'}
</body>
</html>

在实际编码过程中,如果需要在模板文件中临时执行一些PHP代码,可以使用{php}标签,以使PHP代码直接嵌入到模板文件中,如例5-6所示的例子将输出类似“Copyright&#174;2009-2010”的年份信息:

实例演练

例5-6 使用{php}标签在模板文件中插入PHP代码

{php}
   echo 'Copyright&reg;2009-'.date('Y');
{/php}

除了{if}、{foreach}等Smarty内置函数外,Smarty还支持插件机制,以扩充更多更复杂的功能。一般来说,内置函数无须更改或调整,而使用插件机制增加的函数则可以根据需要新增或修改。Smarty的插件机制从2.0版本开始引入,几乎可以用来定制Smarty的各项功能,包括functions(函数)、modifiers(修饰符)、block functions(模块函数)、compiler functions(编译器函数)、prefilters(预过滤器)、postfilters(后过滤器)、outputfilters(输出过滤器)、resources(资源)、inserts(引入项)等。插件目录可以是一个包含目录路径的字符串,也可以是一个包含多个路径信息的数组。安装一个Smarty的插件非常简单,只需放到任何一个插件目录里,Smarty便会自动调用它。

通常插件是按需调用的,只有特定的修饰符、函数和资源等在模板中被调用。此外,即使在同一个请求中存在多个Smarty的运行实例,每一个插件也只被加载一次。预过滤器、后过滤器和输出过滤器是较为特殊的一类,它们并不在模板文件中出现,而必须使用API函数在模板被处理之前显式注册或加载,多个类型相同的过滤器的执行顺序取决于它们被注册或加载的先后顺序。

例5-7给出了使用{html_table}标签的例子。

实例演练

例5-7 使用{html_table}标签将数组变量的值输出到表格中

在程序中设置$data的值:

<?php
$smarty->assign('data', array(1,2,3,4,5,6,7,8,9));
$smarty->display('index.tpl');
?>

在模板文件中输出表格:

{html_table loop=$data}

上述代码输出结果为:

<table border="1">
<tbody>
<tr><td>1</td><td>2</td><td>3</td></tr>
<tr><td>4</td><td>5</td><td>6</td></tr>
<tr><td>7</td><td>8</td><td>9</td></tr>
</tbody>
</table>

在模板文件中输出表格:

{html_table loop=$data cols=4 table_attr='border="0"'}

上述代码输出结果为:

<table border="0">
<tbody>
<tr><td>1</td><td>2</td><td>3</td><td>4</td></tr>
<tr><td>5</td><td>6</td><td>7</td><td>8</td></tr>
<tr><td>9</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>
</tbody>
</table>

5.2 生成静态页面和内容缓存

新闻类网站具有的一个特点是:新闻更新快,旧闻的内容基本很少做更改。对于这种需求,Smarty的常见做法是将程序逻辑执行的结果替换模板文件中的相应部分,并生成HTML静态页面,如例5-8所示的代码片段给出了生成静态页面的例子。

实例演练

例5-8 使用Smarty生成静态页面的例子

<?php
Require 'smarty/Smarty.class.php';
$t = new Smarty;
$t->assign("title", "中国2010年上海世界博览会于5月1日开幕");
$t->assign("body", "中国2010年上海世界博览会展期为6个月,2010年5月1日开幕,10月31日闭幕.2010年是中国改革开放32年,也是上海浦东开发开放20周年,在世博会期间将有一系列重要的庆典及庆祝活动在世博会场内外举行。");
$content = $t->fetch("templates/news.htm");
$fp = fopen("archives/2010/05/0001.html", "w");
fwrite($fp, $content);
fclose($fp);
?>

例5-8中的assign()方法用于显式地将变量及其值组成的键值对或包含键值对的关联数组传递到模板文件中。fetch()方法则用于获得模板文件的输出结果,通常在获得替换键值对的结果后将页面的内容写入到新的HTML静态页面文件中。

Smarty支持对套用模板解释后的内容进行缓存。缓存通过将display()和fetch()函数的输出保存到单独的文件中的方法来加速对其的调用,当存在一个可用的缓存版本时,Smarty将读取其内容并代替重新生成的过程。缓存能够显著改善运行速度,特别是对一些需要大量计算的模板。由于模板的内容是动态的,因此小心地处理缓存的内容并设置缓存时间十分重要,例如,当显示站点首页时,因其内容的更新频率并不是非常快,故设置一个或多个小时的缓存时间可以使其良好地工作;另一方面,当显示包含一个每分钟都有新信息的时间表的页面时,使用缓存就失去了意义。

启用缓存的方法很简单,如例5-9所示,在显示模板之前设置$caching的值为1即可。

实例演练

例5-9 启用Smarty缓存

<?php
Require 'Smarty.class.php';
$smarty = new Smarty;
$smarty->caching = 1;
$smarty->display('index.tpl');
?>

启用缓存后,display('index.tpl')的第一次调用将像平常一样渲染模板,同时将其输出保存到一个文件中(即一个缓存副本),之后在执行display('index.tpl')时,该副本将用来替代再次渲染模板文件被输出。

每个被缓存的页面可通过设置$cache_lifetime的值来指定一个有限的缓存时间(即生命期),其默认值为3600秒,当此时间到达后,缓存将被重新生成。可以通过设置$caching的值为2来为每个缓存文件设置单独的过期时间,如例5-10所示。

实例演练

例5-10 为每个缓存文件设置单独的过期时间

<?php
Require 'Smarty.class.php';
$smarty = new Smarty;
$smarty->caching = 2;
//设置index.tpl的缓存时间为5分钟
$smarty->cache_lifetime = 300;
$smarty->display('index.tpl');
//设置home.tpl的缓存时间为1小时
$smarty->cache_lifetime = 3600;
$smarty->display('home.tpl');
//当$caching设置为2时以下对缓存时间的设置将不起作用,因为上面已经将home.tpl的缓存时间设置为1小时
$smarty->cache_lifetime = 30;
$smarty->display('home.tpl');
?>

如果开启了$compile_check选项,每个模板和配置文件涉及修改时将会检查相应的缓存文件,如果修改时缓存已经建立则将被立刻重新生成。这个功能将会轻微影响功能,所以为了优化起见,可以设置$compile_check为false。如果开启了$force_compile选项,缓存文件将总是重新生成,这意味着关闭了缓存功能。$force_compile选项通常用于调试目的,更直接的关闭缓存的方法是设置$caching的值为0。is_cached()函数用于测试一个模板是否存在有效的缓存文件,如果一个被缓存的模板中必须要完成如获取数据库中的数据等操作,可以使用该函数来跳过相应的内容。例5-11给出了上述内容的例子,其中clear_cache()函数用于清除某个特定的缓存,clear_all_cache()函数用于清空所有缓存文件。

实例演练

例5-11 检测缓存是否存在及清除缓存

<?php
require 'Smarty.class.php';
$smarty = new Smarty;
$smarty->caching = 1;
if(!$smarty->is_cached('index.tpl')) {
      $contents = get_database_contents();
      $smarty->assign($contents);
}
//清除index.tpl的缓存
$smarty->clear_cache('index.tpl');
//清空所有缓存文件
$smarty->clear_all_cache();
$smarty->display('index.tpl');
?>

5.3 页面压缩

在早期宽带还未流行的时候,上网是一件奢侈的事情,很多网友都是驱“猫”上网,那时候一个页面的大小如果达到了10KB,那么即使正常情况下用56KB的Modem也要好几秒才能下载完毕并显示(这还不包括下载过程中出现异常导致网络断开的情况)。这种情况下,页面压缩就成为了一个很不错的选择。即使在动辄就是4MB、8MB的ADSL宽带已经逐渐普及的今天,页面压缩也仍然有其用武之地,例如开发手机或其他智能移动设备专门访问的站点,或者用于用户对流量敏感的情况(比如在手机上使用GPRS上网)等。

在PHP语言中,往往使用GZIP压缩技术和“输出控制”(Output Control)的方式来处理页面内容——使用GZIP将页面在服务器端压缩(要实现GZIP压缩页面需要浏览器和服务器共同支持,现在绝大多数浏览器都支持解析GZIP压缩后的页面),并且让PHP缓存所有由脚本生成的输出(在决定把它们送出之前,浏览器方不会收到任何内容),待所有操作完成后再一次性发送到浏览器。GZIP压缩需要用到zlib扩展,而输出控制操作则需要使用PHP中的ob_系列函数。ob_系列函数对把脚本生成的输出重定向到缓存中十分有用。为支持GZIP的浏览器输出压缩过的缓存数据可以减少载入时间,也可作为缓存机制来减少对数据源(数据库或文件)的存取。究竟页面压缩能够带来怎样的效果呢?先来看一个例子。

例5-12是用于测试的代码,功能很简单——循环输出200次字符串“这里是测试文本内容。”

实例演练

例5-12 测试代码

<?php
for($i=0; $i<200; $i++) {
   echo('这里是测试文本内容。');
}
?>

使用Firefox浏览器的Firebug扩展对例5-12的输出内容进行分析,大小为3.9KB(4000B),页面加载时间约为10毫秒此数据为多次刷新结果的平均值。,如图5-7所示。

图5-7 使用Firefox浏览器分析例5-12的输出内容

例5-13则是在例5-12的基础上添加了使用GZIP方式压缩内容并调用ob_系列函数对输出进行重新处理的代码。

实例演练

例5-13 使用GZIP压缩内容并对输出重新处理的代码

<?php
ob_start('ob_gzip');
for($i=0; $i<200; $i++) {
   echo('这里是测试文本内容。');
}
ob_end_flush();
function ob_gzip($content) {
   if(!headers_sent() && extension_loaded("zlib") && strstr($_
            SERVER["HTTP_ACCEPT_ENCODING"], "gzip")) {
            $content = gzencode($content,9);
            header("Content-Encoding: gzip");
            header("Vary: Accept-Encoding");
            header("Content-Length: ".strlen($content));
   }
   return $content;
}
?>

同样使用Firefox浏览器和Firebug扩展对代码运行结果进行分析,大小为75B,页面加载时间约为3毫秒,如图5-8所示。

图5-8 使用Firefox浏览器分析例5-13的输出内容

从例5-12和例5-13的执行情况来看,采用页面压缩技术能够显著地减少页面尺寸,从而有效地提高带宽利用率。在实际使用zlib扩展和ob_系列函数时,还需要注意php.ini配置文件中的以下相关配置选项。

[PHP-Core-OutputControl]
implicit_flush = Off

用于是否要求PHP输出层在每个输出块之后自动刷新数据。等效于在每个print()、echo()、HTML块之后自动调用flush()函数。打开这个选项对程序执行的性能有严重的影响,通常只推荐在调试时使用。在CLI SAPI的执行模式下,该指令默认为On 。

output_buffering = 0

用来设置输出缓冲区大小(字节),建议在4096到8192之间取值。输出缓冲允许在输出正文内容之后再发送HTTP头(包括cookies),其代价是输出层牺牲一点点速度。设置输出缓冲可以减少写入,有时还能减少网络数据包的发送。这个参数的实际效果很大程度上取决于使用的Web服务器及程序代码的优化程度。

output_handler =

用于将所有脚本的输出重定向到一个输出处理函数,例如,重定向到mb_output_handler()函数时,字符编码将被透明地转换为指定的编码。一旦在这里指定了输出处理程序,输出缓冲将被自动打开(相当于设置output_buffering=4096)。

使用本选项需要注意:

● 此处仅能使用PHP内置函数,自定义函数应在脚本中使用ob_start()函数指定。

● 可移植脚本不能依赖该指令,而应使用ob_start()函数明确指定输出处理函数。

● 不能同时使用mb_output_handler和ob_iconv_handler两个输出处理函数。也不能同时使用ob_gzhandler输出处理函数和zlib.output_compression指令。

● 如果使用zlib.output_handler指令开启zlib输出压缩,该指令必须为空。

[Zlib]
zlib.output_compression = Off

用于是否使用zlib库透明地压缩脚本输出结果,该指令的值可以设置为:Off、On、字节数(压缩缓冲区大小,默认为4096)。

如果启用该指令,当浏览器发送Accept-Encoding:gzip(deflate)头信息时,Content-Encoding:gzip(deflate)和Vary:Accept-Encoding头将加入到应答头信息中。可以在应答头输出之前用ini_set()函数在脚本中启用或禁止这个特性。

使用本选项需要注意:

● 压缩率会受压缩缓冲区大小的影响,如果希望得到更好的压缩质量,请指定一个较大的压缩缓冲区。

● 如果启用了zlib输出压缩,output_handler指令必须为空,同时必须设置zlib.output_handler指令的值。

zlib.output_compression_level = -1

用于设置压缩级别,可选值为0到9,值越高效果越好,但CPU占用越多;0表示不压缩。建议取值在1到5之间。默认值-1表示使用zlib内部的默认值(6)。

zlib.output_handler =

用于在打开zlib.output_compression指令的情况下指定输出处理器,可以使用的处理器有“zlib.inflate”(解压)或“zlib.deflate”(压缩)。如果启用该指令则必须将“output_handler”指令设置为空。

5.4 边学边练:使用Smarty重写2.4节的实例“我的书架”

在本书2.4节中使用PHP+MySQL方式实现了具有基本功能的书籍管理系统——我的书架,本节将引入Smarty来重写一遍,为其增加以下功能:

1.使用模板功能将程序逻辑和页面设计分离,便于单独修改程序或重新设计页面。

2.在1的基础上为每本书籍的信息生成静态页面。

由于是重写,因此数据库部分没有更改,books表结构请参考本书2.4节。引入Smarty后,文件目录结构如表5-3所示。

表5-3 系统目录结构

实例演练

例5-14 重写“我的书架”书籍管理系统。

先来看index.php,其内容比较简单:

<?php
header('Content-Type:text/html; charset=GBK');
file_exists('static/index.htm') ? header('location: static/index.htm') : die('<h2>第一次运行, 初始化静态页面</h2><a href="do.php?initialize=true">如果没有自动跳转, 请单击这里</a><script type="text/javascript">setTimeout(functio n(){location.href="do.php?initialize=true";},2000);</script>');
?>

这段代码的作用主要是判断是否已经生成首页的静态页面,如果已经生成则直接显示其内容,否则就调用do.php进行所有静态页面的初始化并在完成后跳转到首页。

下面给出do.php的代码:

<?php
chdir(dirname(__FILE__));
require_once 'config.php';
require_once 'Smarty/Smarty.class.php';
if ($_POST['submit']) {
     $bookid = $_GET['edit'];
     $title = htmlspecialchars($_POST['title']); //过滤标题变量中的非法字符
     $author = htmlspecialchars($_POST['author']); //过滤作者变量中的非法字符
     $price = (float)htmlspecialchars($_POST['price']);
     //过滤价格变量中的非法字符,并转换为float型
     $year = (int) $_POST['year'];
     $month = (int) $_POST['month'];
     $day = (int) $_POST['day'];
     $setSQL = "bookid='$bookid',title='$title',author='$author',dateline='$year-$month-$day',price='$price'";
     $sql = $bookid ? "UPDATE books SET $setSQL WHERE bookid='$bookid'" : "INSERT INTO books SET $setSQL";
     mysql_query($sql);
     if (!$bookid) {
            $bookid = mysql_insert_id();
     }
     generate_index();
     generate_page($bookid);
     header("location:static/detail-$bookid.htm");
} else {
     if ($delid = $_GET['del']) {
           mysql_query("DELETE FROM books WHERE bookid='$delid'");
           delete_page($delid);
     }
     $generate_all = $_GET['regenerate'] || $_GET['initialize'];
     generate_index($generate_all);
     if ($generate_all) {
           die('<h2>所有页面已经生成</h2><a href="static/index.htm">如果没有自动跳转, 请单击这里</a><script type="text/javascript">setTimeout(function(){location.href="static/index.htm";},2000);</script>');
     }
     header("location:static/index.htm");
}
function generate_index($generate_all = false) {
    //生成首页
    $smarty = getSmarty();
    $smarty->assign('title', '我的书架');
    //生成全部页面
    if ($generate_all) {
           $handle = opendir('static');
           while ($file = readdir($handle)) {
                  if (preg_match('/^detail-\d{1,10}\.htm$|^index\.htm$/',$file)) {
                  @unlink('static/' .$file);
                  }
           }
    }
    $rs = mysql_query("SELECT * FROM books");
    while ($book = mysql_fetch_assoc($rs)) {
           $smarty->append('books', $book);
           if ($generate_all) {
                   generate_page($book['bookid']);
           }
    }
    generate_html("static/index.htm", $smarty->fetch('index.tpl'));
}
function generate_html($filename, $content) {
    file_put_contents($filename, $content) or die('<h2>无法写入HTML文件, 请检查根目录是否有写权限.</h2>');
}
function generate_page($bookid) {
    //生成某本书的页面
    $smarty = getSmarty();
    $rs = mysql_query("SELECT * FROM books WHERE bookid='$bookid'");
    $book = mysql_fetch_assoc($rs);
    $smarty->assign('title', "$book[title] - 详情 - 我的书架");
    $smarty->assign('book', $book);
    generate_html("static/detail-$bookid.htm", $smarty->fetch('detail.tpl'));
}
function delete_page($bookid) {
    //删除某本书的页面
    @unlink("static/detail-$bookid.htm");
}
function getSmarty() {
    $smarty = new Smarty;
    $smarty->register_function('dateselect', 'handle_dateselect');
    return $smarty;
}
function handle_dateselect($params, &$smarty) {
    global $minyear;
    $dateline = $params['dateline'];
    $year = $maxyear = date('Y');
    $month = $day = 1;
    if ($dateline) {
   list($year, $month, $day) = explode('-', $dateline);
    }
    $years = $months = $days = '';
    for ($i = $minyear; $i <= $maxyear; $i++) {
           $selected = intval($year) == $i ? ' selected="selected"' : '';
           $years .= "<option value=\"$i\"$selected>$i</option>";
    }
    for ($i = 1; $i <= 12; $i++) {
           $selected = intval($month) == $i ? ' selected="selected"' : '';
           $months .= "<option value=\"$i\"$selected>$i</option>";
    }
    for ($i = 1; $i <= 31; $i++) {
           $selected = intval($day) == $i ? ' selected="selected"' : '';
           $days .= "<option value=\"$i\"$selected>$i</option>";
    }
    return <<<SCRIPT
    <select name="year" id="dateselect_year"
          onchange="update_dateselect()">$years</select>年
    <select name="month" id="dateselect_month"
    onchange="update_dateselect()">$months</select>月
    <select name="day" id="dateselect_day">$days</select>日
    <script type="text/javascript">
function update_dateselect() {
        var year = parseInt(document.getElementById('dateselect_year').value);
        var month = parseInt(document.getElementById('dateselect_month').value);
        var dayselect = document.getElementById('dateselect_day');
        var to = 30;
        if([0,1,3,5,7,8,10,12].indexOf(month)!=-1) {
                  to = 31;
        } else if(month==2) {
                  to = ((year%100==0&&year%400==0)||(year%100!=0&&yea r%4==0))?29:28;
        }
        while(dayselect.options.length>to) {
            dayselect.options.remove(dayselect.options.length-1);
        }
        while(dayselect.options.length<to) {
                  var n = dayselect.options.length+1;
                  dayselect.options.add(new Option(n,n));
        }
    }
    update_dateselect();
    </script>
SCRIPT;
}
?>

程序中,generate_index()函数用于生成首页和详情页的静态页面文件,generate_page()函数用于生成单个页面的静态页面文件。函数getSmarty()中调用了Smarty的register_function()方法注册了名为handle_dateselect()的函数,用于显示选择书籍出版时间的三个下拉列表。

接下来给出header.tpl、index.tpl、detail.tpl、bookform.tpl等模板文件的代码。

header.tpl的代码如下:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>{$title}</title>
<meta http-equiv="Content-Type" content="text/html; charset=GBK">
<meta http-equiv="Pragma" contect="no-cache">
<meta http-equiv="Cache-Control" content="no-store, no-cache, mustrevalidate, post-check=0, pre-check=0">
<meta http-equiv="Expires" content="-1">
<link rel="stylesheet" type="text/css" href="../templates/css.css" />
</head>
<body>
<div id="wrap">
<center><h2>{$title}</h2></center>

index.tpl的代码如下:

{include file=header.tpl}
<h3>图书列表</h3>
<table width="100%" cellspacing="0" cellpadding="1" border="1" bordercolor="#999">
   <tr>
   <th>ID</th>
   <th>书名</th>
   <th>作者</th>
   <th>出版时间</th>
   <th>定价</th>
   <th>操作</th>
   </tr>
   {foreach from=$books key=k item=item}
   <tr align="center"{if $k%2==1} class="odd"{else} class="even"{/if}>
   <td>{$item.bookid}</td>
   <td align="left">{$item.title}</td>
   <td>{$item.author}</td>
   <td>{$item.dateline}</td>
   <td>{$item.price} 元</td>
   <td><a href="detail-{$item.bookid}.htm">详情</a> <a href="../ do.php?del={$item.bookid}" onclick="return confirm('删除操作不能撤销, 确认继续吗?')">删除</a></td>
   </tr>
   {/foreach}
</table>
<h3>新增图书</h3>
{include file=bookform.tpl}
{include file=footer.tpl}

detail.tpl的代码如下:

{include file=header.tpl}
<table width="100%" cellspacing="0" cellpadding="1" border="1" bordercolor="#999">
   <tr>
   <th width="100">ID</th>
   <td>{$book.bookid}</td>
    </tr>
    <tr>
   <th>书名</th>
   <td align="left">{$book.title}</td>
    </tr>
    <tr>
   <th>作者</th>
   <td>{$book.author}</td>
    </tr>
    <tr>
  <th>出版时间</th>
  <td>{$book.dateline}</td>
    </tr>
    <tr>
  <th>定价</th>
  <td>{$book.price} 元</td>
    </tr>
</table>
<a href="index.htm">返回</a>
<a href="../do.php?del={$book.bookid}" onclick="return confirm('删除操作不能撤销, 确认继续吗?')">删除</a>
<h3>编辑图书</h3>
{include file=bookform.tpl}
{include file=footer.tpl}

bookform.tpl的代码如下:

{literal}
<script type="text/javascript">
Array.prototype.indexOf = function (obj, start) {
    for (var i = (start || 0); i < this.length; i++) {
   if (this[i] == obj) {
        return i;
   }
    }
    return -1;
}
String.prototype.trim = function() {
    var reExtraSpace = /^\s*(.*?)\s+$/;
    return this.replace(reExtraSpace,"$1");
}
function autofix(theform) {
    theform.title.value = theform.title.value.trim();
    if(theform.title.value == "") {
   alert('书名不能为空');
   theform.title.focus();
   return false;
    }
    if(theform.author.value == "") {
   alert('作者不能为空');
   theform.author.focus();
   return false;
    }
    if(theform.title.value.substr(0,1)=='《' && theform.title.value.substr(theform.title.value.length-1,1)=='》') {
   theform.title.value = theform.title.value.substr(1, theform.title.value.length-2);
    }
}
</script>
{/literal}
<form method="POST"
          action="../do.php{if $book.bookid}?edit={$book.bookid}{/if}"
          onsubmit="return autofix(this)">
   <table cellspacing="0" cellpadding="1" border="0">
   <tr>
          <td>书名</td>
          <td><input type="text" name="title" value="{$book.title}"   size="27" /></td>
   </tr>
   <tr>
          <td>作者</td>
          <td><input type="text" name="author" value="{$book.author}" size="27" /></td>
   </tr>
   <tr>
          <td>出版时间</td>
          <td>{dateselect dateline=$book.dateline}</td>
   </tr>
   <tr>
          <td>定价</td>
          <td><input type="text" name="price" value="{$book.price}" size="24" /> 元</td>
   </tr>
   <tr>
          <td colspan="2">
          <input type="hidden" name="submit" value="true"/>
              <input type="submit" value="提交"/> <input type="reset" value="重置"/>
          </td>
   </tr>
   </table>
</form>

本程序运行结果如图5-9所示。

图5-9 新版“我的书架”程序的运行效果

此时在static文件夹下也生成了首页和5本书的详情页共6个静态页面文件,如图5-10所示。

图5-10 调用Smarty生成的6个静态页面文件