The Zend Framework MVC Architecture

 

一、概述:

In this chapter, we will cover the following topics:
1. Zend framework MVC overview
2. The Front Controller
3. The router
4. The dispatcher
5. The Request object
6. The Response object

 

二、詳細介紹:

1、Zend Framework MVC overview

  1)瞭解請求(REQUEST)的產生與處理過程

一個請求被產生,一個相應的響應就被返回。上面這個流程就是發生在前端控制器(Front Controller)內部,這個過程常常是在前端控制器(Front Controller)調用dispatch()方法是觸發的,這個過程可以分解爲下列12個小步驟:

  1. 一個請求Request的產生(創建了一個Request Object 對象);

  2. 路由事件routeStartup 觸發;
  3. 路由器Route r開始處理這個請求,從中獲取請求信息;
  4. 路由時間routeShutdown 觸發,路由過程結束;

  5. 派遣事件dispatchLoopStartup 被觸發;
    //派遣循環開始

    6. 派遣preDispatch 事件觸發;
    7. 派遣過程中調用動作控制器(Action Controller );
    8. 動作控制器(Action Controller )將處理完成信息直接寫入響應對象(Response Object );
    9.派遣postDispatch 時間被觸發;
    
//派遣循環結束

    10. 檢測派遣標誌,即檢查是否還有動作沒有完成,如果有再次進入派遣循環(第6步);
  

  11. 派遣事件dispatchLoopShutdown 被觸發;

  12. 產生的響應Response被返回。

 

  2、 The Front Controller--------前端控制器

   1)介紹:前端控制器是MVC組建中的苦力,因爲它要實例化對象、觸發事件、建立默認的行爲等,它的主要目的是處理所有進入應用的請求。前端 控制器的設計模式被應用於不同的MVC框架中,我們在Zend Framework中指代的前端控制器(Front Controller)實際上是指Zend_Controller_Front類,因爲該類實現了前端控制器的模式;另一定注意的是,前端控制器設計是單 例模式(Singleton),這也就意味着它實現了單例設計模式,也就是僅僅只能有一個實例化的前端控制器,即我們不能直接實例化Front Controller,而是拿取一個:

$front


=
Zend_Controller_Front
::
getInstance();




     2)默認情況下,Front Controller負責實例化很多對象,並且是針對WEB應用的,即這些對象都是默認指定在HTTP環境下被實例化出來的,例如下表:

這個表顯示出了創建對象的類型,抽象類abstract class是被用於實體類concrete class繼承,實體類是被前端控制器調用並實例化使用的!!插件經紀人有些特別因爲它與運行環境無關,即在HTTP環境下和在CLI環境下是一樣的。默 認情況下前端控制器有2個插件可用:a.Zend_Controller_Plugin_ErrorHandler;

b.Zend_Controller_Plugin_ActionStack.錯誤插件ErrorHandler默認是被註冊的,可以通過前端控制器設置其參數noErrorHandler來取消:

$front

->
setParam(
'
noErrorHandler
'
,
true
);


Stack index是用於插件的調用時機,它越大這個插件就將在越後面執行!

默認情況下,前端控制器Front Controller也利用動作助手經濟人(Action Helper Broker)註冊了ViewRenderer Action Helper,我們可以通過noViewRenderer參數來禁止它:

$front

->
setparam(
'
noViewRenderer
'
,
true
);


    3)使用前端控制器

      [A]調用參數

        調用參數可以被用於存儲數據在前端控制器中,然後被傳遞到Action Controller、Router、Dispatcher中去了,調用參數可以很好的實現將共同的對象或者變量傳遞到MVC組件中去:

//

In bootstrap:



$obj

=

new
MyClass();


$front
->
setParam(
'
myObj
'
,
$obj
);


//
We can then retrieve this from one of our controllers using the getInvokeArg() method:



$myObj

=

$this
->
getInvokeArg(
'
myObj
'
);

前端控制器有如下處理調用參數的方法:

  • setParam(String $name, Mixed $value): 設置調用參數
  • setParams(Array $params): 設置多個調用參數
  • getParam(String $name): 獲取一個調用參數
  • getParams(): 獲取所有的調用參數
  • clearParams(String|Array|Null $name): 清除調用參數

      [B]可選項

        像調用參數樣,可選項也可以影響前端控制器的默認行爲,如下列可選項:

  • throwExceptions(Boolean $flag): 在派遣循環過程中是否拋出異常,或者捕獲到Response對象中
  • setBaseUrl(String $base): 設置請求的基本路徑,不要用全部Url。
  • returnResponse(Boolean $flag): 默認情況下,一旦派遣循環結束,前端控制器將渲染Response對象;若設爲true,dispatch()方法將返回Response對象,而不是渲染它;
  • setDefaultControllerName(String $controller): 設置默認的控制器名;默認情況下Index是默認的控制器名,我們可以通過該方法改變它;
  • setDefaultAction(String $action): 設置默認的動作名;默認情況下index是默認動作名稱,我們可以通過該方法來改變它;
  •   setDefaultModule(String $module): 設置默認的模塊名;默認情況下default是默認的模塊名;
  • setModuleControllerDirectoryName(String $name);設置控制器路徑名;默認下是controllers;
$front


=
Zend_Front_Controller
::
getInstance();


$front
->
setDefaultModule(
'
eesmart
'
)


$front
->
setModuleControllerDirectoryName(
'
c
'
);


$front
->
throwExceptions(ture);


$front
->
setBaseUrl(
'
/eesmart
'
);


$front
->
returnResponse(ture);


$front
->
setDefaultControllerName(
'
Eesmart
'
);


$front
->
setDefaultAction(
'
eesmart
'
);

     4)Modules, controllers, and actions

       前端控制器一個主要的部分就是負責modules、controllers、actions的配置,爲了工作正常,前端控制器必須知道如何組織我們的控制器和將它們放在哪裏;

  默認情況下,我們利用Zend_Tool創建的目錄結構大致如下:

  

