Yii2学习笔记系列21——Routing and URL Creation(请求处理之引导路由与URL创建)

一 30 五月 2016

当一个Yii应用程序开始处理一个请求的URL时,它所做的第一步是将该URL转换成路由,然后这个路由被用来实例化相应的控制器动作来处理该请求,这整个的过程被称为引导路由。

引导路由的逆过程被称为URL创建,该过程会根据给定的路由和相关的查询参数创建一个URL,当后面该URL被请求时,引导路由可以将其处理为原始的路由以及查询参数。

负责处理引导路由和URL创建的是Yii应用中的urlManager组件,即URL managerURL manager提供了parseRequest()方法用于解析收到的请求,提供createUrl()方法从给定的路由及相应的查询参数创建一个路由。

通过在应用程序中配置urlManager组件,你可以无需修改任何已有的程序代码就能够让你的应用识别任意的URL格式。例如,你通过使用如下的代码来为post/view动作创建一个URL:

use yii\helpers\Url;

// Url::to() 调用UrlManager::createUrl()来创建一个URL
$url = Url::to(['post/view', 'id' => 100]);

根据urlManager配置,创建好的URL可能会看起来像如下的一种(或集中)格式,并且如果创建好的URL在后面被请求时,它仍然会被解析为原始的引导路由以及相应的查询参数值。

/index.php?r=post%2Fview&id=100
/index.php/post/100
/posts/100

URL格式

URL Manager - URL管理器支持两种URL格式:缺省URL格式和美观URL格式。

缺省URL格式使用了一个名为r的查询参数来表示路由,使用常用的查询参数来代表路由的相关查询参数。例如,URL /index.php?r=post/view&id=100表示路由是post/view,查询参数id是100。缺省的URL格式不需要在URL Manager - URL管理器中做任何设置和工作。

你可以通过设置URL Manager - URL管理器中的enablePrettyUrl属性在两种URL格式之间任意切换,无需修改任何其他的程序代码。

引导路由

引导路由包括两个步骤,第一步,将收到的请求解析成一个路由和相应的查询参数;第二步,创建一个对应到解析后的路由的控制器操作来处理该请求。

当使用缺省的URL格式时,将请求解析为路由只需要简单的获取一下GET的查询参数r即可。

当使用美观URL格式时,URL Manager - URL管理器会检查已注册的URL rules - URL规则,找出其中可以将请求转换为路由的匹配项。如果找不到会返回yii\web\NotFoundHttpException异常。

一旦请求被转换为了一个路由,就该创建由该路由标识的控制器操作了。路由会被根据斜线分割成不同的部分。例如,site/index会被分割成siteindex,每一部分可能是代表模块、控制器或者操作的ID。从路由的第一部分开始,应用会采取如下的步骤来创建模块(如果有的话)、控制器和操作:

  1. 设置应用程序作为当前的模块
  2. 检查当前模块的controller map - 控制器集合是否包含当前ID。如果是的话,会根据在控制器集合中找到的控制器配置创建一个控制器对象,并且会直接跳到第5步去执行路由的其余部分
  3. 检查当前模块的modules属性中列出的是否包含当前ID,如果是的话,跳到第2步中在新创建的模块上下文环境下处理路由的下一部分
  4. 将当前ID当作控制器ID来对待,并且创建一个控制器对象。继续下一步来处理路由的剩余部分
  5. 控制器会在action map - 操作集合中当前ID。如果找到,它会根据在操作集合中找到的配置创建一个操作。否则,控制器会尝试创建一个与该ID对相对应,由某个aciotn方法所定义的行内操作(inline action)

在以上步骤中,如果发生了任何的错误,会抛出一个yii\web\NotFoundHttpException异常,用来指示路由处理过程中的失败项。

缺省路由

如果一个请求最后被转换成了一个空路由,这时候就会采用所谓的缺省路由取而代之。默认情况下,缺省路由是site/index,指代的是site控制器下的index操作。你可以通过类似如下代码配置应用的defaultRoute属性来自定义配置:

