当我们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)上。