本文发表于《程序员》杂志2007年第12期,略有删节。
PHP沉思录之四:Zend Framework
左轻侯
2007.11.11
从理论上来讲,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请求转发到相关的控制器,另外,它还须要完成一些初始化时期的配置工做。它的内容以下:
<?php
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
<?php
class IndexController extends Zend_Controller_Action {
function indexAction() {
echo "<p>in IndexController::indexAction()</p>";
}
function addAction() {
echo "<p>in IndexController::addAction()</p>";
}
function editAction() {
echo "<p>in IndexController::editAction()</p>";
}
function deleteAction() {
echo "<p>in IndexController::deleteAction()</p>";
}
}
全部的控制器类都派生自Zend_Controller_Action。控制器名字必须大写字母开头,其余字母必须都为小写,最后加上Controller结尾,而且存放在同名的php文件中。本例中的控制器是IndexController。(须要指出的是,“其余字母必须都为小写”是一条很诡异的规则,这意味着CurrentUserController这样的名字是不合法的,没法被正确地路由。若是你这样作了,会是个很难发现的bug。)
控制器拥有一个或多个行为(action),体如今代码上,行为是控制器类拥有的public方法,以小写字母开头,以Action结尾。在本例中,indexAction、deleteAction、addAction和editAction是IndexController的四个行为。
在浏览器中,能够经过“控制器/行为”的URL,来访问相应的行为。如上例,URL和行为的对应关系是:
URL Action
http://localhost/root/
IndexController::indexAction()
http://localhost/root/index
IndexController::indexAction()
http://localhost/root/index/add IndexController::addAction()
http://localhost/root/index/edit IndexController::editAction()
http://localhost/root/index/delete 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的内容以下:
<html>
<head>
<title><?php echo $this->escape($this->title); ?></title>
</head>
<body>
<h1><?php echo $this->escape($this->title); ?></h1>
</body>
</html>
最后,咱们终于又一次看到了亲切的“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
<?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:
…
<table>
<tr>
<th>Id</th>
<th>Name</th>
</tr>
<?php foreach($this->user as $user) : ?>
<tr>
<td><?php echo $this->escape($user->id);?></td>
<td><?php echo $this->escape($user->name);?></td>
</tr>
<?php endforeach; ?>
</table>
…
这段代码遍历了$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的生产力将会有极大的提升。 php