當我們load模型交播,類庫等后就可以使用它們了,但是它們到底是怎么載入的践付?CI框架的自動載入是如何實現的秦士?它是如何處理類庫的擴展的?帶著這些疑問荔仁,這節(jié)我們看下加載器的源碼伍宦。
現代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對象應該在系統(tǒng)初始化的時候就加載完畢届吁,我們知道系統(tǒng)初始化是在引導文件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()卻發(fā)現ob_get_level()返回的是1沽损,那是因為在腳本的前文使用了ob_start()灯节,如果依賴ob_get_level()處理相應的邏輯,那么最好使用ob_end_clean()清一下緩存區(qū)绵估。
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---------------------------------
//不管有沒有多環(huán)境碌上,自動載入的配置文件autoload.php是一定要加載的
if (file_exists(APPPATH.'config/autoload.php'))
{
include(APPPATH.'config/autoload.php');
}
//加載當前環(huán)境的配置文件
if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/autoload.php'))
{
include(APPPATH.'config/'.ENVIRONMENT.'/autoload.php');
}
if ( ! isset($autoload))
{
return;
}
//---------------------------------讀取$autoload------------------------------------------------------
//一般情況下資源也就是liarary,model浦徊,help這幾種馏予,并且這幾種資源的目錄已經被規(guī)定死了,
//但萬一你不想把資源歸到這幾個目錄下盔性,那么你可以在$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);
}
}
// 讀取需要自動載入的類庫霞丧,注意:這里對數據庫的載入是做了區(qū)分的,數據庫的連接應該只有一次
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()等的細節(jié)。
類庫加載
類庫加載是通過library()方法實現的焕襟,加載類庫時我們不僅可以單個加載陨收,還可以批量加載,同時還支持為類庫取別名鸵赖,現在我們看下該方法的細節(jié)處理务漩。
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;
}
對于類庫的加載我們發(fā)現最終其實調用的是_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);
//----------------------------------------查找系統(tǒng)內置類庫------------------------------------------------------
//加載內置類庫并做實例化
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;
}
//沒找到的話再次循環(huán)去下一個目錄查找
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()方法中解析出類和路徑后振坚,接下來就是實例化類庫了。
由于加載的類庫可能是我們自己定義的類庫或者系統(tǒng)內置的類庫斋扰,所以在加載成功做實例化的時候做了區(qū)分,分別從_ci_load_stock_library()和_ci_init_library()中實例化內置類庫和我們自定義的類庫啃洋。
先看從_ci_load_stock_library()方法中加載內置類庫传货。
注意:由于系統(tǒng)類庫我們也可以擴展,所在實例化系統(tǒng)內置類庫時會有類名$prefix的判斷宏娄,系統(tǒng)類庫的$prefix是以CI開頭的问裕,而我們擴展的系統(tǒng)類庫的$prefix是在config.php中subclass_prefix上指定的。
protected function _ci_load_stock_library($library_name, $file_path, $params, $object_name)
{
$prefix = 'CI_';
//---------------------------------------獲取擴展類庫的前綴配置---------------------------------------------------
//判斷系統(tǒng)核心類是不是被擴展了孵坚,注意:擴展類繼承了系統(tǒng)核心類的所有功能
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目錄下去加載系統(tǒng)內置類庫
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目錄下去加載系統(tǒng)內置類庫
include_once(BASEPATH.'libraries/'.$file_path.$library_name.'.php');
//跑到APPPATH/libraries目錄下去加載被擴展了的系統(tǒng)內置類庫
$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框架支持你在創(chuàng)建一個類庫時在 APPPATH/config/ 目錄下創(chuàng)建一個和類名相同的配置文件筷畦,所以接下來你會看見在實例化類庫的時候,如果沒有為類庫傳入參數刺洒,就會嘗試去讀該類的配置文件的內容作為實例化的參數鳖宾。
現在看下_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;
}
//找到的話就沒必要再繼續(xù)去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可看成是對已加載模型的緩存的瞒爬,如果發(fā)現該模型已經加載過弓柱,直接返回
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);
}
//加載模型的時候還可以支持動態(tài)切換數據庫連接
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)上酿箭。