题外话

两周之前搬了家,以前搬家从来没觉得东西这么多,收拾起来这么麻烦,基本上整理整理就可以过日子了。这次换了个整租,才发现屋子大了也不好,东西找不到,现在基本上算是步入正轨了,不过nas还没有就位,等我再整理整理在看看吧nas放到哪里。机械硬盘实在是太吵了,这次要放到一个安静的地方。

前情提要

第五篇简单分析了插件,其实什么都没说。上一篇分析了路由部分,也说的比较混乱,本周五和周六我用了一些时间,仔细的读了源码,把自己混乱的部分也都弄清了,所以这篇我就慢慢的再说一次路由,用两个请求,首页和文章页 来分析,将来在模块分析的时候也会把路由需要的部分,在分析。

正文开始

我们再次来到路由的 dispatch 方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
    * 路由分发函数
    *
    * @return void
    * @throws Exception
    */
public static function dispatch()
{
    /** 获取PATHINFO */
    $pathInfo = self::getPathInfo();

    foreach (self::$_routingTable as $key => $route) {
        if (preg_match($route['regx'], $pathInfo, $matches)) {
            self::$current = $key;

            try {
                /** 载入参数 */
                $params = NULL;

                if (!empty($route['params'])) {
                    unset($matches[0]);
                    $params = array_combine($route['params'], $matches);
                }

                $widget = Typecho_Widget::widget($route['widget'], NULL, $params);

                if (isset($route['action'])) {
                    $widget->{$route['action']}();
                }

                Typecho_Response::callback();
                return;

            } catch (Exception $e) {
                if (404 == $e->getCode()) {
                    Typecho_Widget::destory($route['widget']);
                    continue;
                }

                throw $e;
            }
        }
    }

    /** 载入路由异常支持 */
    throw new Typecho_Router_Exception("Path '{$pathInfo}' not found", 404);
}

首先获取 pathInfo ,这个 pathInfo 是从那里获取的呢,是从 Init 里面初始化的,我们看下初始化部分

1
$pathInfo = $this->request->getPathInfo();

进入这个方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
     * 获取当前pathinfo
     *
     * @access public
     * @param string $inputEncoding 输入编码
     * @param string $outputEncoding 输出编码
     * @return string
     */
    public function getPathInfo($inputEncoding = NULL, $outputEncoding = NULL)
    {
        /** 缓存信息 */
        if (NULL !== $this->_pathInfo) {
            return $this->_pathInfo;
        }

        //参考Zend Framework对pahtinfo的处理, 更好的兼容性
        $pathInfo = NULL;

        //处理requestUri
        $requestUri = $this->getRequestUri();
        var_dump($requestUri);
        $finalBaseUrl = $this->getBaseUrl();
        var_dump($requestUri);

        // Remove the query string from REQUEST_URI
        if ($pos = strpos($requestUri, '?')) {
            $requestUri = substr($requestUri, 0, $pos);
        }

        if ((NULL !== $finalBaseUrl)
            && (false === ($pathInfo = substr($requestUri, strlen($finalBaseUrl)))))
        {
            // If substr() returns false then PATH_INFO is set to an empty string
            $pathInfo = '/';
        } elseif (NULL === $finalBaseUrl) {
            $pathInfo = $requestUri;
        }

        if (!empty($pathInfo)) {
            //针对iis的utf8编码做强制转换
            //参考http://docs.moodle.org/ja/%E5%A4%9A%E8%A8%80%E8%AA%9E%E5%AF%BE%E5%BF%9C%EF%BC%9A%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E8%A8%AD%E5%AE%9A
            if (!empty($inputEncoding) && !empty($outputEncoding) &&
            (stripos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false
            || stripos($_SERVER['SERVER_SOFTWARE'], 'ExpressionDevServer') !== false)) {
                if (function_exists('mb_convert_encoding')) {
                    $pathInfo = mb_convert_encoding($pathInfo, $outputEncoding, $inputEncoding);
                } else if (function_exists('iconv')) {
                    $pathInfo = iconv($inputEncoding, $outputEncoding, $pathInfo);
                }
            }
        } else {
            $pathInfo = '/';
        }

        // fix issue 456
        return ($this->_pathInfo = '/' . ltrim(urldecode($pathInfo), '/'));
    }