[
    // ...
    'defaultRout' => 'main/index',
];
catchAll路由

有时候你可能想要将你的Web应用临时设置为维护模式并且对所有的请求都返回相同的提示信息,有很多种方式可一实现这个目标,但其中最简单的方式是类似如下的配置yii\web\Application::catchAll属性:

[
    // ...
    'catchAll' => ['site'/offline'],
]

有了如上的配置之后,site/offline操作会应用到所有进来的请求上。

catchAll属性接收一个数组,数组的第一个元素指定一个路由,其余的元素(键值对)指定绑定到操作上的参数。

注意:开发环境下启用了该属性的话Debug面板就不可用了。

创建URL

Yii提供了yii\helpers\Url::to()方法来通过给定的路由和他们的相关查询参数来创建各种URL。例如:

use yii\helpers\Url;

// 为路由创建一个URL,路由是/index.php?r=post%2Findex
echo Url::to(['post/index']);

// 为带参数的路由创建一个URL,路由是/index.php?r=post%2Fview&id=100
echo Url::to(['post/view', 'id' => 100]);

// 创建一个带锚点的URL,路由是/index.php?r=post%2Fview&id=100#content
echo Url::to(['post/view', 'id' => 100, '#' => 'content']);

// 创建一个绝对路径URL:http://www.example.com/index.php?r=post%2Findex
echo Url::to(['post/index'], true);

// 创建一个带https的绝对URL:https://www.example.com/index.php?r=post%2Findex
echo Url::to(['post/index'], 'https');

需要注意的是在上述示例中,我们假设使用缺省URL。如果启用了美观URL格式,创建出来的URL是不同的,需要根据使用的Url规则来确定。

传递到yii\helpers\Url::to()方法中的路由是上下文相关的,它既可以是一个相对路由也可以是一个绝对路由,路由要根据如下的规则来规范化:

  • 如果路由是一个空字符串,则使用当前请求的路由
  • 如果路由不包含任何斜线,它会被当做当前控制器的操作ID,并且会在它前面追加当前控制器的唯一ID
  • 如果不是以斜线开头的,它会被当做是当前模块的一个相对路由,并且会在它前面追加当前模块的唯一ID

从2.0.2版本开始,你可以通过一个别名来指定路由。如果是这种情况,别名会先转换为实际的路由,然后根据上述规则转换成绝对路由。

例如,假设当前的模块是admin,当前控制器是post

use yii\helpers\Url;

// 当前请求的路由:/index.php?r=admin%2Fpost%2Findex
echo Url::to(['']);

// 一个只包含当前操作ID的路由:/index.php?r=admin%2Fpost%2Findex
echo Url::to(['index']);

// 一个相对路由:/index.php?r=admin%2Fpost%2Findex
echo Url::to(['post/index']);

// 一个绝对路由:/index.php?r=post%2Findex
echo Url::to(['/post/index']);

// /index.php?r=post%2Findex 假设别名是"@posts",代指的是"/post/index"
echo Url::to(['@posts']);

yii\helpers\Url::to()方法是通过调用URL manager - URL管理器createUrl()方法和createAbsoluteUrl()方法来实现的。在接下来的几个章节里,我们会讲解如何配置URL manager - URL管理器来实现自定义创建URL的格式。

yii\helpers\Url::to()方法也支持创建不与特定路由相关的URL,这种情况下,第一个参数不能传数组,而是要传一个字符串。例如:

use yii\helpers\Url;

// 当前的请求URL:/index.php?r=admin%2Fpost%2Findex
echo Url::to();

// 一个别名URL:http://example.com
Yii::setAlias('@example', 'http://example.com/');
echo Url::to('@example');

// 一个绝对URL:http://example.com/images/logo.gif
echo Url::to('/images/logo.gif', true);

除了to()方法,yii\helpers\Url帮助类也提供了几个方便的创建URL的方法,例如:

use yii\helpers\Url;

// 主页URL:/index.php?r=site%2Findex
echo Url::home();

// 基础URL,当应用部署在Web root的子目录下的时候非常有用
echo Url::base();

// 当前请求的URL的规范网址,详情请查看https://en.wikipedia.org/wiki/Canonical_link_element
echo Url::canonical();

// 记住当前请求的URL,稍后将它恢复出来
Url::remember();
echo Url::previous();
使用美观URL

为了使用美观URL,需要像如下的方式在应用的配置文件中配置urlManager组件:

[
    'components' => [
        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'enableStrictParsing' => false,
            'rules' => [
                // ...
            ],
        ],
    ],
]

