深入理解Yii2.0 路由


路由(Route)


引言

这里先说一下Yii2的执行过程,或者运行流程:

  1. 用户向 入口脚本 web/index.php 发起请求。
  2. 入口脚本加载应用 配置 并创建一个 应用 实例去处理请求。
  3. 应用通过 请求 组件解析请求的 路由。
  4. 应用创建一个 控制器 实例去处理请求。
  5. 控制器创建一个 动作 实例并针对操作执行过滤器。
  6. 如果任何一个过滤器返回失败,则动作取消。
  7. 如果所有过滤器都通过,动作将被执行。
  8. 动作会加载一个数据模型,或许是来自数据库。
  9. 动作会渲染一个视图,把数据模型提供给它。
  10. 渲染结果返回给 响应 组件。
  11. 响应组件发送渲染结果给用户浏览器。

请求生命周期:

运行流程:

index.php
 2     ---->引入 vendor/auto_load.php
 3 auto_load.php
 4     ---->引入 ventor/composer/autoload_real.php
 5     ---->执行 ComposerAutoloaderInit240f916b39e20bc11bc03e2039805bd4->getLoader
 6 autoload_real.php
 7     ---->getLoader
 8     ---->单例
 9     ---->spl_autoload_register(array('ComposerAutoloaderInit240f916b39e20bc11bc03e2039805bd4','loadClassLoader'))
10     ---->self::$loader = new \Composer\Autoload\ClassLoader();
11     ---->引入 \Composer\Autoload\ClassLoader
12     ---->引入 autoload_namespaces.php 给作为属性 $loader
13         ---->$vendorDir  $baseDir
14     ---->引入 autoload_psr4.php 作为属性给 $loader
15     ---->$loader->register(true);
16         ---->spl_autoload_register this,loadClass
17         ---->loadClass ----> findFile 
18     ---->引入 autoload_files.php require
19     ---->return $loader
20 index.php
21     ---->初始化了一个$loader  (暂时不知道什么用)
22     ---->引入 /vendor/yiisoft/yii2/Yii.php
23 Yii.php
24     ----> 引入 BaseYii.php ,Yii 继承 BaseYii
25     ---->spl_autoload_register(BaseYii,autoload)
26     ---->Yii::$classMap = include(__DIR__ . '/classes.php'); //引入一堆php class地址
27     ---->Yii::$container = new yii\di\Container;//容器
28  
29 //继承关系梳理
30     yii\di\Container(容器) -> yii\base\Component(实现 属性,事件,行为 功能的基类) -> Object(实现 属性 功能的基类,含有__construct)
31     yii\web\Application(所有web应用类的基类) -> \yii\base\Application(所有应用的基类,__construct) -> Module(所有模块和应用的基类,含有__construct) -> yii\di\ServiceLocator(服务定位器,包含所有模块和应用) -> yii\base\Component -> Object
32  
33 index.php
34     ---->$config 引入 
35     (new yii\web\Application($config))->run();
36     ---->\yii\base\Application __construct()
37         ---->Yii::$app = $this (Application)
38         ---->$this->setInstance($this); 设置当前请求类的实例 (把Application类的对象push进Yii的loadedModules里)
39         ---->$this->preInit($config);
40             ---->$this->_basePath = $_config['basepath']  Yii::aliases[@app] = $_config['basepath']
41             ---->$this->getVendorPath 设置框架路径
42                 ---->setVendorPath Yii::aliases[@vendor] Yii::aliases[@bower] Yii::aliases[@npm]
43             ---->$this->getRuntimePath 同上,设置runtimePath Yii::aliases[@runtime]
44             ---->setTimeZone 设置时区
45             ---->核心组件信息(地址)注入$config log view formatter i18n mailer urlManager assetManager security
46         ---->registerErrorHandler 定义错误处理程序
47         ---->Component::__construct($config); Object中的__construct  //这步发生了很多事情
48             ---->Yii::configure($this)  把$config赋给$this作属性
49  
50             ? $this->bootstrap 中的值哪来的 ?---->配置文件来的。。。。
51  
52         ---->$this->init()
53         ---->$this->bootstrap(); 初始化扩展和执行引导组件。
54             ---->引入@vendor/yiisoft/extensions.php
55             ---->Yii::aliases['xxx'] = 'xxx'; extensions.php中aliase地址
56         <!-- 初始化完成 -->
57  
58     ---->\yii\base\Application->run()
59         ---->$this->trigger($name)  --- $event = new Event;  //$name = beforeRequest 执行 _event[beforeRequest]handler
60             ---->$event->sender = application object
61                  $event->name = $name;
62                  //这两句没懂
63                  $event->data = $handler[1];
64                  call_user_func($handler[0], $event);
65                  Event::trigger($this, $name, $event); //$this = application object 
66  
67         ---->$response = $this->handleRequest($this->getRequest());
68             ---->$this->getRequest() ---->get('request') get方法位于ServiceLocator ,返回指定id的实例(返回request实例到_components['request'])
69             ---->$this->handleRequest(request对象) //request对象的类是yii/web/request
70                 ---->list ($route, $params) = $request->resolve();//解决当前请求的路由和相关参数 ,resolve() 调用 UrlManager对象 的 parseRequest() 方法,再在里面调用 UrlRule类的 parseRequest(),这篇博文讲的就是这个
71                     ---->$params 放置地址栏解析的结果数组Array ( [0] => [1] => Array ( [m] => sds [c] => dasd ) )
72                     ---->runAction($route, $params); //位于Module
73                         ---->list($controller, $actionID) = $this->createController($route) 返回array('0'=>控制器controller对象,'1'=>'action名') 
74                         $controller 赋给Yii::$app->controller
75                         ---->$controller->runAction($actionID, $params);  yii/base/Controller
76  
77                         ---->runAction($actionID, $params);  yii/base/Controller
78                             ---->$action = $this->createAction($id); //生成一个InlineAction对象,赋给Yii::$app->requestedAction
79                                 InlineAction __construct $this->actionMethod = $actionMethod;
80                             ---->beforeAction
81                             ---->$action->runWithParams($params); //位于 yii/base/InlineAction
82                                 ---->$args = $this->controller->bindActionParams($this, $params);//位于yii/web/controller $this=>InlineAction $params=>模块/控制器 数组  --- 将参数绑定到action,返回有效参数数组$args
83                                 ---->赋给Yii::$app->requestedParams = $args;
84                                 ---->call_user_func_array([$this->controller, $this->actionMethod], $args) //执行第一个回调函数 真正执行
85                             ---->afterAction
86                             ---->返回执行结果(页面已出) 给Module中的runAction
87  
88         ---->返回结果给handleRequest
89         ---->$response = $this->getResponse(); 返回一个response对象,具体同上
90         ---->$response->data = $result;  
91         ---->返回$response给yii/base/Application 的 run $response
92         ---->$response->send();输出内容
93         <!-- 页面输出完成 -->

参阅:

https://www.php.cn/faq/390228.html

https://www.jianshu.com/p/5b80f08cbf57

正文

参阅 https://www.kancloud.cn/kancloud/yii-in-depth/50778

Web开发中不可避免的要使用到URL。用得最多的,就是生成一个指向应用中其他某个页面的URL了。 开发者需要一个简洁的. 集中的、统一的方法来完成这一过程。

否则的话,在代码中写入大量的诸如 http://www.digpage.com/post/view/100 的代码,一是过于冗长,二是易出错且难排查, 三是日后修改起来容易有遗漏。因此,从开发角度来讲,需要一种更简洁、可以统一管理、 又能排查错误的解决方案。

同时,我们在 附录2:Yii的安装 部分讲解了如何为Yii配置Web服务器,从中可以发现, 所有的用户请求都是发送给入口脚本 index.php 来处理的。那么,Yii需要提供一种高效的分派 请求的方法, 来判断请求应当采用哪个 controller 哪个 action 进行处理。

结合以上2点需求,Yii为开发者提供了路由和URL管理组件。

所谓路由是指URL中用于标识用于处理用户请求的module, controller, action的部分, 一般情况下由 r 查询参数来指定。 如 http://www.digpage.com/index.php?r=post/view&id=100 , 表示这个请求将由 PostController 的 actionView 来处理。

同时,Yii也提供了一种美化URL的功能,使得上面的URL可以用一个比较整洁、美观的形式表现出来, 如 http://www.digpage.com/post/view/100 。 这个功能的实现是依赖于一个称为 urlManager 的应用组件。

使用 urlManager 开发者可以解析用户的请求,并指派相应的module, controller和action来进行处理, 还可以根据预定义的路由规则,生成需要的URL返回给用户使用。

简而言之,urlManger具有解析请求以便确定指派谁来处理请求和根据路由规则生成URL 2个功能。

美化URL

一般情况下,Yii应用生成和接受形如 http://www.digpage.com/index.php?r=post/view&id=100 的URL。这个URL分成几个部分:

  • 表示主机信息的 http://www.digapge.com
  • 表示入口脚本的 index.php
  • 表示路由的 r=post/view
  • 表示普通查询参数的 id=100

其中,主机信息部分从URL来讲,一般是不能少的。当然内部链接可以使用相对路径,这种情况下可以省略, 只是User Agent最终发出Request时,也是包含有主机信息的。 换句话说,Web Server接收并转交给Yii处理的URL,是完整的、带有主机信息的URL。

而入口脚本 index.php 我们知道,Web Server会将所有的请求都是交由其进行处理。 也就是说,Web Server应当视所有的URL为请求 index.php 脚本。这在 附录Yii2安装 部分, 我们 已经对Web Server进行过相应配置了。如Nginx:

location / {
    try_files $uri $uri/ /index.php?$args;
}

因为总是请求index.php,所以这个没必要每次都加上,可以省去了。

将路由信息转换成路径,一般为 module/controller/action 之类的,上面的可改为:http://www.digpage.com/post/view?id=100

对于查询参数 id=100,还可以改为:http://www.digpage.com/post/view/100 ,也有利于SEO了。

假如所请求的编号100的文章,其标题为 Route , 那么不妨使用用 http://www.digpage.com/post/view/Route 来访问。 这样的话,干脆再加上 .html 好了。 变成 http://www.digpage.com/post/view/Route.html

我们把 URL http://www.digpage.comindex.php?r=post/view&id=100 变成 http://www.digpage.com/post/view/Route.html 的过程就称为URL美化。

Yii有专门的 yii\web\UrlManager 来进行处理,其中:

  • 隐藏入口脚本可以通过 yii\web\UrlManager::showScriptName = false 来实现
  • 路由的路径化可以通过 yii\web\UrlManager::enablePrettyUrl = true 来实现
  • 参数的路径化可以通过路由规则来实现
  • 假后缀(fake suffix) .html 可以通过 yii\web\UrlManager::suffix = '.html' 来实现

