CodeIgniter源码分析 5 - 配置与多环境

记一次填坑经历

在博主刚工作那会是这样区分不同环境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)中被加载的。

之前在加载器的源码分析中看到,加载器处理需要被自动载入的资源的逻辑是在_ci_autoloader()方法进行的,该方法中通过获取需要自动载入的配置文件,调用$this->config($file)方法,传入需要载入的配置文件名,$this->config()方法内部你会看到其最终调用了config(CI_Config)对象的load()方法,将配置文件传给了load()方法。

image.png

理清楚$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()中加载了主配置文件,另一部分需要自动载入的配置文件是交给加载器处理的。

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