Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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."

lazyatom

April 03, 2008
Tweet

More Decks by lazyatom

Other Decks in Programming

Transcript

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

    View full-size slide

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

    View full-size slide

  3. lib
    • added to the $LOAD_PATH
    • classes auto-loaded via Dependencies
    magic
    • order determined by config.plugins

    View full-size slide

  4. 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

    View full-size slide

  5. tasks
    [un]install.rb
    test
    generators

    View full-size slide

  6. Writing Plugins

    View full-size slide

  7. Sharing Code
    lib tasks

    View full-size slide

  8. Adding new behaviour

    View full-size slide

  9. 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

    View full-size slide

  10. Modules
    module Friendly
    def hello
    "hi from #{self}"
    end
    end

    View full-size slide

  11. require 'friendly'
    class Person
    include Friendly
    end
    alice = Programmer.new
    alice.hello
    # => "hi from #"

    View full-size slide

  12. Methods in classes...
    class Person
    def hello
    "hi from #{self}"
    end
    end

    View full-size slide

  13. ... and in modules
    module Friendly
    def hello
    "hi from #{self}"
    end
    end

    View full-size slide

  14. Defining class methods...
    class Person
    def self.is_friendly?
    true
    end
    end

    View full-size slide

  15. ... and in modules?
    module Friendly
    def self.is_friendly?
    true
    end
    def hello
    "hi from #{self}"
    end
    end

    View full-size slide

  16. Not quite :(
    class Person
    include Friendly
    end
    Person.is_friendly?
    # ~> undefined method `is_friendly?'
    for Person:Class (NoMethodError)

    View full-size slide

  17. It’s all about self
    module Friendly
    def self.is_friendly?
    true
    end
    end
    Friendly.is_friendly? # => true

    View full-size slide

  18. Try this instead
    module Friendly::ClassMethods
    def is_friendly?
    true
    end
    end
    class Person
    extend Friendly::ClassMethods
    end
    Person.is_friendly? # => true

    View full-size slide

  19. Mixing in Modules
    class Person
    include AnyModule
    # adds to class definition
    end
    class Person
    extend AnyModule
    # adds to the object (self)
    end

    View full-size slide

  20. Some other ways:
    Person.instance_eval do
    def greetings
    "hello via \
    instance_eval"
    end
    end

    View full-size slide

  21. Some other ways:
    class << Person
    def salutations
    "hello via \
    class << Person"
    end
    end

    View full-size slide

  22. 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)

    View full-size slide

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

    View full-size slide

  24. extended
    module B
    def self.extended(base)
    puts "#{base} extended by B!"
    end
    end
    class A
    extend B
    end
    # => "A extended by B!"

    View full-size slide

  25. 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)

    View full-size slide

  26. 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)

    View full-size slide

  27. class EvilBoss < Person
    end
    EvilBoss.is_friendly? # => true

    View full-size slide

  28. 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...)

    View full-size slide

  29. ... using class methods
    • Ruby class definitions are code
    • So, has_many is a class method

    View full-size slide

  30. Class methods!
    class BlogPost < ActiveRecord::Base
    has_many :comments
    validates_presence_of :title
    end

    View full-size slide

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

    View full-size slide

  32. Class methods!
    class Person
    attr_reader :name
    end

    View full-size slide

  33. class Alpha
    puts self
    end
    # => Alpha
    Self in class definitions
    class SomeClass
    puts self
    end
    # >> SomeClass

    View full-size slide

  34. Calling methods
    class SomeClass
    def self.greetings
    "hello"
    end
    greetings
    end
    # >> hello

    View full-size slide

  35. module AbilityToFly
    def fly!
    true
    end
    # etc...
    end
    class Person
    def self.has_powers
    include AbilityToFly
    end
    end

    View full-size slide

  36. 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

    View full-size slide

  37. Villain.has_powers
    lex.fly! # => true

    View full-size slide

  38. 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)

    View full-size slide

  39. 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)

    View full-size slide

  40. 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)

    View full-size slide

  41. 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 #”

    View full-size slide

  42. Person.extend(MyPlugin)

    View full-size slide

  43. ActiveRecord::Base.extend(MyPlugin)

    View full-size slide

  44. Extending Rails

    View full-size slide

  45. acts_as_archivable
    • when a record is deleted, save a YAML
    version. Just in case.
    • It’s an odd example, but bear with me.

    View full-size slide

  46. 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)

    View full-size slide

  47. 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

    View full-size slide

  48. Redefine via a module
    ActiveRecord::Base.send(:include,
    Archivable)
    class Thing < ActiveRecord::Base
    end
    t = Thing.find(:first)
    t.destroy # => no archive created :’(

    View full-size slide

  49. 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

    View full-size slide

  50. ...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?

    View full-size slide

  51. 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

    View full-size slide

  52. alias_method
    def hello
    "hi!"
    end
    alias_method :greetings,
    :hello
    greetings # => "hi!"

    View full-size slide

  53. alias_method
    alias_method :original_destroy,
    :destroy
    def new_destroy
    original_destroy
    archive_to_yaml
    end
    alias_method :destroy,
    :new_destroy

    View full-size slide

  54. 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'

    View full-size slide

  55. 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)

    View full-size slide

  56. class Thing < ActiveRecord::Base
    end
    t = Thing.find(:first)
    t.destroy # => archive created!

    View full-size slide

  57. 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

    View full-size slide

  58. alias_method_chain
    def destroy_with_archiving
    destroy_without_archiving
    archive_to_yaml
    end
    alias_method_chain :destroy,
    :archiving

    View full-size slide

  59. 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)

    View full-size slide

  60. 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

    View full-size slide

  61. 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)

    View full-size slide

  62. 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)

    View full-size slide

  63. 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)

    View full-size slide

  64. 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"]

    View full-size slide

  65. Plugin
    Photo: http://flickr.com/photos/jreed/322057793/
    Tips

    View full-size slide

  66. Package your code
    • ...in a module
    • domain name, nickname, quirk
    module Lazyatom
    module ActsAsHasselhoff
    # ...
    end
    end
    Tip
    #1

    View full-size slide

  67. 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

    View full-size slide

  68. 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

    View full-size slide

  69. Thanks!
    lazyatom.com/plugins

    View full-size slide