这个方法了里面第一步,如果有 pathInfo 就返回,如果没有就进入后续的流程,我们这里面肯定是没有的,所以继续后续执行

先获取了 $requestUri

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
     * 获取请求地址
     * 
     * @access public
     * @return string
     */
    public function getRequestUri()
    {
        if (!empty($this->_requestUri)) {
            return $this->_requestUri;
        }

        //处理requestUri
        $requestUri = '/';

        if (isset($_SERVER['HTTP_X_REWRITE_URL'])) { // check this first so IIS will catch
            $requestUri = $_SERVER['HTTP_X_REWRITE_URL'];
        } elseif (
            // IIS7 with URL Rewrite: make sure we get the unencoded url (double slash problem)
            isset($_SERVER['IIS_WasUrlRewritten'])
            && $_SERVER['IIS_WasUrlRewritten'] == '1'
            && isset($_SERVER['UNENCODED_URL'])
            && $_SERVER['UNENCODED_URL'] != ''
            ) {
            $requestUri = $_SERVER['UNENCODED_URL'];
        } elseif (isset($_SERVER['REQUEST_URI'])) {
            $requestUri = $_SERVER['REQUEST_URI'];
            $parts       = @parse_url($requestUri);
            
            if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST'])) {
                if (false !== $parts) {
                    $requestUri  = (empty($parts['path']) ? '' : $parts['path'])
                                 . ((empty($parts['query'])) ? '' : '?' . $parts['query']);
                }
            } elseif (!empty($_SERVER['QUERY_STRING']) && empty($parts['query'])) {
                // fix query missing
                $requestUri .= '?' . $_SERVER['QUERY_STRING'];
            }
        } elseif (isset($_SERVER['ORIG_PATH_INFO'])) { // IIS 5.0, PHP as CGI
            $requestUri = $_SERVER['ORIG_PATH_INFO'];
            if (!empty($_SERVER['QUERY_STRING'])) {
                $requestUri .= '?' . $_SERVER['QUERY_STRING'];
            }
        }

        return $this->_requestUri = $requestUri;
    }

进入方法内部,第一步还是判断是否存在,不存在就从 $_SERVER 中获取相关参数,因为我们是在nginx中,所以在下面这个判断中获取参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
} elseif (isset($_SERVER['REQUEST_URI'])) {
    $requestUri = $_SERVER['REQUEST_URI'];
    $parts       = @parse_url($requestUri);
    
    if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST'])) {
        if (false !== $parts) {
            $requestUri  = (empty($parts['path']) ? '' : $parts['path'])
                            . ((empty($parts['query'])) ? '' : '?' . $parts['query']);
        }
    } elseif (!empty($_SERVER['QUERY_STRING']) && empty($parts['query'])) {
        // fix query missing
        $requestUri .= '?' . $_SERVER['QUERY_STRING'];
    }
}

获得 REQUEST_URI,紧接着解析 用 parse_url 解析 获取到的 uri 得到 parts,紧接着判断 如果

1
2
3
4
5
6
7
8
9
    if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST'])) {
                if (false !== $parts) {
                    $requestUri  = (empty($parts['path']) ? '' : $parts['path'])
                                 . ((empty($parts['query'])) ? '' : '?' . $parts['query']);
                }
            } elseif (!empty($_SERVER['QUERY_STRING']) && empty($parts['query'])) {
                // fix query missing
                $requestUri .= '?' . $_SERVER['QUERY_STRING'];
            }

serverhost 存在 并且 urihost 里面,就判断解析的 parts 是否为 false, 然后拼接 uri,这里if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST']))false 所以走下面的判断逻辑,

