NAME
Catalyst::Plugin::ConfigLoader::MultiState - Convenient and flexible config loader for Catalyst.
SYNOPSIS
conf/myapp.conf:
$db = {
host => 'db.myproj.com',
driver => 'Pg',
user => 'ilya',
password => 'rumeev',
};
$var_dir = r('home')->subdir('var');
$log_dir = $var_dir->subdir('log'); $log_dir->mkpath(0, 0755);
rw(host, 'mysite.com');
$uri = URI->new("http://$host");
...
conf/chat.conf
$history_cnt = 10;
$tmp_dir = r(var_dir)->subdir('chat');
$service_uri = URI->new( r(uri)->as_string .'/chat' );
...
conf/myapp.dev
$db = {host => 'dev.myproj.com'};
rewrite(host, 'dev.mysite.com');
...other differences
in MyApp:
my $cfg = MyApp->config;
print $cfg->{db}{user}; # ilya
print $cfg->{db}{host}; # db.myproj.com
print $cfg->{chat}{tmp_dir}; # Path::Class::Dir object (/path/to/myapp/var/chat)
print $cfg->{host}; # mysite.com
print $cfg->{uri}; # URI object http://mysite.com
print $cfg->{chat}{service_uri}; # URI object (http://mysite.com/chat)
Now if in local.conf:
$dev = 1;
Then
print $cfg->{db}{user}; # ilya
print $cfg->{db}{host}; # dev.myproj.com
print $cfg->{host}; # dev.mysite.com
print $cfg->{uri}; # URI object http://dev.mysite.com (magic :-)
print $cfg->{chat}{service_uri}; # URI object http://dev.mysite.com/chat (more magic)
Configure a plugin (Authentication for example)
in conf/Plugin-Authentication.conf:
module();
$default_realm = 'default';
$realms = {
...
};
DESCRIPTION
This plugin provides you with powerful config system for your catalyst project.
It allows you to:
- write convenient variable definitions - your lovest perl language :-) What can be more powerful? You do not need to define a huge hash in config file - you just write separate variables.
- split your configs into separate files, each file with its own namespace (hash depth) or without - on your choice.
- access variables between configs. You can access any variable in any config by uri-like or hash path.
- overload your config hierarchy by *.<group_name> files on demand
- rewrite any previously defined variable. Any variables that depend on initial variable (or on variable that depends on inital, etc) will be recalculated in all configs.
- automatic overload for development servers
This is very useful for big projects where your config might grow over 100kb. Especially when you have number of installations of application that must differ from other without pain to redefine a hundreds of config variables in '_local' file which, in addition to all, cannot be put in svn (cvs).
In most of cases this plugin has to be the first in plugin list.
Config syntax
Syntax is quite simple - it's perl. Just define variable with desired names.
$var_name = 'value';
Values can be any that scalars can be: scalar, hashref, arrayref, subroute, etc. DO NOT write 'use strict' or you will be forced to define variables via 'our' which is ugly for config.
If you define in myapp.conf (root config)
$welcome_msg = 'hello world';
it will be accessible through
MyApp->config->{welcome_msg}
Hashes acts as they are expected:
$msgs = {
welcome => 'hello world',
bye => 'bye world',
};
MyApp->config->{msgs}{bye};
It is a good idea to reuse variables in config to allow real flexibility:
$var_dir = $home->subdir('var');
$log_dir = $var_dir->subdir('log');
$chat_log_dir = $log_dir->subdir('chat');
...
In contrast to:
$var_dir = 'var';
$log_dir = 'log';
$chat_log_dir = 'chat';
or
$var_dir = 'var';
$log_dir = 'var/log';
$chat_log_dir = 'var/log/chat';
...will grow :(
The second and third examples are much less flexible. By means of second example we just hardcoded a part of config logic in our application: it supposes that var_dir is UNDER home and log_dir is UNDER var_dir, etc, which must not be an application's headache anyway. In third example we have a lot of copy-paste and application still supposes that var_dir is under home.
Namespaces
All configs from files are written to separate namespaces by default (except for /myapp.*). Plugin reads all *.conf files in folder 'conf' under app_home (or whatever you set ->config->{'Plugin::ConfigLoader::MultiState'}{dir} to), subdirs too - recursively, and special local config from file local.conf under app_home (or whatever you set ->config->{'Plugin::ConfigLoader::MultiState'}{local} to). Configs from /myapp.* and local.conf are written directly to root namespace (config hash). Other configs are written accordingly to their paths. For example config from chat.conf is written to $cfg->{chat} hash. Config from test/more.conf is written to $cfg->{test}{more} hash.
Sometimes you don't want separate namespace, just split one big file to parts. In this case you can use 'root' or 'inline' pragmas. 'root' pragma brings config file to the root namespace no matter where file is located. 'inline' brings file to one level upper.
Examples:
split root config:
/myapp.conf:
...part of definitions
/misc.conf:
root;
...other part of definitions
split /chat.conf:
/chat/main.conf:
inline;
...definitions
/chat/ban_rules.conf
inline;
...definitions
Catalyst plugins configuration
To make configuration for catalyst plugin in separate file, name it after plugin class name replacing '::' with '-' and use 'module' pragma;
For example Plugin-Authentication.conf:
module;
$default_realm = 'myrealm';
$realms = {
....
};
To embed plugin's config into any root ns file write __ instead of ::
$Plugin__Authentication = {
default_realm => 'myrealm',
realms => {...},
};
Accessing variables from other config files
Files of each group (*.conf, *.dev, *.<group_name>) are processed in alphabetical order (except for local.conf and myapp.conf - they are processed earlier).
Special file app_home/local.conf is processed twice - at start and in the end to have a chance to pre-define something (config file groups for example) in the beggining and rewrite/overload in the end.
You can access variable from any file that has already been processed (use test-like namings: 01chat.conf, 02something.conf, ... - if it is matters, plugin removes ^\d+ from ns).
To access variable in root namespace use r() getter:
$mydir = r('var_dir')->subdir('my');
Quotes is not required (for beauty): r(var_dir)-> but be careful - variable name must be allowed perl unqouted literal and must not be one of perl builtin functions and not one of [root, inline, r, p, u, l, module, rw, rewrite], therefore this is not recommended.
To access variable in local (current) namespace use l() getter.
To access variable in upper namespace use u() getter.
To access any variable use p() getter with uri-like path:
p('/chat/history_cnt') || r('chat')->{history_cnt}
To access variables initially defined by catalyst (home, root, pre-defined config variables) use r('home'), r('root'), etc from anywhere. Note that MultiState tunes 'home' variable - it makes it a Path::Class::Dir object instead of simple string.
Merging
If a config defines variable that already exists (in the same namespace) it will be merged with existing variable (merged if both are hashes and replaced if not). If you have variables in configs that depend on initial variable - SEE 'rewrites' section or they won't be updated!
Overload
Configs can be overloaded by file or group of files that are not loaded by default. The example is *.dev group which is activated when you predefine
$dev=1;
in local.conf (or in MyApp->config before setup phase)
To activate other group(s) you must predefine it in local.conf (or in MyApp->config before setup phase)
$config_group = ['.beta']; #i'am one of beta-servers
Config will be overloaded from conf/*.beta, conf/*/*.beta,... after processing standart configs (i.e. all config variables are accessible to *.beta files to read and overload/rewrite). Group is dot plus files extension.
In myapp.beta for example:
$db = {host => 'beta.myproj.com'};
$debug = {enabled => 1};
rewrite('base_price', 0);
...
In chat.beta for example:
$welcome_msg = l('welcome_msg') . ' (beta server)';
All of the rules described above are applicable to all configs in any groups (i.e. namespaces, visibility, etc).
You can define config groups in application's code as well as in local.conf. To do that just define MyApp->config->{config_group} = [...] BEFORE setup() (runtime overloading is not supported for now).
There is a way to define that in offline scripts and other places that use your application (there are not only myapp_server.pl and Co :-) to customize your application's behaviour:
Create this sub in MyApp.pm:
sub import {
my ($class, $rewrite_cfg) = @_;
_merge_hash($class->config, $rewrite_cfg) if $rewrite_cfg;
}
sub _merge_hash {
my ($h1, $h2) = (shift, shift);
while (my ($k,$v2) = each %$h2) {
my $v1 = $h1->{$k};
if (ref($v1) eq 'HASH' && ref($v2) eq 'HASH') { merge_hash($v1, $v2) }
else { $h1->{$k} = $v2 }
}
}
And just write in an offline script/daemon:
use MyApp {
log => {file => 'otherlog.log'},
something => 'something',
config_group => [qw/.script .maintenance/],
};
But there is a big problem. By writing
__PACKAGE__->setup();
in MyApp.pm we just left no chances for others to customize your application BEFORE setup phase because 'use MyApp' will at the same time execute setup() before import()
Fortunately there is a simple solution: not to write '__PACKAGE__->setup()' :-). Instead write:
sub import { #for places that do 'use MyApp'
my ($class, $rewrite_cfg) = @_;
_merge_hash($class->config, $rewrite_cfg) if $rewrite_cfg;
$class->setup unless $class->setup_finished;
}
sub run { #myapp_server.pl does 'require MyApp', not 'use', so import() is not called
my $class = shift;
$class->setup unless $class->setup_finished;
$class->next::method(@_);
}
sub _merge_hash {
my ($h1, $h2) = (shift, shift);
while (my ($k,$v2) = each %$h2) {
my $v1 = $h1->{$k};
if (ref($v1) eq 'HASH' && ref($v2) eq 'HASH') { merge_hash($v1, $v2) }
else { $h1->{$k} = $v2 }
}
}
That's all. Now 'use MyApp {...}' will work. This is very useful to customize config in service(script)-based way without creating configuration for them in main config. For example to easily change log file or loglevel as in example above.
Also single-file overloading is also supported.
$config_group = ['.beta', 'service', 'maintenance'];
Loads *.beta, 'service.rw' and 'maintenance.rw'. I.e. group is filename without extension (loads filename plus '.rw')
Rewriting variables
'Rewrite' must be used when you want to overload some variable's value and you want all variables that depend on it to be recalculated.
For example if you write in myapp.conf:
$a = 1;
$b = $a+1;
and in myapp.dev:
$a = 10;
then (on dev server)
$cfg->{a}; #10
$cfg->{b}; #2
oops (!) :-)
'Rewrite' fixes that!
myapp.conf:
rw(a, 1);
$b = $a+1;
myapp.dev:
rewrite(a, 10);
$cfg->{a}; #10
$cfg->{b}; #11
Syntax
rw('variable_name', value_to_set);
Tells plugin that 'variable_name' is a rewritable variable. Also creates $variable_name and sets it to value_to_set. The effect is similar to
$variable_name = value_to_set;
but do not write that or rewrite will not work!
rewrite(' /uri/path | relative/path ', value_to_set);
Rewrites variable. Uri path can be absolute or relative to current namespace (namespace of the file where 'rewrite' is). It will croak if this variable is not marked as rewritable.
You can even rewrite properties of objects. Actually you may pass any code that is related to rewrite variable's value/properties to 'rewrite' function. Example:
myapp.conf:
rw('uri', URI->new("http://mysite.com/preved"));
$uri2 = URI->new($uri->as_string.'/medved');
myapp.dev:
rewrite('uri', sub { r('uri')->path('poka') });
Result:
$cfg->{uri}; # http://mysite.com/poka
$cfg->{uri2}; # http://mysite.com/poka/medved
Looks ok :-)
METHODS
- dev
-
Development server flag. $c->dev is true if current installation is development. Also available through $c->cfg->{dev}.
- cfg
-
Fast accessor for getting config hash. It is 70x faster than original ->config method.
- setup
-
Called by catalyst at setup phase. Reads files and initializes config.
- finalize_config
-
This method is called after the config file is loaded. It can be used to implement tuning of config values that can only be done at runtime.
This method has been added for compability.
Startup perfomance
It takes about 30ms to initialize config system with 25 files (25kb summary)
on 2Ghz Xeon.
SEE ALSO
Catalyst::Runtime, Catalyst::Plugin::ConfigLoader.
AUTHOR
Pronin Oleg <syber@cpan.org>
LICENSE
You may distribute this code under the same terms as Perl itself.