10.5. 标准路由器

10.5.1. 简介

Zend_Controller_Router_Rewrite是标准的框架路由器。路由是个过程,在这个过程中它取出URI的端点(跟着基本URL的URI的那部分)并把它分解成参数来决定哪个模块、哪个控制器和控制器中的哪个动作应该接受请求。模块、控制器、动作和其它参数被打包到Zend_Controller_Request_Http对象,接着这个对象由Zend_Controller_Dispatcher_Standard来处理。路由只发生一次:当请求最初被接收和第一个控制器被派遣之前。

Zend_Controller_Router_Rewrite被设计来考虑使用纯php结构时mod_rewrite-like的功能性。它非常宽松地基于Ruby on Rails并且不要求任何先前的web服务器URL rewriting的知识。它被设计来和单个Apache的 mod_rewrite规则(其中之一)一起工作:

RewriteEngine on
RewriteRule !\.(js|ico|gif|jpg|png|css)$ index.php

        

或者:

RewriteEngine on
RewriteCond %{SCRIPT_FILENAME} !-f
RewriteCond %{SCRIPT_FILENAME} !-d
RewriteRule ^(.*)$ index.php/$1 

        

如果Isapi_Rewrite已经用下列的rewrite规则被安装为一个Isapi扩展, rewrite路由器也可以和IIS web服务器一起使用:

RewriteRule ^[\w/\%]*(?:\.(?!(?:js|ico|gif|jpg|png|css)$)[\w\%]*$)? /index.php [I]

        
[注意] IIS Isapi_Rewrite

当使用IIS,$_SERVER['REQUEST_URI']将要么不存在,要么被设置成一个空串。在这个例子中,Zend_Controller_Request_Http将企图使用被Isapi_Rewrite扩展设置的$_SERVER['HTTP_X_REWRITE_URL']的值。

如果使用 Lighttpd,下面的 rewrite 规则有效:

url.rewrite-once = (
    ".*\?(.*)$" => "/index.php?$1",
    ".*\.(js|ico|gif|jpg|png|css)$" => "$0", 
    "" => "/index.php" 
)

10.5.2. 使用路由器

为正确使用rewrite路由器你必须初始化它,添加一些用户定义的路由并注入到控制器。下面的代码示例这个过程:

// Create a router

$router = $ctrl->getRouter(); // returns a rewrite router by default
$router->addRoute(
    'user',
    new Zend_Controller_Router_Route('user/:username', 
                                     array('controller' => 'user', 
                                           'action' => 'info'))
);

        

10.5.3. 基本的Rewrite路由器操作

RewriteRouter的核心是用户定义路由的定义。路由通过调用RewriteRouter的addRoute方法和传递一个由类实现的Zend_Controller_Router_Route_Interface的新的实例被添加。例如:

$router->addRoute('user', 
                  new Zend_Controller_Router_Route('user/:username'));

        

Rewrite 路由器带有四个基本类型的路由(其中一个是特殊的):

路由可以被使用无数次来创建链或用户定义的应用程序路由计划。你可以在任何配置中使用任何数量的路由,除了模块路由以外,它最好被用一次并作为通用路由(例如,作为缺省的)。每个路由将在稍后详细描述。

addRoute的第一个参数是路由名。它用来作为权柄从路由器中取得路由(例如,for URL generation purposes)。第二个参数是路由自己。

[注意] 注意

路由名最普通的用法是通过Zend_View_url助手的方法:

"<?= $this->url(array('username' => 'martel'), 'user') ?>">Martel</a>            

            

它将导致在 href: user/martel.

路由是一个简单的过程,这个过程通过所有提供的路由和匹配它的当前请求的URI定义来迭代。当一个正匹配被发现,变量值从路由实例返回并注入到Zend_Controller_Request对象以备将来在派遣器和用户创建的控制器中使用。如果是负匹配,在链中的下个路由被检查。

[注意] 倒序匹配

用倒序来匹配路由确保最通用的路由被首先定义。

[注意] 返回的值

