callbacks.rb 18.1 KB
Newer Older
1
require 'active_support/concern'
2
require 'active_support/descendants_tracker'
3
require 'active_support/core_ext/class/attribute'
4
require 'active_support/core_ext/kernel/reporting'
5
require 'active_support/core_ext/kernel/singleton_class'
6
require 'active_support/core_ext/object/inclusion'
7

8
module ActiveSupport
9 10 11 12 13
  # \Callbacks are code hooks that are run at key points in an object's lifecycle.
  # The typical use case is to have a base class define a set of callbacks relevant
  # to the other functionality it supplies, so that subclasses can install callbacks
  # that enhance or modify the base functionality without needing to override
  # or redefine methods of the base class.
14
  #
15 16 17 18
  # Mixing in this module allows you to define the events in the object's lifecycle
  # that will support callbacks (via +ClassMethods.define_callbacks+), set the instance
  # methods, procs, or callback objects to be called (via +ClassMethods.set_callback+),
  # and run the installed callbacks at the appropriate times (via +run_callbacks+).
19
  #
20 21 22 23 24
  # Three kinds of callbacks are supported: before callbacks, run before a certain event;
  # after callbacks, run after the event; and around callbacks, blocks that surround the
  # event, triggering it when they yield. Callback code can be contained in instance
  # methods, procs or lambdas, or callback objects that respond to certain predetermined
  # methods. See +ClassMethods.set_callback+ for details.
25
  #
26 27 28
  #   class Record
  #     include ActiveSupport::Callbacks
  #     define_callbacks :save
29 30
  #
  #     def save
31 32 33
  #       run_callbacks :save do
  #         puts "- save"
  #       end
34 35 36
  #     end
  #   end
  #
37
  #   class PersonRecord < Record
38
  #     set_callback :save, :before, :saving_message
39 40 41 42
  #     def saving_message
  #       puts "saving..."
  #     end
  #
43
  #     set_callback :save, :after do |object|
44 45 46 47
  #       puts "saved"
  #     end
  #   end
  #
48 49
  #   person = PersonRecord.new
  #   person.save
50 51 52 53 54
  #
  # Output:
  #   saving...
  #   - save
  #   saved
55
  module Callbacks
56
    extend Concern
57

58 59 60 61
    included do
      extend ActiveSupport::DescendantsTracker
    end

62 63 64 65 66 67 68 69 70 71 72
    # Runs the callbacks for the given event.
    #
    # Calls the before and around callbacks in the order they were set, yields
    # the block (if given one), and then runs the after callbacks in reverse order.
    #
    # If the callback chain was halted, returns +false+. Otherwise returns the result
    # of the block, or +true+ if no block is given.
    #
    #   run_callbacks :save do
    #     save
    #   end
73
    def run_callbacks(kind, &block)
74 75
      runner_name = self.class.__define_callbacks(kind, self)
      send(runner_name, &block)
76 77
    end

78 79 80
    private

    # A hook invoked everytime a before callback is halted.
A
Anatoly Makarevich 已提交
81
    # This can be overridden in AS::Callback implementors in order
82 83 84 85
    # to provide better debugging/logging.
    def halted_callback_hook(filter)
    end

86
    class Callback #:nodoc:#
87 88
      @@_callback_sequence = 0

89
      attr_accessor :chain, :filter, :kind, :options, :klass, :raw_filter
90 91 92

      def initialize(chain, filter, kind, options, klass)
        @chain, @kind, @klass = chain, kind, klass
93
        deprecate_per_key_option(options)
94 95 96 97
        normalize_options!(options)

        @raw_filter, @options = filter, options
        @filter               = _compile_filter(filter)
98
        recompile_options!
99 100
      end

101 102 103 104 105 106
      def deprecate_per_key_option(options)
        if options[:per_key]
          raise NotImplementedError, ":per_key option is no longer supported. Use generic :if and :unless options instead."
        end
      end

107 108 109 110 111 112 113 114
      def clone(chain, klass)
        obj                  = super()
        obj.chain            = chain
        obj.klass            = klass
        obj.options          = @options.dup
        obj.options[:if]     = @options[:if].dup
        obj.options[:unless] = @options[:unless].dup
        obj
115 116
      end

117
      def normalize_options!(options)
118 119
        options[:if] = Array(options[:if])
        options[:unless] = Array(options[:unless])
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
      end

      def name
        chain.name
      end

      def next_id
        @@_callback_sequence += 1
      end

      def matches?(_kind, _filter)
        @kind == _kind && @filter == _filter
      end

      def _update_filter(filter_options, new_options)
135 136
        filter_options[:if].concat(Array(new_options[:unless])) if new_options.key?(:unless)
        filter_options[:unless].concat(Array(new_options[:if])) if new_options.key?(:if)
