2008年06月23日
[PHP-ZF] 解決 ZF 在 Proxy 模式下無法正確解析 Controller Name 的問題
說明
因為要和客戶的內部網路連線,便透過 PieTTY 的 SSH Tunnels 模式來做 Proxy 。 但是這個方式卻讓 Zend Framework 出現了以下的錯誤訊息:
An error occurred with this request: Invalid controller specified (http:).
解決方式是修改 Zend Framework 的 Zend_Controller_Request_Http 這個類別。
先開啟 Zend/Controller/Request/Http.php ,並找到:
$requestUri = $_SERVER['REQUEST_URI'];
改為:
$requestUri = preg_replace(
'/^https?:\/\/' . $_SERVER['HTTP_HOST'] . '/i',
'',
$_SERVER['REQUEST_URI']
);
這樣就能避掉 Proxy 所帶來的問題了。
註 1 :這個方法的風險在於會修改原生的 ZF 程式碼,如果會用到 Subversion 來管理程式碼的話,要特別注意未來更新版本時可能會發生衝突的問題。
註 2 :未來版本應該會修正這個問題。
參考
ZF MVC is not compatible with apache + http protocol proxy requests
2008年03月31日
[PHP-ZF] 如何學習 Zend Framework
今天又被問到怎麼開始學 Zend Framework ,我是覺得要上手並不難,但是基礎一定要有。
那麼哪些東西算是基礎呢?當然 HTML 、 JavaScript (Ajax) 或 CSS 等必備技能就不必多說了;以下我介紹幾個我個人認為滿重要的部份以及學習的次序:
- 物件導向
-
學習 Zend Framework 時,絕大部份時間你一定會接觸到物件導向,因此物件導向的基本觀念是一定要有的;否則你在運用 ZF 的各種套件時,一定會陷入困惑的泥沼中。
至於物件導向的三大要素:封裝、繼承和多型,詳細的說明在書本或網路有很多,我就不在這裡野人獻曝囉。這裡推薦:「深入淺出 Java 程式設計」,其中幾章的物件導向概念值得一讀。
- 設計模式
-
有了物件導向的觀念後,我強烈建議去瞭解設計模式,因為在 Zend Framework 的套件在設計上用到了許多設計模式。而且我個人也認為適當運用設計模式,能讓我們的程式更強固且具有彈性。
不過我很難用明確的字眼去表達設計模式是什麼東西,那遠遠超出我的能力範圍。因此我推薦這本:「深入淺出設計模式」,它簡單易懂的範例與說明,看完後保證你在物件導向上的功力能增進一甲子。
- MVC 概念
-
MVC 觀念常常是傳統 PHP 開發者所不瞭解的,因為如果能快速把工作完成,誰會想去搞一個看起來那麼複雜的玩意兒?但是 MVC 在運用得當的情況下,卻能讓程式的意圖更加清晰,而且也更容易維護。
事實上 MVC 也是設計模式的一種,而上面那本書裡裡也有提到,建議大家參考看看。不過 Zend Framework 的 WebMVC 在實作上的概念上有稍有不同,我個人自薦這篇:「透視 WebMVC 」;該文會告訴你如何將舊有的 PHP 專案轉換成 MVC 架構。
- PHP5
-
由於 Zend Framework 在開發上採用了 PHP5 的特性,所以 PHP5 也變成學習 Zend Framework 時最重要的基礎之一。而 PHP5 在物件導向也著墨甚多,在語法上也參考 Java 的許多特點,學過 Java 的朋友一定倍感親切。
參考資料方面我個人推薦官方手冊的 PHP5 的 Classes and Objects 一節,另外還有 Standard PHP Library (SPL) Functions 。
- 一定的開發經驗
-
Zend Framework 已經幫我們把很多東西都包裝好了,因此我們很多時候都可以不用重造輪子。但是它只是個工具,而且也不是萬能的,因此有些東西還是需要靠你自己來;所以對於 Web 開發你就必須要有一定程度的瞭解,至少你得曾經獨力完成一個以上的專案。
- 入門教學
-
假設以上你都有了一定的基礎,那麼你還需要臨門的一腳。一篇好的入門文章絕對是你學習 Zend Framework 必要的利器之一,因為它能快速帶你進入 Zend Framework 的世界裡。
這裡就不能不提 Rob Allen 寫的文章了,這篇「 Getting Started with Zend Framework 」是學習 Zend Framework 的你一定要載回來仔細研究的好文!不過官方也提供了一篇 Quick Start ,你可以將它和 Rob Allen 的文章交互參考看看 (我個人覺得 Rob 的比較完整) 。
- 參考手冊與源代碼
-
如果你已經入門了,那可別鬆懈。因為 Zend Framework 裡面有許多好用的工具,只是看你懂不懂得去運用而已;所以官方的參考手冊和 API 說明是一定要去看的。而源代碼更是重要,因為當官方手冊的範例或 API 說明明顯不足時,你可以從源代碼裡挖出一些不為人知的秘技,更可以學到許多高深的技巧!
當然以上的基礎不是絕對的,這僅僅是我個人的一些建議而已。希望這篇簡單的說明能幫助大家順利學習 Zend Framework ,也期盼大家能一起分享更多的學習心得。
2008年03月17日
Zend Framework 1.5 正式推出
Zend Framework 1.5 正式推出了!
首先官方網站大改版!變得更漂亮了!而且也提供了官方版的快速入門,讓剛接觸的朋友也能很容易瞭解 ZF 1.5 的能耐。
主要更新特色如下:
-
Forms - 提供類似 PEAR 的 HTML_QuickForm 。
-
Layout - 提供類似 ASP.Net 的 Master Page 模式。
-
LDAP/OpenID authentication - 讓應用程式可以整合 LDAP 及 OpenID 的身份驗證。
-
Lucene search engine - 加強了搜尋語法。
-
Extended Ajax support - 支援動態的 Ajax 內容切換。
-
UTF-8 character sets in Adobe PDF creation - PDF 也能支援 UTF-8 字集。
-
Google GData web services - 加強了 GData 的操作。
-
Table Select - 能夠使用 Select 來操作 Table 了。
然而之前說 1.5 可能會將 Command Line Build 放進來,但是很可惜的是時程沒趕上。不過上面這些 ZF 1.5 的新特色,很值得有用 ZF 開發的朋友參考看看喔。
其他詳細消息請參考: Zend Technologies Releases Zend Framework 1.5
註:剛剛看到 Rob 更新了他的教學: Updated Tutorial for Zend Framework 1.5 。
2008年02月29日
[PHP-ZF] 目錄結構的安排
Zend Framework 目錄結構的安排 (Directory Layout) 也是一個很讓初學者困擾的問題,而且不論是官方或是社群都推薦了很多方法,更讓剛接觸 Zend Framework 的朋友難以適從。
本文將探討 Zend Framework 目錄結構的特點以及幾個常見的目錄結構,讓各位開發者能夠對它們有所認知。
為什麼要有目錄結構
目錄結構主要目的如下:
-
將程式及相關文件分門別類,以便於管理。
-
便於設定相關存取權限。
-
讓開發者很快能找到要修改的檔案。
因此選定一個好的目錄結構,將會為你的專案開發帶來高效率且易維護等好處。
專案目錄要放在哪裡
有些目錄結構需要有修改 Apache httpd.conf 的權限,有些則不需要;也因此專案目錄的存放位置也變得很重要,因為這會牽扯到安全性的問題。
一般來說,需要修改 httpd.conf 權限的目錄結構類型,我們可以放在檔案系統的任何地方,並確定它能讓 Apache 正確存取與執行。而不需要修改 httpd.conf 的類型,通常是為了因應專案的可攜性或是開發者缺少系統管理權;這時專案目錄就要放在 Apache 預設提供可以存取的位置,例如 /home/username/htdocs 。
常見的目錄結構
以下我們來看看幾個常見的目錄結構。
類型一:官方手冊的版本
官方手冊的「 7.1. Zend_Controller Quick Start 」一節裡提供了一個參考用的 Directory Layout ,我稍微做了一些修改,如下:
application/
controllers/
IndexController.php
ErrorController.php
models/
views/
scripts/
index/
index.phtml
error/
error.phtml
helpers/
filters/
library/
Zend/
(other libraries)/
html/
scripts/
images/
styles/
.htaccess (導向 index.php)
index.php
說明
這是一個需要修改 httpd.conf 的目錄結構,也僅適用於單一功能的應用程式 (也就是單一模組) 。在安裝時我們必須修改 httpd.conf ,將專案的 Document Root 指向 html 這個目錄。 html 目錄除了做為 Document Root 外,其目地就是存放可公開的檔案,像是圖片、 JavaScript 或樣式表等等。而 application 目錄即為預設的模組,存放著 MVC 等三種類型的檔案 (即 controllers 、 models 及 views 等三個目錄) 。至於 library 目錄就比較隨意,我們只要確定它的執行權限,並且讓 PHP 能夠找到裡面所需要的類別即可。
在預設模組中, controllers 目錄裡的 IndexController.php 與 ErrorController.php 都是一定要的,另外 views/scripts/index/index.phtml 和 views/scripts/error/error.phtml 也是如此;而原因可以參考 Front Controller 的基本應用技一文,這裡不再詳述。
這裡要特別說明 application/views 這個目錄,其中還區分成 scripts 、 helpers 及 filters 三個目錄。 scripts 主要放的就是 templates ,底下可以用小寫的 controller name 做為資料夾名稱以做為區別;而 helpers 和 filters 其實在實際應用上比較少用,所以通常我會把它們從結構中拿掉。
註:為了節省篇幅,在呈現目錄結構時,預設模組的相關必要檔案我會略掉,但實際應用時請記得加回去。
另外 .htaccess (導向 index.php) 的實際內容如下:
RewriteEngine on
RewriteRule !\.(js|ico|gif|jpg|png|css)$ index.php
php_flag magic_quotes_gpc off
php_flag register_globals off
在 .htaccess 的第一行中,由於 Zend Framework 支援自訂網址的功能,所以我們啟用了 RewriteEngine ,也就是設定為 on 。接著第二行我們加入一個 RewriteRule ,讓 index.php 處理所有非公開資源的 request 。
註:不過 Zend Framework 也支援沒有 mod_rewrite 的環境,我們只要在 Front Controller 中將 baseUrl 設定好就沒問題了,這點有機會再探討。
剩下兩行是 PHP 的設定,這邊就不再加以說明。
這種目錄結構最大的特色是將 index.php 放在 html 目錄下, 這樣的好處在於能提高對 application 目錄的安全掌控度,因為 application 目錄並不會直接曝露給外部的瀏覽者。但是這樣的結構也有一個很大的缺點,那就是可攜性很差!換句話說如果在測試環境所使用的網址是 http://localhost/myproject/ ,而正式上線的環境卻是 http://www.myproject.com/ ,那麼就要修改 httpd.conf 讓 html 目錄對應到正確的網址,這樣才有辦法讓程式正確執行。
類型二: Rob Allen 的版本
在 Rob 的 Getting Started with the Zend Framework 一文中,他採用了和官方不同的目錄結構:
application/
.htaccess (deny from all)
controllers/
models/
views/
filters/
helpers/
scripts/
library/
Zend/
(other libraries)/
.htaccess (deny from all)
public/
.htaccess (RewriteEngine off)
images/
scripts/
styles/
.htaccess (導向 index.php)
index.php
說明
這種目錄結構也是屬於單一功能 (模組) 的類型,它的特色是不必在 httpd.conf 中指定 Document Root 。這種目錄結構將 application 、 library 、 public (即第一種版本的 html) 以及 index.php 放在同一層的專案根目錄下,再利用 .htaccess 去控制每個目錄結構的存取權。
.htaccess (導向 index.php) 內容和官方版相同,請參考上面的說明。比較特別的是 application/.htacess 的內容為 deny from all ,這是要避免用戶直接從網址瀏覽 application 內的檔案;而 public/.htaccess 的內容則是 RewriteEngine off ,亦即關閉原本 .htaccess (導向 index.php) 對此目錄的影響,讓用戶可以直接瀏覽 public 內的檔案 (就是圖片、 JavaScript 及樣式表等等) 。
index.php 則是從 public 目錄中往外移至專案根目錄,這樣就可以讓用戶直接存取 (因為一般我們會在 Apache 設定 index.php 為預設執行頁) 。
這種目錄結構剛好和官方版本相反,它非常俱有可攜性,也是一般開發 Zend Framework 專案常用的目錄結構型態。不過它也有很大的缺點,那就是當 mod_rewrite 被關閉或是 .htaccess 不被解析時,這種目錄會將 application 及 library 兩個目錄曝露出來,在安全性上就大打折扣;不過通常這樣的 httpd.conf 設定比較少見,也因此一般開發 Zend Framework 專案時,大多都會採用這種目錄結構。
類型三: Gavin Vess 的 Conventional Modular 版本
前面兩種版本都是單一功能 (模組) ,不過如果專案需要前後台或多種功能時,通常就會使用模組機制。Zend Framework 支援了模組機制,因此在目錄結構上也需要做一些改變。
有一篇 Gavin Vess 所寫的 Choosing Your Application's Directory Layout 也討論到了有關 Zend Framework 在模組上的目錄結構安排。文章中有個 Conventional Modular 版本,我也做稍做了修改,如下:
application/
config/ (非必要)
default/
config/ (非必要)
controllers/
models/
views/
(module 1)/
controllers/
models/
views/
(module 2)/
controllers/
models/
views/
htdocs/
images/
scripts/
styles/
.htaccess (導向 index.php)
index.php
library/
Zend/
(other libraries)/
tmp/
sessions/
cache/
view_compiles/
說明
很明顯地,這個版本是從官方版本演化而來的;它的主要特色是將原來 application 中的 MVC 三個目錄拿掉了,取而代之的是 default 、 module1 、 module2 等 MVC 模組。
前面兩個版本都是以 application 為預設模組,因此我們不必在程式裡特別設定。但是在多模組的狀況下,我們需要指定 application 下的某個模組為預設模組;這部份的程式碼可以參考我在 「Front Controller 的基本應用技」一文中所提到的「進階的 Front Controller 用法」。
其他的目錄也都能望名生義,這裡不再解說。
類型四:我的版本
最後介紹我自己所採用的版本。基本上它可以算是模組化的 Rob 版本,只是我為了專案開發需求,又再加入一些特別的目錄結構。
app/
.htaccess (deny from all)
base/
cron/
etc/
lib/
tmp/
mod/
default/
caches/
controllers/
models/
views/
(module 1)/
caches/
controllers/
models/
views/
(module 2)/
caches/
controllers/
models/
views/
pub/
.htaccess (RewriteEngine off)
img/
js/
css/
.htaccess (導向 index.php)
index.php
說明
首先我將 application 及 public 的名稱分別縮短為 app 及 pub ,其它目錄名稱也改為較短的名稱。而所有的 .htaccess 則和 Rob 版的相同,因此也會有同樣的限制。
另外我把放置 Zend Framwork 及其他 library 所在的 lib 目錄移入 app 目錄中以方便管理, Gavin Vess 的 config 我也改成了 etc ,這點算是仿照 Gavin Vess 的 Classical (Unix/Linux-like) 版本。
其他幾個特點如下:
-
加入了 base 目錄,這是用來存放專案共用程式的地方。
-
加入了 cron 目錄,這是用來存放排程程式的地方。
-
加入了 tmp 目錄,視需要使用。
-
每個模組裡加入了 caches 資料夾,用來存放各模組所產生的快取資料。
註:以上的特色是在累積了多個 Zend Framework 專案經驗後所得出的結論,但或許各位所開發的專案屬性會和我的不同,因此這些特色僅供參考。
結論
Zend Framework 目錄結構的安排其實有很大的學問,在規劃得當的情況下,它能為我們帶來事半功倍的開發效率。本文簡單地為大家分析了幾種常見的目錄結構,也說明了它們的一些優缺點。各位朋友不妨多參考其他版本的目錄結構,並在瞭解專案的屬性後,選擇一個適合專案的目錄結構。
希望這篇文章能為各位在學習 Zend Framework 時帶來一些幫助,同時也希望對 Framework 有深入研究的朋友也能不吝指正我的謬誤。
2008年02月27日
[PHP-ZF] Front Controller 的基本應用技
剛學 Zend Framework 的朋友通常會遇到兩個麻煩:目錄結構的安排,還有就是怎麼在 index.php (bootstrap) 中使用 Front Controller ?
目錄結構的安排下次有機會再談,這裡我來說說 Front Controller 的用法。
註:在此之前,請大家先看過 Rob Allen 寫的 Getting Started with the Zend Framework 一文 (PDF 版本) ;本文將會以該篇教學為基礎,一步一步來說明。
什麼是 Front Controller
整體而言 Front Controller 比較像是一個中介的角色,以下是它常見的幾個功能:
-
處理應用程式的初始化設定並執行之。
-
管理應用程式執行路徑。
-
管理輸入 (Request) ,使得使用者的動作得以分派 ( Dispatch) 到 Action Controller 中。
-
管理輸出 (Response) ,使得應用程式能將結果傳送出來。
-
可以接受 Plugins 來擴充 Front Controller 的行為。
Front Controller 裡還有更深入的參數設定機制,不過本文暫且先略過不談。
基本的 Front Controller 用法
我們先來看看 Rob 在 Getting Started with the Zend Framework 裡是怎麼用 Front Controller 的:
<?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();
撇開前面 PHP 的設定部份,我們可以看到 Front Controller 主要分成設定與執行兩個部份。
在設定的部份裡,首先我們可以看到 Front Controller 為 Singleton ,換句話說 Front Controller 在整個應用程式執行的過程中只會有一個實體存在。接著我們再呼叫 Front Controller 的 throwExceptions() 方法,它能為我們設定是否要將 Exception 直接丟出或是放到 Response 物件中。最後我們再設定應用程式的 Action Controller 所在路徑,讓 Front Controller 知道到哪裡能找到要執行的 action 。
當設定好 Front Controller 後,就可以呼叫它的 dispatch() 方法來執行整個應用程式了。
以上就是最簡單的 Front Controller 使用方法,接下來我會介紹目前我所使用的方法。
進階的 Front Controller 用法
先來看看程式碼:
<?php
error_reporting(E_ALL|E_STRICT);
date_default_timezone_set('Asia/Taipei');
// set include_path
define('ROOT_PATH', str_replace('\\', '/', dirname(__FILE__)));
$includePath = array(
ROOT_PATH . '/lib',
get_include_path(),
);
set_include_path(join(PATH_SEPARATOR, $includePath));
// autoload
require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();
// setup controller
$frontController = Zend_Controller_Front::getInstance();
$frontController->setControllerDirectory(array(
'default' => './app/mod/default/controllers',
'admin' => './app/mod/admin/controllers'))
->throwExceptions(false)
->setDefaultModule('default')
->returnResponse(true);
// run!
$response = $frontController->dispatch();
if ($response->isException()) {
$response->renderExceptions(true);
}
$response->sendResponse();
基本上我在 PHP 一般設定的部份和 Rob 的差不多 (因為我也是從他的教學入門的) ,只是我在載入類別的地方做了一些修改而已 (例如自動載入) 。
設定部份
接下來我們來看 Front Controller 設定的部份:
// setup controller
$frontController = Zend_Controller_Front::getInstance();
$frontController->setControllerDirectory(array(
'default' => './app/mod/default/controllers',
'admin' => './app/mod/admin/controllers'))
->setDefaultModule('default')
->throwExceptions(false)
->returnResponse(true);
在 Rob 的教學中因為只需要單一的功能 (也就是專輯管理) ,因此他並沒有使用 Front Controller 的模組設定。而在我所開發的專案中通常會有前後台之分,因此我就應用了 Front Controller 裡的模組功能。使用 Front Controller 模組功能的方法很簡單,就是在 setControllerDirectory() 方法中,給它一個陣列參數;這個陣列參數的 key 即為模組名稱, value 則為模組中 Action Controller 的路徑。
然後我們要設定預設執行的模組,這裡使用的是 setDefaultModule() 方法。預設模組的好處是我們在網址上不必特別指定模組名稱,在上面的例子中即為 default 。如果不是預設的模組,那麼就要在網址中明確指定模組名稱;以 admin 這個模組來說,網址就要輸入: http://localhost/admin 。特別要注意的是網址上的模組名稱參考的是前段說明中的 key ,而非 value 裡的目錄名稱。
接下來的 throwExceptions() 方法是個 setter 和 getter 的合體,當我們給它參數值時 (true 或 false) 它就是 setter ,反之不提供任何參數時就會是 getter 。前面提到 throwExceptions() 在設定 true 值時, Front Controller 會直接丟出 Exception ,這時程式就會直接中斷 (或是用 try...catch 來接也行) ;而 throwExceptions() 設定 false 值時, Front Controller 則會將 Exception 放到 Response 物件裡,在稍後處理 Reponse 物件時才會看到 Exception 的內容。
註: throwExceptions() 方法在 setter 時會回傳 Front Controller 物件本身,而成為 getter 時則會回傳 boolean 值;這邊要特別小心,我也曾經在這裡中招。
最後的 returnResponse() 方法也是個 setter 與 getter 的合體,它主要是設定 Front Controller 的 dispatch() 方法要不要回傳一個 Response 物件。這裡我的做法是永遠回傳 Response 物件 (也就是設定 true 值) ,這樣後面執行的程式碼才不會發生錯誤。
執行部份
// run!
$response = $frontController->dispatch();
if ($response->isException()) {
$response->renderExceptions(true);
}
$response->sendResponse();
在執行的部份,由於我們前面已經用 returnResponse() 方法來設定要回傳 Response 物件,因此 Front Controller 在完成 dispatch() 方法後就會將輸出的結果包裝在 Response 物件中回傳給 index.php 。
接著我們從 Response 物件的 isException() 方法來判斷輸出的結果是不是一個 Exception ,是的話就透過 Response 物件的 renderExceptions() 方法設定是否要顯示 Exception 。如果在這裡 renderExceptions() 方法的參數被設定為 false 時,那麼 Front Controller 就會呼叫預設模組底下 ErrorController 中的 errorAction() 方法,這樣就不會讓前端的瀏覽者看到程式錯誤,而是我們自訂的錯誤頁。
註:這個功能在上線環境中特別重要,你可以在這裡用 Response 物件的 getException() 方法取得 Exception 的內容然後寫入 Log 中,而最後只顯示自訂的 Error 頁面。
最後我們再呼叫 Response 物件的 sendResponse() 方法來將輸出的結果傳送給瀏覽器或 Console ,不論它是不是個 Exception 。
結論
以往我們在寫程式時,最頭痛的就是一開始的流程要怎麼寫?程式出現錯誤時要如何呈現?上線環境和開發環境要如何區別?而 Front Controller 正好為我們解決了許多這樣的麻煩;不過相對的它的使用方式也容易讓一般開發者感到迷惑,很多剛接觸的朋友就會在這裡受到很多挫折 (我剛好也是其中之一) 。
其實 Zend Framework 的 Front Controller 是非常靈活且強大的,這裡僅僅只介紹了它的基本應用方式。後面如果有空的話,我會再介紹其他相關的功能。
範例下載
照慣例,範例的下載位址在這裡。
2008年01月15日
[PHP-ZF] 初探 Zend_Db_Table Relationships (三)
前面我已經知道如何設定 Zend_Db_Table 的關連,這次來記一下串聯刪除 (cascading delete) 。
cascading delete 主要的目的是這樣的:假設 a 資料表和 b 資料表間有關連 (a.id = b.a_id) ,而當 a 資料表的某紀錄被刪除後,會使得 b 資料表中的相關紀錄失去作用而需要一併刪除。多數主流的資料庫系統都會內建類似的功能,例如 Oracle 、 MSSQL 及 PostgreSQL 等。但像是 MySQL 4.x 以前 的 MyISAM 格式並沒有提供 foreign key 和 trigger ,所以也沒辦法內建 Cascading Delete 時,這時只好從程式裡來著手。
不過有一點要小心,就算資料庫系統有支援 cascading delete ,也必須在資料表定義 foreign key 時設定 cascading delete 的相關指令 (通常是 ON DELETE CASCADE) ;不然資料表還是不會知道要去刪掉對應的紀錄。
註:以上資訊有錯誤的話,煩請大家不吝指正。
延續上一次的程式碼,這次我要為它們加入 cascading delete 的功能。
範例下載
不提供了,請下載上次的範例照以下說明修改吧。
說明
刪除文章時同時刪除文章與標籤之間的關連
首先我們在 Articles 這個類別中,加入一個 $_dependentTables 屬性:
<?php
class Articles extends Zend_Db_Table_Abstract
{
// ... 略 ...
protected $_dependentTables = array('ArticlesTags');
}
這個 $_dependentTables 必須給它一個陣列,這個陣列要包含你要串聯刪除的資料表類別名稱 (此例即為 ArticlesTags 這個類別) 。
接下來打開 ArticlesTags 這個類別,在原本的 $_referenceMap['Article'] 關連中,再加入一個 onDelete 索引:
<?php
class ArticlesTags extends Zend_Db_Table_Abstract
{
protected $_referenceMap = array(
// ... 略 ...
'Article' => array(
'columns' => array('article_id'),
'refTableClass' => 'Articles',
'refColumns' => 'id',
'onDelete' => self::CASCADE,
),
);
}
onDelete 的值固定就是用 self::CASCADE (其他值就不會動了) 。
註:別忘記原來 $_referenceMap['Article'] 所指定的意思,它是指 articles_tags 這個資料表的 articles_id (columns) 這個欄位要對應到 Articles (refTableClass) 這個資料表類別的 id 欄位 (refColumns) 。
這樣設定好後,就完成串聯刪除的動作了,很簡單吧?不過有一點非常重要:那就是一定要用 Zend_Db_Table_Row 的 delete 方法來刪除紀錄,才能啟動串聯刪除!
舉個例子:假設現在有兩篇文章,標題分別是 jQuery 和 Prototype ;現在我為它們都下了 javascript 和 ajax 兩個標籤。現在資料表紀錄如下:
| id | category_id | title |
|---|---|---|
| 3 | 2 | jQuery |
| 4 | 2 | Prototype |
| id | name |
|---|---|
| 3 | javascript |
| 4 | ajax |
| article_id | category_id |
|---|---|
| 3 | 3 |
| 3 | 4 |
| 4 | 3 |
| 4 | 4 |
註:請依照上面的例子,先在 articles 、 tags 及 artices_tags 三個資料表分別加入相關的紀錄。
接下來在 IndexController 中,加入一個 deleteAction 方法:
<?php
class IndexController extends Zend_Controller_Action
{
// ... 略 ...
function deleteAction()
{
// 不顯示畫面
$this->getHelper('ViewRenderer')->setNoRender();
$articleTable = new Articles();
// 有找到才刪
if ($articleRow = $articleTable->find(3)->current()) {
$articleRow->delete();
}
}
}
現在瀏覽一下 http://localhost/example3/index/delete (實際位置請按照各位的環境自行輸入) ,畫面應該不會出現任何資訊。現在到資料庫觀察一下,應該會發現 articles 資料表的第 3 筆紀錄已經被刪除;再看看 articles_tags ,在 article_id 為 3 的紀錄也跟著一併被刪除了。這樣就完成串聯刪除啦!
再整理一次重點:
-
在要刪除紀錄的資料表類別裡定義 $_dependentTables 。
-
在要串聯刪除紀錄的資料表類別裡定義 $_referenceMap[關連的名稱] 的 onDelete 屬性。
-
要使用串聯刪除,一定要用 Zend_Db_Row 的 delete 方法。
2007年12月1日
2007年09月26日
[PHP-ZF] Zend Framework 1.0.2 版釋出
這次 Zend Framework 1.0.2 主要是修正許多 1.0.1 所發現的 Bug ,詳細內容可以參考 Chagelog 。
Zend_Gdata 修得最多,看來外國人還滿喜歡把 Google 的服務整合到程式裡面。
2007年09月21日
[PHP-ZF] 初探 Zend_Db_Table Relationships (二)
前一篇文章提到了簡單的 Zend_Db_Table 的一對多關連,這次來談一對一 (多對一) 和多對多。
不過我對一對一的定義比較模糊,詳細的資訊請參考良葛格的 Hibernate 學習筆記:
資料表範例
基本上還是前一篇文章的範例,這次加上標籤 (Tag) ;每篇文章 (Article) 可以標上多個標籤,而每個標籤則是可以對應到多篇文章。新增的資料表結構如下:
CREATE TABLE IF NOT EXISTS `tags` (
`id` int(10) unsigned NOT NULL auto_increment,
`name` varchar(200) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `tags` (`id`, `name`) VALUES
(1, 'php'),
(2, 'zend framework');
CREATE TABLE IF NOT EXISTS `articles_tags` (
`article_id` int(10) unsigned NOT NULL default '0',
`tag_id` int(10) unsigned NOT NULL default '0',
PRIMARY KEY (`article_id`,`tag_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `articles_tags` (`article_id`, `tag_id`) VALUES
(1, 1),
(1, 2),
(2, 1),
(2, 2);
其中我們多了一個中間表 articles_tags ,然後在邏輯上 articles.id 會關連到 articles_tags.article_id ,而 tags.id 則對應到 articles_tags.tag_id 。在 articles_tags 中, article_id 和 tag_id 為組合主鍵。
範例下載
註:一樣沒包 Zend Framework ,請自行下載安裝。
說明
一對一 (多對一)
首先我們先來看看如何如何從文章中抓取對應的分類,從一篇文章上只會找到一個分類,也就是一對一關連。
註:其實嚴格來說文章對分類不是一對一的關係 (應該是多對一) ,只是在 Zend_Db_Table 裡,它們的作法是相似的。
然而一對多和多對一只是兩個相關的對應關係,因此我們並不用修改原來的 Articles 和 Categories 兩個 Classes 。在 Zend_Db_Table_Row 中,提供了一個 findParentRow 方法,讓我們能從 Articles 反推回去找到 Categories ;也就是在 IndexController::index 中,我們只要這樣做:
$phpArticle = $articles->find(2)->current();
$this->view->category = $phpArticle->findParentRow('Categories');
這裡會抓取 articles.id 等於 2 的文章,然後透過 findParentRow 來向 Categories 取得對應的分類。另外 Zend_Db_Table_Row 也提供了一個魔術方法,只要用 findParent<ParentClass> 就可以達到同樣的效果,例如:
$this->view->category = $phpArticle->findParentCategories();
然後我們就可以在 index.phtml 顯示相關的分類了:
<?php
var_dump($this->category->toArray());
?>
多對多
文章和標籤是以多對多的關係存在的,所以我們要先加入兩個 Classes ,一個是 Tags ,另一個是 ArticlesTags ;它們分別對應 tags 和 articles_tags 兩個資料表:
class Tags extends Zend_Db_Table_Abstract
{
protected $_name = 'tags';
}
class ArticlesTags extends Zend_Db_Table_Abstract
{
protected $_name = 'articles_tags';
protected $_referenceMap = array(
'Tag' => array(
'columns' => array('tag_id'),
'refTableClass' => 'Tags',
'refColumns' => 'id',
),
'Article' => array(
'columns' => array('article_id'),
'refTableClass' => 'Articles',
'refColumns' => 'id',
),
);
}
註:這裡暫時不考慮 $_dependentTables 屬性,後面有用到再加。
在 ArticlesTags 這個 Class 的 $_referenceMap 屬性裡,我定義了兩個 Rule ,一個為 Tag 、另一個為 Article ;它們分別關連到 Tags 和 Articles 這兩個 Classes 。
在 Zend_Db_Table_Row 裡提供了一個 findManyToManyRowset 的方法,可以協助我們處理多對多的關係。我們沿續第一個例子,假設現在我們想知道該篇文章所對應的標籤有哪些,可以這樣寫:
$this->view->articleTags = $phpArticle->findManyToManyRowset('Tags', 'ArticlesTags', 'Article');
在 findManyToManyRowset 方法裡一般會用到三個參數,第一個參數表示要關連的資料表 Class ,第二個參數則是中間表所對應的 Class ,而第三個為 Rule 的名稱。而要特別注意的是, Rule 所對應的 refTableClass 要和 findManyToManyRowset 方法所在的物件其對應的資料表一樣。可以對照一下上面 $phpArticle 物件所對應的資料表和最後一個參數值 Article 所對應的資料表,這樣就很容易理解了。
最後一樣在 index.phtml 裡顯示結果:
<?php
var_dump($this->articleTags->toArray());
?>
當然反過來要從標籤去取得文章也就很簡單了,只要抓出目前的標籤,然後再利用 findManyToManyRowset 方法來取得關連的文章就可以了 (提示: Rule 要用 Tag) 。
2007年09月20日
[PHP-ZF] 初探 Zend_Db_Table Relationships (一)
早上四點半就醒了...睡不著,想說研究一下 Zend Framework DB 一對多的關連。先前一直搞不定其中的關係,沒想到簡單玩了一下,竟然就搞定了 (可見手冊我根本就沒看懂...Orz) 。
這裡記一下,以免之後又忘記了。
資料表範例
這裡我用文章 (Article) 及文章分類 (Category) 來做為例子,它們的 DB Schema 如下:
CREATE TABLE IF NOT EXISTS `articles` (
`id` int(10) unsigned NOT NULL auto_increment,
`category_id` int(10) unsigned NOT NULL,
`title` varchar(200) NOT NULL,
PRIMARY KEY (`id`),
KEY `table1_id` (`category_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `articles` (`id`, `category_id`, `title`) VALUES
(1, 3, 'Zend_Db_Table'),
(2, 3, 'Zend_Controller_Action'),
(3, 2, 'jQuery'),
(4, 2, 'Prototype');
CREATE TABLE IF NOT EXISTS `categories` (
`id` int(10) unsigned NOT NULL auto_increment,
`parent_id` int(10) unsigned NULL,
`name` varchar(200) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES
(1, NULL, 'PHP'),
(2, NULL, 'JavaScript'),
(3, 1, 'Zend Framework');
其中 articles.category_id 對應到 categories.id ,而 categories.parent_id 也對應到自己的 categories.id 。
程式碼架構
這裡主要我是拿 Rob 的 Getting Started with the Zend Framework 裡面範例來修改,詳細架構就先看看後面附的下載檔。
不過也不一定需要用這樣的架構才能跑 ZF ,這是因為我偷懶不想寫太多不相關的程式,所以直接拿別人的來改。事實上我們可以把 Zend_Db 獨立出來使用,只是這樣要寫的東西就會比較複雜一點點,跟我要研究的東西無關。
範例下載
註:裡面沒包 Zend Framework ,請自行到官方網站下載,然後把 library 目錄解開放到範例資料夾下即可。
說明
首先我們需要兩個繼承 Zend_Db_Table_Abstract 的 Class ,它們分別是 Articles 和 Categories :
class Articles extends Zend_Db_Table_Abstract
{
protected $_name = 'articles';
}
class Categories extends Zend_Db_Table_Abstract
{
protected $_name = 'categories';
}
因為大部份工作已經被 Zend_Db_Table_Abstract 做掉了,所以 Articles 和 Categories 只要很簡單的指定 $_name 屬性即可;這樣一來, Articles 和 Categories 兩個 Classes 就能對應到 articles 和 categories 兩個資料表。
不過這樣還沒有辦法讓這兩個資料表在程式碼裡產生關連,我們必須在 Articles 和 Categories 這兩個 Classes 額外定義一些資訊,讓它們能互相認識。首先我們在 Articles Class 裡加上 $_referenceMap 屬性:
class Articles extends Zend_Db_Table_Abstract
{
protected $_name = 'articles';
protected $_referenceMap = array(
'Category' => array( // 給這個關連一個名字
'columns' => 'category_id', // 對應到 articles.category_id
'refTableClass' => 'Categories', // 對應到 Categories Class
'refColumns' => 'id', // 對應到 categories.id
),
);
}
$_referenceMap 屬性是用來宣告要透過哪個欄位來和另一個資料表做關連,就上面的例子來看,我們可以很清楚的看到是 articles.category_id 關連到 categories.id 。然而 $_referenceMap 屬性可以同時定義多組關連,但為了簡化以便於理解,這裡我只用了一組。
在這裡的 $_referenceMap 屬性裡主要有三個項目:
-
columns :表示目前資料表關連到另一個資料表的欄位,一般就是 Foreign Key (FK) 。
-
refTableClass :關連資料表所對應的 Class ,這裡要注意大小寫。
-
refColumns :關連資料表所對應的欄位,一般就是 Primary Key (PK) 。
註: $_referenceMap 其實還有其他可設定的項目,這裡暫不介紹。
接著 Categories 這個 Class 也必須定義一個 $_dependentTables 屬性,宣告與 categories 有關連的資料表名稱;這是為了保證我們在透過 Categories Class 刪除資料時,能一併刪掉關連資料表裡的資料 (這裡我還在研究,尚無實例) 。
class Categories extends Zend_Db_Table_Abstract
{
protected $_name = 'categories';
protected $_dependentTables = array('Articles'); // 對應到關連的 Table Class
}
註:事實上 $_dependentTables 屬性在目前這個例子裡可以不加,因為我們還用不到關連更新或刪除的功能。
現在我們可以在 IndexController::indexAction 裡測試看看:
class IndexController extends Zend_Controller_Action
{
function indexAction()
{
$categories = new Categories();
$zendframework = $categories->find(3)->current();
$this->view->articles = $zendframework->findDependentRowset('Articles');
}
}
其中 findDependentRowset 是 Zend_Db_Table_Abstract 提供的方法,它可以幫我們找出關連資料表的所有資料。我們在 index.pthml 裡可以將取得的結果表列出來:
<?php
var_dump($this->articles->toArray());
?>
是不是很簡單呢?
那麼第二個問題來了,如果我們要讓 categories._parent_id 和 categories.id 產生關連呢?
其實仿照上面的做法,我們只需要在 Categories Class 裡動手腳即可:
class Categories extends Zend_Db_Table_Abstract
{
protected $_name = 'categories';
protected $_referenceMap = array(
'ParentCategory' => array(
'columns' => 'parent_id', // 對應到 categories.parent_id
'refTableClass' => 'Categories', // 對應到 Categories Class
'refColumns' => 'id', // 對應到 categories.id
),
);
protected $_dependentTables = array('Articles', 'Categories'); // 對應到關連的 Table Class
}
也就是說,我們在 Categories Class 加入一個 $_referenceMap 屬性關連到自己,然後在 $_dependentTables 加入自己。
接著在 IndexController::indexAction 中測試一下:
class IndexController extends Zend_Controller_Action
{
function indexAction()
{
$categories = new Categories();
$php = $categories->find(1)->current();
$this->view->subcategories = $php->findDependentRowset('Categories');
$zendframework = $categories->find(3)->current();
$this->view->articles = $zendframework->findDependentRowset('Articles');
}
}
在 index.phtml 裡顯示結果:
<?php
var_dump($this->subcategories->toArray());
?>
以上就是簡單的 Zend_Db_Table_Actract 一對多關連取得資料集的方式,謝謝收看,下次見。 (補眠去...什麼?已經天亮要上班了喔?)
