The Dark Art of Rails Plugins James Adam reevoo.com This could be - - PowerPoint PPT Presentation

the dark art
SMART_READER_LITE
LIVE PREVIEW

The Dark Art of Rails Plugins James Adam reevoo.com This could be - - PowerPoint PPT Presentation

The Dark Art of Rails Plugins James Adam reevoo.com This could be you! Im hacking ur Railz appz!!1! Photo: http://flickr.com/photos/toddhiestand/197704394/ Anatomy of a plugin Photo: http://flickr.com/photos/guccibear2005/206352128/ lib


slide-1
SLIDE 1

The Dark Art

  • f Rails Plugins

James Adam

reevoo.com

slide-2
SLIDE 2

This could be you!

Photo: http://flickr.com/photos/toddhiestand/197704394/

I’m hacking ur Railz appz!!1!

slide-3
SLIDE 3

Anatomy of a plugin

Photo: http://flickr.com/photos/guccibear2005/206352128/

slide-4
SLIDE 4
slide-5
SLIDE 5

lib

slide-6
SLIDE 6

lib

  • added to the $LOAD_PATH
  • Dependencies
  • order determined by config.plugins
slide-7
SLIDE 7

init.rb

slide-8
SLIDE 8

init.rb

  • evaluated near the end of rails initialization
  • evaluated in order of config.plugins
  • special variables available
  • config, directory, name - see source
  • f Rails::Plugin
slide-9
SLIDE 9

tasks [un]install.rb test generators

slide-10
SLIDE 10

Writing Plugins

slide-11
SLIDE 11

Sharing Code

lib tasks

slide-12
SLIDE 12

Enhancing Rails

slide-13
SLIDE 13
slide-14
SLIDE 14

Modules

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

slide-15
SLIDE 15

require 'friendly' class Person include Friendly end alice = Person.new alice.hello # => "hi from #<Person:0x27704>"

slide-16
SLIDE 16

require 'friendly' class Person end Person.send(:include, Friendly) alice = Person.new alice.hello # => "hi from #<Person:0x27678>"

slide-17
SLIDE 17

Defining class methods

class Person def self.is_friendly? true end end

slide-18
SLIDE 18

... and in modules?

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

slide-19
SLIDE 19

Not quite :(

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

slide-20
SLIDE 20

It’s all about self

module Friendly def self.is_friendly? true end end Friendly.is_friendly? # => true

slide-21
SLIDE 21

Try this instead

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

slide-22
SLIDE 22

Mixing in Modules

class Person include AnyModule # adds to class definition end class Person extend AnyModule # adds to the object (self) end

slide-23
SLIDE 23

Some other ways:

Person.instance_eval do def greetings "hello via \ instance_eval" end end

slide-24
SLIDE 24

Some other ways:

class << Person def salutations "hello via \ class << Person" end end

slide-25
SLIDE 25

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

slide-26
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!"

slide-27
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!"

slide-28
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 ActiveRecord::Base.send(:include, ActsAsFriendly)

slide-29
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 ActiveRecord::Base.send(:include, ActsAsFriendly)

slide-30
SLIDE 30

class Account < ActiveRecord::Base end Account.is_friendly? # => true

slide-31
SLIDE 31

Showing restraint...

  • every subclass gets the methods
  • maybe we only want to apply it to

particular classes

  • particularly if we’re going to change how

the class behaves (see later...)

slide-32
SLIDE 32

... using class methods

  • Ruby class definitions are code
  • So, has_many is a class method
slide-33
SLIDE 33

class Alpha puts self end # => Alpha

Self in class definitions

class SomeClass puts self end # >> SomeClass

slide-34
SLIDE 34

Calling methods

class SomeClass def self.greetings "hello" end puts greetings end # >> hello

slide-35
SLIDE 35

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

slide-36
SLIDE 36

class Hero < Person has_powers end class Villain < Person end clark_kent = Hero.new clark_kent.fly! # => true lex_luthor = Villain.new lex_luthor.fly! # => NoMethodError

slide-37
SLIDE 37

Villain.has_powers lex.fly! # => true

slide-38
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 ActiveRecord::Base.extend(MyPlugin)

slide-39
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 ActiveRecord::Base.extend(MyPlugin)

slide-40
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 ActiveRecord::Base.extend(MyPlugin)

slide-41
SLIDE 41

class Grouch < ActiveRecord::Base end

  • scar = Grouch.new
  • scar.hello # => NoMethodError

class Hacker < ActiveRecord::Base acts_as_friendly end Hacker.is_friendly? # => true james = Hacker.new james.hello # => “hi from #<Hacker:0x123>”

slide-42
SLIDE 42

Changing Behaviour

slide-43
SLIDE 43

acts_as_archivable

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

Archivable

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)

slide-45
SLIDE 45

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

slide-46
SLIDE 46

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

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

slide-48
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 :’(

slide-49
SLIDE 49

Some problems

  • We can’t redefine methods in a class by

simply including a module

  • We don’t want to lose the original method,

because often we want to call it as part of

  • ur new functionality
  • We don’t want to copy the original

implementation either

slide-50
SLIDE 50

What we’d like

  • add an archive method to AR objects
  • destroy should call the archive method
  • destroy should not lose its original

behaviour

  • anything we write should be in a module
  • it should be DRY
slide-51
SLIDE 51

alias_method

alias_method :original_destroy, :destroy def new_destroy

  • riginal_destroy

archive_to_yaml end alias_method :destroy, :new_destroy

slide-52
SLIDE 52

module Archivable alias_method :original_destroy, :destroy def new_destroy

  • riginal_destroy

archive_to_yaml end alias_method :destroy, :new_destroy end # ~> undefined method `destroy' for module `Archivable'

slide-53
SLIDE 53

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

  • riginal_destroy

archive_to_yaml end end ActiveRecord::Base.send(:include, Archivable)

slide-54
SLIDE 54

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

slide-55
SLIDE 55

But what about when some other plugin tries freak with destroy?

slide-56
SLIDE 56

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

slide-57
SLIDE 57

alias_method_chain

def destroy_with_archiving destroy_without_archiving archive_to_yaml end alias_method_chain :destroy, :archiving

slide-58
SLIDE 58

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)

slide-59
SLIDE 59

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

slide-60
SLIDE 60

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)

slide-61
SLIDE 61

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)

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

slide-63
SLIDE 63

class Thing < ActiveRecord::Base end t1 = Thing.create! t1.destroy # => normal destroy called Thing.count # => 0 class PreciousThing < ActiveRecord::Base acts_as_archivable end t2 = PreciousThing.create! t2.destroy PreciousThing.count # => 0 Dir["*.yml"] # => ["1.yml"]

slide-64
SLIDE 64

Plugin

Photo: http://flickr.com/photos/jreed/322057793/

T i p s

slide-65
SLIDE 65

Package your code

  • ...in a module
  • domain name, nickname, quirk

module Lazyatom module ActsAsHasselhoff # ... end end

T i p # 1

slide-66
SLIDE 66

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

T i p # 2

slide-67
SLIDE 67

Gems as plugins

  • coming in Rails 2.1 (it’s in r9101)
  • add rails/init.rb to your gem
  • require “rubygems” in environment.rb

T i p # 3

slide-68
SLIDE 68

Thanks!

lazyatom.com/plugins