上面這種目錄結構不利於管理,因爲所有的控制器就在application/controllers一個控制器文件夾下,這樣不方便管理,爲了達到這點,我們引入modules的概念,這樣可以將controllers,models,views進入一個管理單元!!

所有我們引申出來了下面兩種目錄結構:

(圖1)                           (圖2)

                              

正如所見一樣,圖1和圖2的佈局基本一樣,但圖2的佈局使用了module文件夾,這樣做是爲了告訴前端控制器這個是我們的modules文件路 徑,同時我們也不必一定要採用上面的這兩種模式,它們都可以自定義,然而99%的我們都會採用上面兩種情況,而我最喜歡的還是圖2中的路徑佈局。

★★

如果我們採用圖一中的佈局方式來使它能夠使用前端控制器Front Controller,我們可以使用Front Controller的setControllerDirectory()或者是addControllerDirectory()方法,因此當我們應用 第一種佈局方式(圖1所示)時我們可以這樣做:

$front

->
setControllerDirectory(


  array
(
'
default
'
=>
'
/path/application/default
'
)
,


      '

product
'
=>
'
/path/application/product
'
)

);




//
或者



$front
->
addControllerDirectory(
'
/path/application/product
'
,
'
product
'
);

setControllerDirectory和addControllerDirectory這兩種方法真正區別在於setControllerDirectory() 接受一個module數組並我們要指定默認的模塊名,而addControllerDirectory() 一次僅僅只接受一個模塊。不管怎麼樣,我們一次必須要一次指定一個默認的模塊名,無論我們是採用setControllerDirectory()還是addControllerDirecoty()方法!!

★★★★★

我們第二中佈局(圖2所示)只有一點點不同,所有的modules都被放入一個叫做modules的路徑下,我們可以採用addModuleDirectory() 方法來讓這種佈局使用Front Controller控制器,因此我當我採用第二種佈局方式(圖2所示)時我們可以這樣做:

$front

->
addModuleDirectory(
'
/path/application/modules
'
);

這種方法是最簡單的增加多模塊的方法,同時也意味着你不需要從新定義你新添加的modules。

★★★★★

  當我們使用了模塊後,我們在非默認模塊的的動作控制 器中需要遵循一個防止命名控制衝突的不同的命名協議。因此,在我們的例子中,所有在product模塊中的動作控制器Action Controllers都需要增加一個Product的命名空間,例如,Product模塊中的detail的動作控制器應當命名成 Product_DetailsController,但對於默認模塊(default(global) modules)中的動作控制器則不需要,例如,default模塊中的detail的動作控制器應該命名成DetailsController,然而, 我們通過設置prefixDefaultModule 變量能夠改變這種默認的行爲:

$front

->
setParam(
'
prefixDefaultModule
'
,
true
);

通過這樣,我們的default模塊中的所有動作控制器(如detail動作控制器)都應該加上模塊前綴,即Default_(Default_DetailsController);

    5)自定義MVC組件

  大多數情況下,默認的MVC組件(指Requset、Response、Router、Dispatcher等)基本上可以很好的服務於我們的 應用程序,但是如果我們真的需要自定義其中的一個或者幾個話,前端控制器(Front Controller)提供給我們一些很簡單的方法就可以定義出我們自定義的MVC組件,例如:

//

通過註冊前端控制器而非實例化得到前端控制器



$front

=
Zend_Controller_Front
::
getInstance();


//
實例化我們自己已經定義好的MVC組件



$myRequest

=

new
MyRequest();


$myResponse

=

new
MyResponse();


$myRouter

=

new
MyRouter();


$myDispatcher

=

new
MyDispatcher();


//
通過前端控制器來設置我們自定義的MVC組件讓它們生效



$front
->
setRequest(
$myRequest
);


$front
->
setResponse(
$myResponse
);


$front
->
setRouter(
$myRouter
);


$front
->
setDispatcher(
$myDispatcher
);

在這裏,我們用我們自定義的MVC組件來代替原有ZF默認的MVC組件。要做到這點,我們首先要通過繼承MVC組件的抽象類或者是MVC組件的子類 (這個在我們上面的一個表中可以很清晰的看到)來定義我們自己的MVC組件,這樣再通過上面的代碼就可以設置好我們自己的MVC組件。在大多數的案例中, 你可能只需要自定義其中一兩個MVC組件就行了,例如有可能用到自定義路由!

    6)小結

    前端控制器用一個集中的地方提供給我們控制我們的MVC組件,回顧下前端控制器的主要方法和函數,我們所提到的這些方法和函數是前端控制器 主要的一些方法和函數,在這裏並沒有列出所有的方法來,因爲那樣和手冊上基本又重複了一遍,所有沒有必要。我建議你可以看下手冊熟悉下前端控制器的基本方 法,如果你想了解ZF的前端控制器的每一個過程,你可以看看zf原代碼。

 