从路由返回的值来自于URL参数或用于定义的缺省值。这些变量以后可通过Zend_Controller_Request::getParam()Zend_Controller_Action::_getParam() 方法来访问。

有三个特殊的变量可用于你的路由-'module'、 'controller' 和 'action'。这些特殊的变量被Zend_Controller_Dispatcher用来找出控制器和动作然后派遣过去。

[注意] 特殊变量

如果你选择通过 setControllerKeysetActionKey方法的方式来改变缺省值,这些特殊变量的名字可能会不同。

10.5.4. 缺省路由

Zend_Controller_Router_Rewrite 和缺省路由一起预先配置,它将以controller/action的形式匹配URIs。另外,模块名可以被指定作为第一个路径参数,允许这种module/controller/action形式的URIs。最后,它也将缺省地匹配任何另外的追加到URI的参数-controller/action/var1/value1/var2/value2

一些路由如何匹配的例子:

// Assuming the following:
$ctrl->setControllerDirectory(
    array(
        'default' => '/path/to/default/controllers',
        'news'    => '/path/to/news/controllers',
        'blog'    => '/path/to/blog/controllers'
    )
);

Module only:
http://example/news
    module == news

Invalid module maps to controller name:
http://example/foo
    controller == foo

Module + controller:
http://example/blog/archive
    module     == blog
    controller == archive

Module + controller + action:
http://example/blog/archive/list
    module     == blog
    controller == archive
    action     == list

Module + controller + action + params:
http://example/blog/archive/list/sort/alpha/date/desc
    module     == blog
    controller == archive
    action     == list
    sort       == alpha
    date       == desc

        

缺省路由是存储在RewriteRouter名(index)为'default'的简单的Zend_Controller_Router_Route_Module对象。它被创建多多少少象下面这样:

$compat = new Zend_Controller_Router_Route_Module(array(), 
                                                  $dispatcher, 
                                                  $request);
$this->addRoute('default', $compat);

        

如果你不想这个特别的缺省路由在你的路由计划中,你可以重写你自己的‘缺省’路由(例如,把它存储在'default'名下)或用removeDefaultRoutes()完全清除它:

// Remove any default routes
$router->removeDefaultRoutes();

        

10.5.5. 基本 URL 和子目录

