Using ini files for PHP application settings

Tue, Jan 19, 2010 05:24 PM
At dealnews we have three tiers of servers. First is our development servers, then staging and finally production. The complexity of the environment increases at each level. On a development server, everything runs on the localhost: mysql, memcached, etc. At the staging level, there is a dedicated MySQL server. In production, it gets quite wild with redundant services and two data centers.

One of the challenges of this is where and how to store the connection information for all these services. We have done several things in the past. The most common thing is to store this information in a PHP file. It may be per server or there could be one big file like:

<?php

if(DEV){
    $server = "localhost";
} else {
    $server = "10.1.1.25";
}

?>


This gets messy quickly. Option two is to deploy a single file that has the settings in a PHP array. And that is a good option. But, we have taken that one step further using some PHP ini trickeration. We use ini files that are loaded at PHP's startup and therefore the information is kept in PHP's memory at all times.

When compiling PHP, you can specify the --with-config-file-scan-dir to tell PHP to look in that directory for additional ini files. Any it finds will be parsed when PHP starts up. Some distros (Gentoo I know) use this for enabling/disabling PHP extensions via configuration. For our uses we put our custom configuration files in this directory. FWIW, you could just put the above settings into php.ini, but that is quite messy, IMO.

To get to this information, you can't use ini_get() as you might think.  No, you have to use get_cfg_var() instead. get_cfg_var returns you the setting, in php.ini or any other .ini file when PHP was started. ini_get will only return values that are registered by an extension or the PHP core. Likewise, you can't use ini_set on these variables. Also, get_cfg_var will always reflect the initial value from the ini file and not anything changed with ini_set.

So, lets look at an example.

; db.ini
[myconfig]
myconfig.db.mydb.db     = mydb
myconfig.db.mydb.user   = user
myconfig.db.mydb.pass   = pass
myconfig.db.mydb.server = host


This is our ini file. the group in the braces is just for looks. It has no impact on our usage. Because this is parsed along with the rest of our php.ini, it needs a unique namespace within the ini scope. That is what myconfig is for. We could have used a DSN style here, but it would have required more parsing in our PHP code.

<?php

/**
 * Creates a MySQLi instance using the settings from ini files
 *
 * @author     Brian Moon <brianm@dealnews.com>
 * @copyright  1997-Present dealnews.com, Inc.
 *
 */

class MyDB {

    /**
     * Namespace for my settings in the ini file
     */
    const INI_NAMESPACE = "dealnews";

    /**
     * Creates a MySQLi instance using the settings from ini files
     *
     * @param   string  $group  The group of settings to load.
     * @return  object
     *
     */
    public static function init($group) {

        static $dbs = array();

        if(!is_string($group)) {
            throw new Exception("Invalid group requested");
        }

        if(empty($dbs["group"])){

            $prefix = MyDB::INI_NAMESPACE.".db.$group";

            $db   = get_cfg_var("$prefix.db");
            $host = get_cfg_var("$prefix.server");
            $user = get_cfg_var("$prefix.user");
            $pass = get_cfg_var("$prefix.pass");

            $port = get_cfg_var("$prefix.port");
            if(empty($port)){
                $port = null;
            }

            $sock = get_cfg_var("$prefix.socket");
            if(empty($sock)){
                $sock = null;
            }

            $dbs[$group] = new MySQLi($host, $user, $pass, $db, $port, $sock);

            if(!$dbs[$group] || $dbs[$group]->connect_errno){
                throw new Exception("Invalid MySQL parameters for $group");
            }
        }

        return $dbs[$group];

    }

}

?>


We can now call DB::init("myconfig") and get a mysqli object that is connected to the database we want. No file IO was needed to load these settings except when the PHP process started initially.  They are truly constant and will not change while this process is running.

Once this was working, we created separate ini files for our different datacenters. That is now simply configuration information just like routing or networking configuration. No more worrying in code about where we are.

We extended this to all our services like memcached, gearman or whatever. We keep all our configuration in one file rather than having lots of them. It just makes administration easier. For us it is not an issue as each location has a unique setting, but every server in that location will have the same configuration.

Here is a more real example of how we set up our files.

[myconfig.db]
myconfig.db.db1.db         = db1
myconfig.db.db1.server     = db1hostname
myconfig.db.db1.user       = db1username
myconfig.db.db1.pass       = db1password

myconfig.db.db2.db         = db2
myconfig.db.db2.server     = db2hostname
myconfig.db.db2.user       = db2username
myconfig.db.db2.pass       = db2password