3、Router-----------路由器

  1)概述:路由器主要負責解析一個請求並且決定什麼module、controller、action被請求;它同時也定義了一種方法來實現用戶自定義路由,這也使得它成爲最有用的一個MVC組組件;

  2)設計:作爲一個應用中的路由組件是很專業的,理所當然的路由組件是抽象的,這樣允許作爲開發者的我們很容易的設計出我們自定義的路由協議。 然而,默認的路由組件其實已經服務得我們很好了。記住,如果我們需要一個非標準的路由協議時候,我們就可以自定義一個自己的路由協議,而不用採用默認的路 由協議。

  事實上,路由組件有兩個部分:路由器 (或者稱路由對象《the router》) 路由協議   (或者稱   路由過程 《the route》) 。 路由器主要負責管理和運行路由鏈,路由過程事實上主要負責匹配我們預先定義好的路由協議,意思就是我們只有一個路由器,但我們可以有許多路由協 議。(ps:不知道這樣理解是不是有問題,原文是這樣的:The router actually has two parts, the router and the route. The router is responsible for managing and running the route chain, and a route is responsible for actually matching the request against the predefined rule of the route. This means that we have one router to many routes.看到後面就會清楚一點的。)

 

  路由組件基於兩個接口:Zend_Controller_Router_Interface Zend_Controller_Router_Route_Interface ;同樣路由組件的兩個抽象類:Zend_Controller_Router_Abstact Zend_Controller_Router_Route_Abstract 分 別是實現了上面對應的兩個接口,同時這兩個抽象類也就提供給我們一些基本的函數來操作路由組件。如果我們需要創建我們自定義的路由器(the router)或者路由協議(the route),我們就可以分別繼承上面的兩個抽象類。路由的過程發生派遣過程的最開始,並且路由解析僅僅發生一次。路由協議在任何控制器動作 (controller action)被派遣之前被解析,一旦路由協議被解析後,路由器將會把解析出得到的信息傳遞給請求對象(Request object),這些信息包括moduel、controller、action、用戶params等。然後派遣器(Dispatcher)就會按照這些 信息派遣正確的控制器動作。路由器也有兩個前端控制器插件鉤子,就是在我們之前提到過的routeStartup和routeShutdown,他們在路 由解析前後分別被調用。

  3)默認情況下:

   默認條件下,我們的路由器是使用Zend_Controller_Router_Rewrite,是基於HTTP路由的,意味着它期望一個請 求是HTTP請求並且請求對象是使用Zend_Controller_Request_Http(或者是繼承它的對象),這兩個默認類,在我們之前的那個 表中都有見到過。默認條件下,路由協議是使用Zend_Controller_Router_Route_Module類。

  4)使用路由:

  使用路由既可以讓之很複雜,同時也能讓它很簡單,這是歸於你的應用。然而使用一個路由是很簡單的,你可以創建一個路由器讓它管你的路由協議,然後你可以添加你的路由協議給路由器,這樣就OK了!

  不同的路由協議如下所示:

  • Zend_Controller_Router_Route
  • Zend_Controller_Router_Route_Static
  • Zend_Controller_Router_Route_Regex
  • Zend_Controller_Router_Route_Hostname
  • Zend_Controller_Router_Route_Chain
  • Default Routes

  其中我們使用到的最基本的路由協議類數Zend_Controller_Router_Route,它提供給們少量的控制。如果想要更精密的控 制,我們可以採用正則路由協議:Zend_Controller_Router_Route_Regex,它提供給我們可以通過PHP的正則來使用,很強 大。其他的幾個路由協議類分別有不同的專業化。到此爲止,首先讓我們來看看路由器是如何讓路由協議與之一起工作的。

  在我們添加任何路由協議之前我們必須要得到一個路由器(the router)實例,我們可以通過自己創建一個新的路由器或者是通過前端控制器(Front Controller)來得到一個默認的路由器:

//

我們實例化一個默認的路由器



$router

=

new
Zend_Controller_Router_Rewrite();


//
或者我們可以通過前端控制器的getRouter()方法得到一個默認的路由器實例



$router

=

$front
->
getRouter()

  一旦我們有了路由器實例,我們就能通過它來添加我們自定義的一些路由協議:

$router

->
addRoute(
'
myRoute
'
,
$route
);


$router
->
addRoute(
'
myRoute1
'
,
$route
)

  除此之外,我們還可以通過Zend_Config_Ini和Zend_Config_Xml對象來添加我們的路由協議:

$config


=

new
Zend_Config_Ini(
'
/path/to/config.ini
'
,

'
production
'
);


$router
->
addConfig(
$config
,

'
routes
'
);

  其實路由器也提供給我們不同的方法來得到和設置包含在它內部的信息,一些重要的方法如下:

  • addDefaultRoutes() and removeDefaultRoutes()//添加或者移除默認的路由協議。
  • assemble()//基於給定的路由協議確定URI,這個方法通過Url視圖助手(View Helper)使用提供它的鏈接地址。
  • getCurrentRoute() and getCurrentRouteName()
  • getRoute(), getRoutes(), hasRoute(), and removeRoute();

  5)路由協議詳解:

    【A】Zend_Controller_Router_Route

    Zend_Controlloer_Router_Route路由協議提供了我們很強的功能,同時也提供了一些簡單的操作,爲了能夠使用該路由協議,我們必須先實例化它,然後用路由器加載它:

//

創建一個路由器實例



$router

=

new
Zend_Controller_Router_Rewrite();


//
創建一個路由協議實例



$route

=

new
Zend_Controller_Router_Route(

  

'
product/:ident
'
,


  

array
(

    

'
controller
'

=>

'
products
'
,


    

'
action
'

=>

'
view
'


  )

);


//
使用路由器裝載路由協議



$router
->
addRoute(
'
product
'
,

$route
);

在這個例子中,我們試圖匹配Url指定到一個單一的產品,就像http://domain.com/product/choclolat-bar。 爲了實現這點,我們在路由協議中傳遞了2個變量到路由協議Zend_Controller_Router_Route的構造其中。第一個變量 ('product/:indent')就是匹配的路徑,第二個變量(array變量)是路由到的動作控制器;其實路由協議也提供了第三個變量用於正則匹 配,我們將在第二個路由協議中見到;

路徑使用一個特別的標識來告訴路由協議如何匹配到路徑中的每一個段,這個標識有有兩種,可以幫助我們創建我們的路由協議,如下所示:

  a) :

  b) *