这里点一点,有个印象就可以下,在 Url管理 部分就会讲到了。

路由规则

路由规则是指 urlManager 用于解析请求或生成URL的规则。 一个路由规则必须实现 yii\web\UrlRuleInterface 接口,这个接口定义了两个方法:

  • 用于解析请求的 yii\web\UrlRuleInterface::parseRequest()
  • 用于生成URL的 yii\web\UrlRuleInterface::createUrl()

Yii中,使用 yii\web\UrlRule 来表示路由规则,UrlRule实现了UrlRuleInterface。 如果开发者想自己实现解析请求或生成URL的逻辑,可以以这个类为基类进行派生, 并重载 parseRuquest()createUrl()

以下是配置文件中urlManager组件的路由规则配置部分,以几个相对简单、典型的路由规则的为例:

'components' => [
    'urlManager' => [
        'class' => 'yii\web\UrlManager',
        'enablePrettyUrl' => true,
        'showScriptName' => false,
        'rules' => [
            // 为路由指定了一个别名,以 post 的复数形式来表示 post/index 路由
            'posts' => 'post/index',
        
            // id 是命名参数,post/100 形式的URL,其实是 post/view&id=100
            'post/<id:\d+>' => 'post/view',
        
            // controller action 和 id 以命名参数形式出现
            '<controller:(post|comment)>/<id:\d+>/<action:(create|update|delete)>' => '<controller>/<action>',
        
            // 包含了 HTTP 方法限定,仅限于DELETE方法
            'DELETE <controller:\w+>/<id:\d+>' => '<controller>/delete',
        
            // 需要将 Web Server 配置成可以接收 *.digpage.com 域名的请求
            'http://<user:\w+>.digpage.com/<lang:\w+>/profile' => 'user/profile',
        ],
    ],
],

上面这个数组用于为urlManager声明路由规则。 数组的键相当于请求(需要解析的或将要生成的),而元素的值则对应的路由, 即 controller/action 。 请求部分可称为pattern,路由部分则可称为route。 对于这2个部分的形式,大致上可以这么看:

  • pattern 是从正则表达式变形而来。去除了两端的 / # 等分隔符。 特别注意别在pattern两端画蛇添足加上分隔符。
  • pattern 中可以使用正则表达式的命名参数,以供route部分引用。这个命名参数也是变形了的。 对于原来 (?P<name>pattern) 的命名参数,要变形成 <name:pattern>
  • pattern 中可以使用HTTP方法限定。
  • route 不应再含有正则表达式,但是可以按 <name> 的形式引用命名参数。

也就是说,解析请求时,Yii从左往右使用这个数组;而生成URL时Yii从右往左使用这个数组。

yii\web\UrlRule 的代码:

namespace yii\web;

use Yii;
use yii\base\Object;
use yii\base\InvalidConfigException;

class UrlRule extends Object implements UrlRuleInterface
{
    // 用于 $mode 表示路由规则的2种工作模式:仅用于解析请求和仅用于生成URL。
    // 任意不为1或2的值均表示两种模式同时适用,
    // 一般未设定或为0时即表示两种模式均适用。
    const PARSING_ONLY = 1;
    const CREATION_ONLY = 2;

    // 路由规则名称
    public $name;

    // 用于解析请求或生成URL的模式,通常是正则表达式
    public $pattern;

    // 用于解析或创建URL时,处理主机信息的部分,如 http://www.digpage.com
    public $host;

    // 指向controller 和 action 的路由
    public $route;

    // 以一组键值对数组指定若干GET参数,在当前规则用于解析请求时,
    // 这些GET参数会被注入到 $_GET 中去
    public $defaults = [];

    // 指定URL的后缀,通常是诸如 ".html" 等,
    // 使得一个URL看起来好像指向一个静态页面。
    // 如果这个值未设定,使用 UrlManager::suffix 的值。
    public $suffix;

    // 指定当前规则适用的HTTP方法,如 GET, POST, DELETE 等。
    // 可以使用数组表示同时适用于多个方法。
    // 如果未设定,表明当前规则适用于所有方法。
    // 当然,这个属性仅在解析请求时有效,在生成URL时是无效的。
    public $verb;

    // 表明当前规则的工作模式,取值可以是 0, PARSING_ONLY, CREATION_ONLY。
    // 未设定时等同于0。
    public $mode;

    // 表明URL中的参数是否需要进行url编码,默认是进行。
    public $encodeParams = true;

    // 用于生成新URL的模板
    private $_template;

    // 一个用于匹配路由部分的正则表达式,用于生成URL
    private $_routeRule;

    // 用于保存一组匹配参数的正则表达式,用于生成URL
    private $_paramRules = [];

    // 保存一组路由中使用的参数
    private $_routeParams = [];

    // 初始化
    public function init() {...}

    // 用于解析请求,由UrlRequestInterface接口要求
    public function parseRequest($manager, $request) {...}

    // 用于生成URL,由UrlRequestInterface接口要求
    public function createUrl($manager, $route, $params) {...}
}

从上面代码看, UrlRule 的属性(可配置项)比较多。

我们要着重分析一下初始化函数 yii\web\UrlRule::init() ,来加深对这些属性的理解:

public function init()
{
    // 一个路由规则必定要有 pattern ,否则是没有意义的,
    // 一个什么都没规定的规定,要来何用?
    if ($this->pattern === null) {
        throw new InvalidConfigException('UrlRule::pattern must be set.');
    }

    // 不指定规则匹配后所要指派的路由,Yii怎么知道将请求交给谁来处理?
    // 不指定路由,Yii怎么知道这个规则可以为谁创建URL?
    if ($this->route === null) {
        throw new InvalidConfigException('UrlRule::route must be set.');
    }

    // 如果定义了一个或多个verb,说明规则仅适用于特定的HTTP方法。
    // 既然是HTTP方法,那就要全部大写。
    // verb的定义可以是字符串(单一的verb)或数组(单一或多个verb)。
    if ($this->verb !== null) {
        if (is_array($this->verb)) {
            foreach ($this->verb as $i => $verb) {
                $this->verb[$i] = strtoupper($verb);
            }
        } else {
            $this->verb = [strtoupper($this->verb)];
        }
    }

    // 若未指定规则的名称,那么使用最能区别于其他规则的 $pattern 作为规则的名称
    if ($this->name === null) {
        $this->name = $this->pattern;
    }

    // 删除 pattern 两端的 "/",特别是重复的 "/",
    // 在写 pattern 时,虽然有正则的成分,但不需要在两端加上 "/",
    // 更不能加上 "#" 等其他分隔符
    $this->pattern = trim($this->pattern, '/');

    // 如果定义了 host ,将 host 部分加在 pattern 前面,作为新的 pattern
    if ($this->host !== null) {
        // 写入的host末尾如果已经包含有 "/" 则去掉,特别是重复的 "/"
        $this->host = rtrim($this->host, '/');
        $this->pattern = rtrim($this->host . '/' . $this->pattern, '/');

    // 既未定义 host ,pattern 又是空的,那么 pattern 匹配任意字符串。
    // 而基于这个pattern的,用于生成的URL的template就是空的,
    // 意味着使用该规则生成所有URL都是空的。
    // 后续也无需再作其他初始化工作了。
    } elseif ($this->pattern === '') {
        $this->_template = '';
        $this->pattern = '#^$#u';
        return;

    // pattern 不是空串,且包含有 '://',以此认定该pattern包含主机信息
    } elseif (($pos = strpos($this->pattern, '://')) !== false) {
        // 除 '://' 外,第一个 '/' 之前的内容就是主机信息
        if (($pos2 = strpos($this->pattern, '/', $pos + 3)) !== false) {
            $this->host = substr($this->pattern, 0, $pos2);

        // '://' 后再无其他 '/',那么整个 pattern 其实就是主机信息
        } else {
            $this->host = $this->pattern;
        }

    // pattern 不是空串,且不包含主机信息,两端加上 '/' ,形成一个正则
    } else {
        $this->pattern = '/' . $this->pattern . '/';
    }

    // route 也要去掉两头的 '/'
    $this->route = trim($this->route, '/');

    // 从这里往下,请结合流程图来看

    // route 中含有 <参数> ,则将所有参数提取成 [参数 => <参数>]
    // 存入 _routeParams[],
    // 如 ['controller' => '<controller>', 'action' => '<action>'],
    // 留意这里的短路判断,先使用 strpos(),快速排除无需使用正则的情况
    if (strpos($this->route, '<') !== false &&
        preg_match_all('/<(\w+)>/', $this->route, $matches)) {
        foreach ($matches[1] as $name) {
            $this->_routeParams[$name] = "<$name>";
        }
    }

    // 这个 $tr[] 和 $tr2[] 用于字符串的转换
    $tr = [
        '.' => '\\.',
        '*' => '\\*',
        '$' => '\\$',
        '[' => '\\[',
        ']' => '\\]',
        '(' => '\\(',
        ')' => '\\)',
    ];
    $tr2 = [];

    // pattern 中含有 <参数名:参数pattern> ,
    // 其中 ':参数pattern' 部分是可选的。
    if (preg_match_all('/<(\w+):?([^>]+)?>/', $this->pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
        foreach ($matches as $match) {
            // 获取 “参数名”
            $name = $match[1][0];

            // 获取 “参数pattern” ,如果未指定,使用 '[^\/]' ,
            // 表示匹配除 '/' 外的所有字符
            $pattern = isset($match[2][0]) ? $match[2][0] : '[^\/]+';

            // 如果 defaults[] 中有同名参数,
            if (array_key_exists($name, $this->defaults)) {
                // $match[0][0] 是整个 <参数名:参数pattern> 串
                $length = strlen($match[0][0]);
                $offset = $match[0][1];

                // pattern 中 <参数名:参数pattern> 两头都有 '/'
                if ($offset > 1 && $this->pattern[$offset - 1] === '/' && $this->pattern[$offset + $length] === '/') {
                    // 留意这个 (?P<name>pattern) 正则,这是一个命名分组。
                    // 仅冠以一个命名供后续引用,使用上与直接的 (pattern) 没有区别
                    // 见:http://php.net/manual/en/regexp.reference.subpatterns.php
                    $tr["/<$name>"] = "(/(?P<$name>$pattern))?";
                } else {
                    $tr["<$name>"] = "(?P<$name>$pattern)?";
                }

            // defaults[]中没有同名参数
            } else {
                $tr["<$name>"] = "(?P<$name>$pattern)";
            }

            // routeParams[]中有同名参数
            if (isset($this->_routeParams[$name])) {
                $tr2["<$name>"] = "(?P<$name>$pattern)";

            // routeParams[]中没有同名参数,则将 参数pattern 存入 _paramRules[] 中。
            // 留意这里是怎么对  参数pattern  进行处理后再保存的。
            } else {
                $this->_paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#u";
            }
        }
    }

    // 将 pattern 中所有的 <参数名:参数pattern> 替换成 <参数名> 后作为 _template
    $this->_template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern);

    // 将 _template 中的特殊字符及字符串使用 tr[] 进行转换,并作为最终的pattern
    $this->pattern = '#^' . trim(strtr($this->_template, $tr), '/') . '$#u';

    // 如果指定了 routePrams 还要使用 tr2[] 对 route 进行转换,
    // 并作为最终的 _routeRule
    if (!empty($this->_routeParams)) {
        $this->_routeRule = '#^' . strtr($this->route, $tr2) . '$#u';
    }
}