[myconfig.memcache]
myconfig.memcache.app.servers    = 10.1.20.1,10.1.20.2,10.1.20.3
myconfig.memcache.proxy.servers  = 10.1.20.4,10.1.20.5,10.1.20.6

[myconfig.gearman]
myconfig.gearman.workload1.servers = 10.1.20.20
myconfig.gearman.workload2.servers = 10.1.20.21
31 comments
Gravatar for OnyxRaven

OnyxRaven Says:

very nice usage of the internal parser. Downsides are that a php restart is required to reload settings, and that PHP ini parsing can be somewhat odd at times. Be careful about how strings look and are escaped if you're doing anything too complicated.

Also, the 'dotted notation' in ini is really easy to mess up if you're configuring something with lots of config items.

I prefer yml (syck) just because its easier to read. Downside there is that you dont get the performance boost from the internal on-startup-parser like the post has.

Gravatar for Richard Harrison

Richard Harrison Says:

Thanks for sharing Brian. Have you had any issues with your config data essentially being a global variable? My nose wrinkled up a bit when your DB class plucked the config values out of the air; bad code smell?

Gravatar for Steve Clay

Steve Clay Says:

This design is handy (easy to implement), but one could argue a build process might be the best solution for this. That way on each server the credentials end up hardcoded in opcode-cacheable PHP files only where they're needed, and nowhere they're not. Also I'd think this would be faster than having to parse additional ini strings and call functions to retrieve them on every request.

I believe Richard is making the (IMO good) suggestion that it would be better to call get_cfg_var() outside the class and pass the configs into the constructor. Otherwise your class config becomes "magical" and harder to test/reuse.

Gravatar for Jonas Lejon

Jonas Lejon Says:

Thanks for sharing! Nice info. Do you mind sharing you classes for gearman and memcache also?

Gravatar for Evren Usta

Evren Usta Says:

Good sharing.

Thanks for sharing Brian.

Gravatar for Brian Moon

Brian Moon Says:

Well, the entire reason for this class to exist is to read those values and return me an object I can use. It could very well be a function, but I want it to autoload. I do pass in which configuration I want to load, so, IMO, it is not coming out of thin air.

Steve, my test show that to not be true. Without an opcode cache, this is much faster. With one, things break even. You would propose I call get_cfg_vars every time I need to use the class? That sounds pretty weird.

Another good reason for this that I should have mentioned in the post is that config files are often edited by non-programmers. This makes ini format a good choice as it is a familiary key/value pair format.

Gravatar for George Secrieru

George Secrieru Says:

INI files can be very handy. However, there's a lot of ways of doing this in a simple and easy-maintaining way, IMHO. In your implementation, you would have to change your class every time a new parameter was added to your file.

You're using autoload for INI files and that's why you're using get_cfg_var(). In directories without any config file, you just give PHP unnecessary work to do. Why don't you try something like 'config.ini', a file with all your application's config parameters? In this case, instead of get_cfg_var(), perhaps you could give a try to http://php.net/parse_ini_file

Best regards,
George

Gravatar for Brian Moon

Brian Moon Says:

@Geoarge: parse_ini_file would cause a file io hit for every page request. That is not desired. We actually used that at first before I found get_cfg_var. I still use that in distributed applications where I can not expect the PHP build to support pre-loading of the configuration.

Gravatar for Tommy

Tommy Says:

Interesting approach. But how would it be to use the parse ini to load the config into memcache, and then get variable from that, and only reload the file when the memcache is expired?

Gravatar for Brian Moon

Brian Moon Says:

@tommy, the network overhead for memcached would not be worth it. Plus, we use this to get our memcached configuration as well.

Gravatar for Brian Moon

Brian Moon Says:

@jonas This is not our actual object. Perhaps I will post my full Gearman wrapper at some point. The memcache wrapper looks pretty much like this one, just with memcached.

Gravatar for Michael Stillwell

Michael Stillwell Says:

How much slower was parse_ini_file()? Sure, you have to load and parse a file for every request, but you need to do that for every PHP file you use as well, and you probably have much more of that that configuration. (Unless they're cached in APC, in which case you could store your config in APC as well.)

Gravatar for Brian Moon

Brian Moon Says:

Well, Michael, if we keep following that logic we end up with the dilemma I see in "modern" frameworks. Way too many files because it is always "just one more file".

Gravatar for Shawn Stratton

Shawn Stratton Says:

Interestingly enough, while I see some slight performance benefits I see a lot of possible problems by utilizing this method to store application settings. The main one I see is a security problem, by storing this in the php.ini settings you risk dumping your entire configuration if there's a file calling phpinfo() at any point, or for that matter if someone finds a way to inject code into an eval() (don't think you guys would be doing that for performance reasons as well as other reasons,) the other issue I see with this is you have multiple servers and now you have to either force-reload or restart them all at the same time, after ensuring the configurations are updated at all locations otherwise risk the ensuing chaos. Looks pretty cool otherwise though.