冒號(:) 指定了一個段,這個段包含一個變量用於傳遞到我們動作控制器中的變量,我們要設置好事先的變量名,比如在上面我們的變量名就是'ident',因此,倘若 我們訪問http://domian.com/product/chocoloate-bar將會創建一個變量名爲ident並且其值是 'chocoloate-bar'的變量,我們然後就可以在我們的動作控制器ProductsController/viewAction下獲取到它的 值:$this -> _getParam( ' ident ' );同時我們還可以在路由協議中設置ident的默認的值,即可以在路由協議類的第二個數組變量中增加一個元素(比如我們在這定義了ident默認值爲unknown):

$route


=

new
Zend_Controller_Router_Route(


  '
product/:ident
'
,



  array
(


    '
controller
'

=>

'
products
'
,



    '
action
'

=>

'
view
'
,



    '
ident
'

=>

'
unknown
'


  )

);



星號(*)被用做一個通配符,意思就是在Url中它後面的所有段都將作爲一個通配數據被存儲。例如,如果我們有路徑'path/product /:ident/*'(就是路由協議中設置的第一個變量),並且我們訪問的Url爲http://domain.com/product /chocolate-bar/test/value1/another/value2,那麼所有的在'chocolate-bar'後面的段都將被做成 變量名/值對,因此這樣會給我們下面的結果:

  ident = chocolate-bar
  test = value1
  another = value2

這種行爲也就是我們平常默認使用的路由協議的行爲,記住變量名/值要成對出現,否則像/test/value1/這樣的將不會這種另一個變量,我們 有靜態的路由協議部分,這些部分簡單地被匹配來滿足我們的路由協議,在我們的例子中,靜態部分就是product;就像你現在看到的那樣,我們的 Router_Route路由協議提供給我們極大的靈活性來控制我們的路由;然而,這就就很像正則匹配了,正則匹配使我們能夠提供而外的約束力來限制我們 的路由(這裏的正則匹配是使用PHP的preg引擎)。在我們的產品實例中,我們得到了用戶想觀看的'ident'的產品特性,即我們通過用戶傳遞過來的 參數,通過數據庫的搜索得到正確的產品信息。然而,如果我們得到的需求是系統僅僅只能接受產品ID號作爲我們的產品的標識,那麼我們可以使用路由協議來實 現這點:

考慮下面兩中路由:

//

創建路由器



$router

=

new
Zend_Controller_Router_Rewrite();


//
創建路由協議



$route

=

new
Zend_Controller_Router_Route(


  '
product/:ident
'
,



  array
(


    '
controller
'

=>

'
products
'
,



    '
action
'

=>

'
view
'


  )

,



  array
(


    //
match only alpha, numbers and _-



    '
ident
'

=>

'
[a-zA-Z-_0-9]+
'


  )

);


//
讓路由器裝載路由協議



$router
->
addRoute(
'
productident
'
,

$route
);


//
再定義一個路由協議



$route

=

new
Zend_Controller_Router_Route(


  '
product/:id
'
,



  array
(


    '
controller
'

=>

'
products
'
,



    '
action
'

=>

'
view
'


  )

,



  array
(


    //
match only digits



    '
id
'

=>

'
/d+
'


  )

);


//
讓我們的路由器再裝載一個路由協議



$router
->
addRoute(
'
productid
'
,

$route
);


爲了達到我們的需求,我們創建了2種路由協議。第一種路由協議對Zend_Controller_Router_Route的構造函數添加了第三個 變量--一個正則表達式的變量ident,這個需求就是用戶提供的ident必須是字母、數字和-以及下劃線組成。我們的第二個路由協議試圖匹配一個產品 的ID數,我們利用/d+正則來匹配數字。通過我們增加的路由協議,如果我們現在瀏覽http://domain.com/product/12 ,這個id變量就會被設置,如果我們瀏覽http://domain.com/product/chocoloate-bar ,那麼這個ident變量就會被設置,然後我們可以在同一個動作控制中接受不同的參數來顯示同樣的信息!!

 

   【B】Zend_Controller_Router_Route_Static

   標準路由協議,如果我們不需要任何匹配的變量,我們可以通過使用標準路由協議來實現。這個路由協議匹配到一個靜態URL並且創建一個靜態的路由協議,我們僅僅需要像之前那樣實例化它並把它加載到路由器中就行了:

$route


=

new
Zend_Controller_Router_Route_Static(


  '
products/rss
'
,



  array
(


    '
controller
'

=>

'
feed
'
,



    '
action
'

=>

'
rss
'


  )

);


$router
->
addRoute(
'
rss
'
,

$route
);


就像你看到那樣,Router_Route_Static路由協議就是Router_Route一個非常簡單的版本,在我們的例子中,http://domain.com/products/rss就會去訪問FeedController和rss控制器;

 

   【C】Zend_Controller_Router_Route_Regex

    正則路由協議。到目前爲止,我們之前的路由協議(Router_Route、Router_Route_Static)都很好的完成了基本的路由操作,我 們常用的也是他們,然而它們會有一些限制,這就是我們爲什麼要引進正則路由(Router_Route_Regex)的原因。正則路由給予我們preg 正則的全部力量,但同時也使得我們的路由協議變得更加複雜了一些。即使是他們有點複雜,我還是希望你能好好掌握它,因爲它比標準路由協議(Router_Route)要快一點點。

    一開始,我們先對之前的產品案例改用使用正則路由:

$route


=

new
Zend_Controller_Router_Route_Regex(


  '
product/([a-zA-Z-_0-9]+)
'
,



    array
(


      '
controller
'

=>

'
products
'
,



      '
action
'

=>

'
view
'


  )

);


$router
->
addRoute(
'
product
'
,

$route
);

 

