CodeIgniter源码分析 4 - 加载器

当我们load模型,类库等后就可以使用它们了,但是它们到底是怎么载入的?CI框架的自动载入是如何实现的?它是如何处理类库的扩展的?带着这些疑问,这节我们看下加载器的源码。

现代php框架一般使用composer配合psr-0/psr-4实现类库自动载入的,例如laravel框架,而CI框架并没有采用现代php框架流行的载入机制,它自己实现了一套载入机制通过内置的加载器实现对资源的加载。

加载器能够对类库,模型,视图等进行加载;CI框架对于资源加载的处理逻辑位于CI_ Loader.php类中;

一般情况下,我们都是通过如下的方式加载资源

  $this->load->library('library');
  $this->load->model('model);
  //...


CI_Controller中Loader对象的初始化

加载资源都是通过CI_Loader对象的引用load进行加载的,那么就有个疑问

既然Loader对象可以加载别的资源,那么谁去加载Loader对象呢?答案是控制器对象。因为你必须先有Loader对象才能去加载别的资源, 所以Loader对象应该在系统初始化的时候就加载完毕,我们知道系统初始化是在引导文件CodeIgniter.php中完成的,而引导文件加载了Controller.php对象,加载器就是在控制器对象中被加载并实例化的。

控制器对象中对存储在is_loaded()对象数组中的类库进行遍历并作为它的属性,这也是为什么我们在使用过程可以直接访问类库$this->input,$this->config等原因。

 public function __construct()
 {
        self::$instance =& $this;

        foreach (is_loaded() as $var => $class)
        {
            $this->$var =& load_class($class);
        }

        $this->load =& load_class('Loader', 'core');
        $this->load->initialize();   // 加载器初始化
        log_message('info', 'Controller Class Initialized');
    }

现在进入Loader对象所在的Loder.php去看下它是如何初始化的。


Loader对象

初始化

进入Loader.php中观察其构造方法。我们看到 ob_get_level()这个函数,它返回了缓冲机制的嵌套级别。那么什么是缓冲机制的嵌套级别?

你在你的代码中用了几次ob_start(),用一次就认为是一次嵌套;有时你没有使用ob_start()却发现ob_get_level()返回的是1,那是因为在脚本的前文使用了ob_start(),如果依赖ob_get_level()处理相应的逻辑,那么最好使用ob_end_clean()清一下缓存区。

public function __construct()
    {
        $this->_ci_ob_level = ob_get_level();
        //is_loaded存储了我们加载并实例过的对象
        $this->_ci_classes =& is_loaded();

        log_message('info', 'Loader Class Initialized');
    }

我们在CI_Conroller中看到加载器通过$this->load->initialize()做了初始化,其实这个方法完全可以放在加载器的构造方法中去。

public function initialize()
{
    $this->_ci_autoloader();
}

那么初始化的时候调用_ci_autoloader()这个方法是要做什么?

自动载入

我们知道CI框架中要加载资源我们可以通过加载器$this->load->library()的方式手工加载,但是如果一个类中需要多次加载同一资源怎么办? 于是,CI框架就提供了一个自动载入的机制,在Appliaction/config/autoload.php中可以设置我们希望自动加载的资源,这样就省去了多次手工加载的麻烦。
当然,CI框架也支持composer自动载入,你只需要将composer生成的vendor/目录扔到Appliaction/目录下就可以了。

那么_ci_autoloader()就是用来处理自动载入的。

protected function _ci_autoloader()
    {
        //------------------------------------加载自动载入的配置文件autoload.php---------------------------------

        //不管有没有多环境,自动载入的配置文件autoload.php是一定要加载的
        if (file_exists(APPPATH.'config/autoload.php'))
        {
            include(APPPATH.'config/autoload.php');
        }

        //加载当前环境的配置文件
        if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/autoload.php'))
        {
            include(APPPATH.'config/'.ENVIRONMENT.'/autoload.php');
        }

        if ( ! isset($autoload))
        {
            return;
        }
  
      //---------------------------------读取$autoload------------------------------------------------------

        //一般情况下资源也就是liarary,model,help这几种,并且这几种资源的目录已经被规定死了,
        //但万一你不想把资源归到这几个目录下,那么你可以在$autoload['packages'] = array()中设置你要加载资源的的目录
        if (isset($autoload['packages']))
        {
            foreach ($autoload['packages'] as $package_path)
            {
                $this->add_package_path($package_path);
            }
        }

        //读取需要自动加载的相关配置文件
        if (count($autoload['config']) > 0)
        {
            foreach ($autoload['config'] as $val)
            {
                $this->config($val);
            }
        }

        //读物需要自动加载的帮助函数和语言包
        foreach (array('helper', 'language') as $type)
        {
            if (isset($autoload[$type]) && count($autoload[$type]) > 0)
            {
                $this->$type($autoload[$type]);
            }
        }

        //读取需要自动载入的驱动
        if (isset($autoload['drivers']))
        {
            foreach ($autoload['drivers'] as $item)
            {
                $this->driver($item);
            }
        }

        // 读取需要自动载入的类库,注意:这里对数据库的载入是做了区分的,数据库的连接应该只有一次
        if (isset($autoload['libraries']) && count($autoload['libraries']) > 0)
        {
            // Load the database driver.
            if (in_array('database', $autoload['libraries']))
            {
                $this->database();
                $autoload['libraries'] = array_diff($autoload['libraries'], array('database'));
            }

            foreach ($autoload['libraries'] as $item)
            {
                $this->library($item);
            }
        }

        //读物需要自动载入的模型
        if (isset($autoload['model']))
        {
            $this->model($autoload['model']);
        }
    }

通过分析可以看到,自动载入其本质还是通过library(),model()等方法替我们做了手动载入资源的事。

接下来我们看看具体加载类库library(),模型model(),视图view()等的细节。


类库加载

类库加载是通过library()方法实现的,加载类库时我们不仅可以单个加载,还可以批量加载,同时还支持为类库取别名,现在我们看下该方法的细节处理。

public function library($library, $params = NULL, $object_name = NULL)
   {

       //首先判断单个加载还是批量加载,如果是批量加载就递归自身,
       if (empty($library))
       {
           return $this;
       }
       elseif (is_array($library))
       {
           foreach ($library as $key => $value)
           {
               if (is_int($key))
               {
                   $this->library($value, $params);
               }
               else
               {
                   $this->library($key, $params, $value);
               }
           }

           return $this;
       }

       //参数只支持数组格式
       if ($params !== NULL && ! is_array($params))
       {
           $params = NULL;
       }

       //调用_ci_load_library加载类库
       $this->_ci_load_library($library, $params, $object_name);
       return $this;
   }

对于类库的加载我们发现最终其实调用的是_ci_load_library()这个方法。

载入机制

拿到类库名后,解析出对应的路径和类名,然后从约定好的类库目录(APPPATH/library和BASEPATH/library)下去加载,一旦加载成功后调用相应的函数做实例化。

protected function _ci_load_library($class, $params = NULL, $object_name = NULL)
    {
        //------------------------------解析出路径和类名---------------------------------

        //先对传入的类库名做一个处理,去掉扩展名和两端的路径分割符
        $class = str_replace('.php', '', trim($class, '/'));


        //处理类库位于二级目录的情况,解析到类库所在的目录和类名
        if (($last_slash = strrpos($class, '/')) !== FALSE)
        {
            // Extract the path
            $subdir = substr($class, 0, ++$last_slash);

            // Get the filename from the path
            $class = substr($class, $last_slash);
        }
        else
        {
            $subdir = '';
        }

        $class = ucfirst($class);

        //----------------------------------------查找系统内置类库------------------------------------------------------

        //加载内置类库并做实例化
        if (file_exists(BASEPATH.'libraries/'.$subdir.$class.'.php'))
        {
            return $this->_ci_load_stock_library($class, $subdir, $params, $object_name);
        }

        //------------------------------------查找用户自定义的类库------------------------------------------------

        //接着从约定好的类库目录中去查找我们需要的类,数组_ci_library_paths定义了我们需要查找的目录,
        //$_ci_library_paths =  array(APPPATH, BASEPATH);
        foreach ($this->_ci_library_paths as $path)
        {

            if ($path === BASEPATH)
            {
                continue;
            }

            $filepath = $path.'libraries/'.$subdir.$class.'.php';

            //为了防止多次加载同一类库,先判断一下是不是在前文已经加载过了,
            if (class_exists($class, FALSE))
            {
                //如果已经加载了类库,但是还没有挂载到全局对象$CI(CI_Controller)的话,
                //调用_ci_init_library()实例化该类库并挂载到全局对象$CI下
                if ($object_name !== NULL)
                {
                    $CI =& get_instance();
                    if ( ! isset($CI->$object_name))
                    {
                        return $this->_ci_init_library($class, '', $params, $object_name);
                    }
                }

                log_message('debug', $class.' class already loaded. Second attempt ignored.');
                return;
            }
            //没找到的话再次循环去下一个目录查找
            elseif ( ! file_exists($filepath))
            {
                continue;
            }

            //和前面_ci_init_library()一样,实例化该类库并挂载到全局对象$CI下
            include_once($filepath);
            return $this->_ci_init_library($class, '', $params, $object_name);
        }

        // 如果还没找到的话,假设该类库位于和它同名的一个子目录中,再次尝试加载
        if ($subdir === '')
        {
            return $this->_ci_load_library($class.'/'.$class, $params, $object_name);
        }

        // If we got this far we were unable to find the requested class.
        log_message('error', 'Unable to load the requested class: '.$class);
        show_error('Unable to load the requested class: '.$class);
    }

在_ci_load_library()方法中解析出类和路径后,接下来就是实例化类库了。

由于加载的类库可能是我们自己定义的类库或者系统内置的类库,所以在加载成功做实例化的时候做了区分,分别从_ci_load_stock_library()和_ci_init_library()中实例化内置类库和我们自定义的类库。

先看从_ci_load_stock_library()方法中加载内置类库。

注意:由于系统类库我们也可以扩展,所在实例化系统内置类库时会有类名$prefix的判断,系统类库的$prefix是以CI开头的,而我们扩展的系统类库的$prefix是在config.php中subclass_prefix上指定的。

protected function _ci_load_stock_library($library_name, $file_path, $params, $object_name)
    {
        $prefix = 'CI_';

        //---------------------------------------获取扩展类库的前缀配置---------------------------------------------------

        //判断系统核心类是不是被扩展了,注意:扩展类继承了系统核心类的所有功能
        if (class_exists($prefix.$library_name, FALSE))
        {
            if (class_exists(config_item('subclass_prefix').$library_name, FALSE))
            {
                $prefix = config_item('subclass_prefix');
            }

            //如果该类已经被加载但是还没被挂载到全局对象$CI上的话,调用_ci_init_library()进行实例化并挂载
            if ($object_name !== NULL)
            {
                $CI =& get_instance();
                if ( ! isset($CI->$object_name))
                {
                    return $this->_ci_init_library($library_name, $prefix, $params, $object_name);
                }
            }

            log_message('debug', $library_name.' class already loaded. Second attempt ignored.');
            return;
        }

        //---------------------接下来又是拿到要查找的目录,不厌其烦的去跑到这几个目录中去加载---------------------------------------------

        $paths = $this->_ci_library_paths;
        array_pop($paths);
        array_pop($paths);
        array_unshift($paths, APPPATH);

        //跑到APPPATH/libraries目录下去加载系统内置类库
        foreach ($paths as $path)
        {
            if (file_exists($path = $path.'libraries/'.$file_path.$library_name.'.php'))
            {
                // Override
                include_once($path);
                if (class_exists($prefix.$library_name, FALSE))
                {
                    return $this->_ci_init_library($library_name, $prefix, $params, $object_name);
                }
                else
                {
                    log_message('debug', $path.' exists, but does not declare '.$prefix.$library_name);
                }
            }
        }

        //跑到BASEPATH/libraries目录下去加载系统内置类库

        include_once(BASEPATH.'libraries/'.$file_path.$library_name.'.php');


        //跑到APPPATH/libraries目录下去加载被扩展了的系统内置类库
        $subclass = config_item('subclass_prefix').$library_name;
        foreach ($paths as $path)
        {
            if (file_exists($path = $path.'libraries/'.$file_path.$subclass.'.php'))
            {
                include_once($path);
                if (class_exists($subclass, FALSE))
                {
                    $prefix = config_item('subclass_prefix');
                    break;
                }
                else
                {
                    log_message('debug', APPPATH.'libraries/'.$file_path.$subclass.'.php exists, but does not declare '.$subclass);
                }
            }
        }

        //找到后调用_ci_init_library()进行实例化并挂载
        return $this->_ci_init_library($library_name, $prefix, $params, $object_name);
    }

在上面的源码分析中,我们看到多次_ci_init_library()方法,我们也多次说了该方法是实例化类库并挂载到全局对象$CI(CI_Controller)上。

类库实例化并挂载到全局对象$CI(CI_Controller)上

对于类库实例化的参数有必要说明一下,CI框架支持你在创建一个类库时在 APPPATH/config/ 目录下创建一个和类名相同的配置文件,所以接下来你会看见在实例化类库的时候,如果没有为类库传入参数,就会尝试去读该类的配置文件的内容作为实例化的参数。

现在看下_ci_init_library()方法的源码。

 protected function _ci_init_library($class, $prefix, $config = FALSE, $object_name = NULL)
    {

        //-------------------------------------读取类库的配置问价---------------------------------------

        //如果该类库没有传入参数,那么尝试从/config/目录下去加载一个和当前类库名一样的配置文件作为实例化时的参数。
        if ($config === NULL)
        {
            //$config_component是Config对象的引用,在_ci_get_component()你会看到是从$CI上获取了Config对象
            $config_component = $this->_ci_get_component('config');

            //接下来就是考虑尽可能多的情况,从而require相关的配置文件
            if (is_array($config_component->_config_paths))
            {
                $found = FALSE;
                foreach ($config_component->_config_paths as $path)
                {
                    if (file_exists($path.'config/'.strtolower($class).'.php'))
                    {
                        include($path.'config/'.strtolower($class).'.php');
                        $found = TRUE;
                    }
                    elseif (file_exists($path.'config/'.ucfirst(strtolower($class)).'.php'))
                    {
                        include($path.'config/'.ucfirst(strtolower($class)).'.php');
                        $found = TRUE;
                    }

                    if (file_exists($path.'config/'.ENVIRONMENT.'/'.strtolower($class).'.php'))
                    {
                        include($path.'config/'.ENVIRONMENT.'/'.strtolower($class).'.php');
                        $found = TRUE;
                    }
                    elseif (file_exists($path.'config/'.ENVIRONMENT.'/'.ucfirst(strtolower($class)).'.php'))
                    {
                        include($path.'config/'.ENVIRONMENT.'/'.ucfirst(strtolower($class)).'.php');
                        $found = TRUE;
                    }

                    //找到的话就没必要再继续去require了
                    if ($found === TRUE)
                    {
                        break;
                    }
                }
            }
        }

        //-------------------------------------------类库的实例化与挂载---------------------------------------

        $class_name = $prefix.$class;

        //为了保险起见再次判断一下该类库是否已经加载
        if ( ! class_exists($class_name, FALSE))
        {
            log_message('error', 'Non-existent class: '.$class_name);
            show_error('Non-existent class: '.$class_name);
        }

        if (empty($object_name))
        {
            $object_name = strtolower($class);
            if (isset($this->_ci_varmap[$object_name]))
            {
                $object_name = $this->_ci_varmap[$object_name];
            }
        }

        //如果你设置的类库别名已经存在的话,就说明有冲突了
        $CI =& get_instance();
        if (isset($CI->$object_name))
        {
            if ($CI->$object_name instanceof $class_name)
            {
                log_message('debug', $class_name." has already been instantiated as '".$object_name."'. Second attempt aborted.");
                return;
            }

            show_error("Resource '".$object_name."' already exists and is not a ".$class_name." instance.");
        }

        //我们在构造方法中看到了_ci_classes是对is_loaded()的引用,当我们每实例化一个类后,需要在is_loaded()缓存一下
        $this->_ci_classes[$object_name] = $class;

        //最后实例化该类库并挂载到全局对象$CI上
        $CI->$object_name = isset($config)
            ? new $class_name($config)
            : new $class_name();
    }

类库加载的源码分析就到此,可以看出类库的加载其实就是解析出类名后从定义好的目录中去加载,顺便在初始化的时候把需要自动载入的类库也给调用library()方法也给加载进来,大概的流程图就是这样

类库加载

接下来看下模型加载的源码。

模型加载

模型的加载和类库的加载大同小异,也是解析出对应的模型名称,然后跑到指定的模型所在的目录去加载它,加载成功后一样做实例化并挂载到全局对象$CI上,同时也缓存它。

public function model($model, $name = '', $db_conn = FALSE)
    {
        //是不是批量加载多个模型,是的话递归处理
        if (empty($model))
        {
            return $this;
        }
        elseif (is_array($model))
        {
            foreach ($model as $key => $value)
            {
                is_int($key) ? $this->model($value, '', $db_conn) : $this->model($key, $value, $db_conn);
            }

            return $this;
        }


        //解析模型名,拿到类名,顺便判断下是不是有子目录

        $path = '';

        if (($last_slash = strrpos($model, '/')) !== FALSE)
        {
            $path = substr($model, 0, ++$last_slash);
            $model = substr($model, $last_slash);
        }

        if (empty($name))
        {
            $name = $model;
        }

        //_ci_models可看成是对已加载模型的缓存的,如果发现该模型已经加载过,直接返回
        if (in_array($name, $this->_ci_models, TRUE))
        {
            return $this;
        }

        //判断模型的别名是不是和其他对象的引用有冲突
        $CI =& get_instance();
        if (isset($CI->$name))
        {
            show_error('The model name you are loading is the name of a resource that is already being used: '.$name);
        }

        //加载模型的时候还可以支持动态切换数据库连接
        if ($db_conn !== FALSE && ! class_exists('CI_DB', FALSE))
        {
            if ($db_conn === TRUE)
            {
                $db_conn = '';
            }

            $this->database($db_conn, FALSE, TRUE);
        }

        //因为所有模型都是CI_Model的子类,所以先要确保CI_Model被加载进来了
        if ( ! class_exists('CI_Model', FALSE))
        {
            load_class('Model', 'core');
        }

        //对类名处理下,然后跑到模型所在的目录去加载
        $model = ucfirst(strtolower($model));

        foreach ($this->_ci_model_paths as $mod_path)
        {
            if ( ! file_exists($mod_path.'models/'.$path.$model.'.php'))
            {
                continue;
            }

            //找到了的话,实例化后在_ci_models中缓存,并挂载到全局对象$CI上去
            require_once($mod_path.'models/'.$path.$model.'.php');

            $this->_ci_models[] = $name;
            $CI->$name = new $model();
            return $this;
        }

        // couldn't find the model
        show_error('Unable to locate the model you have specified: '.$model);
    }

关于加载器的源码分析就到此结束了,可以看到加载器对于资源的加载就是做了三件事。

  • 解析类名和路径。
  • 跑到对应的目录去加载。
  • 一旦找到就实例化并挂载到全局对象$CI(CI_controller)上。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容