在开关美观URL格式时,enablePrettyUrl属性是必须的,其它的属性是可选的。然而,上述展示的配置是最常使用的。

  • showScriptName:该属性决定入口脚本是否会被包含在创建的URL中。例如,创建URL /index.php/post/100,将该属性设置为false之后,则会创建/post/100
  • enableStrictParsing:该属性决定是否要启用严格的请求解析。如果启用了严格解析,相应的请求必须要至少能够匹配一个规则才能被视为有效请求,否则会抛出yii\web\NotFoundHttpException异常。如果严格解析被禁用了,当请求的URL没有规则可以匹配到的时候,URL的路径部分会被当成请求的路由
  • rules:该属性包含一个数组,数组中指定了用于解析、创建URL的规则。如果想满足你特殊的应用需求,该属性是你最需要掌握的。

注意:为了在创建的URL中隐藏入口脚本,除了设置showScriptName属性为false之外,你可能还需要配置你的Web服务器,这样儿当请求的URL没有明确指示的时候它可以判断哪个PHP文件应该执行。如果你使用的是Apache服务器,可以参考安装部分的配置。

URL规则

一个URL规则就是一个yii\web\UrlRule的实例或者它的子类。每个URL由一个用来匹配URL路径信息部分的表达式、一个路由和几个查询参数组成。当URL规则的表达式可以匹配到请求的URL时,该URL规则会被用来解析这个请求。当URL的路由和查询参数名与给定的相匹配时,该URL规则会被用来创建URL。

当启用了美观URL格式时,URL manager - URL管理器使用在它的rules属性中声明的URL规则来解析相应的请求和创建URL。其中,为了解析相应的请求,URL manager - URL管理器按照URL rules的声明顺序来逐一进行匹配,并且在其中寻找第一个可以匹配请求的URL的规则。匹配的规则接下来被用来将URL解析成一个路由及其对应的参数。同样的,为了创建一个URL,URL manager - URL管理器也会根据给定的路由和参数寻找第一个匹配的URL规则来创建URL。

你可以将yii\web\UrlManager::rules配置为一个数组,其键是相应的表达式,对应的值是相应的路由。每个“表达式-路由”键值对构成了一条URL规则。例如,如下的规则配置中声明了两条URL规则,第一条规则匹配URL posts并且将其映射到路由post/index上,第二条规则匹配符合正则表达式post/(\d+)的URL并且将其映射带参数名为idpost/view路由上。

[
    'posts' => 'post/index',
    'post/<id:\d+>' => 'post/view',
]

注意:URL规则中的表达式是用来匹配URL的路径信息部分的。例如,/index.php/post/100?source=ad的路径信息部分是post/100(开头和结尾的斜线是忽略掉的),它能够匹配post/(\d+)表达式。

除了声明“表达式-路由”这种键值对的URL规则外,你也可以以配置数组的方式声明URL规则。每个配置数组用来配置单独的一个URL规则对象,一般在你想要配置URL规则的其它属性时使用。例如:

[
    // ...其它URL规则...
    [
        'pattern' => 'posts',
        'route' => 'post/index',
        'suffix' => '.json',
    ],
]

默认情况下你不需要指定规则配置的class属性,它会默认使用yii\web\UrlRule类。

命名参数

