记一次填坑经历
在博主刚工作那会是这样区分不同环境sdk的配置。
- 在配置文件中定义一个区分环境的字段,例如叫做evmt,evmt有两个值,1是测试,2是正式。
- 在部署到正式环境的时候和运维约定手动改一下evmt。这样我就可以读取evmt的值,然后决定使用哪种环境的sdk,那会我被自己这种经验的想法给震撼到了,觉得自己好聪明。
表现在配置文件中大概就是这样
//config.php中的配置
$config['evmt'] = 1; //1测试/2正式
$config['wx_sdk'] =
[
1 => ['appid' => 'xxx', 'appkey' => 'xxx'],
2 => ['appid' => 'ccc', 'appkey' => 'ccc'],
];
//然后在业务逻辑我就通过读取evmt的值使用对应的sdk相关的配置,pay.php
$appid = $wx_sdk[$evmt]['appid'];
$appkey = $wx_sdk[$evmt]['appkey'];
这样的配置看似能够解决业务问题,然而噩梦就来了,终于有一次我们在测试环境把所有功能都测试完成部署到正式环境后,用户反馈说订单有问题,我们就在测试环境测了发现怎么测都没问题,花了快一个小时才发现问题,原来由于部署到正式环境时evmt没有修改导致调用了测试环境sdk的配置,这坑挖的呵呵。
因为在部署时手动更改配置对于运维来说难免会有疏漏,何况对于部署本身来说就是自动化了,你让人怎么手动去改呢?
是人都会犯错误的,又不是机器能够机械性的工作,既然机器能做的事情干嘛要人肉介入呢,写代码不就是为了自动化么?
借由这次事故我就去仔细看了下CI的文档,发现在CI框架早就提供了对环境的支持。
多环境
什么是多环境
多环境在CI框架里指的是你的多个开发环境,一般我们可以把环境分为
- 开发环境development
- 测试环境testing
- 正式环境production
CI框架中是通过index.php中的 ENVIRONMENT 常量来管理多环境,这个常量的值是通过$_SERVER['CI_ENV']读取的,用该常量来管理多环境的配置的就容易多了,只需要在application/config/建一个和ENVIRONMENT相关的目录,然后把相应的sdk配置文件放到对应的目录下就可以了。
例如
application/config/development/sdk_config.php
application/config/production/sdk_config.php
部署的时候读取各自环境下.htaccess文件中CI_ENV的值就可以(注意:部署时要将.htaccess忽略,免得被覆盖),剩下的就交给CI自己去加载就好了。这样也就解决了我们上面遇到了那个问题了。
既然多环境的管理是和配置有关的,那么接下来我们看下配置相关的源码解析。
配置
这块主要看下配置文件加载,配置读取,配置设置这三部分的源码。
配置文件的加载
配置文件的加载是通过config对象(CI_Config.php)的load()方法实现的。
$this->config->load($file, $use_sectipon=false, $fail_gracefully= false);
load()方法提供了三个参数
- 第一个参数$file是要加载的配置文件。
- 第二个参数 $use_sectipon是为了避免你在加载多个配置文件时相同数组的索引引起的冲突,如果设置为true,那么各个配置文件中的配置项会被存储到以该配置文件名为索引的数组中去。
- 第三个参数$fail_gracefully是用于抑制错误信息,如果设置为true,当配置文件不存在时,不会报错。
public function load($file = '', $use_sections = FALSE, $fail_gracefully = FALSE)
{
//解析出文件名
$file = ($file === '') ? 'config' : str_replace('.php', '', $file);
$loaded = FALSE;
//遍历配置文件所在的目录,这里我们看到考虑到了多环境的配置'ENVIRONMENT.DIRECTORY_SEPARATOR.$file'
foreach ($this->_config_paths as $path)
{
foreach (array($file, ENVIRONMENT.DIRECTORY_SEPARATOR.$file) as $location)
{
$file_path = $path.'config/'.$location.'.php';
//is_loaded中存储了已经加载过的配置文件,如果发现已经被加载过,就不需要在往下查找了
if (in_array($file_path, $this->is_loaded, TRUE))
{
return TRUE;
}
//没有找到的话再次循环,去下一个目录中查找
if ( ! file_exists($file_path))
{
continue;
}
include($file_path);
//找到后如果发现文件中的配置不是数组的话,根据$fail_gracefully的值来确定如何报错
if ( ! isset($config) OR ! is_array($config))
{
if ($fail_gracefully === TRUE)
{
return FALSE;
}
show_error('Your '.$file_path.' file does not appear to contain a valid configuration array.');
}
/*
| 将当前文件的配置和系统初始化已经加载的配置合并,
|
| $use_sections的意思是否将文件中的配置存储到以文件名为索引的数组中去,
| 很明显看到$this->config是系统初始化的配置,那么$this->config是怎么来的?
| 这里先留下这个疑问,分析完load()方法我们再去看$this->config怎么来的
*/
if ($use_sections === TRUE)
{
$this->config[$file] = isset($this->config[$file])
? array_merge($this->config[$file], $config)
: $config;
}
else
{
$this->config = array_merge($this->config, $config);
}
//加载成功该配置文件后再is_loaded数组中保存下
$this->is_loaded[] = $file_path;
$config = NULL;
$loaded = TRUE;
log_message('debug', 'Config file loaded: '.$file_path);
}
}
//最后返回配置文件是否加载成功
if ($loaded === TRUE)
{
return TRUE;
}
elseif ($fail_gracefully === TRUE)
{
return FALSE;
}
show_error('The configuration file '.$file.'.php does not exist.');
}
在分析这段代码时还遗留了一个疑问,看样子貌似 $this->config 保存的是系统初始化的一些配置,那现在就需要看看 $this->config 是怎么来的;查找引用发现 $this->config 是在构造方法中被赋值的。
public function __construct()
{
//获取系统初始化的配置。接下来分析get_config()这个方法你会看到系统初始化被加载的配置文件有哪些
$this->config =& get_config();
//base_url是你站点的域名,如果你没有设置的话,会根据请求解析出你的站点域名
if (empty($this->config['base_url']))
{
if (isset($_SERVER['HTTP_HOST']) && preg_match('/^((\[[0-9a-f:]+\])|(\d{1,3}(\.\d{1,3}){3})|[a-z0-9\-\.]+)(:\d+)?$/i', $_SERVER['HTTP_HOST']))
{
$base_url = (is_https() ? 'https' : 'http').'://'.$_SERVER['HTTP_HOST']
.substr($_SERVER['SCRIPT_NAME'], 0, strpos($_SERVER['SCRIPT_NAME'], basename($_SERVER['SCRIPT_FILENAME'])));
}
else
{
$base_url = 'http://localhost/';
}
//解析出你的站点域名后,重设base_url
$this->set_item('base_url', $base_url);
}
log_message('info', 'Config Class Initialized');
}
注意:系统初始化的配置加载是分两部分的,一部分是在get_config()方法中加载的,另一部分是写在autoload.php通过加载器在系统初始化是加载的。
现在看下通过get_config()方法在系统初始化时加载了哪些配置。
function &get_config(Array $replace = array())
{
//首先先定义一个静态变量$config,这也预示着该变量会被后文多次引用。
static $config;
//这里我们会发现,该方法知识加载了主配置文件config.php
if (empty($config))
{
//--------------------------require主配置文件config.php并考虑的多环境-----------------------------------
$file_path = APPPATH.'config/config.php';
$found = FALSE;
if (file_exists($file_path))
{
$found = TRUE;
require($file_path);
}
// Is the config file in the environment folder?
if (file_exists($file_path = APPPATH.'config/'.ENVIRONMENT.'/config.php'))
{
require($file_path);
}
elseif ( ! $found)
{
set_status_header(503);
echo 'The configuration file does not exist.';
exit(3); // EXIT_CONFIG
}
//如果主配置文件有问题的话,就中断整个程序。
if ( ! isset($config) OR ! is_array($config))
{
set_status_header(503);
echo 'Your config file does not appear to be formatted correctly.';
exit(3); // EXIT_CONFIG
}
}
//这里通过传入的参数动态修改某些配置项
foreach ($replace as $key => $val)
{
$config[$key] = $val;
}
//最后将主配置文件config.php中的配置项返回
return $config;
}
}
我们看到get_config()方法只是加载了主配置文件config.php,而其他需要自动载入的配置文件是在加载器(CI_Loader.php)中被加载的。
image.png之前在加载器的源码分析中看到,加载器处理需要被自动载入的资源的逻辑是在_ci_autoloader()方法进行的,该方法中通过获取需要自动载入的配置文件,调用$this->config($file)方法,传入需要载入的配置文件名,$this->config()方法内部你会看到其最终调用了config(CI_Config)对象的load()方法,将配置文件传给了load()方法。
理清楚$this->config是怎么来的,并且知道它保存了整个系统初始化的配置项后,那么对于设置配置和读取配置就很清楚了,就是操作$this->config罢了。
读取配置项
读取配置是通过config对象的item()方法实现的
$this->config->item('appid');
item方法的源码蛮简单
//读取配置项的时候考虑到你可能把配置存在以文件为索引的数组下,
//所以提供了第二个参数$index做为读取配置的索引
public function item($item, $index = '')
{
if ($index == '')
{
return isset($this->config[$item]) ? $this->config[$item] : NULL;
}
return isset($this->config[$index], $this->config[$index][$item]) ? $this->config[$index][$item] : NULL;
}
设置配置项
设置配置项是通过config对象的set_item()方法实现的
$this->config->set_item('appid', 'xxx');
public function set_item($item, $value)
{
$this->config[$item] = $value;
}
配置和多环境的源码分析就到这里结束了,我们总结一下。
对于配置管理说最重要的是正确的读取配置,这里就涉及到多环境的处理,也看到CI框架提供了蛮便捷的多环境处理。
而对于配置初始化我们看到是由两部分完成的,一部分是在get_config()中加载了主配置文件,另一部分需要自动载入的配置文件是交给加载器处理的。
网友评论