你可以看到,我們現在移動我們的正則到我們的path(構造函數的第一個參數)中來了,就像之前的那樣,這個正則路由協議現在應該是匹配是一個數 字、字母、-和_組成的ident變量的字符提供給我們,但是,你一定會問,ident變量在哪呢?好,如果你使用了這個正則路由協議,我們可以通過變量1 (one) 來獲取其值,即可以在控制器裏用:$this->_getParam(1)來獲取,其實這裏如果看過正則的都知道這就是反向引用中的/1。然而,你 一定會想爲什麼要定義的這麼的垃圾,我們不能夠記住或者弄清每一個數字代表的是什麼變量(其實我剛開始看的時候也是一樣的感受)。爲了改變這點,正則路由 協議的構造函數提供了第3個參數來完成數字到變量名的映射:

$route


=

new
Zend_Controller_Router_Route_Regex(

  

'
product/([a-zA-Z-_0-9]+)
'
,


  

array
(

    

'
controller
'

=>

'
products
'
,


    

'
action
'

=>

'
view
'


  )

,


  

array
(


    //
完成數字到字符變量的映射



    
1

=>

'
ident
'


  )

);


$router
->
addRoute(
'
product
'
,

$route
);


這樣,我們就簡單的將變量1映射到了ident變量名,這樣就設置了ident變量,同時你也可以在控制器裏面獲取到它的值。(如果你不是很瞭解正則,我建議你可以看下那個正則入門30分鐘... )

  另外,東西總是就兩面性的,連正則路由也不例外。正則路由的一個負面作用就是表現在其他zf組件如url視圖助手 ($this->baseUrl())不能夠解析正則路由協議成URL,圍繞這點,我們可以爲我們的路由協議提供一個反向重寫,就像 sprintf()工作的那樣:

爲了達到重寫的目的,我們將正則路由協議(Router_Route_Regex)的構造函數中添加第四個變量:

$route


=

new
Zend_Controller_Router_Route_Regex(


  '
product/([a-zA-Z]+)/([a-zA-Z-_0-9]+)
'
,



  array
(


    '
controller
'

=>

'
products
'
,



    '
action
'

=>

'
view
'


  )

,



  array
(


    1

=>

'
category
'



    2

=>

'
ident
'


  )

,



  //
重寫路由協議



  '
product/%s/%s
'


);


$router
->
addRoute(
'
product
'
,

$route
);

那現在我們已經增加了一個反向重寫(reverse rewrite),我們的路由協議能夠很容易的被連接到。如果你看到上面的路由協議,我們實際上可以看作是一類參數的捕獲。我們然後提供了反向重寫 product/%s%s,因此路由協議能夠爲我們提供變量。記住,這裏反向重寫可以先熟悉下sprintf()這個函數。

由於我們感覺這個過程相當複雜,然我們再用一個實例來說明。

設想一下,假設我們一直在忙於我們老商城應用的重構而採用了zf框架。我們已經決定我們想讓我們的產品的URl有一個好的印象針對於搜索引擎。然而 由於我們的產品已經開發完成了很久了,並且裏面的url已經很多的被搜索引擎給收錄了,我們不想失去這些鏈接,爲了完成這些,我們正好可以使用正則路由的 力量。

我們老的URL格式:
http://storefront/products.php/category/{categoryID}/product/{productID}
我們新的URL的格式:
http://storefront/product/{categoryName}/{productID}-{productIdent}.html

因此,一開始,我們就想重定義我們老的請求URL到我們新的請求,我們可以通過這樣做:

//

我們老的url匹配的正則路由協議



$route

=

new
Zend_Controller_Router_Route_Regex(

  

'
products.php/category/(/d+)/product/(/d+)
'
,


  

array
(

    

'
controller
'

=>

'
products
'
,


    

'
action
'

=>

'
old
'


  )

,


  

array
(

    

1

=>

'
categoryID
'
,


    

2

=>

'
productID
'


  )

);



在這裏,我們將我們老的url中的category和product值分別得到後分別映射到了cateforyID和productID兩個變量,並且將 這兩個變量傳遞到我們的ProductsController/oldAction中去,因此,我們可以在我們的old動作中再次重定向到我們新的URl 中:

public


function
oldAction(){

  

$catID

=

$this
->
_getParam(
'
categoryID
'
);

  

$productID

=

$this
->
_getParam(
'
productID
'
);

  

//
model finds the product ident and category names

  //....




  
$ident

=

'
coolproduct
'
;

  

$catName

=

'
coolstuff
'
;


//
重定向到新的url



  
$this
->
_redirect(
'
/product/
'

.

$catName

.

'
/
'

.

$productID

.

'
-
'
.

$ident

.

'
.html
'
,


    

array
(
'
code
'

=>

301
)

  );

}

  這樣我們old動作控制器拿取匹配的變量從路由協議中並且使用它們重定向到一個使用一個301的新的url中去,記住,我們不應當直接將我們獲取到的變量直接應用到我們的一個重定向請求中,因爲這樣會牽扯到安全問題。 

  那現在我們創建一個新的路由協議來接收我們老的URls映射過來的URLs:

//

新url的正則路由協議



$route

=

new
Zend_Controller_Router_Route_Regex(


  '
product/([a-zA-Z-_0-9]+)/(/d+)-([a-zA-Z-_0-9]+).html
'
,



  array
(


    '
controller
'

=>

'
products
'
,



    '
action
'

=>

'
view
'


  )

,



  array
(


    1

=>

'
categoryIdent
'
,



    2

=>

'
productID
'
,



    3

=>

'
productIdent
'


  )

,



  '
product/%s/%d-%s.html
'


);





這個路由協議匹配我們新的URLs,這個正則包含3個捕獲組,分別是產品類名(categoryIdent),產品ID(productID),以及產品特性(ident)。這個'product/%s/%d-%s.html'就是匹配原來的{categoryName}/{productID}-{productIdent}.html這種格式。 最後,建議好好玩玩這個正則路由協議,我可以保證你以後會用得到的。

 

    【D】Zend_Controller_Router_Route_Hostname

    主機域名路由協議,看名字就知道他是關於處理域名的路由協議。一個常見使用就是一個域名下有按用戶的子域名,如,如果我們有一個公共的外部 站點www.domain.com,現在我們的註冊用戶有一個帳號url像ues1.domain.com,那麼我們就可以使用域名路由協議來重寫這個請 求:

 