init() 的前半部分,这些代码提醒我们:

  • 规则的 $pattern 和 $route 是必须配置的。
  • 规则的名称 $name 和主机信息 $host 在未配置的情况下,可以从 $pattern 来获取。
  • $pattern 虽然含有正则的成分,但不需要在两端加入 / ,更不能使用 # 等其他分隔符。 Yii会自动为我们加上。
  • 指定 $pattern 为空串,可以使该规则匹配任意的URL。此时基于该规则所生成的所有URL也都是空串。
  • $pattern 中含有 :\\ 时,Yii会认为其中包含了主机信息。此时就不应当再指定 host 。 否则,Yii会将 host 接在这个 pattern 前,作为新的pattern。这会造成该pattern 两段 :\\ , 而这显然不是我们要的。

init() 的后半段, 我们以一个普通的 ['post/<action:\w+>/<id:\d+>' => 'post/<action>'] 为例。 同时,我们假设这个路由规则默认有 $defaults['id'] = 100 ,表示在未指定 post 的 id 时, 使用100作为默认的id。那么这个UrlRule的初始过程如 UrlRule路由规则初始化过程示意图所示。

后续的初始化过程具体如下:

  1. ['post/<action:\w+>/<id:\d+>' => 'post/<action>'] , 我们有$pattern = '/post/<action:\w+>/<id:\d+>/'$route = 'post/<action>'
  2. 首先从 $route 中提取出由 < > 所包含的部分。这里可以得到 ['action' => '<action>'] 。 将其存入 $_routeParams[ ] 中。
  3. 再从 $pattern 中提取出由 < > 所包含的部分,这里匹配2个部分,1个 <action:\w+> 和1个 <id\d+> 。下面对这2个部分进行分别处理。
  4. 对于 <action:\w+> 由于 $defaults[ ] 中不存在下标为 action 的元素,于是向 $tr[ ] 写入(?P<$name>$pattern) 形式的元素, 得到 $tr['<action>'] = '(?P<$name>\w+)' 。 而对于 <id:\d+> ,由于 $defaults['id'] = 100 , 所以写入 $tr[ ] 的元素形式有所不同, 变成(/(?P<$name>$pattern))? 。于是有 $tr['<id>'] = (/(?P<$id>\d+))?
  5. 由于在第1步只有 $_routeParams['action'] = '<actoin>' ,而没有下标为 id 的元素。 所以对于 <action:\w+> ,往 tr2[ ] 中写入 ['<action>' => '(?P<action>\w+)'] , 而对于 <id:\d+> 则往 $_paramRules[ ] 中写入 ['id' => '#^\d+$#u']
  6. 上面只是准备工作,接下来开始各种替换。首先将 $pattern 中所有 <name:pattern> 替换成 <name> 并作为 $_template 。 因此, $_template = '/post/<action>/<id>/'
  7. 接下来用 $tr[ ]$_template 进行替换,并在两端加上分隔符作为 $pattern 。 于是有$pattern = '#^post/(?P<action>\w+)(/(?P<id>\d+))?$#u'
  8. 最后,由于第1步中 $_routeParams 不为空,所以需要使用 $tr2[ ]$route 进行替换, 并在两端加上分隔符后,作为 $_routeRule ,于是有 $_routeRule = '#^post/(?P<action>\w+)$#'

这些替换的意义在于方便开发者以简洁的方式在配置文件中书写路由规则,然后将这些简洁的规则, 再替换成规范的正则表达式。让我们来看看这个 init() 的成果吧。 仍然以上面的['post/<action:\w+>/<id:\d+>' => 'post/<action>'] 为例,经过 init() 处理后,我们最终得到了:

$urlRule->route = 'post/<action>';
$urlRule->pattern = '#^post/(?P<action>\w+)(/(?P<id>\d+))?$#u';
$urlRule->_template = '/post/<action>/<id>/';
$urlRule->_routeRule = '#^post/(?P<action>\w+)$#';
$urlRule->_routeParams = ['action' => '<action>'];
$urlRule->_paramRules = ['id' => '#^\d+$#u'];
// $tr 和 $tr2 作为局部变量已经完成历史使命光荣退伍了

下面我们来讲讲 UrlRule 是如何创建和解析URL的。

创建URL

URL的创建就 UrlRule 层面来讲,是由 yii\web\UrlRule::createUrl() 负责的, 这个方法可以根据传入的路由和参数创建一个相应的URL来。具体代码如下:

