The Dark Art of Rails Plugins

The Dark Art of Rails Plugins

(RubyFools 2008)

"Plugins are more than just shiny nuggets of code which rain down from the higher echelons of the Rails pantheon; they are loyal and useful servants that anyone can employ to help reuse code between applications. Without a little guidance, however, the process of figuring out just how to tame these beasts can be overwhelming.

"This presentation will help give developers the boost that's often required to get up to speed developing plugins. We'll cover the hooks that Rails' plugin mechanism makes available, and how to put them to best use in practice.

"Once we've covered the groundwork, we'll start to look at more advanced programming techniques for sharing code (and other files) between Rails applications. With a few key programming techniques under our belt, we can use plugins to alter and enhance the Rails framework itself, and become masters of Ruby's object model along the way."

Acd62030df551952268e84c8fff26a5b?s=128

lazyatom

April 03, 2008
Tweet

Transcript

  1. The Dark Art of Rails Plugins James Adam reevoo.com

  2. Anatomy of a plugin Photo: http://flickr.com/photos/guccibear2005/206352128/

  3. None
  4. lib

  5. lib • added to the $LOAD_PATH • classes auto-loaded via

    Dependencies magic • order determined by config.plugins
  6. init.rb

  7. init.rb • evaluated near the end of rails initialization •

    evaluated in order of config.plugins • special variables available • config, directory, name - see source of Rails::Plugin
  8. tasks [un]install.rb test generators

  9. Writing Plugins

  10. Sharing Code lib tasks

  11. Adding new behaviour

  12. What we’re aiming for class Person end class Programmer <

    Person end class ProjectManager < Person end p = Programmer.new p.hello # => "hi!" ProjectManager.is_friendly? # => true
  13. Modules module Friendly def hello "hi from #{self}" end end

  14. require 'friendly' class Person include Friendly end alice = Programmer.new

    alice.hello # => "hi from #<Programmer#123>"
  15. Methods in classes... class Person def hello "hi from #{self}"

    end end
  16. ... and in modules module Friendly def hello "hi from

    #{self}" end end
  17. Defining class methods... class Person def self.is_friendly? true end end

  18. ... and in modules? module Friendly def self.is_friendly? true end

    def hello "hi from #{self}" end end
  19. Not quite :( class Person include Friendly end Person.is_friendly? #

    ~> undefined method `is_friendly?' for Person:Class (NoMethodError)
  20. It’s all about self module Friendly def self.is_friendly? true end

    end Friendly.is_friendly? # => true
  21. Try this instead module Friendly::ClassMethods def is_friendly? true end end

    class Person extend Friendly::ClassMethods end Person.is_friendly? # => true
  22. Mixing in Modules class Person include AnyModule # adds to

    class definition end class Person extend AnyModule # adds to the object (self) end
  23. Some other ways: Person.instance_eval do def greetings "hello via \

    instance_eval" end end
  24. Some other ways: class << Person def salutations "hello via

    \ class << Person" end end
  25. module ActsAsFriendly module ClassMethods def is_friendly? true end end def

    hello "hi from #{self}!" end end Person.send(:include, ActsAsFriendly) Person.extend(ActsAsFriendly::ClassMethods)
  26. included module B def self.included(base) puts "B included into #{base}!"

    end end class A include B end # => "B included into A!"
  27. extended module B def self.extended(base) puts "#{base} extended by B!"

    end end class A extend B end # => "A extended by B!"
  28. module ActsAsFriendly def self.included(base) base.extend(ClassMethods) end module ClassMethods def is_friendly?

    true end end def hello "hi from #{self}!" end end Person.send(:include, ActsAsFriendly)
  29. module ActsAsFriendly def self.included(base) base.extend(ClassMethods) end module ClassMethods def is_friendly?

    true end end def hello "hi from #{self}!" end end Person.send(:include, ActsAsFriendly)
  30. class EvilBoss < Person end EvilBoss.is_friendly? # => true

  31. Showing restraint... • maybe we only want to apply it

    to particular classes • particularly if we’re going to change how the class behaves (see later...)
  32. ... using class methods • Ruby class definitions are code

    • So, has_many is a class method
  33. Class methods! class BlogPost < ActiveRecord::Base has_many :comments validates_presence_of :title

    end
  34. Class methods! class BlogController < ApplicationController before_filter :load_blog_posts end

  35. Class methods! class Person attr_reader :name end

  36. class Alpha puts self end # => Alpha Self in

    class definitions class SomeClass puts self end # >> SomeClass
  37. Calling methods class SomeClass def self.greetings "hello" end greetings end

    # >> hello
  38. module AbilityToFly def fly! true end # etc... end class

    Person def self.has_powers include AbilityToFly end end
  39. class Villain < Person end class Hero < Person has_powers

    end clark_kent = Hero.new clark_kent.fly! # => true lex_luthor = Villain.new lex_luthor.fly! # => NoMethodError
  40. Villain.has_powers lex.fly! # => true

  41. module MyPlugin def acts_as_friendly include MyPlugin::ActsAsFriendly end module ActsAsFriendly def

    self.included(base) base.extend(ClassMethods) end module ClassMethods def is_friendly? true end end def hello "hi from #{self}" end end # of ActsAsFriendly end # of MyPlugin Person.extend(MyPlugin)
  42. module MyPlugin def acts_as_friendly include MyPlugin::ActsAsFriendly end module ActsAsFriendly def

    self.included(base) base.extend(ClassMethods) end module ClassMethods def is_friendly? true end end def hello "hi from #{self}" end end # of ActsAsFriendly end # of MyPlugin Person.extend(MyPlugin)
  43. module MyPlugin def acts_as_friendly include MyPlugin::ActsAsFriendly end module ActsAsFriendly def

    self.included(base) base.extend(ClassMethods) end module ClassMethods def is_friendly? true end end def hello "hi from #{self}" end end # of ActsAsFriendly end # of MyPlugin Person.extend(MyPlugin)
  44. class Grouch < Person end oscar = Grouch.new oscar.hello #

    => NoMethodError class Hacker < Person acts_as_friendly end Hacker.is_friendly? # => true james = Hacker.new james.hello # => “hi from #<Hacker:0x123>”
  45. Person.extend(MyPlugin)

  46. ActiveRecord::Base.extend(MyPlugin)

  47. Extending Rails

  48. None
  49. acts_as_archivable • when a record is deleted, save a YAML

    version. Just in case. • It’s an odd example, but bear with me.
  50. Archivable Module module Archivable def archive_to_yaml File.open("#{id}.yml", 'w') do |f|

    f.write self.to_yaml end end end ActiveRecord::Base.send(:include, Archivable)
  51. Redefine via a module module Archivable def archive_to_yaml File.open("#{id}.yml") #

    ...etc... end def destroy # redefine destroy! connection.delete %{ DELETE FROM #{table_name} WHERE id = #{self.id} } archive_to_yaml end end
  52. Redefine via a module ActiveRecord::Base.send(:include, Archivable) class Thing < ActiveRecord::Base

    end t = Thing.find(:first) t.destroy # => no archive created :’(
  53. Redefining in the class class ActiveRecord::Base def destroy # Actually

    delete the record connection.delete %{ DELETE FROM #{table_name} WHERE id = #{self.id} } # call our new method archive_to_yaml end end
  54. ...it’s evil naughty • ties our new functionality to ActiveRecord,

    in this example • maybe we want to add this to DataMapper? Or Sequel? Or Ambition? •What if Rails changes?
  55. What we want • Our new behaviour should be triggered

    when destroy is called • The record should still be removed from the database • We shouldn’t need to reimplement the existing behaviour
  56. alias_method def hello "hi!" end alias_method :greetings, :hello greetings #

    => "hi!"
  57. alias_method alias_method :original_destroy, :destroy def new_destroy original_destroy archive_to_yaml end alias_method

    :destroy, :new_destroy
  58. module Archivable alias_method :original_destroy, :destroy def new_destroy original_destroy archive_to_yaml end

    alias_method :destroy, :new_destroy end # ~> undefined method `destroy' for module `Archivable'
  59. module Archivable def self.included(base) base.class_eval do alias_method :original_destroy, :destroy alias_method

    :destroy, :new_destroy end end def archive_to_yaml File.open("#{id}.yml") # ... end def new_destroy original_destroy archive_to_yaml end end ActiveRecord::Base.send(:include, Archivable)
  60. class Thing < ActiveRecord::Base end t = Thing.find(:first) t.destroy #

    => archive created!
  61. alias_method again alias_method :destroy_without_archiving, :destroy def destroy_with_archiving destroy_without_archiving # then

    add our new behaviour end alias_method :destroy, :destroy_with_archiving alias_method :destroy_without_archiving, :destroy def destroy_with_archiving destroy_without_archiving archive_to_yaml end alias_method :destroy, :destroy_with_archiving
  62. alias_method_chain def destroy_with_archiving destroy_without_archiving archive_to_yaml end alias_method_chain :destroy, :archiving

  63. module Archivable def self.included(base) base.class_eval do alias_method_chain :destroy, :archiving end

    end def archive_to_yaml File.open("#{id}.yml", "w") do |f| f.write self.to_yaml end end def destroy_with_archiving destroy_without_archiving archive_to_yaml end end ActiveRecord::Base.send(:include, Archivable)
  64. So adding up everything • use extend to add class

    method • include the new behaviour by including a module when class method is called • use alias_method_chain to wrap existing method
  65. module ActsAsArchivable def acts_as_archivable include ActsAsArchivable::Behaviour end module Behaviour def

    self.included(base) base.class_eval do alias_method_chain :destroy, :archiving end end def archive_to_yaml File.open("#{id}.yml") # ... end def destroy_with_archiving destroy_without_archiving archive_to_yaml end end end ActiveRecord::Base.extend(ActsAsArchivable)
  66. module ActsAsArchivable def acts_as_archivable include ActsAsArchivable::Behaviour alias_method_chain :destroy, :archiving end

    module Behaviour def archive_to_yaml File.open("#{id}.yml") # ... end def destroy_with_archiving destroy_without_archiving archive_to_yaml end end end ActiveRecord::Base.extend(ActsAsArchivable)
  67. module ActsAsArchivable def acts_as_archivable include ActsAsArchivable::Behaviour alias_method_chain :destroy, :archiving end

    module Behaviour def archive_to_yaml File.open("#{id}.yml") # ... end def destroy_with_archiving destroy_without_archiving archive_to_yaml end end end ActiveRecord::Base.extend(ActsAsArchivable)
  68. class Thing < ActiveRecord::Base end t1 = Thing.create! t1.destroy #

    => normal destroy called Thing.count # => 0 Dir["*.yml"] # => [] class PreciousThing < ActiveRecord::Base acts_as_archivable end t2 = PreciousThing.create! t2.destroy PreciousThing.count # => 0 Dir["*.yml"] # => ["1.yml"]
  69. Plugin Photo: http://flickr.com/photos/jreed/322057793/ Tips

  70. Package your code • ...in a module • domain name,

    nickname, quirk module Lazyatom module ActsAsHasselhoff # ... end end Tip #1
  71. Developing plugins • Dependencies.load_once_paths • config/environment/development.rb config.after_initialize do Dependencies.load_once_paths. delete_if

    do |path| path =~ /vendor\/plugins/ end end Tip #2
  72. Gems as plugins • coming in Rails 2.1 (it’s in

    r9101) • add rails/init.rb to your gem • require “rubygems” in environment.rb Tip #3
  73. Thanks! lazyatom.com/plugins