一条URL规则可以被关联到几个命名好的查询参数上,这些参数用类似<ParamName:RegExp>的格式来指定,其中,ParamName是参数名,RegExp是可选的用来匹配参数值的正则表达式。如果没有指定RegExp,则表示该参数的值应该是不带任何斜线的字符串。

注意:你只能指定参数的正则表达式,表达式的其它部分是纯文本形式。

当一条规则被用来解析一个URL网址时,它会将匹配相应部分的URL的值作为相关参数的值进行填充,这些参数可以被应用的request组件变成可以通过变量$_GET访问的参数。当一条规则被用来创建URL时,它会提取提供的参数的值并且将其插入到参数声明的地方。

让我们用一些实际的例子来说明命名参数是如何工作的,假设我们已经声明了如下的URL规则:

[
    'posts/<year:\d{4}>/<category>' => 'post/index',
    'posts' => 'post/index',
    'post/<id:\d+>' => 'post/view',
]

当上述规则用来解析URL时:

  • /index.php/posts根据第二条规则被解析成路由post/index
  • /index.php/posts/2014/php根据第一条规则被解析成路由post/index,其中year参数的值是2014,category参数的值是php
  • /index.php/post/100根据第三条规则被解析成路由post/view,其中id参数的值是100
  • /index.php/posts/php会在yii\web\UrlManager::enableStrictParsing属性设置为true的时候引发yii\web\NotFoundException异常。如果yii\web\UrlManager::enableStrictParsing属性设置为false(默认值),posts/php这个路径部分会被作为路由返回

当上述规则用来创建URL时:

  • Url::to(['post/index'])根据第二条规则会被创建为/index.php/posts
  • Url::to(['post/index', 'year' => 2014, 'category' => 'php'])根据第一条规则会被创建为/index.php/posts/2014/php
  • Url::to(['post/view', 'id' => 100])根据第三条规则会被创建为/index.php/post/100
  • Url::to(['post/view', 'id' => 100, 'source' => 'ad'])根据第三条规则会被创建为/index.php/post/100?source=ad,因为source参数并没有在规则中指定,所以它会被当成一个查询参数追加到创建的URL中
  • Url::to(['post/index', 'category' => 'php'])由于没有规则可以匹配所以会被创建为/index.php/post/index?category=php。需要注意的是,由于没有规则可以匹配,所以URL的创建也仅仅是将路径部分作为路由并且将所有的参数作为查询字符串追加。
参数化路由

你可以在URL规则中的路由部分嵌入参数名,这表示允许一条URL规则用来匹配多种路由。例如,以下规则在路由中嵌入了controlleraction参数:

[
    '<controller:(post|comment)>/<id:d+>/<action:(create|update|delete)>' => '<controller>/<action>',
    '<controller:(post|comment)>/<id:d+>' => '<controller>/view',
    '<controller:(post|comment)>s' => '<controller>/index',
]

为了解析/index.php/comment/100/create这样儿的一条URL,第一条规则会被应用,它会将控制器的参数设置为comment,将操作的参数设置为create,路由<controller>/<action>因此会被解析为comment/create

同样的,为了根据路由comment/index创建一个URL,则会应用第三条规则,创建/index.php/comments这样的一个URL。

注意:通过参数化路由,使得大大减少URL规则的条数成为可能,同时也可以显著提高URL管理器的性能表现。

默认情况下,所有在规则中声明的参数都是必需的。如果请求的URL不包含某个特定的参数,或者一个在创建的时候就没有赋予参数,该规则就不会被应用。为了让一些参数变成可选的,你可以通过配置一条规则的defaults属性,在这个属性中列出的参数是可选的,并且如果没有提供这些参数的值的话,它们会采用默认值。

在下面的规则声明中,pagetag参数都是可选的,并且如果当这两个参数没有被提供时,他们的默认值分别是数字1和一个空字符串。

[
    // ...其它规则...
    [
        'pattern' => 'posts/<page:\d+>/<tag>',
        'route' => 'post/index',
        'defaults' => ['page' => 1, 'tag' => ''],
    ],
]

