我们邀请PHP安全专家 — 最新版Zend Frame的贡献者 — Chris Shiflett帮我们写一篇文章介绍一下ZF的主要细节,他爽快地答应了!
这份手把手的完整教程提供了用ZF构建实例的方法,并教你如何编写一个新闻管理系统。
在我们的论坛 上讨论ZF和这篇教程。
Brain BulbZend Framework发布了!虽然仍处于开发初期,这个教程仍突出讲解目前几个最好的功能,并指导你完成一个简单程序的构建。
Zend最早在社区里发布了ZF。基于同样的想法,这个教程写来用于展示ZF现有的功能。由于这个教程是在线发布,我将在ZF变化时对其进行更新,以便尽可能有效。
Zend Framework要求PHP5。为了更好利用本教程的代码,你还需要Apache网页服务器。因为示范程序(一个新闻管理系统)用到了mod_rewrite 。
这个教程的代码可以自由下载,所以你可以自己试一下。你可以从Brain Buld的网站下载到代码:http://brainbulb.com/zend-framework-tutorial.tar.gz 。
当你开始这篇教程时,你需要下载ZF的最新版本。你可以用浏览器手工从http://framework.zend.com/download 选择tar.gz 或zip 文件进行下载,或者使用下列命令:
$ wget http://framework.zend.com/download/tgz $ tar -xvzf ZendFramework-0.1.2.tar.gz
提示: Zend计划提供自有PEAR通道简化下载。
一旦你下载了预览版,把library 目录放到方便的地方。在这个教程,我把library 重命名为lib 以便有个简洁的目录结构:
app/ views/ controllers/ www/ .htaccess index.php lib/
www 目录是文档根目录,controllers 和views 目录是以后会用到的空目录,而lib 目录来自你下载的预览版。
我要介绍的第一个组件是Zend_Controller 。从很多方面看,它为你开发的程序提供了基础,同时也部分决定了Zend Framework不只是个组件的集合。但是,你在用之前需要将所有的得到的请求都放到一个简单的PHP脚本。本教程用的是mod_rewrite 。
用mod_rewrite 自身是一种艺术,但幸运的是,这个特殊的任务特别简单。如果你对mod_rewrite 或Apache的一般配置不熟悉,在文档根目录下创建一个.htaccess 文件,并添加以下内容:
RewriteEngine on RewriteRule !/.(js|ico|gif|jpg|png|css)$ index.php
提示: Zend_Controller 的一个TODO项目就是取消对 mod_rewrite 的依赖。为了提供一个预览版的范例,本教程用了 mod_rewrite 。
如果你直接把这些内容添加到httpd.conf ,你必须重启网页服务器。但如果你用.htaccess 文件,则什么都不必做。你可以放一些具体的文本到index.php 并访问任意路径如/foo/bar 做一下快速测试。如你的域名为example.org ,则访问http://example.org/foo/bar 。
你还要设置ZF库的路径到include_path 。你可以在php.ini 设置,也可以直接在你的.htaccess 文件放下列内容:
php_value include_path "/path/to/lib"
Zend 类包含了一些经常使用的静态方法的集合。下面是唯一一个你要手工添加的类:
<?php include 'Zend.php' ; ?>
一旦你包含了Zend.php ,你就已经包含了Zend 类的所有的类方法。用loadClass() 就可以简单地加载其它类。例如,加载Zend_Controller_Front 类:
<?php include 'Zend.php' ; Zend :: loadClass ( 'Zend_Controller_Front' ); ?>
include_path 能理解loadclass() 及ZF的组织和目录结构。我用它加载所有其它类。
使用这个controller非常直观。事实上,我写本教程时并没有用到它丰富的文档。
提示: 文档目前已经可以在 http://framework.zend.com/manual/zend.controller.html 看到。
我一开始是用一个叫Zend_Controller_Front 的front controller。为了理解它是怎么工作的,请把下列代码放在你的index.php 文件:
<?php include 'Zend.php' ; Zend :: loadClass ( 'Zend_Controller_Front' ); $controller = Zend_Controller_Front :: getInstance (); $controller -> setControllerDirectory ( '/path/to/controllers' ); $controller -> dispatch (); ?>
如果你更喜欢对象链结,可以用以下代码代替:
<?php include 'Zend.php' ; Zend :: loadClass ( 'Zend_Controller_Front' ); $controller = Zend_Controller_Front :: getInstance () -> setControllerDirectory ( '/path/to/controllers' ) -> dispatch (); ?>
现在如果你访问/foo/bar ,会有错误发生。没错!它让你知道发生了什么事。主要的问题是找不到IndexController.php 文件。
在你创建这个文件之前,应先理解一下ZF想让你怎样组织这些事情。ZF把访问请求给拆分开来。假如访问的是/foo/bar ,则foo 是controller,而bar 是action。它们的默认值都是index .
如果foo 是controller,ZF就会去查找controllers 目录下的FooController.php 文件。因为这个文件不存在,ZF就退回到IndexController.php 。结果都没有找到,就报错了。
接下来,在controllers 目录创建IndexController.php 文件(可以用setControllerDirectory() 设置):
<?php Zend :: loadClass ( 'Zend_Controller_Action' ); class IndexController extends Zend_Controller_Action { public function indexAction () { echo 'IndexController::indexAction()' ; } } ?>
就如刚才说明的,IndexController 类处理来自index controller或controller不存在的请求。indexAction() 方法处理action为index 的访问。要记住的是index 是controller和action的默认值。如果你访问/ ,/index 或/index/index ,indexAction() 方法就会被执行。 (最后面的斜杠并不会改变这个行为。) 而访问其他任何资源只会导致出错。
在继续做之前,还要在IndexController 加上另外一个有用的类方法。不管什么时候访问一个不存在的控制器,都要调用noRouteAction() 类方法。例如,在FooController.php 不存在的条件下,访问/foo/bar 就会执行noRouteAction() 。但是访问/index/foo 仍会出错,因为foo 是action,而不是controller.
将noRouteAction() 添加到IndexController.php :
<?php Zend :: loadClass ( 'Zend_Controller_Action' ); class IndexController extends Zend_Controller_Action { public function indexAction () { echo 'IndexController::indexAction()' ; } public function noRouteAction () { $this -> _redirect ( '/' ); } } ?>
例子中使用$this->_redirect('/') 来描述执行noRouteAction() 时,可能发生的行为。这会将对不存在controllers的访问重定向到根文档(首页)。
现在创建FooController.php :
<?php Zend :: loadClass ( 'Zend_Controller_Action' ); class FooController extends Zend_Controller_Action { public function indexAction () { echo 'FooController::indexAction()' ; } public function barAction () { echo 'FooController::barAction()' ; } } ?>
如果你再次访问/foo/bar ,你会发现执行了barAction() ,因为bar 是action。现在你不只支持了友好的URL,还可以只用几行代码就做得这么有条理。酷吧!
你也可以创建一个__call() 类方法来处理像/foo/baz 这样未定义的action。
<?php Zend :: loadClass ( 'Zend_Controller_Action' ); class FooController extends Zend_Controller_Action { public function indexAction () { echo 'FooController::indexAction()' ; } public function barAction () { echo 'FooController::barAction()' ; } public function __call ( $action , $arguments ) { echo 'FooController:__call()' ; } } ?>
现在你只要几行代码就可以很好地处理用户的访问了,准备好继续。
Zend_View 是一个用来帮助你组织好你的view逻辑的类。这对于模板-系统是不可知的,为了简单起见,本教程不使用模板。如果你喜欢的话,不妨用一下。
记住,现在所有的访问都是由front controller进行处理。因此应用框架已经存在了,另外也必须遵守它。为了展示Zend_View 的一个基本应用,将IndexController.php 修改如下:
<?php Zend :: loadClass ( 'Zend_Controller_Action' ); Zend :: loadClass ( 'Zend_View' ); class IndexController extends Zend_Controller_Action { public function indexAction () { $view = new Zend_View (); $view -> setScriptPath ( '/path/to/views' ); echo $view -> render ( 'example.php' ); } public function noRouteAction () { $this -> _redirect ( '/' ); } } ?>
在views 目录创建example.php 文件:
<html> <head> <title>This Is an Example</title> </head> <body> <p>This is an example.</p> </body> </html>
现在,如果你访问自己网站的根资源,你会看到example.php 的内容。这仍没什么用,但你要清楚你要在以一种结构和组织非常清楚的方式在开发网络应用。
为了让Zend_View 的应用更清楚一点,,修改你的模板(example.php )包含以下内容:
<html> <head> <title><?php echo $this -> escape ( $this -> title ); ?> </title> </head> <body> <?php echo $this -> escape ( $this -> body ); ?> </body> </html>
现在已经添加了两个功能。$this->escape() 类方法用于所有的输出。即使你自己创建输出,就像这个例子一样。避开所有输出也是一个很好的习惯,它可以在默认情况下帮助你防止跨站脚本攻击(XSS)。
$this->title 和$this->body 属性用来展示动态数据。这些也可以在controller中定义,所以我们修改IndexController.php 以指定它们:
<?php Zend :: loadClass ( 'Zend_Controller_Action' ); Zend :: loadClass ( 'Zend_View' ); class IndexController extends Zend_Controller_Action { public function indexAction () { $view = new Zend_View (); $view -> setScriptPath ( '/path/to/views' ); $view -> title = 'Dynamic Title' ; $view -> body = 'This is a dynamic body.' ; echo $view -> render ( 'example.php' ); } public function noRouteAction () { $this -> _redirect ( '/' ); } } ?>
现在你再次访问根目录,应该就可以看到模板所使用的这些值了。因为你在模板中使用的$this 就是在Zend_View 范围内所执行的实例。
要记住example.php 只是一个普通的PHP脚本,所以你完全可以做你想做的。只是应努力只在要求显示数据时才使用模板。你的controller (controller分发的模块)应处理你全部的业务逻辑。
在继续之前,我想做最后一个关于Zend_View 的提示。在controller的每个类方法内初始化$view 对象需要额外输入一些内容,而我们的主要目标是让快速开发网络应用更简单。如果所有模板都放在一个目录下,是否要在每个例子中都调用setScriptPath() 也存在争议。
幸运的是,Zend 类包含了一个寄存器来帮助减少工作量。你可以用register() 方法把你的$view 对象存储在寄存器:
<?php Zend :: register ( 'view' , $view ); ?>
用registry() 方法进行检索:
<?php $view = Zend :: registry ( 'view' ); ?>
基于这点,本教程使用寄存器。
本教程讨论的最后一个组件是Zend_InputFilter 。这个类提供了一种简单而有效的输入过滤方法。你可以通过提供一组待过滤数据来进行初始化。
<?php $filterPost = new Zend_InputFilter ( $_POST ); ?>
这会将($_POST )设置为NULL ,所以就不能直接进入了。Zend_InputFilter 提供了一个简单、集中的根据特定规则过滤数据的类方法集。例如,你可以用getAlpha() 来获取$_POST['name'] 中的字母:
<?php /* $_POST['name'] = 'John123Doe'; */ $filterPost = new Zend_InputFilter ( $_POST ); /* $_POST = NULL; */ $alphaName = $filterPost -> getAlpha ( 'name' ); /* $alphaName = 'JohnDoe'; */ ?>
每一个类方法的参数都是对应要过滤的元素的关键词。对象(例子中的$filterPost )可以保护数据不被篡改,并能更好地控制对数据的操作及一致性。因此,当你操纵输入数据,应始终使用Zend_InputFilter 。
提示: Zend_Filter 提供与 Zend_InputFilter 方法一样的静态方法。
虽然预览版提供了许多组件(甚至许多已经被开发),我们已经讨论了构建一个简单程序所需要的全部组件。在这里,你会对ZF的基本结构和设计有更清楚的理解。
每个人开发的程序都会有所不同,而Zend Framework试图包容这些差异。同样,这个教程是根据我的喜好写的,请根据自己的偏好自行调整。
当我开发程序时,我会先做界面。这并不意味着我把时间都花在标签、样式表和图片上,而是我从一个用户的角度去考虑问题。因此我把程序看成是页面的集合,每一页都是一个独立的网址。这个新闻系统就是由以下网址组成的:
/ /add/news /add/comment /admin /admin/approve /view/{id}
你可以直接把这些网址和controller联系起来。IndexController 列出新闻,AddController 添加新闻和评论,AdminController 处理一些如批准新闻之类的管理,ViewController 特定新闻和对应评论的显示。
如果你的FooController.php 还在,把它删除。修改IndexController.php ,为业务逻辑以添加相应的action和一些注释:
<?php Zend :: loadClass ( 'Zend_Controller_Action' ); class IndexController extends Zend_Controller_Action { public function indexAction () { /* List the news. */ } public function noRouteAction () { $this -> _redirect ( '/' ); } } ?>
接下来,创建AddController.php 文件:
<?php Zend :: loadClass ( 'Zend_Controller_Action' ); class AddController extends Zend_Controller_Action { function indexAction () { $this -> _redirect ( '/' ); } function commentAction () { /* Add a comment. */ } function newsAction () { /* Add news. */ } function __call ( $action , $arguments ) { $this -> _redirect ( '/' ); } } ?>
记住AddController 的indexAction() 方法不能调用。当访问/add 时会执行这个类方法。因为用户可以手工访问这个网址,这是有可能的,所以你要把用户重定向到主页、显示错误或你认为合适的行为。
接下来,创建AdminController.php 文件:
<?php Zend :: loadClass ( 'Zend_Controller_Action' ); class AdminController extends Zend_Controller_Action { function indexAction () { /* Display admin interface. */ } function approveAction () { /* Approve news. */ } function __call ( $action , $arguments ) { $this -> _redirect ( '/' ); } } ?>
最后,创建ViewController.php 文件:
<?php Zend :: loadClass ( 'Zend_Controller_Action' ); class ViewController extends Zend_Controller_Action { function indexAction () { $this -> _redirect ( '/' ); } function __call ( $id , $arguments ) { /* Display news and comments for $id. */ } } ?>
和AddController 一样,index() 方法不能调用,所以你可以使用你认为合适的action。ViewController 和其它的有点不同,因为你不知道什么才是有效的action。为了支持像/view/23 这样的网址,你要使用__call() 来支持动态action。
因为Zend Framework的数据库组件还不稳定,而我希望这个演示可以做得简单一点。我使用了一个简单的类,用SQLite进行新闻条目和评论的存储和查询。
<?php class Database { private $_db ; public function __construct ( $filename ) { $this -> _db = new SQLiteDatabase ( $filename ); } public function addComment ( $name , $comment , $newsId ) { $name = sqlite_escape_string ( $name ); $comment = sqlite_escape_string ( $comment ); $newsId = sqlite_escape_string ( $newsId ); $sql = "INSERT INTO comments (name, comment, newsId) VALUES ('$name', '$comment', '$newsId')" ; return $this -> _db -> query ( $sql ); } public function addNews ( $title , $content ) { $title = sqlite_escape_string ( $title ); $content = sqlite_escape_string ( $content ); $sql = "INSERT INTO news (title, content) VALUES ('$title', '$content')" ; return $this -> _db -> query ( $sql ); } public function approveNews ( $ids ) { foreach ( $ids as $id ) { $id = sqlite_escape_string ( $id ); $sql = "UPDATE news SET approval = 'T' WHERE id = '$id'" ; if (! $this -> _db -> query ( $sql )) { return FALSE ; } } return TRUE ; } public function getComments ( $newsId ) { $newsId = sqlite_escape_string ( $newsId ); $sql = "SELECT name, comment FROM comments WHERE newsId = '$newsId'" ; if ( $result = $this -> _db -> query ( $sql )) { return $result -> fetchAll (); } return FALSE ; } public function getNews ( $id = 'ALL' ) { $id = sqlite_escape_string ( $id ); switch ( $id ) { case 'ALL' : $sql = "SELECT id, title FROM news WHERE approval = 'T'" ; break; case 'NEW' : $sql = "SELECT * FROM news WHERE approval != 'T'" ; break; default: $sql = "SELECT * FROM news WHERE id = '$id'" ; break; } if ( $result = $this -> _db -> query ( $sql )) { if ( $result -> numRows () != 1 ) { return $result -> fetchAll (); } else { return $result -> fetch (); } } return FALSE ; } } ?>
(你可以用自己的解决方案随意替换这个类。这里只是为你提供一个完整示例的介绍,并非建议要这么实现。)
这个类的构造器需要SQLite数据库的完整路径和文件名,你必须自己进行创建。
<?php $db = new SQLiteDatabase ( '/path/to/db.sqlite' ); $db -> query ( "CREATE TABLE news ( id INTEGER PRIMARY KEY, title VARCHAR(255), content TEXT, approval CHAR(1) DEFAULT 'F' )" ); $db -> query ( "CREATE TABLE comments ( id INTEGER PRIMARY KEY, name VARCHAR(255), comment TEXT, newsId INTEGER )" ); ?>
你只需要做一次,以后直接给出Database 类构造器的完整路径和文件名即可:
<?php $db = new Database ( '/path/to/db.sqlite' ); ?>
为了进行整合,在lib 目录下创建Database.php ,loadClass() 就可以找到它。你的index.php 文件现在就会初始化$view 和$db 并存储到寄存器。你也可以创建__autoload() 函数自动加载你所需要的类:
<?php include 'Zend.php' ; function __autoload ( $class ) { Zend :: loadClass ( $class ); } $db = new Database ( '/path/to/db.sqlite' ); Zend :: register ( 'db' , $db ); $view = new Zend_View ; $view -> setScriptPath ( '/path/to/views' ); Zend :: register ( 'view' , $view ); $controller = Zend_Controller_Front :: getInstance () -> setControllerDirectory ( '/path/to/controllers' ) -> dispatch (); ?>
接下来,在views 目录创建一些简单的模板。index.php 可以用来显示index视图:
<html> <head> <title>News</title> </head> <body> <h1>News</h1> <?php foreach ( $this -> news as $entry ) { ?> <p> <a href="/view/<?php echo $this -> escape ( $entry [ 'id' ]); ?> "> <?php echo $this -> escape ( $entry [ 'title' ]); ?> </a> </p> <?php } ?> <h1>Add News</h1> <form action="/add/news" method="POST"> <p>Title:<br /><input type="text" name="title" /></p> <p>Content:<br /><textarea name="content"></textarea></p> <p><input type="submit" value="Add News" /></p> </form> </body> </html>
view.php 模板可以用来显示选定的新闻条目:
<html> <head> <title> <?php echo $this -> escape ( $this -> news [ 'title' ]); ?> </title> </head> <body> <h1> <?php echo $this -> escape ( $this -> news [ 'title' ]); ?> </h1> <p> <?php echo $this -> escape ( $this -> news [ 'content' ]); ?> </p> <h1>Comments</h1> <?php foreach ( $this -> comments as $comment ) { ?> <p> <?php echo $this -> escape ( $comment [ 'name' ]); ?> writes: </p> <blockquote> <?php echo $this -> escape ( $comment [ 'comment' ]); ?> </blockquote> <?php } ?> <h1>Add a Comment</h1> <form action="/add/comment" method="POST"> <input type="hidden" name="newsId" value="<?php echo $this -> escape ( $this -> id ); ?> " /> <p>Name:<br /><input type="text" name="name" /></p> <p>Comment:<br /><textarea name="comment"></textarea></p> <p><input type="submit" value="Add Comment" /></p> </form> </body> </html>
最后,admin.php 模板可以用来批准新闻条目:
<html> <head> <title>News Admin</title> </head> <body> <form action="/admin/approve" method="POST"> <?php foreach ( $this -> news as $entry ) { ?> <p> <input type="checkbox" name="ids[]" value="<?php echo $this -> escape ( $entry [ 'id' ]); ?> " /> <?php echo $this -> escape ( $entry [ 'title' ]); ?> <?php echo $this -> escape ( $entry [ 'content' ]); ?> </p> <?php } ?> <p> Password:<br /><input type="password" name="password" /> </p> <p><input type="submit" value="Approve" /></p> </form> </body> </html>
提示: 为了保持简单,这个表单用密码作为验证机制。
使用到模板的地方,你只需要把注释替换成几行代码。如IndexController.php 就变成下面这样:
<?php class IndexController extends Zend_Controller_Action { public function indexAction () { /* List the news. */ $db = Zend :: registry ( 'db' ); $view = Zend :: registry ( 'view' ); $view -> news = $db -> getNews (); echo $view -> render ( 'index.php' ); } public function noRouteAction () { $this -> _redirect ( '/' ); } } ?>
因为条理比较清楚,这个程序首页的整个业务逻辑只有四行代码。AddController.php 更复杂一点,它需要更多的代码:
<?php class AddController extends Zend_Controller_Action { function indexAction () { $this -> _redirect ( '/' ); } function commentAction () { /* Add a comment. */ $filterPost = new Zend_InputFilter ( $_POST ); $db = Zend :: registry ( 'db' ); $name = $filterPost -> getAlpha ( 'name' ); $comment = $filterPost -> noTags ( 'comment' ); $newsId = $filterPost -> getDigits ( 'newsId' ); $db -> addComment ( $name , $comment , $newsId ); $this -> _redirect ( "/view/$newsId" ); } function newsAction () { /* Add news. */ $filterPost = new Zend_InputFilter ( $_POST ); $db = Zend :: registry ( 'db' ); $title = $filterPost -> noTags ( 'title' ); $content = $filterPost -> noTags ( 'content' ); $db -> addNews ( $title , $content ); $this -> _redirect ( '/' ); } function __call ( $action , $arguments ) { $this -> _redirect ( '/' ); } } ?>
因为用户在提交表单后被重定向,这个controller不需要视图。
在AdminController.php ,你要处理显示管理界面和批准新闻两个action:
<?php class AdminController extends Zend_Controller_Action { function indexAction () { /* Display admin interface. */ $db = Zend :: registry ( 'db' ); $view = Zend :: registry ( 'view' ); $view -> news = $db -> getNews ( 'NEW' ); echo $view -> render ( 'admin.php' ); } function approveAction () { /* Approve news. */ $filterPost = new Zend_InputFilter ( $_POST ); $db = Zend :: registry ( 'db' ); if ( $filterPost -> getRaw ( 'password' ) == 'mypass' ) { $db -> approveNews ( $filterPost -> getRaw ( 'ids' )); $this -> _redirect ( '/' ); } else { echo 'The password is incorrect.' ; } } function __call ( $action , $arguments ) { $this -> _redirect ( '/' ); } } ?>
最后是ViewController.php :
<?php class ViewController extends Zend_Controller_Action { function indexAction () { $this -> _redirect ( '/' ); } function __call ( $id , $arguments ) { /* Display news and comments for $id. */ $id = Zend_Filter :: getDigits ( $id ); $db = Zend :: registry ( 'db' ); $view = Zend :: registry ( 'view' ); $view -> news = $db -> getNews ( $id ); $view -> comments = $db -> getComments ( $id ); $view -> id = $id ; echo $view -> render ( 'view.php' ); } } ?>
虽然很简单,但我们还是提供了一个功能较全的新闻和评论程序。最好的地方是由于有较好的设计,增加功能变得很简单。而且随着Zend Framework越来越成熟,只会变得更好。
这个教程只是讨论了ZF表面的一些功能,但现在也有一些其它的资源可供参考。在http://framework.zend.com/manual/ 有手册可以查询,Rob Allen在http://akrabat.com/zend-framework/ 介绍了一些他使用Zend Framework的经验,而Richard Thomas也在http://www.cyberlot.net/zendframenotes 提供了一些有用的笔记。如果你有自己的想法,可以访问Zend Framework的新论坛:http://www.phparch.com/discuss/index.php/f/289// 。
要对预览版进行评价是很容易的事,我在写这个教程时也遇到很多困难。总的来说,我想Zend Framework显示了承诺,加入的每个人都是想继续完善它。
提示 有什么评论、想法或问题,请访问Zend Framework的新论坛: http://www.phparch.com/discuss/index.php/f/289// 。
关于作者— Chris Shiflett是 Brain Bulb 的负责人,而Brain Bulb是一家专业的PHP开发和安全方面的顾问公司。Chris的blog是 http://shiflett.org 。