1
    elseif (!empty(_SERVER['QUERY_STRING']) && empty(parts['query']))

当前这个url

1
http://typecho.test/index.php/archives/1/

也是false,所以请求的 uri 就是

1
/index.php/archives/1/

紧接着获取 getBaseUrl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
     * getBaseUrl  
     * 
     * @access public
     * @return string
     */
    public function getBaseUrl()
    {
        if (NULL !== $this->_baseUrl) {
            return $this->_baseUrl;
        }

        //处理baseUrl
        $filename = (isset($_SERVER['SCRIPT_FILENAME'])) ? basename($_SERVER['SCRIPT_FILENAME']) : '';

        if (isset($_SERVER['SCRIPT_NAME']) && basename($_SERVER['SCRIPT_NAME']) === $filename) {
            $baseUrl = $_SERVER['SCRIPT_NAME'];
        } elseif (isset($_SERVER['PHP_SELF']) && basename($_SERVER['PHP_SELF']) === $filename) {
            $baseUrl = $_SERVER['PHP_SELF'];
        } elseif (isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $filename) {
            $baseUrl = $_SERVER['ORIG_SCRIPT_NAME']; // 1and1 shared hosting compatibility
        } else {
            // Backtrack up the script_filename to find the portion matching
            // php_self
            $path    = isset($_SERVER['PHP_SELF']) ? $_SERVER['PHP_SELF'] : '';
            $file    = isset($_SERVER['SCRIPT_FILENAME']) ? $_SERVER['SCRIPT_FILENAME'] : '';
            $segs    = explode('/', trim($file, '/'));
            $segs    = array_reverse($segs);
            $index   = 0;
            $last    = count($segs);
            $baseUrl = '';
            do {
                $seg     = $segs[$index];
                $baseUrl = '/' . $seg . $baseUrl;
                ++$index;
            } while (($last > $index) && (false !== ($pos = strpos($path, $baseUrl))) && (0 != $pos));
        }

        // Does the baseUrl have anything in common with the request_uri?
        $finalBaseUrl = NULL;
        $requestUri = $this->getRequestUri();

        if (0 === strpos($requestUri, $baseUrl)) {
            // full $baseUrl matches
            $finalBaseUrl = $baseUrl;
        } else if (0 === strpos($requestUri, dirname($baseUrl))) {
            // directory portion of $baseUrl matches
            $finalBaseUrl = rtrim(dirname($baseUrl), '/');
        } else if (!strpos($requestUri, basename($baseUrl))) {
            // no match whatsoever; set it blank
            $finalBaseUrl = '';
        } else if ((strlen($requestUri) >= strlen($baseUrl))
            && ((false !== ($pos = strpos($requestUri, $baseUrl))) && ($pos !== 0)))
        {
            // If using mod_rewrite or ISAPI_Rewrite strip the script filename
            // out of baseUrl. $pos !== 0 makes sure it is not matching a value
            // from PATH_INFO or QUERY_STRING
            $baseUrl = substr($requestUri, 0, $pos + strlen($baseUrl));
        }

        return ($this->_baseUrl = (NULL === $finalBaseUrl) ? rtrim($baseUrl, '/') : $finalBaseUrl);
    }

首先获取从 serverSCRIPT_FILENAME 获取 $filename ,如果SCRIPT_FILENAME 存在,则用 basename 方法获取 $filename ,当前的 filenameindex.phpbasenme 方法的作用就是返回路径中的文件名,当前 如果 SCRIPT_FILENAME 值是 /var/www/typecho/index.php ,所以文件名就是

1
index.php

紧接着判断 server 中的 SCRIPT_NAMEPHP_SELF 的内容经过 basename 处理后的文件名是否跟 filename 相同。我们的请求在 SCRIPT_NAME 这里的判断就符合了条件,所以 baseurl 就是 server 中的 SCRIPT_NAME 的值。

1
/index.php

接下来 判断 baseurlbaseurldirnamerequesturi 中是否开头,我们这里的场景是