rewrite路由器可以被用在子目录(例如,http://domain.com/~user/application-root/),在此例中,应用程序 (/~user/application-root)的基本URL应该能自动被Zend_Controller_Request_Http检测到并使用。

如果基本URL被误删除,你可以通过Zend_Controller_Request_Http 和调用 setBaseUrl() 方法(参见第 10.4.2.2 节 “基地址和子目录”)用你自己的基本路径重写它。

$request->setBaseUrl('/~user/application-root/');

        

10.5.6. Route Types

10.5.6.1. Zend_Controller_Router_Route

Zend_Controller_Router_Route 是标准的框架路由。它结合了灵活路由定义的易用性。每个路由包含了基本的URL映射(静态的和动态的部分(变量))并且可以被缺省地初始化,也可以根据不同的要求初始化。

让我们想象一下我们假设的应用程序将需要一些广域内容作者的信息页面。我们想能够把浏览器指向http://domain.com/author/martel去看一个叫"martel"的信息。有这样功能的路由看起来是这样的:

$route = new Zend_Controller_Router_Route(
    'author/:username',
    array(
        'controller' => 'profile',
        'action'     => 'userinfo'
    )
);

$router->addRoute('user', $route);

    

Zend_Controller_Router_Route里构造函数的第一个参数是路由的定义,它将匹配一个URL。路由定义包含静态的和动态部分,它们由正斜杠('/')符分开。静态部分只是简单的字符:author。动态部分,被叫做变量,用预设的冒号来标记变量名::username

[注意] 字符的的用法

当前的实现允许你使用任何字符(正斜杠除外)作为变量标识符,但强烈建议只使用PHP使用的变量标识符。将来的实现也许会改变这个行为,它可能会导致在你的代码里有隐藏的bugs。

当你把浏览器指向http://domain.com/author/martel这个例子的路由应该被匹配,它所有的变量将被注入到Zend_Controller_Request对象并在ProfileController可访问。由这个例子返回的变量可能会被表示为如下键和值配对的数组:

$values = array(
    'username'   => 'martel',
    'controller' => 'profile',
    'action'     => 'userinfo'
);

    

稍后,基于这些值,Zend_Controller_Dispatcher_Standard应该调用ProfileController类(在缺省模块中)中的userinfoAction()方法。你将依靠Zend_Controller_Action::_getParam()或者Zend_Controller_Request::getParam()方法能够访问所有的变量:

public function userinfoAction()
{
    $request = $this->getRequest();
    $username = $request->getParam('username');

    $username = $this->_getParam('username');
}

    

路由定义可以包一个额外的特别字符-通配符-表示为'*'号。它被用来取得参数,和缺省模块路由类似(在URI中定义的var=>value)。下面的路由多多少少地模仿了模块路由的行为:

$route = new Zend_Controller_Router_Route(
    ':module/:controller/:action/*',
    array('module' => 'default')
);
$router->addRoute('default', $route);

    
10.5.6.1.1. 变量缺省

在路由中每个变量可以有一个缺省值,这就是Zend_Controller_Router_Route中构造函数使用的第二个变量。这个参数是一个数组,在数组中键表示变量名,值就是期望的缺省值:

$route = new Zend_Controller_Router_Route(
    'archive/:year',
    array('year' => 2006)
);
$router->addRoute('archive', $route);

        

上述路由将匹配象http://domain.com/archive/2005http://example.com/archive的URLs。对于后者变量year将有一个初始的缺省值为2006。

这个例子将导致注入一个year变量给请求对象。应为没有路由信息出现(没有控制器和动作参数被定义),应用程序将被派遣给缺省的控制器和动作方法(它们都在Zend_Controller_Dispatcher_Abstract被定义)。为使它更可用,你必须提供一个有效的控制器和动作作为路由的缺省值:

$route = new Zend_Controller_Router_Route(
    'archive/:year',
    array(
        'year'       => 2006,
        'controller' => 'archive',
        'action'     => 'show'
    )
);
$router->addRoute('archive', $route);

        

这个路由将导致派遣给ArchiveController类的showAction()方法。

10.5.6.1.2. 变量请求

当变量请求被设定,第三个参数可以加给Zend_Controller_Router_Route的构造函数。这些被定义为正则表达式的一部分:

$route = new Zend_Controller_Router_Route(
    'archive/:year',
    array(
        'year'       => 2006,
        'controller' => 'archive',
        'action'     => 'show'
    ),
    array('year' => '\d+')
);
$router->addRoute('archive', $route);

        

用上述定义的路由,路由器仅当year变量包含数字数据将匹配它,例如http://domain.com/archive/2345。象http://example.com/archive/test这样的URL将不被匹配并且控制将被传递给在链中的下一个路由。

10.5.6.1.3.  主机名路由

你也可以使用主机名做路由匹配。对简单的匹配使用静态主机名选项:

$route = new Zend_Controller_Router_Route(
    array(
        'host' => 'blog.mysite.com',
        'path' => 'archive'
    ),
    array(
        'module'     => 'blog',
        'controller' => 'archive',
        'action'     => 'index'
    )
);
$router->addRoute('archive', $route);

        

如果你想匹配参数在主机名里,使用 regex 选项。在下面例子中,子域为动作控制器被用作用户名参数。 当组装路由时,你可以给出用户名为参数,就像你用其它路径参数一样:

$route = new Zend_Controller_Router_Route(
    array(
        'host' => array(
            'regex'   => '([a-z]+).mysite.com',
            'reverse' => '%s.mysite.com'
            'params'  => array(
                1 => 'username'
            ) 
        ),
        'path' => ''
    ),
    array(
        'module'     => 'users',
        'controller' => 'profile',
        'action'     => 'index'
    )
);
$router->addRoute('profile', $route);

        

10.5.6.2. Zend_Controller_Router_Route_Static

上面的例子都使用动态路由--包含模型来匹配的路由。然而有时候,特定的路由被设定成型,启动正则表达式引擎将有过渡的杀伤力。对这种情形的答案是使用静态路由:

$route = new Zend_Controller_Router_Route_Static(
    'login',
    array('controller' => 'auth', 'action' => 'login')
);
$router->addRoute('login', $route);

    

上面的路由将匹配http://domain.com/login的URL,并分派到 AuthController::loginAction().

10.5.6.3. Zend_Controller_Router_Route_Regex

除了缺省的和静态的路由类型外,正则表达式路由类型也可用。这个路由比其它路由更强更灵活,只是稍微有点复杂。同时,它应该比标准路由快。

象标准路由一样,这个路由必须用路由定义和一些缺省条件来初始化。让我们创建一个archive路由作为例子,和先前定义的类似,这次只是用了Regex:

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)',
    array(
        'controller' => 'archive',
        'action'     => 'show'
    )
);
$router->addRoute('archive', $route);

    