public function createUrl($manager, $route, $params)
{
    // 判断规则是否仅限于解析请求,而不适用于创建URL
    if ($this->mode === self::PARSING_ONLY) {
        return false;
    }
    $tr = [];

    // 如果传入的路由与规则定义的路由不一致,
    // 如 post/view 与 post/<action> 并不一致
    if ($route !== $this->route) {

        // 使用 $_routeRule 对 $route 作匹配测试
        if ($this->_routeRule !== null && preg_match($this->_routeRule, $route, $matches)) {

            // 遍历所有的 _routeParams
            foreach ($this->_routeParams as $name => $token) {
                // 如果该路由规则提供了默认的路由参数,
                // 且该参数值与传入的路由相同,则可以省略
                if (isset($this->defaults[$name]) && strcmp($this->defaults[$name], $matches[$name]) === 0) {
                    $tr[$token] = '';
                } else {
                    $tr[$token] = $matches[$name];
                }
            }
        // 传入的路由完全不能匹配该规则,返回
        } else {
            return false;
        }
    }

    // 遍历所有的默认参数
    foreach ($this->defaults as $name => $value) {
        // 如果默认参数是路由参数,如 <action>
        if (isset($this->_routeParams[$name])) {
            continue;
        }

        // 默认参数并非路由参数,那么看看传入的 $params 里是否提供该参数的值。
        // 如果未提供,说明这个规则不适用,直接返回。
        if (!isset($params[$name])) {
            return false;

        // 如果 $params 提供了该参数,且参数值一致,则 $params 可省略该参数
        } elseif (strcmp($params[$name], $value) === 0) {
            unset($params[$name]);

            // 且如果有该参数的转换规则,也可置为空。等下一转换就消除了。
            if (isset($this->_paramRules[$name])) {
                $tr["<$name>"] = '';
            }

        // 如果 $params 提供了该参数,但又与默认参数值不一致,
        // 且规则也未定义该参数的正则,那么规则无法处理这个参数。
        } elseif (!isset($this->_paramRules[$name])) {
            return false;
        }
    }

    // 遍历所有的参数匹配规则
    foreach ($this->_paramRules as $name => $rule) {

        // 如果 $params 传入了同名参数,且该参数不是数组,且该参数匹配规则,
        // 则使用该参数匹配规则作为转换规则,并从 $params 中去掉该参数
        if (isset($params[$name]) && !is_array($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) {
            $tr["<$name>"] = $this->encodeParams ? urlencode($params[$name]) : $params[$name];
            unset($params[$name]);

        // 否则一旦没有设置该参数的默认值或 $params 提供了该参数,
        // 说明规则又不匹配了
        } elseif (!isset($this->defaults[$name]) || isset($params[$name])) {
            return false;
        }
    }

    // 使用 $tr 对 $_template 时行转换,并去除多余的 '/'
    $url = trim(strtr($this->_template, $tr), '/');

    // 将 $url 中的多个 '/' 变成一个
    if ($this->host !== null) {
        // 再短的 host 也不会短于 8
        $pos = strpos($url, '/', 8);
        if ($pos !== false) {
            $url = substr($url, 0, $pos) . preg_replace('#/+#', '/', substr($url, $pos));
        }
    } elseif (strpos($url, '//') !== false) {
        $url = preg_replace('#/+#', '/', $url);
    }

    // 加上 .html 之类的假后缀
    if ($url !== '') {
        $url .= ($this->suffix === null ? $manager->suffix : $this->suffix);
    }

    // 加上查询参数们
    if (!empty($params) && ($query = http_build_query($params)) !== '') {
        $url .= '?' . $query;
    }
    return $url;
}

我们以上面提到 ['post/<action:\w+>/<id:\d+>' => 'post/<action>'] 路由规则来创建一个URL, 就假设要创建路由为 post/view , id=100 的URL吧。 具体的流程如 UrlRule创建URL的流程示意图 所示。

结合代码,URL的创建过程大体上分4个阶段:

第一阶段

调用 createUrl(Yii::$app->urlManager, 'post/view', ['id'=>101])

传入的路由为 post/view 与规则定义的路由 post/<action> 不同。 但是, post/view 可以匹配路由规则的 $_routeRule = '#^post/(?P<action>\w+)$#' 。 所以,认为该规则是适用的,可以接着处理。而如果连正则也匹配不上,那就说明该规则不适用,返回 false 。

遍历路由规则的所有 $_routeParams ,这个例子中, $_routeParams['action' => '<action>'] 。 这个我们称为路由参数规则,即出现在路由部分的参数。

对于这个路由参数规则,我们并未为其设置默认值。但实际使用中,有的时候可能会提供默认的路由参数, 比如对于形如 post/index 之类的,我们经常想省略掉 index ,那么就可以为 <action> 提供一个默认值 index 。

对于有默认值的情况,我们的头脑要清醒,目前是要用路由规则来创建URL, 规则所定义的默认值并非意味着可以处理不提供这个路由参数值的路由, 而是说在处理路由参数值与默认值相等的路由时,最终生成的URL中可以省略该默认值。

即默认是体现在最终的URL上,是体现在URL解析过程中的默认,而不是体现在创建URL的过程中。 也就是说,对于 post/index 类的路由,如果默认 index ,则生成的URL中可以不带有 index 。

这里没有默认值,相对简单。由于 $_routeRule 正则中,使用了命名分组,即 (?P<action>...) 。 所以,可以很方便地使用 $matches['action'] 来捕获 \w+ 所匹配的部分,这里匹配的是 view 。 故写入 $tr['<action>'] = view

第二阶段

接下来遍历所有的默认参数,当然不包含路由参数部分,因为这个在前面已经处理过了。这里只处理余下的参数。 注意这个默认值的含义,如同我们前面提到的,这里是创建时必须提供,而生成出来的URL可以省略的意思。 因此,对于 $params 中未提供相应参数的,或提供了参数值但与默认值不一致,且规则没定义参数的正则的, 均说明规则不适用。 只有在 $params 提供相应参数,且参数值与默认值一致或匹配规则时方可进行后续处理。

这里我们有一个默认参数 ['id' => 100] 。传入的 $params['id'] => 100 。两者一致,规则适用。

于将该参数从 $params 中删去。

接下来,看看是否为参数 id 定义了匹配规则。还真有 $_paramRules['id'] => '#^\d+$#u' 。 但这也用不上了,因为这是在创建URL,该参数与默认值一致,等下的 id 是要从URL中去除的。 因此,写入 $tr['<id>'] = ''

第三阶段

再接下来,就是遍历所有参数匹配规则 $_paramRules 了。对于这个例子, 只有$_paramRules = ['id' => '#^\d+$#u']

如果 $params 中并未定义该 id 参数,那么这一步什么也不用做,因为没有东西要写到URL中去。

而一旦定义了 id ,那么就需要看看当前路由规则是否适用了。 判断的标准是所提供的参数不是数组,且匹配 $_paramRules 所定义的规则。 而如果 $parasm['id'] 是数组,或不与规则匹配,或定义了 id 的参数规则却没有定义其默认值而 $params['id'] 又未提供,则规则不适用。

这里,我们在是在前面处理默认参数时,已经将 id 从 $params 中删去。 但判断到规则为 id 定义了默认值的,所以认为规则仍然适用。 只是,这里实际上不用做任何处理。 如果需要处理的情况,也是将该参数从 $params 中删去,然后写入 $tr['<id>'] = 100

第四阶段

上面一切准备就绪之后,就可以着手生成URL了。主要用 $tr 对路由规则的 $_template 进行转换。 这里, $_template = '/post/<action>/<id>/' ,因此,转换后再去除多余的 / 就变成了$url = 'post/view' 。 其中 id=100 被省略掉了。

最后再分别接上 .html 的后缀和查询参数,一个URL post/view.html 就生成了。 其中,查询参数串是 $params 中剩下的内容,使用PHP的 http_build_query() 生成的。

从创建URL的过程来看,重点是完成这么几项工作:

  • 看规则是否适用,主要标准是路由与规则定义的是否匹配,规则通过默认值或正则所定义的参数,是否都提供了。
  • 看看当前要创建的URL是否与规则定义的默认的路由参数和查询参数一致,对于一致的,可以省略。
  • 看将这些与默认值一致的,规则已经定义了的参数从 $params 删除,余下的,转换成最终URL的查询参数串。

解析URL

说完了路由规则生成URL的过程,再来看看其逻辑上的逆过程,即URL的解析。

先从路由规则 yii\web\UrlRule::parseRequest() 的代码入手:

public function parseRequest($manager, $request)
{
    // 当前路由规则仅限于创建URL,直接返回 false。
    // 该方法返回false表示当前规则不适用于当前的URL。
    if ($this->mode === self::CREATION_ONLY) {
        return false;
    }

    // 如果规则定义了适用的HTTP方法,则要看当前请求采用的方法是否可以接受
    if (!empty($this->verb) && !in_array($request->getMethod(), $this->verb, true)) {
        return false;
    }

    // 获取URL中入口脚本之后、查询参数 ? 号之前的全部内容,即为PATH_INFO
    $pathInfo = $request->getPathInfo();
    // 取得配置的 .html 等假后缀,留意 (string)null 转成空串
    $suffix = (string) ($this->suffix === null ? $manager->suffix : $this->suffix);

    // 有假后缀且有PATH_INFO
    if ($suffix !== '' && $pathInfo !== '') {
        $n = strlen($suffix);

        // 当前请求的 PATH_INFO 以该假后缀结尾,留意 -$n 的用法
        if (substr_compare($pathInfo, $suffix, -$n, $n) === 0) {
            $pathInfo = substr($pathInfo, 0, -$n);

            // 整个PATH_INFO 仅包含一个假后缀,这是无效的。
            if ($pathInfo === '') {
                return false;
            }

        // 应用配置了假后缀,但是当前URL却不包含该后缀,返回false
        } else {
            return false;
        }
    }

    // 规则定义了主机信息,即 http://www.digpage.com 之类,那要把主机信息接回去。
    if ($this->host !== null) {
        $pathInfo = strtolower($request->getHostInfo()) .
            ($pathInfo === '' ? '' : '/' . $pathInfo);
    }

    // 当前URL是否匹配规则,留意这个pattern是经过 init() 转换的
    if (!preg_match($this->pattern, $pathInfo, $matches)) {
        return false;
    }

    // 遍历规则定义的默认参数,如果当前URL中没有,则加入到 $matches 中待统一处理,
    // 默认值在这里发挥作用了,虽然没有,但仍视为捕获到了。
    foreach ($this->defaults as $name => $value) {
        if (!isset($matches[$name]) || $matches[$name] === '') {
            $matches[$name] = $value;
        }
    }
    $params = $this->defaults;
    $tr = [];

    // 遍历所有匹配项,注意这个 $name 的由来是 (?P<name>...) 的功劳
    foreach ($matches as $name => $value) {
        // 如果是匹配一个路由参数
        if (isset($this->_routeParams[$name])) {
            $tr[$this->_routeParams[$name]] = $value;
            unset($params[$name]);

        // 如果是匹配一个查询参数
        } elseif (isset($this->_paramRules[$name])) {
            // 这里可能会覆盖掉 $defaults 定义的默认值
            $params[$name] = $value;
        }
    }

    // 使用 $tr 进行转换
    if ($this->_routeRule !== null) {
        $route = strtr($this->route, $tr);
    } else {
        $route = $this->route;
    }
    Yii::trace("Request parsed with URL rule: {$this->name}", __METHOD__);
    return [$route, $params];
}

我们以 http://www.digpage.com/post/view.html 为例,来看看上面的代码是如何解析成路由['post/view', ['id'=>100]] 的。 注意,这里我们仍然假设路由规则提供了 id=100 默认值。 而如果路由规则未提供该默认值, 则请求形式要变成 http://www.digapge.com/post/view/100.html 。 同时,规则的 $pattern 也会不同:

// 未提供默认值,id 必须提供,否则匹配不上
$pattern = '#^post/(?P<action>\w+)/(?P<id>\d+)?$#u';

// 提供了默认值,id 可以不提供,照样可以匹配上
$pattern = '#^post/(?P<action>\w+)(/(?P<id>\d+))?$#u';

这个不同的原因在于 UrlRule::init() ,读者朋友们可以回头看看。

在讲URL的解析前,让我们先从请求的第一个经手人Web Server说起,在 附录2:Yii的安装 中讲到Web Server的配置时, 我们将所有未命中的请求转交给入口脚本来处理:

location / {
    try_files $uri $uri/ /index.php?$args;
}

# fastcgi.conf
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

以Nginx为例, try_files 会依次尝试处理:

  1. /post/view.html ,这个如果真有一个也就罢了,但其实多半不存在。
  2. /post/view.html/ ,这个目录一般也不存在。
  3. /index.php ,这个正是入口脚本,可以处理。至此,Nginx与Yii顺利交接。

由于请求最终交给入口脚本来处理,且我们隐藏了URL中入口脚本名,上述请求还原回来的话, 应该是 http://www.digapge.com/index.php/post/view.html 。 自然,这 post/view.html 就是 PATH_INFO 了。 有关 PATH_INFO 的更多知识,请看 Web应用Request 部分的内容。

好了,在Yii从Web Server取得控制权之后,就是我们大显身手的时候了。在解析过程中,UrlRule主要做了这么几件事:

  • 通过 PATH_INFO 还原请求,如去除假后缀,开头接上主机信息等。还原后的请求为 post/view。
  • 看看当前请求是否匹配规则,这个匹配包含了主机、路由、参数等各方面的匹配。如不匹配,说明规则不适用, 返回 false 。 在这个例子中,规则并未定义主机信息方面的规则, 规则中$pattern = '#^post/(?P<action>\w+)(/(?P<id>\d+))?$#u' 。 这与还原后的请求完全匹配。 如果URL没有使用默认值 id = 100 ,如 post/view/101.html ,也是同样匹配的。
  • 看看请求是否提供了规则已定义了默认值的所有参数,如果未提供,视为请求提供了这些参数,且他的值为默认值。 这里URL中并未提供 id 参数,所以,视为他提供了 id = 100 的参数。简单粗暴而有效。
  • 使用规则定义的路由进行转换,生成新的路由。 再把上一步及当前所有参数作为路由的参数,共同组装成一个完整路由。

具体的转换流程可以看看 UrlRule路由规则解析URL的过程示意图 。

源码

yii\web\UrlManager

yii\web\UrlManager 类:

<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\web;

use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\caching\CacheInterface;
use yii\di\Instance;
use yii\helpers\Url;

/**
 * UrlManager handles HTTP request parsing and creation of URLs based on a set of rules.
 *
 * UrlManager is configured as an application component in [[\yii\base\Application]] by default.
 * You can access that instance via `Yii::$app->urlManager`.
 *
 * You can modify its configuration by adding an array to your application config under `components`
 * as it is shown in the following example:
 * 
 * 'urlManager' => [
 *     'enablePrettyUrl' => true,
 *     'rules' => [
 *         // your rules go here
 *     ],
 *     // ...
 * ]
 * 
 *
 * Rules are classes implementing the [[UrlRuleInterface]], by default that is [[UrlRule]].
 * For nesting rules, there is also a [[GroupUrlRule]] class.
 *
 * For more details and usage information on UrlManager, see the [guide article on routing](guide:runtime-routing).
 *
 * @property string $baseUrl The base URL that is used by [[createUrl()]] to prepend to created URLs.
 * @property string $hostInfo The host info (e.g. `http://www.example.com`) that is used by
 * [[createAbsoluteUrl()]] to prepend to created URLs.
 * @property string $scriptUrl The entry script URL that is used by [[createUrl()]] to prepend to created
 * URLs.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class UrlManager extends Component
{
    /**
     * @var bool whether to enable pretty URLs. Instead of putting all parameters in the query
     * string part of a URL, pretty URLs allow using path info to represent some of the parameters
     * and can thus produce more user-friendly URLs, such as "/news/Yii-is-released", instead of
     * "/index.php?r=news%2Fview&id=100".
     */
    public $enablePrettyUrl = false;
    /**
     * @var bool whether to enable strict parsing. If strict parsing is enabled, the incoming
     * requested URL must match at least one of the [[rules]] in order to be treated as a valid request.
     * Otherwise, the path info part of the request will be treated as the requested route.
     * This property is used only when [[enablePrettyUrl]] is `true`.
     */
    public $enableStrictParsing = false;
    /**
     * @var array the rules for creating and parsing URLs when [[enablePrettyUrl]] is `true`.
     * This property is used only if [[enablePrettyUrl]] is `true`. Each element in the array
     * is the configuration array for creating a single URL rule. The configuration will
     * be merged with [[ruleConfig]] first before it is used for creating the rule object.
     *
     * A special shortcut format can be used if a rule only specifies [[UrlRule::pattern|pattern]]
     * and [[UrlRule::route|route]]: `'pattern' => 'route'`. That is, instead of using a configuration
     * array, one can use the key to represent the pattern and the value the corresponding route.
     * For example, `'post/<id:\d+>' => 'post/view'`.
     *
     * For RESTful routing the mentioned shortcut format also allows you to specify the
     * [[UrlRule::verb|HTTP verb]] that the rule should apply for.
     * You can do that  by prepending it to the pattern, separated by space.
     * For example, `'PUT post/<id:\d+>' => 'post/update'`.
     * You may specify multiple verbs by separating them with comma
     * like this: `'POST,PUT post/index' => 'post/create'`.
     * The supported verbs in the shortcut format are: GET, HEAD, POST, PUT, PATCH and DELETE.
     * Note that [[UrlRule::mode|mode]] will be set to PARSING_ONLY when specifying verb in this way
     * so you normally would not specify a verb for normal GET request.
     *
     * Here is an example configuration for RESTful CRUD controller:
     *
     * ```php
     * [
     *     'dashboard' => 'site/index',
     *
     *     'POST <controller:[\w-]+>' => '<controller>/create',
     *     '<controller:[\w-]+>s' => '<controller>/index',
     *
     *     'PUT <controller:[\w-]+>/<id:\d+>'    => '<controller>/update',
     *     'DELETE <controller:[\w-]+>/<id:\d+>' => '<controller>/delete',
     *     '<controller:[\w-]+>/<id:\d+>'        => '<controller>/view',
     * ];
     * ```
     *
     * Note that if you modify this property after the UrlManager object is created, make sure
     * you populate the array with rule objects instead of rule configurations.
     */
    public $rules = [];
    /**
     * @var string the URL suffix used when [[enablePrettyUrl]] is `true`.
     * For example, ".html" can be used so that the URL looks like pointing to a static HTML page.
     * This property is used only if [[enablePrettyUrl]] is `true`.
     */
    public $suffix;
    /**
     * @var bool whether to show entry script name in the constructed URL. Defaults to `true`.
     * This property is used only if [[enablePrettyUrl]] is `true`.
     */
    public $showScriptName = true;
    /**
     * @var string the GET parameter name for route. This property is used only if [[enablePrettyUrl]] is `false`.
     */
    public $routeParam = 'r';
    /**
     * @var CacheInterface|array|string|bool the cache object or the application component ID of the cache object.
     * This can also be an array that is used to create a [[CacheInterface]] instance in case you do not want to use
     * an application component.
     * Compiled URL rules will be cached through this cache object, if it is available.
     *
     * After the UrlManager object is created, if you want to change this property,
     * you should only assign it with a cache object.
     * Set this property to `false` or `null` if you do not want to cache the URL rules.
     *
     * Cache entries are stored for the time set by [[\yii\caching\Cache::$defaultDuration|$defaultDuration]] in
     * the cache configuration, which is unlimited by default. You may want to tune this value if your [[rules]]
     * change frequently.
     */
    public $cache = 'cache';
    /**
     * @var array the default configuration of URL rules. Individual rule configurations
     * specified via [[rules]] will take precedence when the same property of the rule is configured.
     */
    public $ruleConfig = ['class' => 'yii\web\UrlRule'];
    /**
     * @var UrlNormalizer|array|string|false the configuration for [[UrlNormalizer]] used by this UrlManager.
     * The default value is `false`, which means normalization will be skipped.
     * If you wish to enable URL normalization, you should configure this property manually.
     * For example:
     *
     * ```php
     * [
     *     'class' => 'yii\web\UrlNormalizer',
     *     'collapseSlashes' => true,
     *     'normalizeTrailingSlash' => true,
     * ]
     * ```
     *
     * @since 2.0.10
     */
    public $normalizer = false;

    /**
     * @var string the cache key for cached rules
     * @since 2.0.8
     */
    protected $cacheKey = __CLASS__;

    private $_baseUrl;
    private $_scriptUrl;
    private $_hostInfo;
    private $_ruleCache;


    /**
     * Initializes UrlManager.
     */
    public function init()
    {
        parent::init();

        if ($this->normalizer !== false) {
            $this->normalizer = Yii::createObject($this->normalizer);
            if (!$this->normalizer instanceof UrlNormalizer) {
                throw new InvalidConfigException('`' . get_class($this) . '::normalizer` should be an instance of `' . UrlNormalizer::className() . '` or its DI compatible configuration.');
            }
        }

        if (!$this->enablePrettyUrl) {
            return;
        }

        if (!empty($this->rules)) {
            $this->rules = $this->buildRules($this->rules);
        }
    }

    /**
     * Adds additional URL rules.
     *
     * This method will call [[buildRules()]] to parse the given rule declarations and then append or insert
     * them to the existing [[rules]].
     *
     * Note that if [[enablePrettyUrl]] is `false`, this method will do nothing.
     *
     * @param array $rules the new rules to be added. Each array element represents a single rule declaration.
     * Please refer to [[rules]] for the acceptable rule format.
     * @param bool $append whether to add the new rules by appending them to the end of the existing rules.
     */
    public function addRules($rules, $append = true)
    {
        if (!$this->enablePrettyUrl) {
            return;
        }
        $rules = $this->buildRules($rules);
        if ($append) {
            $this->rules = array_merge($this->rules, $rules);
        } else {
            $this->rules = array_merge($rules, $this->rules);
        }
    }

    /**
     * Builds URL rule objects from the given rule declarations.
     *
     * @param array $ruleDeclarations the rule declarations. Each array element represents a single rule declaration.
     * Please refer to [[rules]] for the acceptable rule formats.
     * @return UrlRuleInterface[] the rule objects built from the given rule declarations
     * @throws InvalidConfigException if a rule declaration is invalid
     */
    protected function buildRules($ruleDeclarations)
    {
        $builtRules = $this->getBuiltRulesFromCache($ruleDeclarations);
        if ($builtRules !== false) {
            return $builtRules;
        }

        $builtRules = [];
        $verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS';
        foreach ($ruleDeclarations as $key => $rule) {
            if (is_string($rule)) {
                $rule = ['route' => $rule];
                if (preg_match("/^((?:($verbs),)*($verbs))\\s+(.*)$/", $key, $matches)) {
                    $rule['verb'] = explode(',', $matches[1]);
                    $key = $matches[4];
                }
                $rule['pattern'] = $key;
            }
            if (is_array($rule)) {
                $rule = Yii::createObject(array_merge($this->ruleConfig, $rule));
            }
            if (!$rule instanceof UrlRuleInterface) {
                throw new InvalidConfigException('URL rule class must implement UrlRuleInterface.');
            }
            $builtRules[] = $rule;
        }

        $this->setBuiltRulesCache($ruleDeclarations, $builtRules);

        return $builtRules;
    }

    /**
     * @return CacheInterface|null|bool
     */
    private function ensureCache()
    {
        if (!$this->cache instanceof CacheInterface && $this->cache !== false && $this->cache !== null) {
            try {
                $this->cache = Instance::ensure($this->cache, 'yii\caching\CacheInterface');
            } catch (InvalidConfigException $e) {
                Yii::warning('Unable to use cache for URL manager: ' . $e->getMessage());
                $this->cache = null;
            }
        }

        return $this->cache;
    }

    /**
     * Stores $builtRules to cache, using $rulesDeclaration as a part of cache key.
     *
     * @param array $ruleDeclarations the rule declarations. Each array element represents a single rule declaration.
     * Please refer to [[rules]] for the acceptable rule formats.
     * @param UrlRuleInterface[] $builtRules the rule objects built from the given rule declarations.
     * @return bool whether the value is successfully stored into cache
     * @since 2.0.14
     */
    protected function setBuiltRulesCache($ruleDeclarations, $builtRules)
    {
        $cache = $this->ensureCache();
        if (!$cache) {
            return false;
        }

        return $cache->set([$this->cacheKey, $this->ruleConfig, $ruleDeclarations], $builtRules);
    }

    /**
     * Provides the built URL rules that are associated with the $ruleDeclarations from cache.
     *
     * @param array $ruleDeclarations the rule declarations. Each array element represents a single rule declaration.
     * Please refer to [[rules]] for the acceptable rule formats.
     * @return UrlRuleInterface[]|false the rule objects built from the given rule declarations or boolean `false` when
     * there are no cache items for this definition exists.
     * @since 2.0.14
     */
    protected function getBuiltRulesFromCache($ruleDeclarations)
    {
        $cache = $this->ensureCache();
        if (!$cache) {
            return false;
        }

        return $cache->get([$this->cacheKey, $this->ruleConfig, $ruleDeclarations]);
    }

    /**
     * Parses the user request.
     * @param Request $request the request component
     * @return array|bool the route and the associated parameters. The latter is always empty
     * if [[enablePrettyUrl]] is `false`. `false` is returned if the current request cannot be successfully parsed.
     */
    public function parseRequest($request)
    {
        if ($this->enablePrettyUrl) {
            /* @var $rule UrlRule */
            foreach ($this->rules as $rule) {
                $result = $rule->parseRequest($this, $request);
                if (YII_DEBUG) {
                    Yii::debug([
                        'rule' => method_exists($rule, '__toString') ? $rule->__toString() : get_class($rule),
                        'match' => $result !== false,
                        'parent' => null,
                    ], __METHOD__);
                }
                if ($result !== false) {
                    return $result;
                }
            }

            if ($this->enableStrictParsing) {
                return false;
            }

            Yii::debug('No matching URL rules. Using default URL parsing logic.', __METHOD__);

            $suffix = (string) $this->suffix;
            $pathInfo = $request->getPathInfo();
            $normalized = false;
            if ($this->normalizer !== false) {
                $pathInfo = $this->normalizer->normalizePathInfo($pathInfo, $suffix, $normalized);
            }
            if ($suffix !== '' && $pathInfo !== '') {
                $n = strlen($this->suffix);
                if (substr_compare($pathInfo, $this->suffix, -$n, $n) === 0) {
                    $pathInfo = substr($pathInfo, 0, -$n);
                    if ($pathInfo === '') {
                        // suffix alone is not allowed
                        return false;
                    }
                } else {
                    // suffix doesn't match
                    return false;
                }
            }

            if ($normalized) {
                // pathInfo was changed by normalizer - we need also normalize route
                return $this->normalizer->normalizeRoute([$pathInfo, []]);
            }

            return [$pathInfo, []];
        }

        Yii::debug('Pretty URL not enabled. Using default URL parsing logic.', __METHOD__);
        $route = $request->getQueryParam($this->routeParam, '');
        if (is_array($route)) {
            $route = '';
        }

        return [(string) $route, []];
    }

    /**
     * Creates a URL using the given route and query parameters.
     *
     * You may specify the route as a string, e.g., `site/index`. You may also use an array
     * if you want to specify additional query parameters for the URL being created. The
     * array format must be:
     *
     * ```php
     * // generates: /index.php?r=site%2Findex&param1=value1&param2=value2
     * ['site/index', 'param1' => 'value1', 'param2' => 'value2']
     * ```
     *
     * If you want to create a URL with an anchor, you can use the array format with a `#` parameter.
     * For example,
     *
     * ```php
     * // generates: /index.php?r=site%2Findex&param1=value1#name
     * ['site/index', 'param1' => 'value1', '#' => 'name']
     * ```
     *
     * The URL created is a relative one. Use [[createAbsoluteUrl()]] to create an absolute URL.
     *
     * Note that unlike [[\yii\helpers\Url::toRoute()]], this method always treats the given route
     * as an absolute route.
     *
     * @param string|array $params use a string to represent a route (e.g. `site/index`),
     * or an array to represent a route with query parameters (e.g. `['site/index', 'param1' => 'value1']`).
     * @return string the created URL
     */
    public function createUrl($params)
    {
        $params = (array) $params;
        $anchor = isset($params['#']) ? '#' . $params['#'] : '';
        unset($params['#'], $params[$this->routeParam]);

        $route = trim($params[0], '/');
        unset($params[0]);

        $baseUrl = $this->showScriptName || !$this->enablePrettyUrl ? $this->getScriptUrl() : $this->getBaseUrl();

        if ($this->enablePrettyUrl) {
            $cacheKey = $route . '?';
            foreach ($params as $key => $value) {
                if ($value !== null) {
                    $cacheKey .= $key . '&';
                }
            }

            $url = $this->getUrlFromCache($cacheKey, $route, $params);
            if ($url === false) {
                /* @var $rule UrlRule */
                foreach ($this->rules as $rule) {
                    if (in_array($rule, $this->_ruleCache[$cacheKey], true)) {
                        // avoid redundant calls of `UrlRule::createUrl()` for rules checked in `getUrlFromCache()`
                        // @see https://github.com/yiisoft/yii2/issues/14094
                        continue;
                    }
                    $url = $rule->createUrl($this, $route, $params);
                    if ($this->canBeCached($rule)) {
                        $this->setRuleToCache($cacheKey, $rule);
                    }
                    if ($url !== false) {
                        break;
                    }
                }
            }

            if ($url !== false) {
                if (strpos($url, '://') !== false) {
                    if ($baseUrl !== '' && ($pos = strpos($url, '/', 8)) !== false) {
                        return substr($url, 0, $pos) . $baseUrl . substr($url, $pos) . $anchor;
                    }

                    return $url . $baseUrl . $anchor;
                } elseif (strncmp($url, '//', 2) === 0) {
                    if ($baseUrl !== '' && ($pos = strpos($url, '/', 2)) !== false) {
                        return substr($url, 0, $pos) . $baseUrl . substr($url, $pos) . $anchor;
                    }

                    return $url . $baseUrl . $anchor;
                }

                $url = ltrim($url, '/');
                return "$baseUrl/{$url}{$anchor}";
            }

            if ($this->suffix !== null) {
                $route .= $this->suffix;
            }
            if (!empty($params) && ($query = http_build_query($params)) !== '') {
                $route .= '?' . $query;
            }

            $route = ltrim($route, '/');
            return "$baseUrl/{$route}{$anchor}";
        }

        $url = "$baseUrl?{$this->routeParam}=" . urlencode($route);
        if (!empty($params) && ($query = http_build_query($params)) !== '') {
            $url .= '&' . $query;
        }

        return $url . $anchor;
    }

    /**
     * Returns the value indicating whether result of [[createUrl()]] of rule should be cached in internal cache.
     *
     * @param UrlRuleInterface $rule
     * @return bool `true` if result should be cached, `false` if not.
     * @since 2.0.12
     * @see getUrlFromCache()
     * @see setRuleToCache()
     * @see UrlRule::getCreateUrlStatus()
     */
    protected function canBeCached(UrlRuleInterface $rule)
    {
        return
            // if rule does not provide info about create status, we cache it every time to prevent bugs like #13350
            // @see https://github.com/yiisoft/yii2/pull/13350#discussion_r114873476
            !method_exists($rule, 'getCreateUrlStatus') || ($status = $rule->getCreateUrlStatus()) === null
            || $status === UrlRule::CREATE_STATUS_SUCCESS
            || $status & UrlRule::CREATE_STATUS_PARAMS_MISMATCH;
    }

    /**
     * Get URL from internal cache if exists.
     * @param string $cacheKey generated cache key to store data.
     * @param string $route the route (e.g. `site/index`).
     * @param array $params rule params.
     * @return bool|string the created URL
     * @see createUrl()
     * @since 2.0.8
     */
    protected function getUrlFromCache($cacheKey, $route, $params)
    {
        if (!empty($this->_ruleCache[$cacheKey])) {
            foreach ($this->_ruleCache[$cacheKey] as $rule) {
                /* @var $rule UrlRule */
                if (($url = $rule->createUrl($this, $route, $params)) !== false) {
                    return $url;
                }
            }
        } else {
            $this->_ruleCache[$cacheKey] = [];
        }

        return false;
    }

    /**
     * Store rule (e.g. [[UrlRule]]) to internal cache.
     * @param $cacheKey
     * @param UrlRuleInterface $rule
     * @since 2.0.8
     */
    protected function setRuleToCache($cacheKey, UrlRuleInterface $rule)
    {
        $this->_ruleCache[$cacheKey][] = $rule;
    }

    /**
     * Creates an absolute URL using the given route and query parameters.
     *
     * This method prepends the URL created by [[createUrl()]] with the [[hostInfo]].
     *
     * Note that unlike [[\yii\helpers\Url::toRoute()]], this method always treats the given route
     * as an absolute route.
     *
     * @param string|array $params use a string to represent a route (e.g. `site/index`),
     * or an array to represent a route with query parameters (e.g. `['site/index', 'param1' => 'value1']`).
     * @param string|null $scheme the scheme to use for the URL (either `http`, `https` or empty string
     * for protocol-relative URL).
     * If not specified the scheme of the current request will be used.
     * @return string the created URL
     * @see createUrl()
     */
    public function createAbsoluteUrl($params, $scheme = null)
    {
        $params = (array) $params;
        $url = $this->createUrl($params);
        if (strpos($url, '://') === false) {
            $hostInfo = $this->getHostInfo();
            if (strncmp($url, '//', 2) === 0) {
                $url = substr($hostInfo, 0, strpos($hostInfo, '://')) . ':' . $url;
            } else {
                $url = $hostInfo . $url;
            }
        }

        return Url::ensureScheme($url, $scheme);
    }

    /**
     * Returns the base URL that is used by [[createUrl()]] to prepend to created URLs.
     * It defaults to [[Request::baseUrl]].
     * This is mainly used when [[enablePrettyUrl]] is `true` and [[showScriptName]] is `false`.
     * @return string the base URL that is used by [[createUrl()]] to prepend to created URLs.
     * @throws InvalidConfigException if running in console application and [[baseUrl]] is not configured.
     */
    public function getBaseUrl()
    {
        if ($this->_baseUrl === null) {
            $request = Yii::$app->getRequest();
            if ($request instanceof Request) {
                $this->_baseUrl = $request->getBaseUrl();
            } else {
                throw new InvalidConfigException('Please configure UrlManager::baseUrl correctly as you are running a console application.');
            }
        }

        return $this->_baseUrl;
    }

    /**
     * Sets the base URL that is used by [[createUrl()]] to prepend to created URLs.
     * This is mainly used when [[enablePrettyUrl]] is `true` and [[showScriptName]] is `false`.
     * @param string $value the base URL that is used by [[createUrl()]] to prepend to created URLs.
     */
    public function setBaseUrl($value)
    {
        $this->_baseUrl = $value === null ? null : rtrim(Yii::getAlias($value), '/');
    }

    /**
     * Returns the entry script URL that is used by [[createUrl()]] to prepend to created URLs.
     * It defaults to [[Request::scriptUrl]].
     * This is mainly used when [[enablePrettyUrl]] is `false` or [[showScriptName]] is `true`.
     * @return string the entry script URL that is used by [[createUrl()]] to prepend to created URLs.
     * @throws InvalidConfigException if running in console application and [[scriptUrl]] is not configured.
     */
    public function getScriptUrl()
    {
        if ($this->_scriptUrl === null) {
            $request = Yii::$app->getRequest();
            if ($request instanceof Request) {
                $this->_scriptUrl = $request->getScriptUrl();
            } else {
                throw new InvalidConfigException('Please configure UrlManager::scriptUrl correctly as you are running a console application.');
            }
        }

        return $this->_scriptUrl;
    }

    /**
     * Sets the entry script URL that is used by [[createUrl()]] to prepend to created URLs.
     * This is mainly used when [[enablePrettyUrl]] is `false` or [[showScriptName]] is `true`.
     * @param string $value the entry script URL that is used by [[createUrl()]] to prepend to created URLs.
     */
    public function setScriptUrl($value)
    {
        $this->_scriptUrl = $value;
    }

    /**
     * Returns the host info that is used by [[createAbsoluteUrl()]] to prepend to created URLs.
     * @return string the host info (e.g. `http://www.example.com`) that is used by [[createAbsoluteUrl()]] to prepend to created URLs.
     * @throws InvalidConfigException if running in console application and [[hostInfo]] is not configured.
     */
    public function getHostInfo()
    {
        if ($this->_hostInfo === null) {
            $request = Yii::$app->getRequest();
            if ($request instanceof \yii\web\Request) {
                $this->_hostInfo = $request->getHostInfo();
            } else {
                throw new InvalidConfigException('Please configure UrlManager::hostInfo correctly as you are running a console application.');
            }
        }

        return $this->_hostInfo;
    }

    /**
     * Sets the host info that is used by [[createAbsoluteUrl()]] to prepend to created URLs.
     * @param string $value the host info (e.g. "http://www.example.com") that is used by [[createAbsoluteUrl()]] to prepend to created URLs.
     */
    public function setHostInfo($value)
    {
        $this->_hostInfo = $value === null ? null : rtrim($value, '/');
    }
}