1
else if (0 === strpos($requestUri, dirname($baseUrl))) 

这里的判断中达成的,所以

1
$finalBaseUrl = rtrim(dirname($baseUrl), '/');

就是吧 dirname 后的 $baseUrl 去掉右侧的/后的值。最后 baseurl 就是

1
(NULL === $finalBaseUrl) ? rtrim($baseUrl, '/') : $finalBaseUrl

判断 finalBaseUrl 是否为 null ,如果为 null 就是把 baseurl 去掉右侧的 / ,否则就是 finalBaseUrl 。我们这里 finalBaseUrl 不是 null,所以 baseurl就是 finalBaseUrl。为 /index.php

接下来判断 requesturi 中是否包含 ? ,如果包含,就截取 前面的不部分,我们这边不包含,所以 requesturi 依然是

1
/index.php/archives/1/

然后判断 $finalBaseUrl 是否为 null, 如果是 null$pathInfo = $requestUri; ,如果不是并且

1
false===(pathInfo =substr(requestUri, strlen(finalBaseUrl))) 

substr 后的 pathinfofalse ,就是没有提取到子串的时候 pathinfo/。 我们的场景下,成功提取到了,所以 pathinfo 就是

1
/archives/1/

接下来,pathinfo 不为空的时候对 iis 请求的编码,这里不存在,就忽略了,如果pathinfo是空,就赋值 /。最后完整的 pathinfo就是

1
'/' . ltrim(urldecode($pathInfo), '/')

去掉左侧的 / 在拼接一个 / 这个目的就是防止做的没有 /。 最后 pathinfo就是

1
/archives/1/

最后把,Typecho_Router::setPathInfo($pathInfo); 设置到路由里面。

接下来就回到了路由的 dispatch 方法。首先获取一下 pathinfo 。 然后用配置里面的 routeTable 进行匹配,这个 routeTable 就是在数据库里面的配置,可以看 option 表里面的数据。

遍历 routeTable ,用路由里面的 regex 来匹配 pathInfo ,如果没有匹配到,就抛出 路由 没有匹配到的 404。

如果匹配到了,把 路由的 key 设置到 current

如果设置了路由的 params ,就把匹配到的参数跟 params 组合成数组。例如

1
2
3
4
5
6
7
8
9
if (!empty($route['params'])) {    
    unset($matches[0]);    $params = array_combine($route['params'], $matches)
;}



http://typecho.test/index.php/archives/1/

array(6) { ["url"]=> string(24) "/archives/[cid:digital]/" ["widget"]=> string(14) "Widget_Archive" ["action"]=> string(6) "render" ["regx"]=> string(26) "|^/archives/([0-9]+)[/]?$|" ["format"]=> string(13) "/archives/%s/" ["params"]=> array(1) { [0]=> string(3) "cid" } } array(2) { [0]=> string(12) "/archives/1/" [1]=> string(1) "1" }

上面这种路径的话,就会把匹配到的 1params 组合,合成参数数组

1
array(1) { ["cid"]=> string(1) "1" }

给后续的方法使用。

紧接着初始化,路由对应的组件,上面这个文章详情的例子就是 Widget_Archive , 然后,判断是否设置了 路由的 action , 如果设置了就执行这个方法

1
2
3
if (isset($route['action'])) {    
    $widget->{$route['action']}();
}

最后调用,

1
Typecho_Response::callback();

最后就返回了,如果执行相关方法出错了,就执行异常部分。

1
2
3
4
5
if (404 == $e->getCode()) {
    Typecho_Widget::destory($route['widget']);    
    continue;
}
throw $e;

到这,整个路由就跑完了,大家可以多多的测试各种页面看看各种结果。

下期预告

下次我们就来具体的分析插件,这个好玩的东西,刚开始学 php 的时候,就觉得很高级,后来看过 thinkphp3.2 的源码的时候也在其他地方看到了类似的东西,这个做法真的很好玩。敬请期待。