Skip to content

Tutorial.Custom Route.zh_cn

liuchuangww edited this page Jul 4, 2013 · 2 revisions

English

  1. 引言

1.1 概念介绍

在之前的章节里,我们介绍过通过在action方法或模板里调用$this->url()方法来获取指定的action的URL,同时在浏览器里通过URL可以调用相应的action来处理请求。这个过程就是URL的组装和解析,在这个过程中是有相应的类或方法来完成的,也就是这章所要介绍的路由(Route)。即路由就是通过给定的参数组装出用户需要的URL,同时能够根据浏览器请求的URL解析到正确的Controller & Action。

1.2 路由机制

在之前的介绍里,我们没有在代码里提及如何组装路由,但还是可实现Controller & Action的请求,这就是系统默认路由的作用。也就是default和admin路由,分别用于前后台URL的组装和解析。它们对应的类已经封装在Pi Engine里,不需要开发者去定义如何实现。另外系统默认的路由还有home,user和feed,分别用于首页、用户在系统的注册登陆和RSS订阅。

当然Pi Engine也支持开发者在模块中自定义路由以满足不同的需要及SEO。在Pi Engine里,路由和配置参数都保存在数据库里,即core_route表,因此开发者需要在模块里配置自定义路由的相关参数,这样模块安装的时候路由信息及其相关参数就会被写进core_route表。之后路由组装和解析的时候就可以从表里读取有用信息了。

URL解析的逻辑在整个页面请求中执行的优先级是比较高的,因为通过URL解析出来的参数在之后处理中会被用到。整个系统的URL解析过程可用下图来描述:

fig7-1

图7-1 URL解析流程图

首先所有的路由数据被从core_route表里读取出来并保存在变量里,接着根据优先级,优先级高的路由将被调用并实例化相应的类,这个类里的match()方法将被调用,来解析当前请求的URL,如果匹配成功将会返回一个Zend\Mvc\Router\Http\RouteMatch实例,所有解析出来的参数也保存在这个实例里,若不成功将会返回NULL,然后就会寻找第二优先的路由继续匹配,直到匹配成功。若所有路由都没匹配成功,请求将会作为404页面进行处理。

因此,在创建模块的自定义路由时,开发者需要创建一个路由类来继承Pi的路由处理基类Pi\Mvc\Router\Http\Standard。

  1. 自定义路由

自定义路由涉及路径的参数配置,路由相关类(这里暂时叫做Route类)的定义,因此这一节的内容主要包括:

  • 路由配置
  • URL组装
  • URL解析

2.1 路由配置

路由的配置和前面介绍的导航配置一样,需要在config目录下创建一个route.php文件,并用数组的形式定义路由参数:

路径:usr/module/member/config/route.php

Code 7.2.1

<?php
return array(
    'member'    => array(
        'section'    => 'front',
        'priority'   => 100,

        'type'       => 'Module\\Member\\Route\\Member',
        'options'    => array(
            'structure_delimiter'    => '/',
            'param_delimiter'        => '/',
            'key_value_delimiter'    => '-',
            'route'                  => '/member-route',
            'defaults'               => array(
                'module'      => 'member',
                'controller'  => 'register',
                'action'      => 'index',
            ),
        ),
    ),
);

在上述的配置里,member字段名就是这个路由的名称,由于它是模块的自定义路由,因此在数据表里将会以{module name}-{route name}的形式存放,这里的路由就是member-member。member数组里定义的就是路由的基本信息:

  • section: 指定路由为前台路由或后台路由,一般都是前台会用自定义路由;
  • priority: 路由的优先级,优先级高的将最先被匹配,默认值为0;
  • type: 路由指定的用于组装或解析URL的类(Route类),以命名空间的形式书写;
  • options: type指定的类里的参数定义;
  • structure_delimiter: URL结构分割符,默认值为'/',可以省略;
  • param_delimiter: URL参数间的分割符,默认值为'/',可以省略;
  • key_value_delimiter: URL里参数键和值的分割符,默认为'-',可以省略;
  • route: URL前缀,若定义将会覆盖Route类里的$prefix变量,并且组装路由时必须加前缀,否则不能解析,可省略;
  • default: 定义默认请求的Controller & Action,将会覆盖Route类里的$defaults变量,如果组装URL时都不写Controller & Action参数,将会生成default里对应的URL。

**注意:**一般来说越通用的路由优先级应设置越小,否则很多URL将被通用的路由解析了。

最后需要在module.php里添加代码,告诉系统要将这个配置写入数据库:

路径:usr/module/member/config/module.php

Code 7.2.2

<?php
return array(
    ...
    'maintenance' => array(
        'resource'      => array(
            ...
            'block'         => 'block.php',
            'navigation'    => 'navigation.php',
            'route'         => 'route.php',
        ),
    ),
);

