Beefy Boxes and Bandwidth Generously Provided by pair Networks
There's more than one way to do things

Re: Configuration Best Practices for Web Apps

by clinton (Priest)
on Jan 24, 2006 at 18:40 UTC ( #525279=note: print w/replies, xml ) Need Help??

in reply to Configuration Best Practices for Web Apps

I have a couple of sites which are configuration heavy, and I've written a module which has made life really easy for me.

I would welcome feedback on the module itself (included below - see readmore)

This covers the application and machine specific configuration mentioned above.

All config is kept in nested YAML files (YAML is a REALLY easy way to express complex Perl variables simply). These nested files form a tree of keys. But you can also have trees within each file itself, so a file may look like:

/configdir/global.conf: site: name: My website name url: langs: en fr es handlers: login: My::Login home: My::Home etc
So you can could say something like :
print C('')
Alternatively, you could have the directory structure:
/configdir/global.conf /configdir/global/site/handlers.conf
or a combination of the two. The sub-directories are read first, then the config files, so that the handlers info in global.conf would be added to the handlers info already read from handlers.conf

Then, to handle machine specific config, you can have a file only on that machine called local.conf (at any level in the directory structure) in which you specify particular keys that you would like to override. For instance:

/configdir/global/site/local.conf: handlers: login: New::Login
would only override the handler for login.

I can think of a number of improvements, such as being able to specify different config dirs depending on the application, but it does what it says it does.

You can even reload the config while the website is running, but the config would no longer be shared between processes and you would need to reload the config for each apache process.

Like I say, I would appreciate feedback on this - anything stupid that I've done, ways to improve it etc. It uses the new YAML module YAML::Syck which is a lot faster than the pure Perl version, and also handles UTF8.

package Config; use strict; use warnings FATAL => 'all'; use File::Glob qw(:glob); use YAML::Syck(); BEGIN { $YAML::Syck::ImplicitUnicode=1; } use Data::Dumper; =head1 Config; The first time you use() this module, must be used as: use Config '/path/to/config/directory'; In all other packages where you want the config available, just use as +: use Config; Always imports the function C() into your package, to allow you to access your configuration. =cut our $Config_Dir = ''; our $Config; =head2 C() Returns a value for the key indicated, or throws an error if it is not defined. print C('namespace[.key1[.keyn]]'[,$config_variable); The second parameter, if passed, is the config variable to search. If +not passed, then the Config hash built from the config files will be searc +hed. =cut #========================================== sub C { #========================================== my $path = shift||''; my ($config,$namespace,@keys); if (@_) { $config = $_[0]; $namespace = "PRIVATE"; @keys = split(/\./,$path); } else { ($namespace,@keys) = split(/\./,$path); $namespace||=''; die ("Namespace '$namespace' not defined") unless $namespace && exists $Config->{$namespace}; $config = $Config->{$namespace}; } my $key_path = $namespace; foreach my $key (@keys) { next unless defined $key && length($key); if (ref $config eq 'ARRAY' && $key=~/^[0-9]+/ && exists $confi +g->[$key]) { $config = $config->[$key]; $key_path.='::'.$key; next; } elsif (ref $config && exists $config->{$key}) { $config = $config->{$key}; $key_path.='::'.$key; next; } die ("Invalid key '$key' specified for $key_path : \n" .Dumper($config)); } return wantarray ? ref $config eq 'ARRAY' ? (@$config) : ref $config eq 'HASH' ? (%$config) : $config : ($config); } =head2 copy_C() Works just like C() but returns a ref to a private copy of the data ra +ther than a reference. This means the data can be changed without changing the version stored in Config. This is not exported. =cut #========================================== sub copy_C { #========================================== my $data = C(@_); my $VAR1; return eval(Dumper($data)); } =head2 load_config() Store loaded config in %Config =cut #========================================== sub load_config { #========================================== $Config = _load_config(); } =head2 _load_config() Looks at all files called *.conf in the config directory and its subdirectories and tries to parse them. The configuration in each file gets loaded into its own name space. A direcory name is also considered a name space. Directories are loaded before config files of the same name, so for instance, you can have: confdir: syndication/ --data_types/ --traffic.conf --headlines.conf --data_types.conf syndication.conf The config items in syndication.conf will be added to (or overwrite) the items loaded into the syndication namespace via the subdirectory called syndication. In any sub-directory, you can have a file called local.conf which is used for storing information local to this installation only. This data will be merged with the existing data, but the namespace must be specified in the local file. For instance, if we have: confdir: db.conf local.conf and db.conf has : connections: default_settings: host: localhost table: abc password: 123 And in local.conf: db: connections: default_settings: password: 456 the resulting configuration will look like this: db: connections: default_settings: host: localhost table: abc password: 456 =cut #========================================== sub _load_config { #========================================== my $dir = shift || $Config_Dir; my $config = {}; my @config_files = sort{$a cmp $b} grep {!/\/local.conf$/} glob($d +ir."*"); foreach my $config_file (@config_files) { my ($data,$name); if (-f $config_file && $config_file=~/\.conf$/) { $data = _load_config_file ($config_file); ($name) = ($config_file=~m|.*/(.*)\.conf$|); } elsif (-d $config_file) { $data = _load_config ($config_file.'/'); ($name) = ($config_file=~m|.*/(.*)$|); } else { next; } if (exists $config->{$name}) { map {$config->{$name}->{$_} = $data->{$_}} keys %$data; } else { $config->{$name} = $data; } } if (-e $dir.'local.conf') { my $data = _load_config_file ($dir.'local.conf'); $config = _merge_local($config,$data); } return $config; } =head2 _merge_local() Used to merge local.conf files into the configuration. =cut #========================================== sub _merge_local { #========================================== my $config = shift; my $local = shift; foreach my $key (keys %$local) { if (ref $local->{$key} eq 'HASH' && exists $config->{$key}) { $config->{$key} = _merge_local ($config->{$key},$local->{$ +key}); } else { $config->{$key} = $local->{$key} } } return $config; } =head2 _load_config_file() Parses the YAML file and throws an error if it is not correctly formatted =cut #========================================== sub _load_config_file { #========================================== my $config_file = shift; my $data; eval { $data = YAML::Syck::LoadFile($config_file); }; if ($@) { die ("Error loading config file $config_file:\n\n" .$@); }; return $data; } =head2 import() Used to set config directory the first time this module is loaded. The config directory cannot be changed by subsequent uses; Also exports the function C() into the namespace in which it is being used. =cut #========================================== sub import { #========================================== shift; my $callpkg = caller(0); no strict 'refs'; *{$callpkg."::C"} = \&C; use strict; # If we have already loaded this module correctly, do nothing return 1 if $Config_Dir; my $dir = shift; if ($dir && -d $dir && -r _) { $dir=~s|/?$|/|; $Config_Dir = $dir; load_config(); return 1; } my @caller = caller (1); die <<ERROR FATAL ERROR LOADING Config The default configuration directory must be specified the first time that Config is loaded. use Config '/path/to/config_dir'; Error in : Package "$caller[0]", Filename "$caller[1]", Line $call +er[2]; ERROR } 1

Log In?

What's my password?
Create A New User
Node Status?
node history
Node Type: note [id://525279]
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others meditating upon the Monastery: (5)
As of 2019-11-11 22:30 GMT
Find Nodes?
    Voting Booth?
    Strict and warnings: which comes first?

    Results (63 votes). Check out past polls.