zoukankan      html  css  js  c++  java
  • Yii源码阅读笔记

    2014-11-12 三

    By youngsterxyf

    概述

    Yii框架的路由解析功能由核心组件urlManager来完成。路由的形式有两种:

    • get:通过URL中查询字符串(query string)参数r来指定路由,如:r=controllerID/actionID
    • path:直接通过URL来指定,如:/controllerID/actionID

    默认使用get路由形式。由于Yii中controller类命名和action方法都是按照规则命名的,而路由也是按照规则来匹配的,所以完全可以不用额外配置urlManager。

    若需要使用path方式,则可如下配置:

    "components" => array(
        'urlManager' => array(
            'urlFormat' => 'path',
            'rules' => array(
                ...
            ),
    ),
    

    进一步说明可参考RESTful API设计的一点经验一文。

    分析

    在“请求处理基本流程”一篇可以看到Yii框架路由解析流程的入口在类CWebApplication的processRequest方法中:

    $route=$this->getUrlManager()->parseUrl($this->getRequest());
    

    其中getUrlManager方法定义于类CApplication中,作用是初始化获取URL管理组件(ID为urlManager),实现如下:

    public function getUrlManager()
    {
        return $this->getComponent('urlManager');
    }
    

    在获取urlManager组件对象过程中,会对对象做初始化,调用对象的init方法,见类CUrlManager的init方法实现:

    public function init()
    {
        parent::init();
        $this->processRules();
    }
    

    其中调用的方法processRules,是根据配置的rules解析创建规则对象,放到属性_rules中,实现如下:

    protected function processRules()
    {
        // 如果未配置rules,或使用的路由形式是get,则根本无需解析路由规则
        if(empty($this->rules) || $this->getUrlFormat()===self::GET_FORMAT)
            return;
        // 否则尝试从缓存中读取解析好的路由规则
        if($this->cacheID!==false && ($cache=Yii::app()->getComponent($this->cacheID))!==null)
        {
            $hash=md5(serialize($this->rules));
            if(($data=$cache->get(self::CACHE_KEY))!==false && isset($data[1]) && $data[1]===$hash)
            {
                $this->_rules=$data[0];
                return;
            }
        }
        // 否则逐条路由规则解析
        foreach($this->rules as $pattern=>$route)
            $this->_rules[]=$this->createUrlRule($route,$pattern);
        // 尝试缓存解析好的路由规则
        if(isset($cache))
            $cache->set(self::CACHE_KEY,array($this->_rules,$hash));
    }
    

    从上述代码中,在解析创建规则对象前会先检查是否已缓存了解析创建好的规则,如果没有,则在解析创建好规则后,将这些规则缓存起来。这样就避免了每次请求处理都要解析一次rules列表。 但这里需要注意的是urlManager组件默认使用ID为cache的缓存组件(CUrlManager类的属性cacheID默认值为cache),而核心组件并不包含ID为cache的缓存组件,所以若希望缓存解析好路由规则, 则应该配置ID为cache的缓存组件,如果缓存组件的ID不是cache,则需要配置urlManager组件的cacheID属性

    如果没有设置缓存组件,或者缓存中未找到解析好的路由规则,则需要对配置的rules逐条解析,解析过程见类CUrlManager的createUrlRule方法实现:

    /**
     * Creates a URL rule instance.
     * The default implementation returns a CUrlRule object.
     * @param mixed $route the route part of the rule. This could be a string or an array
     * @param string $pattern the pattern part of the rule
     * @return CUrlRule the URL rule instance
     * @since 1.1.0
     */
    protected function createUrlRule($route,$pattern)
    {
        // 说明可以配置自定义的路由规则解析类
        if(is_array($route) && isset($route['class']))
            return $route;
        else
        {
            $urlRuleClass=Yii::import($this->urlRuleClass,true);
            return new $urlRuleClass($route,$pattern);
        }
    }
    

    以以下rules配置为例:

    'rules' => array(
        array('industry/index', 'pattern' => '/v1/partner/industry/', 'verb' => 'GET'),
        array('token/create', 'pattern' => '/v1/partner/token', 'verb' => 'POST'),
    )
    

    在处理第一条规则时,方法createUrlRule的参数$pattern的值为数组的索引0,$route的值为关联数组array('industry/index', 'pattern' => '/v1/partner/industry/', 'verb' => 'GET'), 但因为$route里没有设置class字段,所以走的是else分支 - 先引入类CUrlRule($this->urlRuleClass的默认值),然后根据$route、$pattern实例化类CUrlRule,该类也定义在文件yii/framework/web/CUrlManager.php中, 直接继承自抽象类CBaseUrlRule。CUrlRule的构造方法实现如下:

    public function __construct($route,$pattern)
    {
        if(is_array($route))
        {
            // 从这里可知$route支持'urlSuffix', 'caseSensitive', 'defaultParams', 'matchValue', 'verb', 'parsingOnly'这些配置项
            foreach(array('urlSuffix', 'caseSensitive', 'defaultParams', 'matchValue', 'verb', 'parsingOnly') as $name)
            {
                if(isset($route[$name]))
                    $this->$name=$route[$name];
            }
            // 如果$route中有pattern配置项,则将配置值赋值给$pattern
            if(isset($route['pattern']))
                $pattern=$route['pattern'];
            // 而$route的第一个配置项才是真正的目标路由
            $route=$route[0];
        }
        $this->route=trim($route,'/');
    
        $tr2['/']=$tr['/']='\/';
    
        if(strpos($route,'<')!==false && preg_match_all('/<(w+)>/',$route,$matches2))
        {
            foreach($matches2[1] as $name)
                $this->references[$name]="<$name>";
        }
    
        // 是否带协议头
        $this->hasHostInfo=!strncasecmp($pattern,'http://',7) || !strncasecmp($pattern,'https://',8);
    
        // 如果原$route有verb配置项
        // verb配置支持多个HTTP方法,以空格或逗号分隔,如:“GET,POST”
        if($this->verb!==null)
            $this->verb=preg_split('/[s,]+/',strtoupper($this->verb),-1,PREG_SPLIT_NO_EMPTY);
    
        // $pattern中类正则片段支持两种形式:命名的和未命名的,如“<id:d+>”和“<d+>”
        if(preg_match_all('/<(w+):?(.*?)?>/',$pattern,$matches))
        {
            $tokens=array_combine($matches[1],$matches[2]);
            foreach($tokens as $name=>$value)
            {
                if($value==='')
                    $value='[^/]+';
                $tr["<$name>"]="(?P<$name>$value)";
                if(isset($this->references[$name]))
                    $tr2["<$name>"]=$tr["<$name>"];
                else
                    $this->params[$name]=$value;
            }
        }
        // 好吧,之后的这段代码我还没太看懂作用
        // 就是为了将$pattern转换成一个真正的正则表达式?
        $p=rtrim($pattern,'*');
        $this->append=$p!==$pattern;
        $p=trim($p,'/');
        $this->template=preg_replace('/<(w+):?.*?>/','<$1>',$p);
        $this->pattern='/^'.strtr($this->template,$tr).'/';
        if($this->append)
            $this->pattern.='/u';
        else
            $this->pattern.='$/u';
    
        if($this->references!==array())
            $this->routePattern='/^'.strtr($this->route,$tr2).'$/u';
    
        if(YII_DEBUG && @preg_match($this->pattern,'test')===false)
            throw new CException(Yii::t('yii','The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.',
                array('{route}'=>$route,'{pattern}'=>$pattern)));
    }
    

    在得到urlManager组件对象后,调用其parseUrl方法,实现如下:

    public function parseUrl($request)
    {
        if($this->getUrlFormat()===self::PATH_FORMAT)
        {
            $rawPathInfo=$request->getPathInfo();
            $pathInfo=$this->removeUrlSuffix($rawPathInfo,$this->urlSuffix);
            foreach($this->_rules as $i=>$rule)
            {
                if(is_array($rule))
                    $this->_rules[$i]=$rule=Yii::createComponent($rule);
                // 逐个路由规则匹配
                if(($r=$rule->parseUrl($this,$request,$pathInfo,$rawPathInfo))!==false)
                    // 即使匹配到了路由规则,也还是得看一下URL中是否指定了路由,是的话则优先使用URL中指定的路由
                    return isset($_GET[$this->routeVar]) ? $_GET[$this->routeVar] : $r;
            }
            // 如果一定要匹配到某个路由规则才行,那么执行到这里就表示未有匹配的路由规则,所以就抛404错误了。
            if($this->useStrictParsing)
                throw new CHttpException(404,Yii::t('yii','Unable to resolve the request "{route}".',
                    array('{route}'=>$pathInfo)));
            // 否则先返回请求路径作为目标路由
            else
                return $pathInfo;
        }
        // 如果使用的是get路由形式,则从GET请求的查询字符串或POST请求的请求体找目标路由
        elseif(isset($_GET[$this->routeVar]))
            return $_GET[$this->routeVar];
        elseif(isset($_POST[$this->routeVar]))
            return $_POST[$this->routeVar];
        else
            return '';
    }
    

    方法的参数是一个request组件对象。

    先判断应用使用的路由形式是否为path,如果不是,则根据路由的参数名(默认为r,由于urlManager类的routeVar属性是public的,所以可以通过配置routeVar的值来修改路由参数名)获取路由。并且路由可以通过GET方法放在URL查询字符串中,也可以通过POST方法放在请求体中。

    对于path形式的路由,解析过程则要复杂一些。先通过request组件对象的getPathInfo方法取到请求的URL(会对原本的请求URL做一定的处理),然后根据解析好的路由规则列表逐个匹配。其中CUrlRule类的parseUrl方法实现如下:

    public function parseUrl($manager,$request,$pathInfo,$rawPathInfo)
    {
        // 先检查HTTP谓词(verb)是否匹配
        if($this->verb!==null && !in_array($request->getRequestType(), $this->verb, true))
            return false;
        // 是否关心大小写
        if($manager->caseSensitive && $this->caseSensitive===null || $this->caseSensitive)
            $case='';
        else
            $case='i';
    
        // urlSiffix配置项是用来干嘛的?
        if($this->urlSuffix!==null)
            $pathInfo=$manager->removeUrlSuffix($rawPathInfo,$this->urlSuffix);
    
        // URL suffix required, but not found in the requested URL
        if($manager->useStrictParsing && $pathInfo===$rawPathInfo)
        {
            $urlSuffix=$this->urlSuffix===null ? $manager->urlSuffix : $this->urlSuffix;
            if($urlSuffix!='' && $urlSuffix!=='/')
                return false;
        }
    
        if($this->hasHostInfo)
            $pathInfo=strtolower($request->getHostInfo()).rtrim('/'.$pathInfo,'/');
    
        $pathInfo.='/';
    
        // 正则匹配:用pattern来匹配路径
        if(preg_match($this->pattern.$case,$pathInfo,$matches))
        {
            // 可以配置defaultParams数组来为请求未提供的必要参数指定默认值
            foreach($this->defaultParams as $name=>$value)
            {
                if(!isset($_GET[$name]))
                    $_REQUEST[$name]=$_GET[$name]=$value;
            }
            $tr=array();
            foreach($matches as $key=>$value)
            {
                if(isset($this->references[$key]))
                    $tr[$this->references[$key]]=$value;
                elseif(isset($this->params[$key]))
                    $_REQUEST[$key]=$_GET[$key]=$value;
            }
            if($pathInfo!==$matches[0]) // there're additional GET params
                $manager->parsePathInfo(ltrim(substr($pathInfo,strlen($matches[0])),'/'));
            if($this->routePattern!==null)
                return strtr($this->route,$tr);
            else
                return $this->route;
        }
        else
            return false;
    }
    

    从上述代码可以看出,路由解析关键是根据$pattern匹配请求URL,并从URL取出需要的东西作为请求参数,一旦匹配,就以$route作为该次请求的目标路由。

    获得目标路由后,就可以根据目标路由查找调用对应的controller和action了。

  • 相关阅读:
    MyBatis
    泛型集合(经典)
    Java高级特性--------->>>>>实用类
    Java高级特性----->>>>集合
    【java面试题】StringBuilder与StringBuffer和String 的区别
    Java面向对象------>try{}catch(){}异常
    Java面向对象----->接口和抽象类
    Java修饰符------>static、native和final以及super和this关键字
    面向对象------->多态
    面向对象--->封装和继承
  • 原文地址:https://www.cnblogs.com/sunscheung/p/4863865.html
Copyright © 2011-2022 走看看