$route


=

new
Zend_Controller_Router_Route_Hostname(


  '
:username.domain.com
'
,



  array
(


    '
controller
'

=>

'
account
'
,



    '
action
'

=>

'
index
'


  )

,



  array
(


    //
Match subdomain excluding www.



    '
username
'

=>

'
(?!.*www)[a-zA-Z-_0-9]+
'


  )

  );


$router
->
addRoute(
'
account
'
,

$route
);


正如你所見,域名路由協議(Router_Route_Hostname)很像基本路由協議(Router_Route),我們能夠得到變量,設置默認值,同時還能通過正則匹配,在這裏的正則匹配我們過掉了www。

 

  6)在配置文件中配置Zend_Config

  當我們有許多路由協議的時候,管理他們就開始變得很棘手了,這樣我們就可以通過路由器來調用配置文件。

[production]

routes

.
rss
.
type
=

"
Zend_Controller_Router_Route_Static
"


routes

.
rss
.
route
=

"
products/rss
"


routes

.
rss
.
defaults
.
controller
=
feed

routes

.
rss
.
defaults
.
action
=
rss



routes

.
oldproducts
.
type
=

"
Zend_Controller_Router_Route_Regex
"


routes

.
oldproducts
.
route
=

"
products.php/category/(/d+)/product/(/d+)
"


routes

.
oldproducts
.
defaults
.
controller
=
products

routes

.
oldproducts
.
defaults
.
action
=
old

routes

.
oldproducts
.
map
.
categoryID
=

1


routes

.
oldproducts
.
map
.
productID
=

2




routes

.
product
.
type
=

"
Zend_Controller_Router_Route_Regex
"


routes

.
product
.
route
=

"
product/([a-zA-Z-_0-9]+)/(/d+)-([a-zA-Z-_0-9]+).html
"


routes

.
product
.
defaults
.
controller
=
products

routes

.
product
.
defaults
.
action
=
view

routes

.
product
.
map
.
categoryIdent
=

1


routes

.
product
.
map
.
productID
=

2


routes

.
product
.
map
.
productIdent
=

3


routes

.
product
.
reverse
=

"
product/%s/%d-%s.html
"




routes

.
user
.
route
=