137 138
      end

139 140
      def recompile!(_options)
        deprecate_per_key_option(_options)
141 142
        _update_filter(self.options, _options)

143
        recompile_options!
144 145
      end

B
Bogdan Gusiev 已提交
146
      # Wraps code with filter
147
      def apply(code)
P
Pavel Gorbokon 已提交
148 149
        case @kind
        when :before
150 151
          <<-RUBY_EVAL
            if !halted && #{@compiled_options}
152 153 154
              # This double assignment is to prevent warnings in 1.9.3 as
              # the `result` variable is not always used except if the
              # terminator code refers to it.
155
              result = result = #{@filter}
P
Pavel Gorbokon 已提交
156
              halted = (#{chain.config[:terminator]})
157 158 159
              if halted
                halted_callback_hook(#{@raw_filter.inspect.inspect})
              end
P
Pavel Gorbokon 已提交
160
            end
B
Bogdan Gusiev 已提交
161
            #{code}
P
Pavel Gorbokon 已提交
162 163
          RUBY_EVAL
        when :after
164
          <<-RUBY_EVAL
B
Bogdan Gusiev 已提交
165
          #{code}
166
          if #{!chain.config[:skip_after_callbacks_if_terminated] || "!halted"} && #{@compiled_options}
167 168 169
            #{@filter}
          end
          RUBY_EVAL
P
Pavel Gorbokon 已提交
170
        when :around
B
Bogdan Gusiev 已提交
171
          name = define_conditional_callback
172
          <<-RUBY_EVAL
B
Bogdan Gusiev 已提交
173 174
          #{name}(halted) do
            #{code}
175 176 177
            value
          end
          RUBY_EVAL
178
        end
179 180 181 182
      end

      private

B
Bogdan Gusiev 已提交
183 184 185
      # Compile around filters with conditions into proxy methods
      # that contain the conditions.
      #
186
      # For `set_callback :save, :around, :filter_name, :if => :condition':
B
Bogdan Gusiev 已提交
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
      #
      # def _conditional_callback_save_17
      #   if condition
      #     filter_name do
      #       yield self
      #     end
      #   else
      #     yield self
      #   end
      # end
      def define_conditional_callback
        name = "_conditional_callback_#{@kind}_#{next_id}"
        @klass.class_eval <<-RUBY_EVAL,  __FILE__, __LINE__ + 1
          def #{name}(halted)
           if #{@compiled_options} && !halted
             #{@filter} do
               yield self
             end
           else
             yield self
           end
         end
        RUBY_EVAL
        name
      end

213 214 215
      # Options support the same options as filters themselves (and support
      # symbols, string, procs, and objects), so compile a conditional
      # expression based on the options
216
      def recompile_options!
217
        conditions = ["true"]
218 219

        unless options[:if].empty?
220
          conditions << Array(_compile_filter(options[:if]))
221 222
        end

223
        unless options[:unless].empty?
224
          conditions << Array(_compile_filter(options[:unless])).map {|f| "!#{f}"}
225
        end
226

227
        @compiled_options = conditions.flatten.join(" && ")
228 229
      end

230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
      # Filters support:
      #
      #   Arrays::  Used in conditions. This is used to specify
      #             multiple conditions. Used internally to
      #             merge conditions from skip_* filters
      #   Symbols:: A method to call
      #   Strings:: Some content to evaluate
      #   Procs::   A proc to call with the object
      #   Objects:: An object with a before_foo method on it to call
      #
      # All of these objects are compiled into methods and handled
      # the same after this point:
      #
      #   Arrays::  Merged together into a single filter
      #   Symbols:: Already methods
      #   Strings:: class_eval'ed into methods
      #   Procs::   define_method'ed into methods
      #   Objects::
      #     a method is created that calls the before_foo method
      #     on the object.
      def _compile_filter(filter)
        method_name = "_callback_#{@kind}_#{next_id}"
        case filter
        when Array
          filter.map {|f| _compile_filter(f)}
        when Symbol
          filter
        when String
          "(#{filter})"
        when Proc
          @klass.send(:define_method, method_name, &filter)
          return method_name if filter.arity <= 0

          method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ")
264
        else
265 266 267
          @klass.send(:define_method, "#{method_name}_object") { filter }

          _normalize_legacy_filter(kind, filter)
268
          scopes = Array(chain.config[:scope])
269 270 271 272 273 274 275 276 277
          method_to_call = scopes.map{ |s| s.is_a?(Symbol) ? send(s) : s }.join("_")

          @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
            def #{method_name}(&blk)
              #{method_name}_object.send(:#{method_to_call}, self, &blk)
            end
          RUBY_EVAL

          method_name
278 279 280
        end
      end

281 282
      def _normalize_legacy_filter(kind, filter)
        if !filter.respond_to?(kind) && filter.respond_to?(:filter)
283 284 285
          filter.singleton_class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
            def #{kind}(context, &block) filter(context, &block) end
          RUBY_EVAL
286 287
        elsif filter.respond_to?(:before) && filter.respond_to?(:after) && kind == :around && !filter.respond_to?(:around)
          ActiveSupport::Deprecation.warn("Filter object with #before and #after methods is deprecated. Define #around method instead.")
288 289 290 291 292 293
          def filter.around(context)
            should_continue = before(context)
            yield if should_continue
            after(context)
          end
        end
294
      end
295
    end
296

297
    # An Array with a compile method
298
    class CallbackChain < Array #:nodoc:#
299 300 301 302 303 304 305 306
      attr_reader :name, :config

      def initialize(name, config)
        @name = name
        @config = {
          :terminator => "false",
          :scope => [ :kind ]
        }.merge(config)
307 308
      end

309
      def compile
310 311 312 313
        method = []
        method << "value = nil"
        method << "halted = false"

314
        callbacks = "value = !halted && (!block_given? || yield)"
315
        reverse_each do |callback|
316
          callbacks = callback.apply(callbacks)
317
        end
B
Bogdan Gusiev 已提交
318
        method << callbacks
319

320
        method << "value"
321
        method.join("\n")
B
Bogdan Gusiev 已提交
322
      end
323 324

    end
325

326
    module ClassMethods
327

328 329
      # This method defines callback chain method for the given kind
      # if it was not yet defined.
330
      # This generated method plays caching role.
331
      def __define_callbacks(kind, object) #:nodoc:
332
        name = __callback_runner_name(kind)
333
        unless object.respond_to?(name, true)
334
          str = object.send("_#{kind}_callbacks").compile
335
          class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
336
            def #{name}() #{str} end
337 338
            protected :#{name}
          RUBY_EVAL
339
        end
340
        name
341
      end
342

343 344 345 346 347 348 349 350 351
      def __reset_runner(symbol)
        name = __callback_runner_name(symbol)
        undef_method(name) if method_defined?(name)
      end

      def __callback_runner_name(kind)
        "_run__#{self.name.hash.abs}__#{kind}__callbacks"
      end

352 353 354 355
      # This is used internally to append, prepend and skip callbacks to the
      # CallbackChain.
      #
      def __update_callbacks(name, filters = [], block = nil) #:nodoc:
356
        type = filters.first.in?([:before, :after, :around]) ? filters.shift : :before
357 358
        options = filters.last.is_a?(Hash) ? filters.pop : {}
        filters.unshift(block) if block
359

360
        ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse.each do |target|
361
          chain = target.send("_#{name}_callbacks")
362
          yield target, chain.dup, type, filters, options
363
          target.__reset_runner(name)
364
        end
365 366
      end

367
      # Install a callback for the given event.
368 369 370
      #
      #   set_callback :save, :before, :before_meth
      #   set_callback :save, :after,  :after_meth, :if => :condition
371
      #   set_callback :save, :around, lambda { |r| stuff; result = yield; stuff }
372
      #
373 374 375 376
      # The second arguments indicates whether the callback is to be run +:before+,
      # +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This
      # means the first example above can also be written as:
      #
377 378
      #   set_callback :save, :before_meth
      #
379 380 381 382 383 384 385 386 387 388 389
      # The callback can specified as a symbol naming an instance method; as a proc,
      # lambda, or block; as a string to be instance evaluated; or as an object that
      # responds to a certain method determined by the <tt>:scope</tt> argument to
      # +define_callback+.
      #
      # If a proc, lambda, or block is given, its body is evaluated in the context
      # of the current object. It can also optionally accept the current object as
      # an argument.
      #
      # Before and around callbacks are called in the order that they are set; after
      # callbacks are called in the reverse order.
390 391 392
      # 
      # Around callbacks can access the return value from the event, if it
      # wasn't halted, from the +yield+ call.
393 394 395 396 397 398 399 400 401
      #
      # ===== Options
      #
      # * <tt>:if</tt> - A symbol naming an instance method or a proc; the callback
      #   will be called only when it returns a true value.
      # * <tt>:unless</tt> - A symbol naming an instance method or a proc; the callback
      #   will be called only when it returns a false value.
      # * <tt>:prepend</tt> - If true, the callback will be prepended to the existing
      #   chain rather than appended.
402
      def set_callback(name, *filter_list, &block)
403 404
        mapped = nil

405
        __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options|
406
          mapped ||= filters.map do |filter|
407 408 409
            Callback.new(chain, filter, type, options.dup, self)
          end

410
          filters.each do |filter|
411
            chain.delete_if {|c| c.matches?(type, filter) }
412 413
          end

414
          options[:prepend] ? chain.unshift(*(mapped.reverse)) : chain.push(*mapped)
415 416

          target.send("_#{name}_callbacks=", chain)
417 418 419
        end
      end

420 421
      # Skip a previously set callback. Like +set_callback+, <tt>:if</tt> or <tt>:unless</tt>
      # options may be passed in order to control when the callback is skipped.
422 423 424 425
      #
      #   class Writer < Person
      #      skip_callback :validate, :before, :check_membership, :if => lambda { self.age > 18 }
      #   end
426
      def skip_callback(name, *filter_list, &block)
427
        __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options|
428 429 430 431
          filters.each do |filter|
            filter = chain.find {|c| c.matches?(type, filter) }

            if filter && options.any?
432 433
              new_filter = filter.clone(chain, self)
              chain.insert(chain.index(filter), new_filter)
434
              new_filter.recompile!(options)
435
            end
436 437

            chain.delete(filter)
438
          end
439
          target.send("_#{name}_callbacks=", chain)
440 441 442
        end
      end

443
      # Remove all set callbacks for the given event.
444
      def reset_callbacks(symbol)
445
        callbacks = send("_#{symbol}_callbacks")
446

447
        ActiveSupport::DescendantsTracker.descendants(self).each do |target|
448
          chain = target.send("_#{symbol}_callbacks").dup
449
          callbacks.each { |c| chain.delete(c) }
450
          target.send("_#{symbol}_callbacks=", chain)
451
          target.__reset_runner(symbol)
452 453
        end

454
        self.send("_#{symbol}_callbacks=", callbacks.dup.clear)
455 456

        __reset_runner(symbol)
457 458
      end

459
      # Define sets of events in the object lifecycle that support callbacks.
460 461
      #
      #   define_callbacks :validate
462
      #   define_callbacks :initialize, :save, :destroy
463
      #
464
      # ===== Options
465
      #
466 467 468 469
      # * <tt>:terminator</tt> - Determines when a before filter will halt the callback
      #   chain, preventing following callbacks from being called and the event from being
      #   triggered. This is a string to be eval'ed. The result of the callback is available
      #   in the <tt>result</tt> variable.
470
      #
471
      #     define_callbacks :validate, :terminator => "result == false"
472
      #
473 474 475
      #   In this example, if any before validate callbacks returns +false+,
      #   other callbacks are not executed. Defaults to "false", meaning no value
      #   halts the chain.
476
      #
477 478 479 480 481
      # * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after callbacks should be terminated
      #   by the <tt>:terminator</tt> option. By default after callbacks executed no matter
      #   if callback chain was terminated or not.
      #   Option makes sence only when <tt>:terminator</tt> option is specified.
      #
482 483
      # * <tt>:scope</tt> - Indicates which methods should be executed when an object
      #   is used as a callback.
484
      #
485 486 487 488
      #     class Audit
      #       def before(caller)
      #         puts 'Audit: before is called'
      #       end
489
      #
490 491 492 493
      #       def before_save(caller)
      #         puts 'Audit: before_save is called'
      #       end
      #     end
494
      #
495 496
      #     class Account
      #       include ActiveSupport::Callbacks
497
      #
498 499
      #       define_callbacks :save
      #       set_callback :save, :before, Audit.new
500
      #
501 502 503 504 505 506
      #       def save
      #         run_callbacks :save do
      #           puts 'save in main'
      #         end
      #       end
      #     end
507
      #
508 509
      #   In the above case whenever you save an account the method <tt>Audit#before</tt> will
      #   be called. On the other hand
510
      #
511
      #     define_callbacks :save, :scope => [:kind, :name]
512
      #
513 514 515 516 517
      #   would trigger <tt>Audit#before_save</tt> instead. That's constructed by calling
      #   <tt>#{kind}_#{name}</tt> on the given instance. In this case "kind" is "before" and
      #   "name" is "save". In this context +:kind+ and +:name+ have special meanings: +:kind+
      #   refers to the kind of callback (before/after/around) and +:name+ refers to the
      #   method on which callbacks are being defined.
518
      #
519
      #   A declaration like
520
      #
521
      #     define_callbacks :save, :scope => [:name]
522
      #
523
      #   would call <tt>Audit#save</tt>.
524 525 526
      def define_callbacks(*callbacks)
        config = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
        callbacks.each do |callback|
527 528
          class_attribute "_#{callback}_callbacks"
          send("_#{callback}_callbacks=", CallbackChain.new(callback, config))
529 530
        end
      end
531 532 533
    end
  end
end