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 Slide

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

    View Slide

  3. View Slide

  4. lib

    View Slide

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

    View Slide

  6. init.rb

    View Slide

  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

    View Slide

  8. tasks
    [un]install.rb
    test
    generators

    View Slide

  9. Writing Plugins

    View Slide

  10. Sharing Code
    lib tasks

    View Slide

  11. Adding new behaviour

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

  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)

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  35. Class methods!
    class Person
    attr_reader :name
    end

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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)

    View Slide

  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)

    View Slide

  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)

    View Slide

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

    View Slide

  45. Person.extend(MyPlugin)

    View Slide

  46. ActiveRecord::Base.extend(MyPlugin)

    View Slide

  47. Extending Rails

    View Slide

  48. View Slide

  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.

    View Slide

  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)

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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?

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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'

    View Slide

  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)

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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)

    View Slide

  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

    View Slide

  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)

    View Slide

  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)

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  73. Thanks!
    lazyatom.com/plugins

    View Slide