里面在buildRules()方法有一段正则匹配的内容:

if (preg_match("/^((?:($verbs),)*($verbs))\\s+(.*)$/", $key, $matches)) {
    $rule['verb'] = explode(',', $matches[1]);
    $key = $matches[4];
}

我们看个例子理解下:

$key = 'DELETE,POST <controller:\w+>/<id:\d+>';
$verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS';
preg_match("/^((?:($verbs),)*($verbs))\\s+(.*)$/", $key, $matches);
print_r($matches);
// 输出
Array
(
    [0] => DELETE,POST <controller:\w+>/<id:\d+>
    [1] => DELETE,POST
    [2] => DELETE
    [3] => POST
    [4] => <controller:\w+>/<id:\d+>
)

$key = 'DELETE <controller:\w+>/<id:\d+>';
$verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS';
preg_match("/^((?:($verbs),)*($verbs))\\s+(.*)$/", $key, $matches);
print_r($matches);
// 输出
Array
(
    [0] => DELETE <controller:\w+>/<id:\d+>
    [1] => DELETE
    [2] => 
    [3] => DELETE
    [4] => <controller:\w+>/<id:\d+>
)

$key = 'DELETE,POST,PUT,GET <controller:\w+>/<id:\d+>';
$verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS';
preg_match("/^((?:($verbs),)*($verbs))\\s+(.*)$/", $key, $matches);
print_r($matches);
// 输出
Array
(
    [0] => DELETE,POST,PUT,GET <controller:\w+>/<id:\d+>
    [1] => DELETE,POST,PUT,GET
    [2] => PUT
    [3] => GET
    [4] => <controller:\w+>/<id:\d+>
)

