PHP沉思录
--左轻侯
工作模型
PHP的工作模型非常特殊。从某种程度上说,PHP和ASP、ASP.NET、JSP/Servlet等流行的Web技术,有着本质上的区别。
以Java为例,Java在Web应用领域,有两种技术:Java Servlet和JSP(Java Server
Page)。Java
Servlet是一种特殊类型的Java程序,它通过实现相关接口,处理Web服务器发送过来的请求,完成相应的工作。JSP在形式上是一种类似于PHP
的脚本,但是事实上,它最后也被编译成Servlet。
也就是说,在Java解决方案中,JSP和Servlet是作为独立的Java应用程序执行的,它们在初始化之后就驻留内存,通过特定的接口和
Web服务器通信,完成相应工作。除非被显式地重启,否则它们不会终止。因此,可以在JSP和Servlet中使用各种缓存技术,例如数据库连接池。
ASP.NET的机制与此类似。至于ASP,虽然也是一种解释型语言,但是仍然提供了Application对象来存放应用程序级的全局变量,它依托于ASP解释器在IIS中驻留的进程,在整个应用程序的生命期有效。
PHP却完全不是这样。作为一种纯解释型语言,PHP脚本在每次被解释时进行初始化,在解释完毕后终止运行。这种运行是互相独立的,每一次请求都会创建一
个单独的进程或线程,来解释相应的页面文件。页面创建的变量和其他对象,都只在当前的页面内部可见,无法跨越页面访问。
在终止运行后,页面中申请的、没有被代码显式释放的外部资源,包括内存、数据库连接、文件句柄、Socket连接等,都会被强行释放。
也就是说,PHP无法在语言级别直接访问跨越页面的变量,也无法创建驻留内存的对象。见下例:
class StaticVarTester {
public static $StaticVar = 0;
}
function TestStaticVar() {
StaticVarTester :: $StaticVar += 1;
echo "StaticVarTester :: StaticVar = " . StaticVarTester :: $StaticVar;
}
TestStaticVar();
echo "
";
TestStaticVar();
?>
在这个例子中,定义了一个名为StaticVarTester的类,它仅有一个公共的静态成员$StaticVar,并被初始化为0。然后,在
TestStaticVar()函数中,对StaticVarTester :: $StaticVar进行累加操作,并将它打印输出。
熟悉Java或C++的开发者对这个例子应该并不陌生。$StaticVar作为StaticVarTester类的一个静态成员,只在类被装载时进行初
始化,无论StaticVarTester类被实例化多少次,$StaticVar都只存在一个实例,而且不会被多次初始化。因此,当第一次调用
TestStaticVar()函数时,$StaticVar进行了累加操作,值为1,并被保存。第二次调用TestStaticVar()函
数,$StaticVar的值为2。
打印出来的结果和我们预料的一样:
StaticVarTester :: StaticVar = 1
StaticVarTester :: StaticVar = 2
但是,当浏览器刷新页面,再次执行这段代码时,不同的情况出现了。在Java或C++里面,$StaticVar的值会被保存并一直累加下去,我们将会看到如下的结果:
StaticVarTester :: StaticVar = 3
StaticVarTester :: StaticVar = 4
…
但是在PHP中,由于上文叙及的机制,当前页面每次都解释时,都会执行一次程序初始化和终止的过程。也就是说,每次访问时,StaticVarTester都会被重新装载,而下列这行语句
public static $StaticVar = 0;
也会被重复执行。当页面执行完成后,所有的内存空间都会被回收,$StaticVar这个变量(连同整个StaticVarTester类)也就不复存
在。因此,无论刷新页面多少次,$StaticVar变量都会回到起点:先被初始化为0,然后在TestStaticVar()函数调用中被累加。所以,
我们看到的结果永远是这个:
StaticVarTester :: StaticVar = 1
StaticVarTester :: StaticVar = 2
PHP这种独特的工作模型的优势在于,基本上解决了令人头疼的资源泄漏问题。Web应用的特点是大量的、短时间的并发处理,对各种资源的申请和释放工作非常频繁,很容易导致泄漏。
同时,大量的动态html脚本的存在,使得追踪和调试的工作都非常困难。PHP的运行机制,以一种非常简单的方式避免了这个问题,同时也避免了
将程序员带入到繁琐的缓冲池和同步等问题中去。在实践中,基于PHP的应用往往比基于Java或.NET的应用更加稳定,不会出现由于某个页面的BUG而
导致整个站点崩溃的问题。
(相比之下,Java或.NET应用可能因为缓冲池崩溃或其他的非法操作,而导致整个站点崩溃。)后果是,即使PHP程序员水平不高,也无法写
出使整个应用崩溃的代码。PHP脚本可以一次调用极多的资源,从而导致页面执行极为缓慢,但是执行完毕后所有的资源都会被释放,应用仍然不会崩溃。
甚至即使执行了一个死循环,也会在30秒(默认时间)后因为超时而中止。从理论上来说,基于PHP的应用是不太可能崩溃的,因为它的运行机制决定它不存在常规的崩溃这个问题。在实践中,很多开发者也认为PHP是最稳定的Web应用。
但是,这种机制的缺点也非常明显。最直接的后果是,PHP在语言级别无法实现跨页面的缓冲机制。这种缓冲机制缺失造成的影响,可以分成两个方面:
一是对象的缓冲。如我们所知,很多设计模式都依赖于对象的缓冲机制,对于需要频繁应付大量并发的服务端软件更是如此。因此,对象缓冲的缺失,理论上会极大
地降低速度。但是,由于PHP本身的定位和工作机制等原因,它在实际工作中的速度非常快。就作者自己的经验来看,在小型的Web应用中,PHP至少不比
Java慢。
在大型的应用中,为了榨干每一分硬件资源,即使PHP本身足够快,一个优秀的对象缓冲机制仍然是必要的。在这种情况下,可以使用第三方的内存缓
冲软件,如Memcached。由于Memcached本身的优异特性(高性能,支持跨服务器的分布式存储,和PHP的无缝集成等),在大型的PHP应用
中,Memcached几乎已经成为不可或缺的基础设施了。比起使用PHP语言自己实现对象缓冲来,这种第三方解决方案似乎更好一些。
二是数据库连接的缓冲。对MySQL,PHP提供了一种内置的数据库缓冲机制,使用起来非常简单,程序员需要做的只是用mysql_pconnect()代替mysql_connect()来打开数据库而已。
PHP会自动回收被废弃的数据库连接,以供重复使用。具有讽刺意味的是,在实际应用中,这种持久性数据库连接往往会导致数据库连接的伪泄漏现象:在某个时间,并发的数据库连接过多,超过了MySQL的最大连接数,从而导致新的进程无法连接数据库。
但是过一段时间,当并发数减少时,PHP会释放掉一些连接,网站又会恢复正常。出现这种现象的原因是,当使用pconnect时,Apache
的httpd进程会不释放connect,而当Apache的httpd进程数超过了mysql的最大连接数时,就会出现无法连接的情况。因此,需要小心
地调整Apache和Mysql的配置,以使Apache的httpd进程数不会超出MySQL的最大连接数。在某些情况下,一些有经验的PHP程序员宁
可继续使用mysql_connect(),而不是mysql_pconnect()。
就作者所知,在PHP未来的roadmap中,对于工作模型这一部分,没有根本性的变动。这是PHP的缺点,也是PHP的优势,从本质上说,这就是PHP
的独特之处。因此,我们很难期待PHP在近期内会对这一问题做出重大的改变。但是,在对待这个问题带来的一系列后果时,我们必须谨慎应对。
数据库访问接口
长期以来,PHP都缺乏一个象ADO或JDBC那样的统一的数据库访问接口。PHP在访问不同的数据库时,使用不同的专门API。例如,使用
mysql_connect函数连接MySQL,使用ora_logon函数连接Oracle。平心而论,这种方式并没有象我们想象的那样麻烦。
在真实项目中,把系统从一种数据库完全迁移到另一种数据库的要求是比较少见的,特别是对于LAMP这样的小型项目而言。而且,只要将访问数据库的代码进行了良好的封装,迁移的工作量也会较少。另外,使用专门API,在效率上多少会有一些优势。
虽然如此,PHP的开发人员仍然在努力构建PHP的统一的数据库访问接口。从PHP 5.1开始,PHP的发行包内置了PDO(PHP Data Objects,PHP数据对象)。PDO具有如下特性:
统一的数据库访问接口。PDO为访问不同的数据库提供了统一的接口,并且能够通过切换数据库驱动程序,方便地支持各种流行的数据库。
面向对象。PDO完全基于PHP 5的对象机制,因此区别于基于过程的专用API。
高性能。PDO的底层用C编写,比起用纯PHP开发的其他类似解决方案,有更高的性能。
一个典型的PDO应用如下例:
$pdo = new PDO("mysql:host=localhost;dbname=justtest", " mysql_user ", " mysql_password");
$query = "SELECT id, username FROM userinfo ORDER BY ID";
foreach ($pdo->query($query) as $row) {
echo $row['id']." | ".$row['username']."
";
}
PME模型
在大规模的程序设计中,组件(component)已经成为一种非常流行的技术。常见的组件技术都基于PME模型,即属性(Property)、方法(Method)和事件(Event)。
基于PME的组件技术可以方便地实现IoC(Inversion of Control,控制反转),是从IDE的plugin到应用服务器的“热发布”等许多技术的基础。
PHP从版本5开始,大大完善了对OO的支持,以前不能被应用的许多pattern现在都可以在PHP5中实现。因此,是否能够实现基于PHP的组件技术,也就成了一个值得讨论的问题。
下面对PHP对于PME模型的支持,逐一进行讨论:
属性(Property)
PHP并不支持类似Delphi或者C#的property语法,但这并不是问题。Java也不支持property语法,但是通过getXXX()和setXXX()的命名约定,同样可以支持属性。
PHP也可以通过这一方式来支持属性。但是,PHP提供了另一种也许更好的方法,那就是__set()和__get()方法。
在PHP中,每一个class都会自动继承__set()和__get()方法。它们的定义如下:
void __set ( string name, mixed value )
mixed __get ( string name )
这两个方法将在下列情况下被触发:当程序访问一个当前类没有显式定义的属性时。在这个时候,被访问的属性名称作为参数被传入相应的方法。任何类都可以重载__set()和__get()方法,以实现自己的功能。
如下例:
class PropertyTester {
public function __get($PropName) {
echo "Getting Property $PropNamen";
}
public function __set($PropName, $Value) {
echo "Setting Property $PropName to '$Value'n";
}
}
$Prop = new PropertyTester();
$Prop->Name;
$Prop->Name = "some string";
类PropertyTester重载了__set()和__get()方法,为了测试,仅仅将参数打印输出,没有做更多的工作。测试代码创建了
PropertyTester类的实例,并试图读写它并不存在的一个属性Name。此时,__set()和__get()相继被调用,并打印出相关参数。
它的输出结果如下:
Getting Property Name
Setting Property Name to 'some string'
基于这种机制,我们可以将属性的值放在一个private的List中,在读写属性时,通过重载__set()和__get()方法,读写List中的属性值。
但
是,__set()和__get()方法的有趣之处远不止及。通过这两个方法,可以实现动态属性,也就是不在程序中显式定义,而是在运行时动态生成的属
性。只要想想这种技术在OR
Mapping中的作用就能够明白它的重要性了。配合__call()方法(用于实现动态方法,在下一节中详述),它能够取代丑陋的代码生成器(code
generator)的大部分功能。
方法(Method)
PHP对方法的支持比较简单,没有太多可以讨论的。值得一提的是,PHP从版本5开始支持类的静态方法(static method),这使得程序员再也不用无谓地增加许多全局函数了。
事件(Event)
事件也许是PHP遇到的最复杂的问题。PHP并没有在语法层面提供对事件的支持,我们只能考虑通过别的途径来实现。因此,我们需要先对事件的概念和其他语言对事件的实现方式进行讨论。
事件模型可以简述如下:充当事件触发者的代码本身并不处理事件,而仅仅是在事件发生时,把程序控制权转交给事件的处理者,在事件处理完成后,再收回控制权。事件触发者本身并不知道事件将会被如何处理,在大多数情况下,事件触发者的代码要先于事件处理者的代码被完成。
在传统的面向过程的语言(例如C或者PASCAL)中,事件可以通过函数指针来实现。具体来说,事件触发者定义一个函数指针,这个函数指针可以
在以后被指向某个处理事件的函数。在事件发生时,调用该函数指针指向的处理函数,并将事件的上下文作为参数传入。处理完成后,控制权再回到事件触发者。
在面向对象的语言中,方法指针(指向某个类的方法的指针)取代了函数指针。以Delphi为例,事件处理的例子如下:
type
TNotifyEvent = procedure(Sender: TObject) of object;
TMainForm = class(TForm)
procedure ButtonClick(Sender: TObject);
…
End;
Var
MainForm: TMainForm;
OnClick: TNotifyEvent;
…
可以看出,TNotifyEvent被定义为所谓的过程类型(Procedural
Type),事实上就是一个方法指针。TMainForm的ButtonClick方法是一个事件处理者,符合TNotifyEvent的签名。
OnClick是一个事件触发者。在实际使用时,通过如下代码:
OnClick := MainForm.ButtonClick;
将MainForm.ButtonClick方法绑定到了OnClick事件。当OnClick事件触发时,MainForm.ButtonClick方法将被调用,并且将Sender(触发事件的组件对象)作为参数传入。
回到PHP,由于PHP不支持指针,因此无法使用函数指针这一技术。但是,PHP支持所谓的“函数变量”,可以把函数赋予某个变量,其作用类似于函数指针。如下例:
function EventHandler($Sender) {
echo "Calling EventHandler(), arv = $Sendern";
}
$Func = 'EventHandler';
$Func('Sender Name');
由于PHP是一种动态语言,变量可以为任何类型,所以无须先定义函数指针的类型作为事件的签名。直接定义了一个函数
EventHandler作为事件处理者,然后将它赋予变量$Func(注意直接使用了字符串形式的函数名),最后触发该事件,并将一个字符串
“Sender Name”传给它作为参数。输出的结果是:
Calling EventHandler(), arv = Sender Name
同样地,PHP也提供了类似方法指针的机制。如下例:
Class EventHandler {
public function DoEvent($Sender) {
echo "Calling EventHandler.DoEvent(), arg = $Sendern";
}
}
$EventHanler = new EventHandler();
$HandlerObject = $EventHanler;
$Method = 'DoEvent';
$HandlerObject->$Method('Sender Name');
由于PHP中没有能够直接引用对象方法的变量,因此需要使用两个变量来间接实现:$HandlerObject指向对象,$Method指向对象方法。通过$HandlerObject->$Method方式的调用,可以动态地指向任何对象方法。
为了使代码更加优雅和更适合复用,可以定义一个专门的类NotifyEvent,并使用一段新的调用代码:
final class NotifyEvent {
private $HandlerObject;
private $Method;
public function __construct($HandlerObject, $Method) {
$this->HandlerObject = $HandlerObject;
$this->Method = $Method;
}
public function Call($Sender) {
$Method = $this->Method;
$this->HandlerObject->$Method($Sender);
}
}
$EventHanler = new EventHandler();
$NotifyEvent = new NotifyEvent($EventHanler, 'DoEvent');
$NotifyEvent->Call('Sender Name');
NotifyEvent类定义了两个私有变量$HandlerObject和$Method,分别指向事件处理者对象和处理方法。在构造函数中对这两个变量赋值,再通过Call方法来调用。
熟悉C#的读者可以发现,NotifyEvent类与C#中的Delegate十分类似。Delegate超过NotifyEvent的地方在
于支持多播(Multicast),也就是一个事件可以绑定多个事件处理者。只要事件触发者自己维护一个NotifyEvent对象数组,支持多播也不是
一件难事。
至此,PHP对事件的支持已经得到了比较圆满的解决。但是,人的求知欲是无穷无尽的。还有没有可能通过其他的方式来实现事件呢?
除了方法指针,接口(interface)也可以用于实现事件。在Java中,这种技术被广泛应用。其核心思想是,将事件处理者的处理函数定义抽象为一个接口(相当于函数指针的签名),事件触发者针对这个接口编程,事件处理者则实现这个接口。
这种方式的好处在于,不需要语言支持函数指针或方法指针,让代码显得更加清晰和优雅,缺陷在于,实现同一种功能,要使用更多的代码。如下例:
interface IEventHandler {
public function DoEvent($Sender, $Arg);
}
class EventHanlerAdapter implements IEventHandler {
public function DoEvent($Sender, $Arg) {
echo "Calling EventHanlerAdapter.DoEvent(), Sender = $Sender, arg = $Argn";
}
}
class EventRaiser {
private $EventHanlerVar;
public function __construct($EventHanlerAdapter) {
$this->EventHanlerVar = $EventHanlerAdapter;
}
public function RaiseEvent() {
if ($this->EventHanlerVar != null) {
$this->EventHanlerVar->DoEvent($this, 'some string');
}
}
public function __tostring() {
return 'Object EventRaier';
}
}
$EventHanlerAdapter = new EventHanlerAdapter();
$EventRaiser = new EventRaiser($EventHanlerAdapter);
$EventRaiser->RaiseEvent();
首先定义了一个接口IEventHandler,它包含了方法的签名。EventHanlerAdapter类作为事件处理者,实现了这个接
口,并提供了相应的处理方法。EventRaiser类作为事件触发者,针对$EventHanlerVar变量(它应该是IEventHandler接
口类型,但是在PHP中不用显式定义)编码。
在实际应用中,将EventHanlerAdapter的实例作为参数赋予传给EventRaiser类的构造函数,当事件发生时,相应的处理方法将被调用。输出结果如下:
Calling EventHanlerAdapter.DoEvent(), Sender = Object EventRaier, arg = some string
最后,让我们回到现实世界中来。虽然我们用PHP完整地实现了PME模型,但是这到底有什么用呢?毕竟,我们不会用PHP去编写IDE,也不会用它编写应用服务器。回答是,基于PME模型的组件技术可以实现更加方便和更大规模的代码复用。
在基于PHP的应用系统中,虽然插件已经被广泛使用,但是通过组件技术可以实现功能更强大、更加规范和更容易维护的插件。此外,组件技术在实现一些大的Framework(例如,针对Web UI的Framework)时,也是不可或缺的。
Smarty
在任何Web应用中,如何将程序代码和界面设计,或者说,将逻辑层和表现层分离开来,都会是一个问题。对于PHP这种类型的嵌入网页的脚本语
言,这一问题尤其突出。在新手编写的代码中,把访问数据库的代码和操纵HTML元素的代码写在同一个页面里,是很常见的情况。为了避免这一问题,开发者倾
向于将涉及业务逻辑的代码封装在某些单独的库文件中,再在负责显示界面的文件中将它们include进来。但是,这仍然无法避免在显示界面的文件中包含大
量的PHP代码。究其所以然,是因为除了涉及业务逻辑的代码以外,即使仅仅在显示层,也往往涉及到复杂的显示逻辑。在一个典型的显示页面中,程序需要先包
含所有必需的库文件,初始化上下文环境,创建相关业务逻辑对象(假如数据库访问代码已经被业务逻辑对象封装,可以节省数据库相关的代码),最后在HTML
的空隙中把对象格式化为HTML元素进行显示。于是我们看到了无数这样的页面,在第一行HTML开始之前,就已经包含了数十行甚至更多的PHP代码,在
HTML的内部,仍然充满了各种各样的PHP代码。因此,PHP代码和HTML代码搅和在一块的问题仍然无法解决,对HTML的修改仍然可能导致整个
PHP程序崩溃。更加麻烦的是,这种不清晰的结构妨碍了PHP应用在规模上的进一步扩张。
为了解决这一问题,模板(template)技术应运而生。模板技术的基本原理是,通过一个解析器(parser),读取指定的模板文件(包含
了某些特定标签的HTML文件),将这些标签替换为相关的PHP变量,再输出为标准的HTML。通过这种方式,不但分离了业务逻辑层和表现层,而且也尽可
能地分离了显示逻辑和HTML代码。通过替换不同的模板文件,可以方便地生成各种格式的输出,例如HTML,XML,WML,后期稍加处理甚至可以生成
PDF和Flash。早期较为著名的PHP模板引擎有PHPLib中的Template和FastTemplate。
但是,模板技术也有其先天的缺陷。
◆无法彻底分离逻辑。显示逻辑和HTML代码很难通过简单的标签替换,实现彻底的分离。例如,遍历并显示一个数组,在PHP中可以用简单的
foreach语句实现,但是使用模板时,就需要进行对整个模板文件进行多次替换操作,造成效率的极大降低;或者根据不同的数据值显示不同的格式,如果模
板文件完全不包含PHP代码,那么将很难做到这一点。
◆解析导致的性能损失。由于每次PHP页面被访问时,解析器都必须对模板文件进行替换操作,无疑会降低PHP应用的性能。尤其在多次的替换操作时更是如此。因此,不使用模板比使用模板往往更加快速,这也是许多PHP程序员摒弃模板技术的原因之一。
在经过数年的发展之后,“编译型”的模板技术渐渐占据了主流。所谓“编译型”,是指解析器读取模板文件以后,并不直接生成静态HTML,而是“
编译”成一个新的PHP文件,并将它保存起来。以后访问该页面时,模板引擎会直接执行“编译”后的PHP文件。Smarty是这种模板引擎的代表。
针对以上的两个问题,Smarty作了如下处理:
◆独立语法。Smarty实现了一套自己的语法,这套语法不但支持变量替换和简单的判断,而且支持循环,修饰符(modifier),内置了很
多功能强大的函数,而且还支持自定义函数。这套系统保证Smarty能够完全独立地处理显示输出,无须再和PHP有什么瓜葛。事实上,在Smarty模板
中,是不能直接使用PHP代码的(通过显式定义可以使用),这也是一种强制分离逻辑层和表现层的方式。(理论上来说,Smarty的模板文件也可以应用于
其它语言。)但是,这种解决方式也受到了指责,因为Smarty的语法过于强大,几乎变成了一门新的语言,指责者认为,这反而增加了复杂性。但是,根据作
者的实际经验,Smarty的语法不但非常简单直观,而且只需要掌握一些最初级的语法,就足可以应付绝大多数的应用。即使是不懂编程的网页设计师,也很容
易就能够掌握。
◆编译机制。Smarty的“编译”机制,节省了用于反复解析模板文件的时间,极大地提高了速度。由于“编译”后生成的是标准的PHP文件,因
此从理论上来说,执行的速度不会低于没有模板的PHP应用的速度。在一些和解析型模板引擎进行的对比测试中,Smarty在第一次访问时落后,但是在以后
的访问中速度远远超出竞争对手。而且,这种编译过程是智能的,在模板文件的内容被改变后,Smarty会自动重新编译,因此对于开发者来说,编译过程完全
无需人工干预。另外,如果你愿意的话,生成的PHP文件还可以方便地应用于Zend Accelerator这样的工具,进行二次编译。
除此之外,Smarty还拥有其他一些优秀的特性:
◆缓存机制。由于实现了编译机制,在接收到对某个模板文件的访问请求时,Smarty会自动将它重定向到编译后的PHP文件。但是,这也意味
着,Smarty也可以将它重定向到任何其他的文件——例如静态的HTML文件。在此基础之上,Smarty实现了自己的基于页面的缓存机制。
Smarty能够将编译后的PHP文件产生的结果——静态HTML——保存起来,将重复发送的请求直接重定向给它,这意味着对于第一次之后的请求,不需要
执行任何PHP代码(Smarty本身的代码当然除外)。对于不需要频繁更新的页面(我们知道这种网页往往在整个网站中占大多数),通过这种缓存机制获取
的性能提升是惊人的。而且,由于它是在页面级实现的,因此完全无须涉及到复杂的对象级缓存问题,保持了逻辑上的简单性。
◆可配置性。Smarty在开发之初就将高度的可配置性作为自己的一个设计目标。它本身以100%的PHP编写,以源代码的方式发行,只需要将
Smarty简单地拷贝到你的文件路径中,就可以使用了。Smarty的各项配置变量,都可以通过修改config文件或者手动编码进行定制。例
如,Smarty默认的定界符是花括号({}),但是这往往和Javascript以及CSS中的花括号冲突。为了解决这一问题,可以简单地将默认定界符
修改为其他的字符(例如ASP风格的“<%”和“%>”)。
◆可扩展性。Smart的实现基于面向对象的架构,并且提供了插件机制,非常便于用户修改和扩展其默认的认为。当然,你也可以直接修改它的源代码来达到目的。(对于基于脚本语言的开源应用来说,这是非常惬意的,因为你甚至不需要重新编译。)
让我们来看一个最简单的Smarty应用。这个应用包括两个文件:
TestSmarty.php→调用Smarty类库,初始化变量,并解析相应的模板文件
TestSmarty.tpl→模板文件,其实就是包含了Smarty标签的HTML,放在指定的模板目录下,默认是./templates
TestSmarty.php的内容如下:
include_once("./smarty/Smarty.class.php");
$Smarty = new Smarty();
$Smarty->assign("HelloStr", "Hello, world");
$Smarty->display("TestSmarty.tpl");
?>
这个文件的内容非常简单,任何有过PHP经验的程序员都应该能够理解:首先将Smarty类库所在的文件include进来,然后创建一个新的Smarty对象,并对HelloStr变量进行赋值,最后解析TestSmarty.tpl文件。
TestSmarty.php的内容如下:
This is a string from Smarty: {$HelloStr}
解析的结果为:
This is a string from Smarty: Hello, world
此时检查存放编译后的PHP文件的子目录(默认是./templates_c),可以找到一个名叫%%65^650^65099D8B%%TestSmarty.tpl.php的文件,内容如下:
compiled from TestSmarty.tpl */ ?>
This is a string from Smarty: _tpl_vars['HelloStr']; ?>
这就是Smarty引擎编译生成的结果。
为了启用缓存,可以在TestSmarty.php文件中加入这么一行(当然必须在display方法之前):
$Smarty->caching = 1;
重新访问该页面,然后检查存放缓存文件的子目录(默认是./cache),可以找到一个名叫%%65^650^65099D8B%%TestSmarty.tpl的文件,内容如下:
136
a:4:{s:8:"template";a:1:{s:14:"TestSmarty.tpl";b:1;}s:9:"timestamp";i:1186888266;s:7:"expires";i:1186891866;s:13:"cache_serials";a:0:{}}This
is a string from Smarty: Hello, world
这就是生成的缓存文件,在静态的HTML文件之前,包含了已经序列化的PHP信息。虽然这些信息无法被直接阅读,但是多少还是能够猜测出来:模板的子目录,模板文件名,时间戳,生存期(过期时间),等等。如果读者有兴趣研究它们的详细定义,可以阅读Smarty的源代码。
注意,上述信息中包含了一项:生存期,即当前缓存在多长时间以后过期。Smarty默认的生存期是1小时,即3600秒。可以通过修改Smarty属性来设置生命期,代码如下:
$Smarty->cache_lifetime = 1800;
时间单位是秒,设置为1800表示当前缓存半小时后过期。
Smarty还支持为同一个模板创建多个缓存实例,这在实际应用中是非常常见的。举例来说,假设某个博客系统中,显示article的页面为
Article.php,对应的模板文件为Article.tpl。但是,article页面的内容根据不同的article
ID而不同,因此,必须为同一个页面创建不同的缓存实例。Smarty可以轻松做到这一点:
$Smarty->display("Article.tpl", $ArticleId);
只要将一个唯一标识符(在这个例子中是article的ID)作为第二个参数传给display方法,Smarty就会自动完成一切。
Smarty出现的时间虽然较老牌的PHPLib
Template和FastTemplate为晚,但是发展非常迅速,而且已经成为PHP的官方子项目,拥有二级域名
。正如它的官方站点上所说,与其说Smarty是一个模板引擎,不如说它是一个表现层的
Framework。这句话极为重要。
作者个人认为,Smarty诞生和逐渐取得主流地位的意义,不仅仅是提供了一个优秀的模板引擎,而是表示PHP在解决更大规模的应用上迈出了坚实的一步。我们可以看到,PHP,或者说LAMP,正在以稳健而持续的步伐,向企业级应用迈进。■
Zend Framework
从理论上来说,PHP是一种通用的动态语言,它可以替代Perl实现通用的脚本,甚至可以创建客户端GUI程序(通过GTK+)。但是,在实际应用
中,PHP在绝大多数情况下都被用来开发Web应用。即使在Java和.NET这样有软件业界巨头支持的重量级竞争对手面前,出身草莽的PHP也毫不逊
色,尤其是在应用最为广泛的轻量级web开发领域,PHP一直牢牢占据着领先的位置。在这一领域参与竞争的其他语言,例如Python和Perl,虽然各
具特色,但是仍然无法撼动PHP的地位。PHP是当之无愧的“Web开发第一语言”,而且似乎没有什么能对它构成挑战。随着web应用在软件界的地位越来
越重要,PHP也逐渐从脚本小子手中的玩具,转变成重要的工业语言,并获得了IBM这样的巨人的支持。
但是,就在近两三年,一种新的Web开发解决方案的迅速崛起,震动了整个业界,让PHP开始感到王座不稳。这个解决方案,当然就是Ruby on Rails。
本文无意对PHP和Ruby这两种语言进行全面的比较(虽然这的确是一个非常有意思的话题)。无论读者对Ruby和ROR持什么看法,有一点却是不争的事
实:Ruby作为一种新的语言,虽然极具特色,但是并没有得到业界的普遍接受;只有在ROR诞生以后,以其惊人的生产力征服了无数开发者,Ruby这才一
飞冲天。也就是说,Ruby至于在很大程度上是依靠ROR这个框架,才能起迅速崛起。
对于感到严重威胁的PHP社区,如果要对Ruby进行有效的反击,这似乎是一条可行的道路:基于PHP语言,实现一个新的web开发框架,能够达到甚至超
过ROR那样的生产力。这既是对Ruby的反击,其实也是PHP自身发展的必然结果。因为PHP在其发展过程中,早已经出现了数十个各种各样的框架,只是
尚没有一个能具有ROR那样的生产力和影响力。
但是,实现这样一个框架,也面临着不少的问题:
PHP语言本身能否完成这一任务?PHP原本只是用于快速解决web应用的简单脚本,虽然经过不断的发展,已经具有了许多高级的语言特征,但是是否能够实现象ROR那样精巧和强大的框架,能够达到ROR那样的生产力?
PHP社群能否接受这样一个框架?PHP的使用者,大多数是脚本小子出身,习惯于快速、敏捷、直观的开发方式,对于重量级的框架,未必能够普遍认同。
无论这些问题的答案如何,重要的是,有人这么做了。Zend Framework就是这一思路的结果。
如上所述,Zend Framework就是这样一个完全基于PHP语言的、向ROR学习的、针对web应用开发的框架。与其它同类的框架相比,Zend Framework有两个特点让它显得与众不同:
Zend
Framework由Zend公司开发,因此它是一个“官方的”框架。众所周知,Zend公司是PHP编译器的维护者,也是Zend
Optimizer、Zend Studio等一系列PHP相关产品的拥有者。由于这一关系,Zend
Framework虽然没有被内置到PHP发行包中,但也算得上PHP的官方解决方案了。(顺便说句题外话,Zend公司最近发布了Zend
Studio for Eclipse的beta版本,有兴趣的读者不妨尝试一下。)
Zend
Framework跟ROR实在太象了。和ROR一样,Zend Framework使用了同样的Front
Controller模式,同样的Model/Controller/View模型,同样的ORM思路,甚至连命名规则都十分相似。当然了,Zend
Framework跟ROR的不同之处还是很多的,至于底层的实现细节,肯定区别更大。但是,不管是有意还是无意(好吧,我承认“无意”绝对不可能做成这
样),Zend Framework的实现思路和ROR是非常相似的。
这是一个典型的Zend Framework应用的目录:
/root
/application
/controllers
/models
/views
/filters
/helpers
/scripts
/library
/public
/images
/scripts
/styles
其中,library目录下存放的是Zend
Framework所有的库文件,如果还有其他第三方的库文件,也可以存放在这里。Public目录是唯一一个可以通过web方式直接访问的目录,但它并
不存放php文件,只存放CSS、JavaScript脚本、图片等静态文件。Application目录中存放着实现应用逻辑的所有PHP文件,它又拥
有三个子目录:controllers、models、views。顾名思义,它们分别用于存放控制器/模型/视图的相应PHP文件。
Zend Framework实现了Front
Controller模式,这意味着,所有的http请求都会被转发到同一个入口,然后再路由到相应的控制器。这个入口就是root根目录下的
index.php文件。Zend Framework通过URL
Rewrite技术来实现这一点。在root根目录下,存在一个.htaccess文件,内容如下:
RewriteEngine on
RewriteRule .* index.php
php_flag magic_quotes_gpc off
php_flag register_globals off
熟悉apache的URL
Rewrite技术的读者,很容易理解它的意义:将所有的请求转发到index.php。当然,对于图片之类的静态文件的请求不需要被转发,这可以通过在
public目录下放置另一个.htaccess来覆盖上级目录的Rewrite规则。
(习惯在IIS下运行PHP的读者可能会问,Zend Framework是否可以在IIS下运行呢?由于IIS不支持基于.
htaccess的URL Rewrite,因此Zend
Framework无法简单地在IIS下运行。但是,由于IIS支持基于HttpModule的URL
Rewrite,因此理论上经过某些修改,是有可能让Zend Framework在IIS下运行的。有兴趣的读者可以自行尝试。)
Index.php的功能是将http请求转发到相关的控制器,另外,它还需要完成一些初始化时期的配置工作。它的内容如下:
error_reporting(E_ALL|E_STRICT);
date_default_timezone_set('Europe/London');
set_include_path('.' . PATH_SEPARATOR . './library'
. PATH_SEPARATOR . './application/models/'
. PATH_SEPARATOR . get_include_path());
include "Zend/Loader.php";
Zend_Loader::loadClass('Zend_Controller_Front');
// setup controller
$frontController = Zend_Controller_Front::getInstance();
$frontController->throwExceptions(true);
$frontController->setControllerDirectory('./application/controllers');
// run!
$frontController->dispatch();
?>
开始的几行,分别用于配置报错等级、设置时区、设置包含文件路径,以及载入相关文件。Zend_Loader::loadClass是一个静态方法,用于
装载指定的类。在载入Zend_Controller_Front以后,程序建立了这个类的实例,通知它抛出所有的异常,并且设置了控制器所在的目录。最
后,调用dispatch()方法,将http请求转发到相应的控制器。
在application/controllers目录下,存放着所有的控制器类。那么路由器是根据什么规则寻找相应的控制器呢?很简单,它按照一定的命名规范来进行匹配。下面是一个控制器文件的内容:
root/application/controllers/IndexController.php
class IndexController extends Zend_Controller_Action {
function indexAction() {
echo "
in IndexController::indexAction()
";
}
function addAction() {
echo "
in IndexController::addAction()
";
}
function editAction() {
echo "
in IndexController::editAction()
";
}
function deleteAction() {
echo "
in IndexController::deleteAction()
";
}
}
所有的控制器类都派生自Zend_Controller_Action。控制器名字必须大写字母开头,其他字母必须都为小写,最后加上
Controller结尾,并且存放在同名的php文件中。本例中的控制器是IndexController。(需要指出的是,“其他字母必须都为小写”
是一条很诡异的规则,这意味着CurrentUserController这样的名字是不合法的,无法被正确地路由。如果你这样做了,会是个很难发现的
bug。)
控制器拥有一个或多个行为(action),体现在代码上,行为是控制器类拥有的public方法,以小写字母开头,以Action结尾。在本例
中,indexAction、deleteAction、addAction和editAction是IndexController的四个行为。
在浏览器中,可以通过“控制器/行为”的URL,来访问相应的行为。如上例,URL和行为的对应关系是:
URL Action
IndexController::indexAction()
IndexController::indexAction()
IndexController::addAction()
IndexController::editAction()
IndexController::deleteAction()
可以看到,indexAction是一个控制器的缺省行为:如果没有指定行为,路由器将把请求转发到indexAction方法。同
样,IndexController是缺省的控制器:如果没有指定控制器,路由器将把请求转发到IndexController控制器。
在上例中,行为方法直接输出一行代码作为响应。在规范的应用中,与显示相关的代码应该存放在视图中。一个规范的控制器行为方法内容如下:
function indexAction() {
$view = new Zend_View();
$view->setScriptPath('./application/views/scripts’);
$view->title = "Hello world";
echo $view->render();
}
首先,创建了一个视图类(Zend_View)的实例,然后设置其路径,并对它的title属性赋值,最后调用render()方法,渲染相应的模板文件,将结果返回给浏览器。
同样的,视图根据特定的命名规范来匹配相应的模板文件。对于IndexController::indexAction()方法,视图会查找如下模板文
件:root/application/views/scripts/index/index.phtml。后缀名为.phtml的模板文件事实上就是一
个标准的PHP文件。(熟悉.rhtml后缀名的读者有何感觉?)在本例中,index.phtml的内容如下:
escape($this->title); ?> escape($this->title); ?>
最后,我们终于又一次看到了亲切的“Hello world”。
Zend
Framework使用Zend_Db_Table来实现类似于ActiveRecord的ORM功能。和任何ORM一样,首先需要为它配置数据库的相关
信息(服务器,用户名,密码,数据库名,等等)。虽然这并不复杂,但本文不拟讨论这些细节问题。(Zend
Framework在它的Roadmap中计划支持YAML!)一旦配置完成后,可以在/application/models子目录下创建和数据库表相
对应的模型类。
举例来说,在数据库中存在表user,结构如下:
CREATE TABLE user (
id int(11) NOT NULL auto_increment,
name varchar(100) NOT NULL,
password varchar(100) NOT NULL,
PRIMARY KEY (id)
)
相应地,可以在models目录下创建一个模型:
root/application/models/User.php
class User extends Zend_Db_Table
{
protected $_name = 'user';
}
所有数据库相关的代码,都已经被封装在抽象类Zend_Db_Table中。有ROR经验的读者,可以和ROR中的模型对比一下:
Class User < ActiveRecord::Base
End
Zend Framework的模型仅仅多出了一行,就是把数据库表名赋予$_name属性。
下面来看看怎么使用这个User模型类。新的indexAction方法如下:
function indexAction() {
…
$user = new User();
$view->user = $user->fetchAll();
echo $view->render();
}
User类的fetchAll()返回一个数组,包含该表中所有的内容。这个数组被存放在$view的user属性。接下来,看看新的index.phtml:
…
Id |
Name |
user as $user) : ?>
escape($user->id);?> |
escape($user->name);?> |
…
这段代码遍历了$this->user,并将它的属性依次打印出来。
除了基本的MVC模型以外,Zend Framework还提供了一系列高级功能,下面是一个不全面的列表:
Zend_Acl(access control list):实现了非常灵活的权限控制机制,通过对特定的角色和资源进行规则定义,可以方便地实现各种复杂的权限控制规则,并且权限可以按照角色的树型结构实现继承。
Zend_Cache:提供了一种通用的数据缓存方式,可以将任何数据缓存到文件系统、数据库、内存,能够与Memcached和SQLite这样的第三方软件很好地集成。
Zend_Log:提供了一种通用的log解决方案,支持多个log后端,支持log信息格式化,支持信息过滤。
Zend_Pdf:完全用PHP语言实现的PDF文件操作引擎。
Zend_Feed:封装了对RSS和ATOM的操作。
Zend_Gdata:封装了对Google Data API的操作。
Zend_Json:JSON是AJAX中必不可少的数据格式。Zend_Json封装了数据在PHP和JSON格式之间的转换操作。
Zend_Rest:REST是越来越流行的Web Service协议,有逐渐凌驾传统的SOAP协议之势。Zend_Rest封装了对REST的操作,可以方便地实现REST的客户端或服务端应用。
Zend_Search_Lucene:封装了对著名的全文检索引擎Lucene的操作。
Zend Framework在7月份发布1.0正式版,现在最新的版本为1.0.2。可以说,Zend Framework还是一个非常新的事物。但是在它漫长的测试期间,已经有不少人在尝试,甚至用于正式的项目,因为它的种种功能实在太让人馋涎欲滴了。
回到本文开头的两个问题,Zend
Framework的前途将会如何呢?虽然作者深知揣测未来的风险,但是还是很愿意尝试回答这两个问题:第一,Zend
Framework的出现,已经证明了PHP
5有能力实现大型的、复杂的框架,而由于PHP的编译器掌握在Zend公司手中,能够保证PHP语言会持续发展,以满足进一步的需求。第二,PHP本来就
已经在逐步进入企业开发的领域,出现一个重量级的Framework是必然的事情。也许PHP未来的开发主力,将由经过短期培训的、偏向快速解决问题的初
级程序员,变为经验丰富的、熟悉各种复杂的IT平台、侧重应用的安全性和健壮性的资深工程师。毕竟,只要经过不懈的努力,脚本小子也可能有成为大师的一
天,对吗?
至于Zend Framework和ROR之间的竞争,现在没有人能够给出答案。也许到目前为止,Zend
Framework还无法达到ROR
100%的生产力,但是它拥有更广大的用户基础,更成熟的语言,更短的学习曲线,类似C和Java的语法也更让人容易掌握。ROR中的DRY、“惯例重于
配置”等原则,在Zend Framework中也得了很好的秉承和发扬。无论它是否能够有效地狙击ROR,毫无疑问的是,一旦Zend
Framework被PHP社群广泛接受,PHP的生产力将会有极大的提高。
阅读(1203) | 评论(1) | 转发(0) |