php

Zend Framework MVC的结构

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)的产生与处理过程

2010071213382043

一个请求被产生,一个相应的响应就被返回。上面这个流程就是发生在前端控制器(Front Controller)内部,这个过程常常是在前端控制器(Front Controller)调用dispatch()方法是触发的,这个过程可以分解为下列12个小步骤:

1. 一个请求Request的产生(创建了一个Request Object对象);

2. 路由事件routeStartup触发;
3. 路由器Router开始处理这个请求,从中获取请求信息;
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环境下被实例化出来的,例如下表:

2010071214245396

这个表显示出了创建对象的类型,抽象类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是用于插件的调用时机,它越大这个插件就将在越后面执行!

2010071214345557

默认情况下,前端控制器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创建的目录结构大致如下:

2010071215334316

上面这种目录结构不利于管理,因为所有的控制器就在application/controllers一个控制器文件夹下,这样不方便管理,为了达到这点,我们引入modules的概念,这样可以将controllers,models,views进入一个管理单元!!

所有我们引申出来了下面两种目录结构:

(图1)                           (图2)

2010071215350751                             2010071215385615

正如所见一样,图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_AbstactZend_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组件能够在实际中帮你解决很多问题。我个人认为了解文章刚开始的第一张图是相当重要的,我希望你能看完这篇文章后能够对着图想象每一个组件是如何工作的,以及一个请求时如何被产生,然后被调用,最后产生一个对应的响应。可能里面有的地方讲解的不是很详细,毕竟这只是一个大体的理解,详细的每个组件的具体使用还要各位自己亲自去实践,有什么问题欢迎提出,毕竟我也是个菜鸟!文中如果有什么理解错误的地方欢迎指出,谢谢!!

Leave a Reply

Your email address will not be published. Required fields are marked *