每个定义的regex子模式将被注入到请求对象里。同上述的例子,再成功匹配http://domain.com/archive/2006之后,结果值的数组看起来象这样:

$values = array(
    1            => '2006',
    'controller' => 'archive',
    'action'     => 'show'
);

    
[注意] 注意

在匹配之前,开头和结尾的斜杠从路由器里的URL中去除掉了。结果,匹配http://domain.com/foo/bar/,需要foo/bar这样的regex,而不是/foo/bar

[注意] 注意

行开头和行结尾符号(分别为'^' 和 '$')被自动预先追加到所有表达式。这样,你不需要在你的正则表达式里用它们,你应该匹配整个字符串。

[注意] 注意

这个路由类使用#符作为分隔符。这意味着你将需要避免哈希符('#')但不是正斜杠('/')在你的路由定义里。因为'#'符(名称为锚)很少被传给webserver,你将几乎不需要在你的regex里使用它。

你可以用通常的办法获得已定义的子模式的内容:

public function showAction()
{
    $request = $this->getRequest();
    $year    = $request->getParam(1); // $year = '2006';
}

    
[注意] 注意

注意这个键是整数(1) 而不是字符串('1')。

因为'year'的缺省没有设置,这个路由将和它的标准路由副本不是非常精确地相同。即使我们为'year'声明一个缺省并使子模式可选,也不清楚是否会在拖尾斜杠(trailing slash)上还将有问题。方案是使整个'year'部分和斜杠一起可选但只抓取数字部分:(这段比较绕口,请校对者仔细看看,谢谢 Jason Qi)

$route = new Zend_Controller_Router_Route_Regex(
    'archive(?:/(\d+))?',
    array(
        1            => '2006',
        'controller' => 'archive',
        'action'     => 'show'
    )
);
$router->addRoute('archive', $route);

    

让我们看看你可能注意到的问题。 给参数使用基于整数的键不是容易管理的办法,今后可能会有问题。这就是为什么有第三个参数。这是个联合数组表示一个regex子模式到参数名键的映射。我们来看看一个简单的例子:

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)',
    array(
        'controller' => 'archive',
        'action' => 'show'
    ),
    array(
        1 => 'year'
    )
);
$router->addRoute('archive', $route);

    

这将导致下面的值被注入到请求:

$values = array(
    'year'       => '2006',
    'controller' => 'archive',
    'action'     => 'show'
);

    

这个映射被任何目录来定义使它能工作于任何环境。键可以包含变量名或子模式索引:

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)',
    array( ... ),
    array(1 => 'year')
);

// OR

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)',
    array( ... ),
    array('year' => 1)
);

    
[注意] 注意

子模式键必须用整数表示。

注意在请求值中的数字索引不见了,代替的是一个名字变量。当然如果你愿意可以把数字和名字变量混合使用:

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)/page/(\d+)',
    array( ... ),
    array('year' => 1)
);

    

这将导致在请求中有混合的值可用。例如:URLhttp://domain.com/archive/2006/page/10将在下列结果中:

$values = array(
    'year'       => '2006',
    2            => 10,
    'controller' => 'archive',
    'action'     => 'show'
);

    