上述的规则可以被用来解析或者创建以下任何的URL:

  • /index.php/postspage参数是1,tag参数是''
  • /index.php/posts/2page参数是2,tag参数是''
  • /index.php/posts/2/newspage参数是2,tag参数是'news'
  • /index.php/posts/newspage参数是1,tag参数是'news'

如果不使用可选参数的话,你就必须要创建四条规则才能实现同样的效果。

带Server Name的规则

在URL规则的表达式中可以加入Web Server Name,这在你的应用程序会根据不同的Web Server Name有不同的行为时尤其有用。例如,下面的规则用来将http://admin.example.com/login解析成路由admin/user/login,将http://www.example.com/login解析成路由site/login

[
    'http://admin.example.com/login' => 'admin/user/login',
    'http://www.example.com/login' => 'site/login',
]

你也可以在Server Name中嵌入参数用来提取动态信息。例如,以下的规则将会把URL http://en.example.com/posts解析成路由post/index并且其参数是language=en

[
    'http://<language:\w+>.example.com/posts' => 'post/index',
]

注意:带有Server Name的规则不应该将入口脚本的子目录也包含在表达式中,例如,如果应用程序在http://www.example.com/sandbox/blog下,那么你应该使用表达式http://www.example.com/posts,而不是http://www.example.com/sandbox/blog/posts,它可以允许你讲应用程序部署在任何目录下而无需修改程序代码。

URL后缀

你可能因为不同的目的想要给URL加上后缀。例如,你可能想添加.html后缀,这样儿URL看起来就像是静态的HTML页面了;你也可能想要添加.json后缀,用来指示响应的内容的格式。你可以类似如下的代码来配置应用程序配置中的yii\web\UrlManager::suffix属性来实现这样儿的目标:

[
    'components' => [
        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'enableStrictParsing' => true,
            'suffix' => '.html',
            'rules' => [
                // ...
            ],
        ],
    ],
]

上述配置允许URL manager - URL管理器识别请求的URL以及创建带.html后缀的URL。

提示:你可以设置/作为URL的后缀,这样URL就会以斜线结尾。

注意:当你配置了一个URL后缀时,如果请求的URL不包含这个后缀,会被当成未识别的URL。这在做SEO的时候是一个推荐手段(针对搜索引擎)。

有时你可能想要针对不同的URL使用不同的后缀,这种需求可以通过配置单独的URL规则的suffix属性来实现。当一条URL规则设置了该属性的时候,它会在URL manager - URL管理器级别重写后缀设置。例如,如下的配置中包含了一条自定义URL规则,它使用了.json作为后缀,而不是全局的.html作为后缀。

[
    'components' => [
        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'enableStrictParsing' => true,
            'suffix' => '.html',
            'rules' => [
                // ...
                [
                    'pattern' => 'posts',
                    'route' => 'post/index',
                    'suffix' => '.json',
                ],
            ],
        ],
    ],
]
HTTP方法

当实现RESTful API时,通常需要根据用到的HTTP方法将相同的URL解析成不同的路由。这个可以很容易的通过在规则的表达式中将支持的HTTP方法作为前缀来实现。如果一条规则支持多个HTTP方法,将方法名用逗号分隔开。例如,以下规则有着相同的表达式post/<id:\d+>,但是却支持不同的HTTP方法。一个对post/100PUT请求会被解析成post/create,一个对post/100GET请求会被解析成post/view

[
    'PUT,POST post/<id:\d+>' => 'post/create',
    'DELETE post/<id:\d+>' => 'post/delete',
    'post/<id:\d+>' => 'post/view',
]

注意:如果一条URL规则的表达式中包含HTTP方法,那么这条规则只能被用作解析目的,当调用URL manager - URL管理器来创建URL的时候会跳过。

提示:为了简化RESTful API的引导路由,Yii提供了一个专门的URL规则类yii\rest\UrlRule,它非常高效,并且可以支持许多花哨的功能比如控制器ID的自动多元化。更多详情请参考RESTful API开发的Routing章节。

自定义规则