2.2 URL组装

接下来我们需要根据type字段定义的Route类创建相应的类来完成URL的组装和解析功能。在src目录下创建Route目录并添加Member.php文件:

member
|- src
   |- Route
      |- Member.php

路径:usr/module/member/src/Route/Member.php

Code 7.2.3

<?php
namespace Module\Member\Route;

use Pi\Mvc\Router\Http\Standard;
use Zend\Mvc\Router\Http\RouteMatch;
use Zend\Stdlib\RequestInterface as Request;

class member extends Standard
{
    protected $prefix = '/inner-prefix';

    protected $defaults = array(
        'module'     => 'system',
        'controller' => 'index',
        'action'     => 'index',
    );

    public function assemble(array $params = array(), array $options = array())
    {
        $mergedParams = array_merge($this->defaults, $params);
        if (!$mergedParams) {
            return $this->prefix;
        }

        $url = '';
        $controller = $mergedParams['controller'];
        $action     = $mergedParams['action'];
        $id         = isset($mergedParams['id']) ? $mergedParams['id'] : null;
        if ('register' == $controller and 'index' == $action) {
            $url = 'register';
        } elseif ('login' == $controller and 'login' == $action) {
            $url = 'login';
        } elseif ('profile' == $controller and 'edit' == $action and !empty($id)) {
            $url = 'edit';
            $url .= $this->paramDelimiter . 'id' . $this->keyValueDelimiter . $id;
        } else {
            $url = '';
        }

        return $this->paramDelimiter . trim($this->prefix, $this->paramDelimiter) . $this->paramDelimiter . $url;
    }
}

Pi\Mvc\Router\Http\Standard类为默认的路由类,因此我们需要将其作为父类并继承;Zend\Mvc\Router\Http\RouteMatch类主要用于保存解析成功的URL的参数;Zend\Stdlib\RequestInterface类保存了请求的相关信息,因此这三个类都需要引用。

在类里定义了两个保护变量$prefix和$defaults,它们和route配置里的route和default字段功能一致,这两个字段的值也将会被配置里的两个值覆盖。需要说明的是,对于$prefix,如果URL里不想使用前缀,需要将其赋值为空字符串,同时route配置里也要去掉route字段,否则URL解析时将出错。

接下来,我们需要定义一个assemble()方法来覆盖父类的相同方法,这样在使用url()方法来组装URL时就会调用这个方法。assemble()方法的两个参数也是固定的,$params为组装URL时用到的参数。

在这个例子中,我们需要将几个URL作如下定制:

  • /course/www/member/login/login -> /course/www/{prefix}/login
  • /course/www/member/register/ -> /course/www/{prefix}/register
  • /course/www/member/profile/edit/id-{$id} -> /course/www/{prefix}/edit/id-{$id}

这里为了介绍prefix的使用,在URL里加了prefix,实际情况可以不需要。

在代码里通过判断controller和action值对URL作相应的更改,其中$this->paramDelimiter和$this->keyValueDelimiter就是route配置里param_delimiter和key_value_delimiter的值。最后将组装好的URL加上前缀后返回。

现在我们修改前台IndexController的indexAction方法来查看下组装的结果:

路径:usr/module/member/src/Controller/Front/IndexController.php

Code 7.2.4

...
    public function indexAction()
    {
        $login    = $this->url('.member', array('controller' => 'login', 'action' => 'login'));
        $content .= sprintf('%s: %s</br>', __('Login Route'), $login);
        $register = $this->url('.member', array('controller' => 'register', 'action' => 'index'));
        $content .= sprintf('%s: %s</br>', __('Registration Route'), $register);
        $edit     = $this->url('.member', array('controller' => 'profile', 'action' => 'edit', 'id' => 1));
        $content .= sprintf('%s: %s</br>', __('Edition Route'), $edit);
        $default  = $this->url('.member', array());
        $content .= sprintf('%s: %s</br>', __('Default Page'), $default);
        $this->view()->setTemplate(false);
        $this->view()->assign('content', $content);
    }
...

在代码里我们调用url() plugin组装了四个URL,分别为登陆页面、注册页面、资料编辑页和默认页,url()的调用在后面会具体介绍,执行的结果为:

fig7-2

图7-2 自定义路由组装的URL结果

可以看到assemble()方法已经实现了我们的要求。对于Default Page一项,当不传递任何参数时,由于route配置里默认的Controller & Action对应的是注册页,因此最终生成注册页的URL,而不是Member类里的系统首页。

2.3 URL解析

有了URL,我们还需要一个解析URL的规则,否则请求的URL就不能正确地解析成需要的参数,这个解析规则也同样定义在Member类里,并且覆盖了父类的match方法,代码如下:

路径:usr/module/member/src/Route/Member.php

Code 7.2.5

