Sprinkling constants doth maketh a mad-man
You’ve a bunch of configuration constants in your application (email addresses, host addresses, etc) and you’d like a nice, cozy place to keep them, and a method of overriding the defaults for each environment.
There’s a million ways to skin this cat, but here’s the simple approach I took in my latest app.
...and yes, a constant that changes is not a constant, but I’ll pretend you didn’t say that.
1st update: I originally had a typo: the variables should be declared with @@, not @.
2nd update: It’s active_support, not activesupport!
Creatin tha module
Firstly, we’ll set up a new module to store our config, let’s call it MyAppConfig and it can live in RAILS_ROOT/config/my_app_config.rb. In it, we’ll create a standard Ruby module:
module MyApp
module Config
end
end
Hookin it in
To allow each environment to customise the config we’ll want to set this up before Rails::Initializer#run is called within environment.rb (see my previous article Environments and the Rails Initialisation Process for more info).
We’ll simply require the file after boot in our environment.rb:
require File.join(File.dirname(__FILE__), 'boot')
require 'my_app_config'
Rails::Initializer.run do |config|
# ...
Next we’ll add a configurable variable, with a default, to our module:
module MyApp
module Config
@@paypal_host = "https://www.sandbox.paypal.com"
end
end
This works just fine, apart from the fact you can’t access the instance variable outside of the module:
irb> MyApp::Config.paypal_host
NoMethodError: undefined method `paypal_host' for MyApp::Config:Module
To access and modify the instance variable we need a set of good ‘ol accessors. If this were a class, not a module, we’d sprinkle an attr_accessor and be on our way, but as this is a module we have to define the methods ourself.
module MyApp
module Config
@@paypal_host = "https://www.sandbox.paypal.com"
def self.paypal_host
@@paypal_host
end
def self.paypal_host=(paypal_host)
@@paypal_host = paypal_host
end
end
end
irb> MyApp::Config.paypal_host
=> "https://www.sandbox.paypal.com"
Sprinklin the sexy
Ok, so far it’s not looking too pretty is it? Don’t worry… good ‘ol ActiveSupport ships with a nice little helper, mattr_accessor, which provides exactly the same functionality as Ruby’s attr_accessor but for modules. Unfortunately the author chose for it a life of seclusion, with a tattoo of :nodoc:, but that’s never stopped us brave venturers before.
ActiveSupport isn’t loaded until the Rails::Initializer#run method is executed, so we’ll also need to require 'activesupport' before we can use mattr_accessor:
require 'active_support'
module MyApp
module Config
mattr_accessor :paypal_host
@@paypal_host = "https://www.sandbox.paypal.com"
end
end
So now we have our environment.rb requiring this module and we can access it anywhere in our Rails application with a simple MyApp::Config. paypal_host.
Overidin the defaults
To override config vars in different environments you can simply call its mattr_writer. For example, we’ll want to use the live paypal server in production, so we add the following line to the bottom of RAILS_ROOT/config/environments/production.rb:
MyApp::Config.paypal_host = "https://www.paypal.com"
and voila! That’s all there is to it.
So you wanna get funky
...and here’s some exercises for the reader (all of which are a purely academic pursuit I’d most probably not bother using):
- Extending
Modulewithmattr_accessor_with_default - Extending
Rails::Configto provide amy_appmethod, so instead ofMyApp::Config.paypal_hostin your environments you could just usemy_app.paypal_host - Implement
MyApp.config(&block)for block-style configuration
Archived comments
Comments were previously allowed on articles. Though no new comments are being accepted you can see the old comments below.
-
I dig this approach. In order to get an app up and running quickly I recently had to do this:
ssl_required :checkout, :process_payment if RAILS_ENV == ‘production’
Mostly because I just didn’t have time to get Apache proxying to my development mongrel for local SSL testing.
But after seeing this I’m thinking making SSL a configurable option would be a reusable approach since any of the three environments may or may not support SSL in any given scenario.
-
Gabe: Exactly. Not to mention if you added more environments, such as staging. In this app I used the config to specify what protocol to use for
default_url_optionsin myActionMailer’s:module TicketSystem module Config # Protocol used when generating URLs in ActionMailers mattr_accessor :mailer_protocol @@mailer_protocol = "http" end end -
And if you’re too lazy to repeat the attribute name, throw this in at the end:
module Config @@some_var="some value" class_variables.each do |v| mattr_accessor v[(2..-1)] end end -
mr max, dat, is sexy.
-
I’m getting an error trying to start my mongrel process because it is choking on the activesupport include line:
require 'activesupport'/usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:21:in `require__': no such file to load -- activesupport (LoadError) from /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:21:in `require' from /home/cjolicoeur/rp_badge_signup/trunk/config/../config/my_app_config.rb:1 from /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:21:in `require__' from /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:21:in `require' from /home/cjolicoeur/rp_badge_signup/trunk/config/environment.rb:19 from /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:21:in `require__' from /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:21:in `require' from /usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel/rails.rb:155:in `rails' ... 8 levels... from /usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/lib/mongrel/command.rb:211:in `run' from /usr/lib/ruby/gems/1.8/gems/mongrel-1.0.1/bin/mongrel_rails:243 from /usr/bin/mongrel_rails:18:in `load' from /usr/bin/mongrel_rails:18any thoughts?
-
Whoops sorry Dr J, that should read
require 'active_support' -
Thanks for this. It was very helpful!
-
Very helpful post Tim. The info here plus your “Environments and the Rails initialisation process” helped me my solve my config problems and that has made my day. Thanks very much.
-
I’ve tried this method, both with rails v1.2.3 and v2.0.2, and in both instances, my module is populated as expected with my first hit to a controller, but in all subsequent hits all the properties on the Module are nil’d our, or set to whatever is defined in the Module, essentially losing any “override” set in my specific environment file. Did you run into anything like this?
-
Found the issue; it’s due to the config.cache_classes=false within development.rb. The problem is that the Module containing the configuration is reloaded with every request, but the values set within development.rb are not. I really like your approach, but it seems difficult to use in development due to this issue. Would be interesting to see if somehow the Module could be cached, or if the the development configuration could be placed in some location that would cause it to be reloaded along with the Module itself, rather than declaring development configuration directly on the Module. Keep up the good work, very good information! :)
-
@Justin Winkler did you “require” the file from environment.rb? Rails 2 only reload files that have been auto-required, anything manually required shouldn’t be reloaded on every request.
-
You are correct sir! :) I was not aware that’s how rails reloaded classes. Thanks much!
-
Thanx. Works like a charm. Much appreciated!
-
Worked for me too, thanks. The n00b mistake I made was that my app has the same name as some of my models, and so when I followed the naming convention I ended up with several name conflicts. I resolved the conflict by naming the Modules something other than the Model name. Thanks!