2012 Feed

Configuration with OX

OX - 2012-12-16

OX has no built-in configuration management system. Instead, the design of OX makes it easy to roll your own. Typically configuration will be a layer outside of your application, such that your configuration initializes your application.

This article describes one opinionated way to structure your application's configuration using Config::JFDI. There are many like it, but this one is mine.

Site-Specific Configuration

As an application grows out of its prototype and early design phases, it becomes more and more important that it adapt to each environment it runs in. For example, you might need to start running the production database on a different machine just to spread the load around. So you change the dsn from localhost to db.example.com. Done deal!

Except now all the developers are testing their local changes against the production database. That's nuts!

What you really want is a configuration file for developers which specifies that their database lives on localhost, and a production configuration file which points to db.example.com. This approach scales to additional environments like a QA site, a continuous-integration machine, and so on. You might even have a second, unversioned config file where each developer can put config specific to their machine.

Implementation

MyApp::Config

The centerpiece of this opinionated design is an MyApp::Config which is a class that, unsurprisingly, manages the configuration of this application. This class is responsible for deciding which config file to load (production? development?) and keeping track of the contents of that file. Here's what it might look like:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 

 

package MyApp::Config;
use 5.16.0;
use Moose;
use Config::JFDI;

has filename => (
    is => 'ro',
    isa => 'Str',
    default => sub { "myapp.$ENV{MYAPP_CONFIG_SUFFIX}" },
);

has config => (
    is => 'ro',
    isa => 'Config::JFDI',
    lazy => 1,
    default => sub {
        my ($self) = @_;
        return Config::JFDI->new(
            name => $self->filename,
            path => 'config/',
        );
    },
);

sub get {
    my ($self, $key) = @_;
    return $self->config->config->{$key};
}

sub as_hash {
    my ($self) = @_;
    return $self->config->config;
}

 

The filename attribute looks at the MYAPP_CONFIG_SUFFIX environment variable to decide which config file to use. The suffix corresponds to the environment the application is in. For developers the suffix might be dev, and for production the suffix might be prod. This means that Config::JFDI will try to load config/myapp.dev.yml for developers and config/myapp.prod.yml in production. (Config::JFDI supports loading many different file formats, but for config I prefer YAML.)

I like providing get and as_hash methods in my config class to hide the implementation details of Config::JFDI as much as possible. Otherwise, pulling a setting's value out would involve calling $config->config->config->{dsn}, and that is an awful lot to ask of anybody. Instead, now $config->get("dsn") suffices.

config/myapp.*.yml

The config file is simply YAML. The developer config might look like:


1: 
2: 
3: 

 

---
dsn: 'mysql:database=myapp;host=localhost'
compile_templates: 0

 

The production config might look like:


1: 
2: 
3: 

 

---
dsn: 'mysql:database=myapp;host=db.example.com'
compile_templates: 1

 

No problem!

app.psgi

We have a config file and a config loader, but these aren't actually being used anywhere. OX certainly won't intuit that these components exist, since explicit declarations are The OX Way. So how do we use our config?

The answer is probably what you'd expect: you load config to use when instantiating your application. You simply just pass the config variables to MyApp->new(...). Your PSGI script would then look like this:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 

 

use MyApp;
use MyApp::Config;
my $config = MyApp::Config->new;

MyApp->new(
    dsn => $config->get('dsn'),
    compile_templates => $config->get('compile_templates'),
)->to_app;

 

Since you're probably going to end up adding many different options, you'll want that to have as little friction as possible. After all, that kind of code is a breeding ground for merge conflicts. Instead, just pass everything the config specifies into MyApp->new.


1: 
2: 
3: 

 

MyApp->new(
    %{ $config->as_hash },
);

 

MyApp

The final piece is making MyApp accept and use these new constructor parameters. Since MyApp uses Bread::Board::Declare you can create services for each of these parameters with the familiar "has" in Moose syntax.


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 

 

has dsn => (
    is => 'ro',
    isa => 'Str',
    required => 1,
);

has compile_templates => (
    is => 'ro',
    isa => 'Bool',
    required => 1,
);

 

Finally, using the magic of Bread::Board you can wire these new services into your existing database and view attributes.


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 

 

has database => (
    is => 'ro',
    dependencies => [qw[dsn]],
    block => sub {
        my ($s) = @_;
        my $dsn = $s->param('dsn');
        return DBI->connect($dsn);
    },
);

has view => (
    is => 'ro',
    dependencies => [qw[compile_templates]],
    block => sub {
        my ($s) = @_;
        my $compile_templates = $s->param('compile_templates');
        return ...
    },
);

 

Developer-Specific Configuration

One of the many benefits of using a system like Config::JFDI is that it supports overlays. An individual developer can adjust specific settings in a file called config/myapp.dev_local.yml which will be merged with the baseline config in config/myapp.dev.yml. Say one of the developers runs their MySQL on a different machine. That developer can put this into a config/myapp.dev_local.yml:


1: 
2: 

 

---
dsn: 'mysql:database=myapp;host=arrakis.local'

 

Now, when DBI connects to the database on this developer's machine, it will get the correct dsn. And it won't disrupt anyone else, because this config file is specific to that developer. Finally, since _local configs inherit settings from the baseline config file, options like compile_templates will trickle down from the ordinary developer config file.

This works especially well if you version config/myapp.dev.yml and put config/myapp.dev_local.yml into .gitignore. That way if a developer adds new baseline config to config/myapp.dev.yml, everyone gets it for free; no vigilance needed.

Gravatar Image This article contributed by: Shawn M Moore <shawn.moore@iinteractive.com>