Gravatar for Brian Moon

Brian Moon Says:

@shawn, yeah, you don't do this for 2 servers. It is a waste. As for eval(), one would be fired for using eval. The settings are not in php.ini and do not show up in phpinfo(). So, no worries there. Once someone can inject code, you are kind of lost anyhow. How you store passwords is not the issue anymore.

As for the restart issue, how often do you really change your main database server name? Or add/remove servers from your memcache pool? For us? Once a year, maybe. We restart for apache/php/mysql upgrades much more often than we do for changing this stuff. Lots of people seem to be hung up on that though. Not sure why. I find that odd.

Gravatar for Soenke Ruempler

Soenke Ruempler Says:

we do it this way, too, but with parse_ini_file() function. this:

a) does not need php scan dir magic/'abuse'
b) has the same syntax
c) does not interference with php.ini variables
d) does not introduce

personally, i'm not a fan of mixin php.ini and applicationconfig namespaces. but yes, the the confg-file-approach imho is the right way :)

soenke

Gravatar for Nils

Nils Says:

You could also do it in the vhost config depending on the webserver you use. In nginx you could set it via fastcgi_param like this:

fastcgi_param DB_SERVER[0] mysql://

You can then access it via $_SERVER['DB_SERVER'][0].

Gravatar for Brian Moon

Brian Moon Says:

@Nils, yes, but that would make it useless for my Gearman workers or cron jobs.

Gravatar for junke

junke Says:

I've used a similar approach to this at companies in the past. Unless you only have a single site/application running on your servers, consolidating configuration data into a single/global file is NOT a good idea.

In some ways, for the very reason you mention (the config files are not
always edited by the programmer)

My preffered method is to use the vhost to drop a $_SERVER variable of something like config_ini = "/configs/mysite.com/config.ini", and parse that. Then use either parse_ini_file( $_SERVER['config_ini'] ) or even parse_ini_file( 'config'. DIRECTORY_SEPARATOR . $_SERVER['HTTP_HOST'] . DIRECTORY_SEPARATOR . 'config.ini' ) if you want to avoid the custom vhost.

Just a few of the many reasons for this are:
1. You secure site specific database/user/auth/schema information isolated between sites, which increased security.
2. If you're usnig a VCS (like you should be), your config file's revision # doesn't continually cycle unrelated to your site/project's revision #
3. No code EVER needs to reference which site/project its using. (you don't have to pass anything since its based on the $_SERVER['HTTP_HOST'] value. -the key result here is that copying a chunk of code from one site/project to the next does not incur risk of missing updating that identifier variable.
4. Specifically for the cases of database information, you can use lazy load techniques to eliminate that file IO you're so concerned with -as requests that don't need the database will never parse the file at all.
5. You can make updates without apache restarts.
6. You can even extend and/or override parts or all of the base site config file data by either changing the value of $_SERVER['config_ini'] before using your config/db class or extending the class to accept an additional config file (for example passing an additional ini file specific to just one section or app on the site)







Gravatar for junke

junke Says:

In short, the single global config file is imho the tool of a lazy/overworked sysadmin, but not the choice for proper data isolation, security, and re-usable code design.

Gravatar for Brian Moon

Brian Moon Says:

@junke: we run one application. It is called..... dealnews.com

Gravatar for Erwinus

Erwinus Says:

Use defines dude, with a prefix. it cannot be changed at run-time. If there is an error in the php config file php reports is and no class
is needed to parse the stuff.

for example:
define( 'MYCONFIG_DB_HOSTNAME', 'myhostname' );
etc.

example function:
function getDbConfig( $sConfigName )
{
  $sConfigName = strtoupper( $sConfigName );
  return array( 'hostname'=>constant( $sConfigName.'_DB_HOSTNAME ), ....);
}

$aMyConfig = getDbConfig( 'MYCONFIG' );

That's easy or not?

Gravatar for Brian Moon

Brian Moon Says:

@Erwinus: As I said, I use a class because I want it to autoload. It is just a static function after all that returns a singleton.

ini format is more non-php programmer friendly. A) our sysadmins can edit them with ease. B) Puppet can easily auto-build these files C) We can use the in non PHP applications.

Add A Comment

Your Name:


Your Email:


Your URL:


Your Comment: