Evil Twin Plugin
Seriously, I think I have something against Rails’ lib directory. We jumped from keeping gems in lib to vendor/gems back in March. Then we jumped from keeping generic Rake tasks in lib/tasks to Sake. Now we’re gonna jump again.
Hacking Plugins
It’s really not that big of a deal, and pretty common—you want to change the behavior of some existing plugin. Maybe you Piston it and commit your changes. Sure. But maybe you just want to leave the original code alone.
A classic approach has been to stick some kind of hack in the lib directory. Issues abound, for sure. First: the load order. Who gets loaded first? Who reloads and who doesn’t? Second: location. You’ve got one bit of code messing with another bit of code in a totally separate location. Third: testing. Are you testing it? Maybe.
None of these things are deal breakers, but we can certainly address them. And we will.
The Evil Twin Plugin
Here’s the simple solution: create a plugin called whatever_hacks, where whatever is the name of the plugin you’re hacking. That’s it. An evil twin, if you will.
Adding the _hacks suffix ensures it will always be loaded after the target plugin (assuming you haven’t messed with the default plugin load order—alphabetical). Keeping it right next to the target plugin also ensures anyone who peers into vendor/plugins will instantly know tomfoolery is afoot.
You can now build out a tested, hack happy plugin. Or, y’know, just stick it all in init.rb. With caution.
Caution: init.rb
Caution: init.rb does not always do what you expect it to do. It’s loaded in the context of Rails::Plugin in 2.0 and Rails::Initializer in 1.2.5, not Object. Come again? Like this: re-opening existing classes isn’t as straightforward as elsewhere.
=> init.rb
class Hash end puts Hash.inspect
Guess what that prints. Ready?
$ ./script/runner Rails::Plugin::Hash
That’s right—we didn’t re-open Hash, we created a new Rails::Plugin::Hash class. Any methods we add in there won’t be added to Hash proper.
If we want to grab a real class and stuff some methods in it, we need to use class_eval or module_eval:
=> init.rb
Hash.class_eval do def duck_punched? true end end puts({}.duck_punched?)
As expected:
$ ./script/runner true
Doing it this way (class_eval) forces a constant lookup, making Ruby happily run up the chain and find the class or module in question.
attachment_fu_cropper
Okay, time for a real example. I wanted to change attachment_fu’s ImageScienceProcessor to crop thumbnails before resizing them. As this is a hack I use on all my apps, I also want to keep it out of my models. Hence, attachment_fu_hacks.
=> vendor/plugins/attachment_fu_hacks/init.rb
klass = Technoweenie::AttachmentFu::Processors::ImageScienceProcessor klass.module_eval do ## # Hacked to use image_science's #cropped_thumbnail method def resize_image(img, size) # create a dummy temp file to write to filename.sub! /gif$/, 'png' self.temp_path = write_to_temp_file(filename) grab_dimensions = lambda do |img| self.width = img.width if respond_to?(:width) self.height = img.height if respond_to?(:height) img.save temp_path callback_with_args :after_resize, img end size = size.first if size.is_a?(Array) && size.length == 1 if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum)) if size.is_a?(Fixnum) img.cropped_thumbnail(size, &grab_dimensions) else img.cropped_thumbnail(size.first, &grab_dimensions) end else new_size = [img.width, img.height].dim size.to_s img.cropped_thumbnail(new_size.first, &grab_dimensions) end end end
Works like a charm.
When heavysixer wanted to hack acts_as_taggable, he took the same approach: http://pastie.caboo.se/119904. Feel free to follow suit.


Recent comments
1 year 23 weeks ago
1 year 23 weeks ago
1 year 25 weeks ago
1 year 27 weeks ago
1 year 42 weeks ago
1 year 45 weeks ago
1 year 45 weeks ago
1 year 45 weeks ago
1 year 46 weeks ago
1 year 48 weeks ago