"
user/profile/:username/*
"


routes

.
user
.
defaults
.
controller
=
user

routes

.
user
.
defaults
.
action
=
profile

routes

.
user
.
defaults
.
username
=

"
Unknown
"


routes

.
user
.
reqs
.
username
=

"
([a-zA-Z-_0-9]+)
"


一旦我們創建了一個ini文件,我們就能把它加載到路由器中:

$config


=

new
Zend_Config_Ini(
'
config.ini
'
,

'
production
'
);


$router

=

new
Zend_Controller_Router_Rewrite();


$router
->
addConfig(
$config
,

'
routes
'
);


   7)總結

    路由器屬於ZF mvc組件中很重要的一個,我們上面描述的這些路由是經常可以用到的。可能在一開始的時候就去用router會感覺不適應,你可以先熟悉瞭解它,但我希望 你還是對這個有個比較詳細的瞭解,再次建議,你應該好好的用一下上面的路由,如果你想掌握它的話!!

 

4、Dispatch----------------派遣器

  1)概述:派遣器主要負責我們實際調用動作控制器,事實上,派遣器是一個MCV的內部組件,並且Front Controller和它交互最密切!

  2)設計:派遣器主要是負責派遣正確的動作控制器中的動作方法,意味着它必須加載所有的控制器類,並且實例化他們,並且調用其中的 Action。派遣器把持着所有的MVC組建的命名的規則的設置,這些設置包括默認的module,controller,action 命名。派遣器像其他MVC組件樣,同樣提供了一個接口和抽象類供我們創建我們自己的或者繼承父類的派遣 器:a.Zend_Controller_Dispatcher_Interface b.Zend_Controller_Dispatcher_Abstract.

  3)請求被派遣的過程:

    請求派遣過程就是派遣器Dispatcher從請求對象 Request object中提取出Module,Controller,Action來,並且調用其中的Action Controller;該過程牽扯到了3個組件:Front Controller,Dispatcher,Request object;派遣的過程是發生在派遣循環中;該循環大體過程是:

    a.前端控制器開始派遣循環

    b.前端控制器調用派遣器

    c.派遣器獲取Request object請求對象,並分析它

    d.派遣器從請求對象中找到對應的Action Controller動作控制器名、

    e.派遣器嘗試加載該動作控制器類

    f.動作控制器類加載成功,派遣器實例化動作控制器類

    g.派遣器器從請求對象中找到對應的Action動作名

    h.派遣器將派遣標誌設爲true,標誌着派遣完成

    i.派遣器開始派遣動作控制器類中的Action方法

    j.派遣動作完成,派遣器檢測請求對象Request object派遣完成標識是否爲false,如果是false則表示還有派遣沒有完成,派遣器就再次進入派遣循環過程中;

派遣標識案例說明:

 

//

_forward()方法使用



class
IndexController
extends
Zend_Controller_Action{

  

public

function
indexAction(){

    

$this
->
_forward(
'
index
'
,

'
index
'
,

'
product
'
);

  }

}




//
同_forward()方法實現機理一樣的代碼



class
IndexController
extends
Zend_Controller_Action{

  

public

function
indexAction(){

    

$request

=

$this
->
getRequest();

    

$request
->
setModuleName(
'
product
'
)

       

->
setControllerName(
'
index
'
)

       

->
setActionName(
'
index
'
)

       

->
setDispatched(
false
);

    }

}

 

注意到可以通過設置請求對象中的派遣標識就可以再次進入派遣循環中!!

  幾乎所有與派遣器(Dispatch)相關的都是影響到它的默認行爲,比如我們之前在前端控制器中所見的:setDefaultAction (string $action)、setDefaultControllerName (string $controller)、setDefaultModule (string $module),他們都是通過前端控制器調用,然後代理到派遣器中正確的方法。在這其中我們要注意一個重要的方法setParam(),我們可以通過它 來將一個變量或者一個對象傳遞到所有的控制器中:

$front

->
setParam(
'
myGlobal
'
,
'
globalvar
'
);


   要注意的一個問題就是設置變量的時間,因爲有時候我們會遇見這樣的問題:就是爲什麼我們已經使用了$front->setParam(),但它沒有設置到我們的值。這個原因就是當$front->dispatch()被調用時,變量從前端控制器傳遞到派遣器的過程是發生在routeStartup之前時間被觸發 因此,任何前端控制器變量在這一點之後再來設置就無法將變量傳遞到派遣器中和動作控制器中! 派遣器和派遣過程是zf MVC實現過程中一個很重要的部分,同時它也允許控制如何派遣它以及何時派遣它(比如可以通過派遣標識來設置)。接下來讓我看看請求對象(Request object)。

 

5、Http Request object 請求對象

  1)概述:請求對象,它提供給我們一種方式,這種方式可以封包我們的一個請求,並且可以讓其他MVC組件(Front Controller、Router、Dispatcher、Response)可以與之交互。沒有它的話,我的應用將不會工作!!

  2)設計:請求可以由許多方式產生(HTTP,CLI等),當一個請求發出的時候與之相關的請求對象也產生了!所有的請求對象都是基於 Zend_Controller_Request_Abstract抽象類設計的,這個抽象類提供給其他MVC組件操作的基本的方法,這些基本方法包括設 置將被派遣的module、controller以及action名字。它也包括設置請求變量和設置派遣狀態(這點在我們之前的的Dispatcher中 見過的)。請求對象被設計成爲抽象的意味着我們很容易的通過繼承這個抽象類來創建我們自己的請求,同時也意味着ZF也沒有鎖定任何請求的環境,因此,我們 能夠使用ZF MVC組件在HTTP、CLI或者我們喜歡的任何指定的環境。

  3)默認情況:

  默認的請求對象是使用Zend_Controller_Request_Http,默認就被註冊到了前端控制器中。這個HTTP請求對象被設計成在HTTP環境下,因此它包含而外的屬性比如像$_GET和$_POST數據,同時我們也可以使用下面的請求對象:

  a.Zend_Controller_Request_Simple;

  b.Zend_Controller_Request_Apache404;

Zend_Controller_Request_Simple提供了抽象類中的方法,被用於CLI MVC操作!

Zend_Controller_Request_Apache404是繼承了默認的HTTP對象(Zend_Controller_Reuqest_Http),它提供了在重寫過程中用於替代mod_rewrite或者PT標誌的HTTP函數。

  4)使用請求對象:

   請求對象可以被前端控制器(Front Controller)和動作控制器(Action Controller)通過調用getRequest()方法來調用請求對象!請求對象即可以通過前端控制器實例化(默認情況),也可以被我們自定義來實例化:

//

通過前端控制器拿請求對象



$front

=
Zend_Controller_Front
::
getInstance();


$request

=

$front
->
getRequest();


//
在控制器內,通過動作控制器拿:



$this
->
getRequest();


我們自定義請求對象:

$front


=
Zend_Controller_Front
::
getInstance();


$myRequest

=

new
My_Controller_Request_Custom();


//
通過前端控制器的setRequest()方法來自定義我們自己的請求對象



$request

=

$front
->
setRequest(
$myRequest
);


我們設置自定義的請求對象,要注意一點,要在派遣過程中的routeStartup之前就設置!!

  一旦我們有了一個請求對象,我們就可以設置或者獲得請求對象中的屬性,最重要的幾個:

  • getModuleName() and setModuleName()
  • getControllerName() and setControllerName()
  • getActionName() and setActionName()
  • isDispatched() and setDispatched()

所有的這些方法得到或者設置信息被派遣器用來決定什麼模塊、控制器、動作將被派遣,我們在我們上面的派遣器部分見到過了,所有這裏就不再重複。

  請求對象提供給我們一種方法設置和得到他們的變量,這些方法如下:

  • getParam() and setParam()
  • getParams() and setParams()
  • getUserParams() and getUserParam()

  這些方法可以讓人能夠存儲有關環境和請求的信息,在用戶變量和環境變量之間很有大的不同。用戶變量是通過setParam()或者setParams()方法直接被設置到請求的對象中,其他變量自動的從環境中被創建並被請求所設置。

  5)總結:請求對象是在前端控制器,路由器,分發器,以及控制類間傳遞的簡單值對象。請求對象封裝了請求的模塊,控制器,動作以及可選的參數, 還包括其他的請求環境,如HTTP,CLI,PHP-GTK。 請求對象先被傳入到前端控制器。如果沒有提供請求對象,它將在分發過程的開始、任何路由過程發生之前實例化。請求對象將被傳遞到分發鏈中的每個對象。而 且,請求對象在測試中是很有用的。開發人員可根據需要搭建請求環境,包括模塊、控制器、動作、參數、URI等等,並且將其傳入前端控制器來測試程序流向。 如果與響應對象配合,可以對MVC程序進行精確巧妙的單元測試(unit testing )。

 

6、The Response object 響應對象

  1)概述:接下來,我們進入到我們的最後一個組件,也是派遣過程中的最後一個部分---響應對象,響應對象邏輯上是請求對象的搭檔.目的在於收集消息體和/或消息頭,因而可能返回大批的結果。

  2)默認情況:和請求對象一樣,響應對象也可以由許多方式產生。所有的請對象都是基於Zend_Controller_Response_Abstract。 響 應可用在整個派遣過程中,我們能夠在派遣的過程中增加它,我們後面會有例子說明的。響應對象中可以包含頭部、異常、以及其他數據像用於響應一個請求生成的 HTML代碼。另一個與請求對象相似的地方是請求對象也是針對環境的,意思就是我們可以針對不同的環境給不同的響應類型,同時也意味着我們的MVC組件能 夠被用在CLI、HTTP或者是其他特定的環境。我們同樣也很容易的繼承響應對象抽象類 (Zend_Controller_Response_Abstract)並且實例化他們,默認環境下,我們使用的Response Object響應對象是Zend_Controller_Response_Http(),這些我們在一開始的時候就見過了!!

  3)使用響應對象(Request Object):響應對象處理3中類型的數據,他們分別是:異常(exception)、頭部(headers)、響應主體(response body);響應主體可以是針對請求返回的任何信息,比如一個請求是一個web頁面,我們可以返回html、xml、txt、binary Image數據等。設置response body,我們可以通過響應對象的setBody()方法:

$response


=

new
Zend_Controller_Response_Http();


$response
->
setBody(
'
<h1>Default Body</h1>
'
);


當使用了setBody()方法後,響應對象(Response Object)就會產生一個默認的段(default segment),這點常常來自view視圖,同時它也將覆蓋掉我們在響應對象中原有的段,像上面,這樣就可能產生一個下面的段:

array

(


  '
default
'

=>

'
<h1>Default Body</h1>
'


);



如果我們給setBody()增加一個參數,該參數就是段名:

//

向response object中寫入2個段



$response

=

new
Zend_Controller_Response_Http();


$response
->
setBody(
'
<h1>Default Body</h1>
'
);


$response
->
setBody(
'
<p>More body</p>
'
,

'
test
'
);


//
這將會提供2個段:



array
(


  '
default
'

=>

'
<h1>Default Body</h1>
'
,



  '
test
'

=>

'
<p>More body</p>
'


);



除了設置它以外,響應對象還提供給我一些其他的方法來控制段的位置:

  • append (string $name, string $content)
  • appendBody (string $content, null|string $name = null)
  • prepend (string $name, string $content)
  • insert (string $name, string $content, string $parent = null,boolean $before = false)

考慮下下面的例子(你可以在自己的zf測試項目中嘗試下,):

$response


=

new
Zend_Controller_Response_Http();


$response
->
setBody(
'
<h1>Default Body</h1>
'
);


$response
->
setBody(
'
<p>More body</p>
'
,

'
test
'
);


$response
->
appendBody(
'
<p>append to test segment</p>
'
,
'
test
'
);


$response
->
prepend(
'
header
'
,
'
<html><body>
'
);


$response
->
append(
'
footer
'
,
'
</body></html>
'
);


$response
->
insert(
'
extra
'
,
'
<h2>Extra body</h2>
'
,
'
default
'
,

false
);


$response
->
insert(
'
more
'
,

'
<p>Before footer</p>
'
,

'
footer
'
,

true
);


通過上面的設置,我們將得到下面的段:

array

(


  '
header
'

=>

'
<html><body>
'
,



  '
default
'

=>

'
<h1>Default Body</h1>
'
,



  '
extra
'

=>

'
<h2>Extra body</h2>
'
,



  '
test
'

=>

'
<p>More body</p><p>append to test segment</p>
'
,



  '
more
'

=>

'
<p>Before footer</p>
'
,



  '
footer
'

=>

'
</body></html>
'
,


);

另外前端控制器可能傳遞任何異常到響應對象,允許開發人員優美的處理異常。可以通過設置 Zend_Controller_Front::throwExceptions(true) 覆蓋這項功能:

$front

->
throwExceptions(
true
);


如果要發送響應輸出包括消息頭,使用sendResponse()

$response

->
sendResponse();


默認地,前端控制器完成分發請求後調用sendResponse() ;一般地,你不需要調用它。但是,如果你想處理響應或者用它來測試你可以使用Zend_Controller_Front::returnResponse(true) 設置returnResponse 標誌覆蓋默認行爲:

$front

->
returnResponse(
true
);


$response

=

$front
->
dispatch();


//
可以在這裏設置日誌什麼的..

//...


 
$response
->
sendResponse();




4)總結:響應對象的目的首先在於從大量的動作和插件中收集消息頭和內容,然後返回到客戶端;其次,響應對象也收集發生的任何異常,以處理或者返回這些異常,再或者對終端用戶隱藏它們。響應的基類是Zend_Controller_Response_Abstract ,創建的任何子類必須繼承這個類或它的衍生類。前面的章節中已經列出了大量可用的方法。子類化響應對象的原因包括基於請求環境修改返回的內容的輸出方式(例如:在CLI和PHP-GTK請求中不發送消息頭)增加返回存儲在命名片段中內容的最終視圖的功能等等。

  總結

  好了,終於完了,我知道這篇文章可能很長,我也確信你可能會有點暈。在這整篇文章中,我們已經觀看了一次zf處理一次請求的過程,熟悉和了解每 一個主要的ZF MVC組件能夠在實際中幫你解決很多問題。我個人認爲了解文章剛開始的第一張圖是相當重要的,我希望你能看完這篇文章後能夠對着圖想象每一個組件是如何工 作的,以及一個請求時如何被產生,然後被調用,最後產生一個對應的響應。可能裏面有的地方講解的不是很詳細,畢竟這只是一個大體的理解,詳細的每個組件的 具體使用還要各位自己親自去實踐,有什麼問題歡迎提出,畢竟我也是個菜鳥!文中如果有什麼理解錯誤的地方歡迎指出,謝謝!!

 

 

轉自:http://www.cnblogs.com/terryglp/articles/1775778.html

發佈了50 篇原創文章 · 獲贊 6 · 訪問量 30萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章