因为regex模型不容易颠倒,如果你想用URL助手或这个类中的 assemble方法,你需要准备一个颠倒的URL。这个颠倒的路径用可由sprintf()解析的字符串来表示并定义为第四个构造参数:

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)',
    array( ... ),
    array('year' => 1),
    'archive/%s'
);

    

所有这些都已经可能由标准路由对象完成,那么使用Regex路由的好处在哪里?首先,它允许你不受限制地描述任何类型的URL。想象一下你有一个博客并希望创建象http://domain.com/blog/archive/01-Using_the_Regex_Router.html这样的URLs,还有把解析它路径元素中的最后部分,01-Using_the_Regex_Router.html,到一个文章的ID和文章的标题/描述;这不可能由标准路由完成。用Regex路由,你可以做象下面的方案:

$route = new Zend_Controller_Router_Route_Regex(
    'blog/archive/(\d+)-(.+)\.html',
    array(
        'controller' => 'blog',
        'action'     => 'view'
    ),
    array(
        1 => 'id',
        2 => 'description'
    ),
    'blog/archive/%d-%s.html'
);
$router->addRoute('blogArchive', $route);

    

正如你所看到的,这个在标准路由上添加了巨大的灵活性。

10.5.7. 使用 Zend_Config with the RewriteRouter

有时候,用新路由更新配置文件比修改代码更方便。这个可能通过addConfig()方法来做。基本上,你创建一个Zend_Config-compatible配置,并在你的代码中读入然后传递给RewriteRouter。

作为例子,考虑下面的 INI 文件:

[production]
routes.archive.route = "archive/:year/*"
routes.archive.defaults.controller = archive
routes.archive.defaults.action = show
routes.archive.defaults.year = 2000
routes.archive.reqs.year = "\d+"

routes.news.type = "Zend_Controller_Router_Route_Static"
routes.news.route = "news"
routes.news.defaults.controller = "news"
routes.news.defaults.action = "list"

routes.archive.type = "Zend_Controller_Router_Route_Regex"
routes.archive.route = "archive/(\d+)"
routes.archive.defaults.controller = "archive"
routes.archive.defaults.action = "show"
routes.archive.map.1 = "year"
; OR: routes.archive.map.year = 1

        

上述的INI文件可以被读进Zend_Config对象:

$config = new Zend_Config_Ini('/path/to/config.ini', 'production');
$router = new Zend_Controller_Router_Rewrite();
$router->addConfig($config, 'routes');

        

在上面的例子中,我们告诉路由器去使用INI文件'routes'一节给它的路由。每个在这个节下的顶级键将用来定义路由名;上述例子定义了路由'archive'和'news'。每个路由接着要求,至少,一个'route'条目和一个或更多'defaults'条目;可选地,一个或更多'reqs'('required'的简写)可能要求提供。总之,这些相对应的三个参数提供给Zend_Controller_Router_Route_Interface对象。一个选项键,'type',可用来指定路由类的类型给特殊的路由;缺省地,它使用Zend_Controller_Router_Route。在上述例子中,'news'路由被定义来使用Zend_Controller_Router_Route_Static

10.5.8. Subclassing the Router

标准的rewrite路由器应当最大限度提供你所需的功能;大多时候,为了通过已知的路由提供新的或修改的功能,你将只需要创建一个新的路由类型

那就是说,你可能想要用不同的路由范例。接口Zend_Controller_Router_Interface提供了需要最少的信息来创建路由器,并包含一个单个的方法。

interface Zend_Controller_Router_Interface
{
  /**
   * @param  Zend_Controller_Request_Abstract $request
   * @throws Zend_Controller_Router_Exception
   * @return Zend_Controller_Request_Abstract
   */
  public function route(Zend_Controller_Request_Abstract $request);
}

        

路由只发生一次:当请求第一次接收到系统。路由器的意图是基于请求的环境决定控制器、动作和可选的参数,并把它们发给请求。请求对象接着传递给派遣器。如果不可能映射一个路由到一个派遣令牌,路由器对请求对象就什么也不做。