yii\web\UrlRule

yii\web\UrlRule 类:

<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\web;

use Yii;
use yii\base\BaseObject;
use yii\base\InvalidConfigException;

/**
 * UrlRule represents a rule used by [[UrlManager]] for parsing and generating URLs.
 *
 * To define your own URL parsing and creation logic you can extend from this class
 * and add it to [[UrlManager::rules]] like this:
 *
 * 
 * 'rules' => [
 *     ['class' => 'MyUrlRule', 'pattern' => '...', 'route' => 'site/index', ...],
 *     // ...
 * ]
 * 
 *
 * @property-read null|int $createUrlStatus Status of the URL creation after the last [[createUrl()]] call.
 * `null` if rule does not provide info about create status.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class UrlRule extends BaseObject implements UrlRuleInterface
{
    /**
     * Set [[mode]] with this value to mark that this rule is for URL parsing only.
     */
    const PARSING_ONLY = 1;
    /**
     * Set [[mode]] with this value to mark that this rule is for URL creation only.
     */
    const CREATION_ONLY = 2;
    /**
     * Represents the successful URL generation by last [[createUrl()]] call.
     * @see createStatus
     * @since 2.0.12
     */
    const CREATE_STATUS_SUCCESS = 0;
    /**
     * Represents the unsuccessful URL generation by last [[createUrl()]] call, because rule does not support
     * creating URLs.
     * @see createStatus
     * @since 2.0.12
     */
    const CREATE_STATUS_PARSING_ONLY = 1;
    /**
     * Represents the unsuccessful URL generation by last [[createUrl()]] call, because of mismatched route.
     * @see createStatus
     * @since 2.0.12
     */
    const CREATE_STATUS_ROUTE_MISMATCH = 2;
    /**
     * Represents the unsuccessful URL generation by last [[createUrl()]] call, because of mismatched
     * or missing parameters.
     * @see createStatus
     * @since 2.0.12
     */
    const CREATE_STATUS_PARAMS_MISMATCH = 4;

    /**
     * @var string the name of this rule. If not set, it will use [[pattern]] as the name.
     */
    public $name;
    /**
     * On the rule initialization, the [[pattern]] matching parameters names will be replaced with [[placeholders]].
     * @var string the pattern used to parse and create the path info part of a URL.
     * @see host
     * @see placeholders
     */
    public $pattern;
    /**
     * @var string|null the pattern used to parse and create the host info part of a URL (e.g. `http://example.com`).
     * @see pattern
     */
    public $host;
    /**
     * @var string the route to the controller action
     */
    public $route;
    /**
     * @var array the default GET parameters (name => value) that this rule provides.
     * When this rule is used to parse the incoming request, the values declared in this property
     * will be injected into $_GET.
     */
    public $defaults = [];
    /**
     * @var string the URL suffix used for this rule.
     * For example, ".html" can be used so that the URL looks like pointing to a static HTML page.
     * If not set, the value of [[UrlManager::suffix]] will be used.
     */
    public $suffix;
    /**
     * @var string|array the HTTP verb (e.g. GET, POST, DELETE) that this rule should match.
     * Use array to represent multiple verbs that this rule may match.
     * If this property is not set, the rule can match any verb.
     * Note that this property is only used when parsing a request. It is ignored for URL creation.
     */
    public $verb;
    /**
     * @var int a value indicating if this rule should be used for both request parsing and URL creation,
     * parsing only, or creation only.
     * If not set or 0, it means the rule is both request parsing and URL creation.
     * If it is [[PARSING_ONLY]], the rule is for request parsing only.
     * If it is [[CREATION_ONLY]], the rule is for URL creation only.
     */
    public $mode;
    /**
     * @var bool a value indicating if parameters should be url encoded.
     */
    public $encodeParams = true;
    /**
     * @var UrlNormalizer|array|false|null the configuration for [[UrlNormalizer]] used by this rule.
     * If `null`, [[UrlManager::normalizer]] will be used, if `false`, normalization will be skipped
     * for this rule.
     * @since 2.0.10
     */
    public $normalizer;

    /**
     * @var int|null status of the URL creation after the last [[createUrl()]] call.
     * @since 2.0.12
     */
    protected $createStatus;
    /**
     * @var array list of placeholders for matching parameters names. Used in [[parseRequest()]], [[createUrl()]].
     * On the rule initialization, the [[pattern]] parameters names will be replaced with placeholders.
     * This array contains relations between the original parameters names and their placeholders.
     * The array keys are the placeholders and the values are the original names.
     *
     * @see parseRequest()
     * @see createUrl()
     * @since 2.0.7
     */
    protected $placeholders = [];

    /**
     * @var string the template for generating a new URL. This is derived from [[pattern]] and is used in generating URL.
     */
    private $_template;
    /**
     * @var string the regex for matching the route part. This is used in generating URL.
     */
    private $_routeRule;
    /**
     * @var array list of regex for matching parameters. This is used in generating URL.
     */
    private $_paramRules = [];
    /**
     * @var array list of parameters used in the route.
     */
    private $_routeParams = [];


    /**
     * @return string
     * @since 2.0.11
     */
    public function __toString()
    {
        $str = '';
        if ($this->verb !== null) {
            $str .= implode(',', $this->verb) . ' ';
        }
        if ($this->host !== null && strrpos($this->name, $this->host) === false) {
            $str .= $this->host . '/';
        }
        $str .= $this->name;

        if ($str === '') {
            return '/';
        }

        return $str;
    }

    /**
     * Initializes this rule.
     */
    public function init()
    {
        if ($this->pattern === null) {
            throw new InvalidConfigException('UrlRule::pattern must be set.');
        }
        if ($this->route === null) {
            throw new InvalidConfigException('UrlRule::route must be set.');
        }
        if (is_array($this->normalizer)) {
            $normalizerConfig = array_merge(['class' => UrlNormalizer::className()], $this->normalizer);
            $this->normalizer = Yii::createObject($normalizerConfig);
        }
        if ($this->normalizer !== null && $this->normalizer !== false && !$this->normalizer instanceof UrlNormalizer) {
            throw new InvalidConfigException('Invalid config for UrlRule::normalizer.');
        }
        if ($this->verb !== null) {
            if (is_array($this->verb)) {
                foreach ($this->verb as $i => $verb) {
                    $this->verb[$i] = strtoupper($verb);
                }
            } else {
                $this->verb = [strtoupper($this->verb)];
            }
        }
        if ($this->name === null) {
            $this->name = $this->pattern;
        }

        $this->preparePattern();
    }

    /**
     * Process [[$pattern]] on rule initialization.
     */
    private function preparePattern()
    {
        $this->pattern = $this->trimSlashes($this->pattern);
        $this->route = trim($this->route, '/');

        if ($this->host !== null) {
            $this->host = rtrim($this->host, '/');
            $this->pattern = rtrim($this->host . '/' . $this->pattern, '/');
        } elseif ($this->pattern === '') {
            $this->_template = '';
            $this->pattern = '#^$#u';

            return;
        } elseif (($pos = strpos($this->pattern, '://')) !== false) {
            if (($pos2 = strpos($this->pattern, '/', $pos + 3)) !== false) {
                $this->host = substr($this->pattern, 0, $pos2);
            } else {
                $this->host = $this->pattern;
            }
        } elseif (strncmp($this->pattern, '//', 2) === 0) {
            if (($pos2 = strpos($this->pattern, '/', 2)) !== false) {
                $this->host = substr($this->pattern, 0, $pos2);
            } else {
                $this->host = $this->pattern;
            }
        } else {
            $this->pattern = '/' . $this->pattern . '/';
        }

        if (strpos($this->route, '<') !== false && preg_match_all('/<([\w._-]+)>/', $this->route, $matches)) {
            foreach ($matches[1] as $name) {
                $this->_routeParams[$name] = "<$name>";
            }
        }

        $this->translatePattern(true);
    }

    /**
     * Prepares [[$pattern]] on rule initialization - replace parameter names by placeholders.
     *
     * @param bool $allowAppendSlash Defines position of slash in the param pattern in [[$pattern]].
     * If `false` slash will be placed at the beginning of param pattern. If `true` slash position will be detected
     * depending on non-optional pattern part.
     */
    private function translatePattern($allowAppendSlash)
    {
        $tr = [
            '.' => '\\.',
            '*' => '\\*',
            '$' => '\\$',
            '[' => '\\[',
            ']' => '\\]',
            '(' => '\\(',
            ')' => '\\)',
        ];

        $tr2 = [];
        $requiredPatternPart = $this->pattern;
        $oldOffset = 0;
        if (preg_match_all('/<([\w._-]+):?([^>]+)?>/', $this->pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
            $appendSlash = false;
            foreach ($matches as $match) {
                $name = $match[1][0];
                $pattern = isset($match[2][0]) ? $match[2][0] : '[^\/]+';
                $placeholder = 'a' . hash('crc32b', $name); // placeholder must begin with a letter
                $this->placeholders[$placeholder] = $name;
                if (array_key_exists($name, $this->defaults)) {
                    $length = strlen($match[0][0]);
                    $offset = $match[0][1];
                    $requiredPatternPart = str_replace("/{$match[0][0]}/", '//', $requiredPatternPart);
                    if (
                        $allowAppendSlash
                        && ($appendSlash || $offset === 1)
                        && (($offset - $oldOffset) === 1)
                        && isset($this->pattern[$offset + $length])
                        && $this->pattern[$offset + $length] === '/'
                        && isset($this->pattern[$offset + $length + 1])
                    ) {
                        // if pattern starts from optional params, put slash at the end of param pattern
                        // @see https://github.com/yiisoft/yii2/issues/13086
                        $appendSlash = true;
                        $tr["<$name>/"] = "((?P<$placeholder>$pattern)/)?";
                    } elseif (
                        $offset > 1
                        && $this->pattern[$offset - 1] === '/'
                        && (!isset($this->pattern[$offset + $length]) || $this->pattern[$offset + $length] === '/')
                    ) {
                        $appendSlash = false;
                        $tr["/<$name>"] = "(/(?P<$placeholder>$pattern))?";
                    }
                    $tr["<$name>"] = "(?P<$placeholder>$pattern)?";
                    $oldOffset = $offset + $length;
                } else {
                    $appendSlash = false;
                    $tr["<$name>"] = "(?P<$placeholder>$pattern)";
                }

                if (isset($this->_routeParams[$name])) {
                    $tr2["<$name>"] = "(?P<$placeholder>$pattern)";
                } else {
                    $this->_paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#u";
                }
            }
        }

        // we have only optional params in route - ensure slash position on param patterns
        if ($allowAppendSlash && trim($requiredPatternPart, '/') === '') {
            $this->translatePattern(false);
            return;
        }

        $this->_template = preg_replace('/<([\w._-]+):?([^>]+)?>/', '<$1>', $this->pattern);
        $this->pattern = '#^' . trim(strtr($this->_template, $tr), '/') . '$#u';

        // if host starts with relative scheme, then insert pattern to match any
        if ($this->host !== null && strncmp($this->host, '//', 2) === 0) {
            $this->pattern = substr_replace($this->pattern, '[\w]+://', 2, 0);
        }

        if (!empty($this->_routeParams)) {
            $this->_routeRule = '#^' . strtr($this->route, $tr2) . '$#u';
        }
    }

    /**
     * @param UrlManager $manager the URL manager
     * @return UrlNormalizer|null
     * @since 2.0.10
     */
    protected function getNormalizer($manager)
    {
        if ($this->normalizer === null) {
            return $manager->normalizer;
        }

        return $this->normalizer;
    }

    /**
     * @param UrlManager $manager the URL manager
     * @return bool
     * @since 2.0.10
     */
    protected function hasNormalizer($manager)
    {
        return $this->getNormalizer($manager) instanceof UrlNormalizer;
    }

    /**
     * Parses the given request and returns the corresponding route and parameters.
     * @param UrlManager $manager the URL manager
     * @param Request $request the request component
     * @return array|bool the parsing result. The route and the parameters are returned as an array.
     * If `false`, it means this rule cannot be used to parse this path info.
     */
    public function parseRequest($manager, $request)
    {
        if ($this->mode === self::CREATION_ONLY) {
            return false;
        }

        if (!empty($this->verb) && !in_array($request->getMethod(), $this->verb, true)) {
            return false;
        }

        $suffix = (string) ($this->suffix === null ? $manager->suffix : $this->suffix);
        $pathInfo = $request->getPathInfo();
        $normalized = false;
        if ($this->hasNormalizer($manager)) {
            $pathInfo = $this->getNormalizer($manager)->normalizePathInfo($pathInfo, $suffix, $normalized);
        }
        if ($suffix !== '' && $pathInfo !== '') {
            $n = strlen($suffix);
            if (substr_compare($pathInfo, $suffix, -$n, $n) === 0) {
                $pathInfo = substr($pathInfo, 0, -$n);
                if ($pathInfo === '') {
                    // suffix alone is not allowed
                    return false;
                }
            } else {
                return false;
            }
        }

        if ($this->host !== null) {
            $pathInfo = strtolower($request->getHostInfo()) . ($pathInfo === '' ? '' : '/' . $pathInfo);
        }

        if (!preg_match($this->pattern, $pathInfo, $matches)) {
            return false;
        }
        $matches = $this->substitutePlaceholderNames($matches);

        foreach ($this->defaults as $name => $value) {
            if (!isset($matches[$name]) || $matches[$name] === '') {
                $matches[$name] = $value;
            }
        }
        $params = $this->defaults;
        $tr = [];
        foreach ($matches as $name => $value) {
            if (isset($this->_routeParams[$name])) {
                $tr[$this->_routeParams[$name]] = $value;
                unset($params[$name]);
            } elseif (isset($this->_paramRules[$name])) {
                $params[$name] = $value;
            }
        }
        if ($this->_routeRule !== null) {
            $route = strtr($this->route, $tr);
        } else {
            $route = $this->route;
        }

        Yii::debug("Request parsed with URL rule: {$this->name}", __METHOD__);

        if ($normalized) {
            // pathInfo was changed by normalizer - we need also normalize route
            return $this->getNormalizer($manager)->normalizeRoute([$route, $params]);
        }

        return [$route, $params];
    }

    /**
     * Creates a URL according to the given route and parameters.
     * @param UrlManager $manager the URL manager
     * @param string $route the route. It should not have slashes at the beginning or the end.
     * @param array $params the parameters
     * @return string|bool the created URL, or `false` if this rule cannot be used for creating this URL.
     */
    public function createUrl($manager, $route, $params)
    {
        if ($this->mode === self::PARSING_ONLY) {
            $this->createStatus = self::CREATE_STATUS_PARSING_ONLY;
            return false;
        }

        $tr = [];

        // match the route part first
        if ($route !== $this->route) {
            if ($this->_routeRule !== null && preg_match($this->_routeRule, $route, $matches)) {
                $matches = $this->substitutePlaceholderNames($matches);
                foreach ($this->_routeParams as $name => $token) {
                    if (isset($this->defaults[$name]) && strcmp($this->defaults[$name], $matches[$name]) === 0) {
                        $tr[$token] = '';
                    } else {
                        $tr[$token] = $matches[$name];
                    }
                }
            } else {
                $this->createStatus = self::CREATE_STATUS_ROUTE_MISMATCH;
                return false;
            }
        }

        // match default params
        // if a default param is not in the route pattern, its value must also be matched
        foreach ($this->defaults as $name => $value) {
            if (isset($this->_routeParams[$name])) {
                continue;
            }
            if (!isset($params[$name])) {
                // allow omit empty optional params
                // @see https://github.com/yiisoft/yii2/issues/10970
                if (in_array($name, $this->placeholders) && strcmp($value, '') === 0) {
                    $params[$name] = '';
                } else {
                    $this->createStatus = self::CREATE_STATUS_PARAMS_MISMATCH;
                    return false;
                }
            }
            if (strcmp($params[$name], $value) === 0) { // strcmp will do string conversion automatically
                unset($params[$name]);
                if (isset($this->_paramRules[$name])) {
                    $tr["<$name>"] = '';
                }
            } elseif (!isset($this->_paramRules[$name])) {
                $this->createStatus = self::CREATE_STATUS_PARAMS_MISMATCH;
                return false;
            }
        }

        // match params in the pattern
        foreach ($this->_paramRules as $name => $rule) {
            if (isset($params[$name]) && !is_array($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) {
                $tr["<$name>"] = $this->encodeParams ? urlencode($params[$name]) : $params[$name];
                unset($params[$name]);
            } elseif (!isset($this->defaults[$name]) || isset($params[$name])) {
                $this->createStatus = self::CREATE_STATUS_PARAMS_MISMATCH;
                return false;
            }
        }

        $url = $this->trimSlashes(strtr($this->_template, $tr));
        if ($this->host !== null) {
            $pos = strpos($url, '/', 8);
            if ($pos !== false) {
                $url = substr($url, 0, $pos) . preg_replace('#/+#', '/', substr($url, $pos));
            }
        } elseif (strpos($url, '//') !== false) {
            $url = preg_replace('#/+#', '/', trim($url, '/'));
        }

        if ($url !== '') {
            $url .= ($this->suffix === null ? $manager->suffix : $this->suffix);
        }

        if (!empty($params) && ($query = http_build_query($params)) !== '') {
            $url .= '?' . $query;
        }

        $this->createStatus = self::CREATE_STATUS_SUCCESS;
        return $url;
    }

    /**
     * Returns status of the URL creation after the last [[createUrl()]] call.
     *
     * @return null|int Status of the URL creation after the last [[createUrl()]] call. `null` if rule does not provide
     * info about create status.
     * @see createStatus
     * @since 2.0.12
     */
    public function getCreateUrlStatus()
    {
        return $this->createStatus;
    }

    /**
     * Returns list of regex for matching parameter.
     * @return array parameter keys and regexp rules.
     *
     * @since 2.0.6
     */
    protected function getParamRules()
    {
        return $this->_paramRules;
    }

    /**
     * Iterates over [[placeholders]] and checks whether each placeholder exists as a key in $matches array.
     * When found - replaces this placeholder key with a appropriate name of matching parameter.
     * Used in [[parseRequest()]], [[createUrl()]].
     *
     * @param array $matches result of `preg_match()` call
     * @return array input array with replaced placeholder keys
     * @see placeholders
     * @since 2.0.7
     */
    protected function substitutePlaceholderNames(array $matches)
    {
        foreach ($this->placeholders as $placeholder => $name) {
            if (isset($matches[$placeholder])) {
                $matches[$name] = $matches[$placeholder];
                unset($matches[$placeholder]);
            }
        }

        return $matches;
    }

    /**
     * Trim slashes in passed string. If string begins with '//', two slashes are left as is
     * in the beginning of a string.
     *
     * @param string $string
     * @return string
     */
    private function trimSlashes($string)
    {
        if (strncmp($string, '//', 2) === 0) {
            return '//' . trim($string, '/');
        }

        return trim($string, '/');
    }
}






参考资料

深入理解Yii2.0 » 请求与响应(TBD) » 路由(Route)

http://www.digpage.com/route.html

https://www.yiichina.com/tutorial/121

https://www.kancloud.cn/kancloud/yii-in-depth/50778

https://www.shouce.ren/api/view/a/9375

https://www.shouce.ren/api/view/a/9392

http://www.niuguwen.cn/kaifashouce-cat-177-16315.html


返回