Introduction
When starting a Rails project, four golden folders are predefined: Models, Views, Controllers, Helpers. Could we possibly need anything more? In my experience, the answer is yes. This leads to the question of, where do these extra files go? Compared to models, views, controllers and helpers, Rails provides little guidance about where to put this other stuff. Fortunately, Rails comes packaged with two folders to put additional Ruby code: lib and config/initializers. Each have their unique properties, and they can be used together to neatly package your Ruby code.
The weakling lib folder
In Agile Web Development on Rails, the authors describe how to use a naming convention, such that your lib file names match the classes they contain. For example, a class of FooBar must be in the file foo_bar.rb. Classes can even be namespaced. For example, Foo::Bar will be loaded if placed inside of foo/bar.rb.
Files in lib are not loaded when Rails starts. Rails has overridden both Class.const_missing and Module.const_missing to dynamically load the file based on the class name. In fact, this is exactly how Rails loads your models and controllers.
While the functionality of the lib folder is nice, it is extremely limited. It prevents writing modules to extend or override the functionality of another class, because they will never get loaded.
The bloated initializers folder
Rails first loads the framework, then plugins, and finally, it loads the files inside of config/initializers (in alphabetic order, no less). Previous to 2.0, naughty developers pasted code at the bottom of environment.rb, and the initializer folder was a welcome convention to help organize this madness.
I find myself using the initializer folder to configure plugins such as HAML, or loading the action_mailer configuration out of a YML file.
Unfortunately, I see many code snippets showing how to extend the functionality of Rails simply by creating a new file in initializers, and pasting in the code. In a matter of fact, my last post suggested doing this. This is OK in light doses, but after a few months of this practice, you will find yourself with an initializers folder with many files and little structure, not to mention that they get loaded in alphabetic order.
Lib folder, meet initializers
I think I hurt lib and initializer's feelings. I feel bad about this, so maybe it is time to introduce them to each other. A simple example is in order:
We want to add a new 'user_logger' mixin to ActionController::Base. Any controller can choose to enable 'user_logger', and it will put the current user's name in the log file for each request they make. This won't be the last time we extend ActionController's functionality in this project, so let's do it the Right Way.
We first write the module. Start by creating a new folder inside of /lib called rails_extensions. Now paste this code inside the file user_logger.rb:
1 module ActionControllerExtra
2 module UserLogger
3 def self.included(base)
4 base.extend(ClassMethods)
5 end
6
7 module ClassMethods
8 # Logs that a user requested the current action:
9 #
10 # MyController < ApplicationController
11 # user_logger
12 # end
13 #
14 # The same options for ActionController::Filters are available. For example,
15 # to only log the user in the create and destroy actions:
16 #
17 # MyController < ApplicationController
18 # user_logger :only => [:create, :destroy]
19 # end
20 #
21 def user_logger(options = {})
22 include InstanceMethods
23 before_filter :log_user, options
24 end
25 end
26
27 module InstanceMethods
28 def log_user
29 logger.info("#{session[:username]} requested #{controller_name}/#{action_name}")
30 end
31 end
32 end
33 end
Next, create the file rails_extensions.rb in /config/initializers. Paste this code inside:
1 require 'rails_extensions/user_logger'
2
3 ActionController::Base.class_eval do
4 include ActionController::UserLogger
5 end
You may want to create an even deeper directory structure inside of rails_extensions. In my larger projects, I keep a folder for action_controller, active_record, etc. Nonetheless, using initializers and lib together can help you avoid a bloated initializer folder and weak set of functionality in the lib folder.
7 comments:
Nice post. I appreciate the explanation and examples. Thanks
Name the module RailsExtensions::UserLogger and you won't even have to bother loading it. It'll also properly reload in development mode.
You can also write a small plugin for this. That's what the plugin system was designed for, actually.
@rick - Renaming it to RailsExtensions::UserLogger would not be the same, because the controller would need to first include RailsExtensions::UserLogger before using the user_logger function.
The plug-in folder is certainly an alternative, and is probably what most developers already use. However, I like to keep my plugins generic and useful across Rails applications, whereas the lib folder is application specific.
Sorry to be nitpicky, but wouldn't your example of user logging be better achieved through the user of an observer:
http://api.rubyonrails.org/classes/ActiveRecord/Observer.html
@theo - The UserLogger example is completely arbitrary, and was only intended to show how files in the lib folder can be more than just simple classes.
In this case, we are simply tracking user requests, so it is appropriate to use a filter in the controller layer. If we wanted to track data changes, then an observer at the model layer is a much better choice.
One of the tricks I use is to set config.load_paths in environment.rb
Our application has a permissions definition module for each role (there are dozens) in our application. I could put these in the models or lib folders. But it's easy to create a new folder inside app, and:
config.load_paths += %W( #{RAILS_ROOT}/app/permissions )
I think in your last code snippet, we should read:
include ActionControllerExtra::UserLogger
Post a Comment