<?php
...
class member extends Standard
{
    ...

    public function match(Request $request, $pathOffset = null)
    {
        $result = $this->canonizePath($request, $pathOffset);
        if (null === $result) {
            return null;
        }
        list($path, $pathLength) = $result;
        if (empty($path)) {
            return null;
        }
        $matches = array();
        $params  = explode($this->paramDelimiter, $path);
        $first   = isset($params[0]) ? $params[0] : null;
        $second  = isset($params[1]) ? $params[1] : null;
        if ('login' === $first) {
            $matches['controller'] = $first;
            $matches['action']     = 'login';
            unset($params[0]);
        } elseif ('register' === $first) {
            $matches['controller'] = $first;
            $matches['action']     = 'index';
            unset($params[0]);
        } elseif ('edit' === $first) {
            $matchString = '/^id' . $this->keyValueDelimiter . '/';
            if (empty($second) or !preg_match($matchString, $second)) {
                return null;
            }
            $matches['controller'] = 'profile';
            $matches['action']     = 'edit';
            unset($params[0]);
            list($key, $id) = explode($this->keyValueDelimiter, $second);
            if (!is_numeric($id)) {
                return null;
            }
            $matches['id'] = $id;
            unset($params[1]);
        }
        if (!empty($params)) {
            return null;
        }

        return new RouteMatch(array_merge($this->defaults, $matches), $pathLength);
    }
}

在Member类里定义了一个公有方法--match(),其中$request参数保存了当前请求的相关信息,$pathOffset为当前URL里域名的长度,我们需要根据这个长度将URL里后面有会用的部分提取出来。通过调用canonizePath()方法获取除域名外的部分URL,在这个方法里,URL的前缀也将被剔除掉,例如对于/course/www/member-route/login,我们将得到login部分。

接下来的部分就是根据剩余的URL判断应该去请求哪个Controller & Action,先把URL根据参数分割符(paramDelimiter)分割并保存到数组里,再根据数组的元素值判断要请求的Controller & Action,如login将被解析到LoginController的loginAction、register将被解析到RegisterController和indexAction。对于编辑页,还需要判断第二个参数的格式,同时把id值提取出来。在每一步匹配成功之后,我们就删除$params里相应的元素,最后再判断$params是否为空,若不为空,说明URL后还附带了其他无用参数,比如/course/www/member-route/login/ewef,这种情况应该当作无效URL来处理,因此需要返回NULL,说明这个URL和Member路由不匹配。

若匹配成功,将解析出来的module, controller, action等参数与默认值合并,并作为RouteMatch实例的参数,另外URL的长度也需要作为其参数。最后将这个实例返回,Pi检测到这个实例的类型后就知道URL已经匹配到正确的路由,就会把路由及参数的数据保存下来。

现在我们访问注册页URL就会看到注册页了:

fig7-3

图7-3 自定义URL访问注册页

注意,对于非ASCII码的参数,在组装的时候需要对其进行编码,方法就是使用urlencode()方法,而解析时也需要解码,使用urldecode()。

  1. 调用组装方法

在前面的章节里,我们经常提到在模板或action里使用url()方法来组装URL,另外还有一种方法就是在非action和模板里调用,即直接调用assemble方法。下面将详细介绍这两种方法的使用。

  • url()方法

url()方法在action里就是plugin,而在模板里就是helper,虽然两种方法的调用机制不一样,但url()方法的使用方法都一致。即:

$this->url({route name}, array('controller' => {controller name}, ...));

这种方法已经使用了多次,就不再赘述,不清楚的还可以参考Pi文档,这里重点介绍下,在模块里使用自定义路由和非模块里使用。

前面已经介绍过,模块的自定义路由是以{module name}-{route name}的形式存在数据库的,所以需要在url()方法的第一个参数里赋给这种形式的参数才能正确地使用模块自定义路由。在该路由所在模块,第一个参数只需要这样写即可:.{route name},它将会被解析成{current module name}-{route name}的形式,如我们在member模块里这样使用:

$this->url('.member', array(...));

相当于:

$this->url('member-member', array(...));

这将会正确地调用Module\Member\Route\Member类来处理URL。

若在demo模块这样使用:

$this->url('.member', array(...));

相当于:

$this->url('demo-member', array(...));

由于目前没有demo-member路由,因此将发生错误。所以在非路由所在模块需要这样使用:

$this->url('member-member', array(...));
  • assemble()方法

有些时候,我们并不只会在action和模板里组装URL,因此就不能使用url()方法了,这样我们只能直接调用assemble方法来组装了。使用的方法就是:

Pi::engine()->application()
            ->getRouter()
            ->assemble(array('controller' => 'index', ...), array('name' => '.member'));

其中第二个参数为路由的名称,以name字段标识,使用方式与上面介绍一致。

Clone this wiki locally