在之前的例子中,URL规则主要是以“表达式-路由“这样儿的键值对的形式来声明的,这是一种常用的快捷方式。在某些场景下,你可能想要通过配置一些其他属性来自定义一条URL规则,例如yii\web\UrlRule::suffix属性,这时候我们就可以通过使用一个配置数组来指定一条规则。以下的例子是从URL Suffix - URL后缀部分提取出来的:

[
    // ...其他URL规则...
    [
        'pattern' => 'posts',
        'route' => 'post/index',
        'suffix' => '.json',
    ],
]

信息: 默认情况下你不需要指定一条规则配置的class选项,它会采用默认的yii\web\UrlRule

动态添加规则

URL规则可以被动态地添加到URL manager - URL管理器中,通常这种情况出现在一个可以被再次发行的模块,并且想要自行管理URL规则时。为了使得动态添加的URL可以在路由处理过程中生效,你应该在启动引导的过程中将其添加进来。对于模块来说,这意味着他们应该实现yii\base\BootstrapInterface接口并且在bootstrap()方法中添加类似如下的代码:

public function bootstrap($app)
{
    $app->getUrlManager()->addRules([
        // 规则声明在这里
    ], false);
}

注意,你应该还要将这些模块列在yii\web\Application::bootstrap()中,这样他们就可以参与启动引导过程了。

创建规则类

尽管默认的yii\web\UrlRule对大多数的项目来说已经足够灵活了,但有些情况下你可能依然不得不自己创建一个规则类。例如,在一个汽车经销商网站里,你可能想要支持类似/Manufacturer/Model这样的URL格式,其中,ManufacturerModel都必须能够匹配数据库表中存储的某些数据,这时默认的规则类就无法工作了,因为默认规则类是依赖静态声明的表达式的。

我们可以通过创建如下的URL规则类来解决这个问题。

namespace app\components;

use yii\web\UrlRuleInterface;
use yii\base\Object;

class CarUrlRule extends Object implements UrlRuleInterface
{

    public function createUrl($manager, $route, $params)
    {
        if ($route === 'car/index') {
            if (isset($params['manufacturer'], $params['model'])) {
                return $params['manufacturer'] . '/' . $params['model'];
            } elseif (isset($params['manufacturer'])) {
                return $params['manufacturer'];
            }
        }
        return false;  // 不满足该规则
    }

    public function parseRequest($manager, $request)
    {
        $pathInfo = $request->getPathInfo();
        if (preg_match('%^(\w+)(/(\w+))?$%', $pathInfo, $matches)) {
            // 检查 $matches[1] 和 $matches[3] 看他们是否能够匹配数据库中的
            // 一条manufacturer和一条model记录
            // 如果满足, 设置 $params['manufacturer'] 和/或 $params['model']
            // 并且返回 ['car/index', $params]
        }
        return false;  // 不满足该规则
    }
}

并且在yii\web\UrlManager::rules配置中使用新建的规则类:

[
    // ...其他规则...
    [
        'class' => 'app\components\CarUrlRule',
        // ...配置其他的属性...
    ],
]
性能考虑

在开发一个复杂的Web应用的时候,组织URL规则是很重要的一项工作,因为这样做可以减少解析请求、创建URL的所需时间。

通过使用参数化的路由,你可以减少URL规则的条数,从而显著提升性能表现。

当解析或创建URL时,URL manager - URL管理器根据URL规则声明的顺序来逐一检查。因此,你可以考虑调整URL规则的顺序,将某些指定的和/或某些更常用的URL规则放在那些很少使用的规则之前。

如果某些URL规则在他们的表达式或者路由中有着相同的前缀,你可以考虑使用yii\web\GroupUrlRule,这样儿他们可以更有效的被URL manager - URL管理器当做一个组来检查。当你的应用程序由多个模块组成,而每个模块又有以它自己的模块ID作为前缀的URL规则集合时,这时候就可以使用yii\web\GroupUrlRule了。

Category: PHP Develop

Comments