作者:Chris Shiflett
翻譯:ShiningRay
我們邀請了PHP安全專家——兼最新發佈的Zend Framework的貢獻者——Chris Shiflett來為我們寫一篇關於ZF主要特點的文章。
這份完整的、按部就班的教程通過向你展示如何應用框架寫出一個簡單的新聞管理系統,為你提供了構建實際應用的獨特視角。
Zend Framework終於掀開了其神秘的面紗!儘管它尚處於開發過程的早期階段,但本文將現在所能用的中最好的部分特別呈現給讀者,並通過構建一個簡單的應用這個過程引導你瞭解這個框架。
Zend很早就發佈了框架並引入社區運作。寫本指南只能針對框架今天的情況來列出其特點。因為本指南是在線發佈的,所以我會在框架發生變化的時候及時更新本文,這樣就能盡可能保持一致。
要求
Zend Framework 要求使用 PHP 5。為了能完全利用本指南中展示的代碼,你還需要Apache Web服務器,因為範例應用(一個新聞管理系統)用到了mod_rewrite。
本指南中的代碼可以自由下載,所以你可以親自嘗試一下。可以從Brain Bulb的網站上下載到:http://brainbulb.com/zend-framework-tutorial.tar.gz.
下載框架
在開始閱讀本指南之前,你還需要先下載一份框架的預覽發佈版。可以用瀏覽器察看 http://framework.zend.com/download 並選擇 tar.gz 或者 zip 文件手工下載,也可以使用下面的命令行:
清單1
$ wget http://framework.zend.com/download/tgz $ tar -xvzf ZendFramework-0.1.2.tar.gz
注意Zend已經計劃提供一個獨立的PEAR通道來方便下載。
下載了預覽發佈版之後,將這個庫的目錄放到一個方便的地方。在本教程中,我將庫的目錄名改稱了lib,以便提供一個簡單乾淨的目錄結構:
清單2
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文件,並加入一下指令:
清單3
RewriteEngine on RewriteRule !\.(js|ico|gif|jpg|png|css)$ index.php
Zend_Controller目前的任務之一就是去掉對mod_rewrite的依賴。為了提供一個預覽版可以使用的例子,本教程便使用了mod_rewrite。
如 果你直接在httpd.conf中添加這些指令,還必須重新啟動Web服務。但是,如果使用.htaccess文件,就不用了,而且這樣更好。你可以隨便 在index.php中寫點東西,然後任意請求某些路徑來測試一下,比如/foo/bar。例如,如果你的主機是example.org,那麼就請求 URL http://example.org/foo/bar。
可能你還想在 include_path 中包含框架庫的路徑。你可以直接在php.ini中配置,或者可以將以下指令放入.htaccess文件中:
清單4
php_value include_path "/path/to/lib"
Zend
Zend 類包含了一系列十分通用也十分有用的靜態方法。這是唯一一個需要手工進行包含的類:
清單5
<?php include ‘Zend.php’; ?>
一旦引用了Zend.php,就可以訪問Zend類中的所有方法了。使用loadClass()方法,載入其他類也變得簡單了。例如載入Zend_Controller_Front類:
清單6
<?php include ‘Zend.php’; Zend::loadClass(‘Zend_Controller_Front’); ?>
loadClass()方法會考慮到include_path,同時它還知道框架的目錄組織結構。我就使用它來載入所有其他的類。
Zend_Controller
這個控制器的使用還是比較直觀的。實際上,我寫這份指南的時候可沒有官方文檔可用!
現在ZF網站上已經有官方文檔了。
首先講Zend_Controller_Front,這是一個前端控制器。你可以將下面的代碼放入index.php中來理解它是如何工作的:
清單7
<?php include ‘Zend.php’; Zend::loadClass(‘Zend_Controller_Front’); $controller = Zend_Controller_Front::getInstance(); $controller->setControllerDirectory(‘/path/to/controllers’); $controller->dispatch(); ?>
如果你更希望使用對像鏈方式,可以如下改寫:
清單8
<?php include ‘Zend.php’; Zend::loadClass(‘Zend_Controller_Front’); $controller = Zend_Controller_Front::getInstance() ->setControllerDirectory(‘/path/to/controllers’) ->dispatch(); ?>
現在,當進行/foo/bar的請求時,就會出現一個錯誤。這很好!它至少能告訴你有動作了。主要的問題是未發現IndexController.php。
在創建這個文件之前,最好首先瞭解框架組織東西的方式。框架會將一個請求分解成幾個部分,在這個例子中,請求/foo/bar,foo是控制器,bar是動作。兩者的默認值都是index。
當foo作為控制器時,框架首先在控制器目錄中查找名叫FooController.php的文件。因為不存在這個文件,於是框架退一步查找IndexController.php。如果還是沒有發現,就報告錯誤。
下面,在controllers目錄(可以使用setControllerDirectory()自己設置)中創建IndexController.php:
清單9
<?php Zend::loadClass(‘Zend_Controller_Action’); class IndexController extends Zend_Controller_Action { public function indexAction() { echo ‘IndexController::indexAction()’; } } ?>
IndexController類處理控制器為index的請求或者指定的控制器不存在的請求,就像剛才所說的。indexAction()方 法將處理動作為index的請求。記住無論是控制器還是動作,默認的值都是index,前面也說過了。如果嘗試請求/、/index或者 /index/index,都會執行indexAction()方法(結尾的斜槓不會改變這種行為)。對任何其他資源的請求都可能會產生錯誤。
繼 續之前還要為IndexController添加一個很有用的方法noRouteAction()。一旦請求某個控制器且其不存在時便調用 noRouteAction()方法。例如,請求/foo/bar時,如果FooController.php不存在,那麼就會執行 noRouteAction()。不過,/index/foo的請求還是會產生錯誤,因為這裡foo是一個動作,而非控制器。
在IndexController中添加 noRouteAction() :
清單10
<?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()中一般可能出現的動作。這可令對不存在的控制器的請求被重定向到根文檔(首頁)。
現在來創建 FooController.php:
清單11
<?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。這樣不僅可以支持友好的URL,同時也可以用極少的代碼就可以很好得進行組織。酷啊!
還可以創建一個__call()方法來處理未定義的動作的請求,如 /foo/baz:
清單12
<?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
Zend_View是一個可以協助你組織視圖邏輯的類。它並不使用特定的模版系統,為了簡便起見,在本例中我也不會使用模版系統。然而,你可以隨意使用你喜歡的。
記住所有進入的請求都是由前端控制器來處理的。因此,既然應用程序的框架已經這樣存在了,那麼以後的天價都必須適應它。為了演示Zend_View最基本的用法,我們將IndexController.php中的代碼修改成這樣:
清單13
<?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的文件:
清單14 –
<html> <head> <title>This Is an Example</title> </head> <body> <p>This is an example.</p> </body> </html>
現在,當請求網站的根的資源時,應該可以看到example.php的內容。雖然現在這還不是很有用,不過記住你的工作目標是按照一個結構化、有組織的方法來開發Web應用。
為了更清楚地利用Zend_View,將模版(example.php)修改一下,包含一些數據:
清單15
<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特性在這裡是用於演示動態數據的。它們應該在控制器中定義,所以讓我們修改IndexController.php來給他們賦值:
清單16
<?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腳本,所以你可以在其中做任意你想做的事情。最好還是盡量遵守規則,僅僅在模版中進行顯示數據的工作。控制器(或者是控制器進行分配的模塊)則應該承擔所有的業務邏輯。
在繼續講之前,我要最後對Zend_View做一個補充。在每個控制器的方法中實例化$view對像會要額外輸入很多東西,而我們的主要目標是更簡單、更快速地進行開發。而且,如果模版都存放在同一個目錄下的話,每次都要調用setScriptPath()也是一件麻煩事。
幸好,Zend類包含了一個註冊表,它可以幫助我們消除這種反覆的工作。你可以使用register()方法將$view對像存放在註冊表中:
清單17
<?php Zend::register(‘view’, $view); ?>
可以使用registry()方法來獲取它:
清單18
<?php $view = Zend::registry(‘view’); ?>
從今往後,本教程就會使用註冊表了。
Zend_InputFilter
The last component this tutorial covers is Zend_InputFilter. This class provides a simple but rigid approach to input filtering. You instantiate it by passing an array of data to be filtered:
清單19
<?php $filterPost = new Zend_InputFilter($_POST); ?>
他會把數組($_POST)設置為NULL,這樣就不可能再直接進行訪問了。Zend_InputFilter提供了一些根據特定條件篩選數據的方法。例如,如果你需要$_POST['name']都是字母,可以使用getAlpha()方法:
清單20
<?php /* $_POST{{’name’}} = ‘John123Doe’; */ $filterPost = new Zend_InputFilter($_POST); /* $_POST = NULL; */ $alphaName = $filterPost->getAlpha(‘name’); /* $alphaName = ‘JohnDoe’; */ ?>
傳給每一個篩選方法的參數是對應要進行篩選的數組元素的鍵。該對像(在這個例子中是$filterPost)是一個包含了有錯誤的數據的保護籠,這使得對數據的訪問更加可控和一致。因此,當要訪問輸入數據的時候,應該使用Zend_InputFilter。
Zend_Filter提供了一些靜態的過濾方法,這些方法和Zend_InputFilter的方法遵循同樣的規則。
創建一個新聞管理系統
儘管預覽版還包含了很多組件(甚至還有更多的正在進行開發),但是只需要使用前面討論過的組件就可以構建簡單的應用了。在構建應用的過程中,你就會對框架的基本結構和設計有一個更加清晰的理解。
每個人開發應用的方式都有所不同,所以Zend Framework盡可能地嘗試包容這些差異。同樣,本教程是根據我的偏好寫的,所以你可以將它們調整為適合自己口味的方式。
當我開始開發某個應用時,我是先從界面開始的。這並不是說我喜歡做表面文章,花大量功夫在樣式、圖片上,其實我是站在一個用戶的角度來透視問題。這樣,我將一個應用看作是一系列頁面的集合,每個頁面對應一個唯一的URL。這個新聞管理系統有以下一些URL組成:
清單21
/
/add/news
/add/comment
/admin
/admin/approve
/view/{id}
馬上開始根據控制器考慮這些URL。IndexController顯示新聞、AddController處理新聞和評論的添加,AdminController處理諸如審批新聞之類的管理工作,以及ViewController用於查看指定的新聞條目和相應的評論。
首先,如果有FooController.php,先將其刪除,並修改IndexController.php並添加合適的動作,並加入一些關於業務邏輯的註釋放著:
清單22
<?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:
清單23
<?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時才會調用這個方法。因為用戶也許會手工輸入URL,還是有可能會被調用的,所以這時應該將用戶重定向到首頁、顯示一個錯誤或者你覺得合適的動作。
下面,創建AdminController.php:
清單24 –
<?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:
清單25
<?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()方法應該是不可能會被調用的,所以可以在其中安排任何動作。ViewController和其他的有些不同,因為還不知道什麼是有效的動作。為了能支持類似/view/23這樣的URL,必須使用__call()來支持動態動作。
與數據庫進行交互
因為Zend Framework的數據庫組件相對還不太穩定,同時我又希望例子更容易使用,所以我使用了SQLite的類來存儲和獲取新聞條目以及評論:
清單26
<?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數據庫的完整路徑和文件名,數據庫必須事先創建好:
清單27
<?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類就行了:
清單28
<?php $db = new Database(‘/path/to/db.sqlite’); ?>
整合
為了能將所有的東西放到一起,先在lib目錄中創建Database.php,這樣loadClass()就能找到它了。而index.php文件現在要實例化$view和$db並將它們存儲在註冊表中。也可以創建一個叫做__autoload()的函數來自動加載所有需要用到的類:
清單29
<?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(); ?>
下面,在視圖目錄中創建一些簡單的模版。index.php文件可以用於顯示index視圖:
清單30 –
<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 模版可以用於顯示特定的新聞條目:
清單31
<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模版可以用於審批新聞條目:
清單32
<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就變成了:
清單33
<?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還要再處理一下,需要更多的代碼:
清單34
<?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(‘/’); } } ?>
因為用戶在提交了一個表單之後被重定向了,所以在這個控制器裡面不需要視圖。
在AdminController.php中,要處理兩個動作,顯示管理界面和審批新聞:
清單35
<?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:
清單36
<?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的逐漸程序,各方面都會變得更好。
Written by 傻仔仔
