module Mongoid::Interceptable
This module contains all the callback hooks for Mongoid
.
Constants
- CALLBACKS
Attributes
Public Instance Methods
Execute the after callbacks.
@param [ Array<ActiveSupport::Callbacks::CallbackSequence, ActiveSupport::Callbacks::Filters::Environment> ] callback_list List of
pairs of callback sequence and environment.
# File lib/mongoid/interceptable.rb, line 249 def _mongoid_run_child_after_callbacks(callback_list: []) callback_list.reverse_each do |next_sequence, env| next_sequence.invoke_after(env) return false if env.halted end end
Execute the before callbacks of given kind for embedded documents.
@param [ Symbol ] kind The type of callback to execute. @param [ Array<Document> ] children Children to execute callbacks on. @param [ Array<ActiveSupport::Callbacks::CallbackSequence, ActiveSupport::Callbacks::Filters::Environment> ] callback_list List of
pairs of callback sequence and environment. This list will be later used to execute after callbacks in reverse order.
@api private
# File lib/mongoid/interceptable.rb, line 225 def _mongoid_run_child_before_callbacks(kind, children: [], callback_list: []) children.each do |child| chain = child.__callbacks[child_callback_type(kind, child)] env = ActiveSupport::Callbacks::Filters::Environment.new(child, false, nil) next_sequence = compile_callbacks(chain) unless next_sequence.final? Mongoid.logger.warn("Around callbacks are disabled for embedded documents. Skipping around callbacks for #{child.class.name}.") Mongoid.logger.warn("To enable around callbacks for embedded documents, set Mongoid::Config.around_callbacks_for_embeds to true.") end next_sequence.invoke_before(env) return false if env.halted env.value = !env.halted callback_list << [next_sequence, env] if (grandchildren = child.send(:cascadable_children, kind)) _mongoid_run_child_before_callbacks(kind, children: grandchildren, callback_list: callback_list) end end callback_list end
Run the callbacks for embedded documents.
@param [ Symbol ] kind The type of callback to execute. @param [ Array<Document> ] children Children to execute callbacks on. If
nil, callbacks will be executed on all cascadable children of the document.
@api private
# File lib/mongoid/interceptable.rb, line 153 def _mongoid_run_child_callbacks(kind, children: nil, &block) if Mongoid::Config.around_callbacks_for_embeds _mongoid_run_child_callbacks_with_around(kind, children: children, &block) else _mongoid_run_child_callbacks_without_around(kind, children: children, &block) end end
Execute the callbacks of given kind for embedded documents including around callbacks.
@param [ Symbol ] kind The type of callback to execute. @param [ Array<Document> ] children Children to execute callbacks on. If
nil, callbacks will be executed on all cascadable children of the document. @api private
# File lib/mongoid/interceptable.rb, line 170 def _mongoid_run_child_callbacks_with_around(kind, children: nil, &block) children = (children || cascadable_children(kind)) with_children = !Mongoid::Config.prevent_multiple_calls_of_embedded_callbacks return block&.call if children.empty? fibers = children.map do |child| Fiber.new do child.run_callbacks(child_callback_type(kind, child), with_children: with_children) do Fiber.yield end end end fibers.each do |fiber| fiber.resume raise Mongoid::Errors::InvalidAroundCallback unless fiber.alive? end block&.call fibers.reverse.each(&:resume) end
Execute the callbacks of given kind for embedded documents without around callbacks.
@param [ Symbol ] kind The type of callback to execute. @param [ Array<Document> ] children Children to execute callbacks on. If
nil, callbacks will be executed on all cascadable children of the document.
@api private
# File lib/mongoid/interceptable.rb, line 203 def _mongoid_run_child_callbacks_without_around(kind, children: nil, &block) children = (children || cascadable_children(kind)) callback_list = _mongoid_run_child_before_callbacks(kind, children: children) return false if callback_list == false value = block&.call callback_list.each do |_next_sequence, env| env.value &&= value end return false if _mongoid_run_child_after_callbacks(callback_list: callback_list) == false value end
Is the provided type of callback executable by this document?
@example Is the callback executable?
document.callback_executable?(:save)
@param [ Symbol ] kind The type of callback.
@return [ true | false ] If the callback can be executed.
# File lib/mongoid/interceptable.rb, line 62 def callback_executable?(kind) respond_to?("_#{kind}_callbacks") end
Is the document currently in a state that could potentially require callbacks to be executed?
@example Is the document in a callback state?
document.in_callback_state?(:update)
@param [ Symbol ] kind The callback kind.
@return [ true | false ] If the document is in a callback state.
# File lib/mongoid/interceptable.rb, line 75 def in_callback_state?(kind) [ :create, :destroy ].include?(kind) || new_record? || flagged_for_destroy? || changed? end
Returns the stored callbacks to be executed later.
@return [ Array<Symbol> ] Method symbols of the stored pending callbacks.
@api private
# File lib/mongoid/interceptable.rb, line 261 def pending_callbacks @pending_callbacks ||= [].to_set end
Stores callbacks to be executed later. A good use case for this is delaying the after_find and after_initialize callbacks until the associations are set on the document. This can also be used to delay applying the defaults on a document.
@param [ Array<Symbol> ] value Method symbols of the pending callbacks to store.
@return [ Array<Symbol> ] Method symbols of the stored pending callbacks.
@api private
# File lib/mongoid/interceptable.rb, line 275 def pending_callbacks=(value) @pending_callbacks = value end
Run only the after callbacks for the specific event.
@note ActiveSupport does not allow this type of behavior by default, so
Mongoid has to get around it and implement itself.
@example Run only the after save callbacks.
model.run_after_callbacks(:save)
@param [ Symbol… ] *kinds The events that are occurring.
@return [ Object
] The result of the chain executing.
# File lib/mongoid/interceptable.rb, line 90 def run_after_callbacks(*kinds) kinds.each do |kind| run_targeted_callbacks(:after, kind) end end
Run only the before callbacks for the specific event.
@note ActiveSupport does not allow this type of behavior by default, so
Mongoid has to get around it and implement itself.
@example Run only the before save callbacks.
model.run_before_callbacks(:save, :create)
@param [ Symbol… ] *kinds The events that are occurring.
@return [ Object
] The result of the chain executing.
# File lib/mongoid/interceptable.rb, line 107 def run_before_callbacks(*kinds) kinds.each do |kind| run_targeted_callbacks(:before, kind) end end
Run the callbacks for the document. This overrides active support’s functionality to cascade callbacks to embedded documents that have been flagged as such.
@example Run the callbacks.
run_callbacks :save do save! end
@param [ Symbol ] kind The type of callback to execute. @param [ true | false ] with_children Flag specifies whether callbacks
of embedded document should be run.
@param [ Proc | nil ] skip_if If this proc returns true, the callbacks
will not be triggered, while the given block will be still called.
# File lib/mongoid/interceptable.rb, line 127 def run_callbacks(kind, with_children: true, skip_if: nil, &block) if skip_if&.call return block&.call end if with_children cascadable_children(kind).each do |child| if child.run_callbacks(child_callback_type(kind, child), with_children: with_children) == false return false end end end if callback_executable?(kind) super(kind, &block) else true end end
Run the pending callbacks. If the callback is :apply_defaults, we will apply the defaults for this document. Otherwise, the callback is passed to the run_callbacks
function.
@api private
# File lib/mongoid/interceptable.rb, line 284 def run_pending_callbacks pending_callbacks.each do |cb| if [:apply_defaults, :apply_post_processed_defaults].include?(cb) send(cb) else self.run_callbacks(cb, with_children: false) end end pending_callbacks.clear end
Private Instance Methods
We need to hook into this for autosave, since we don’t want it firing if the before callbacks were halted.
@api private
@example Was a before callback halted?
document.before_callback_halted?
@return [ true | false ] If a before callback was halted.
# File lib/mongoid/interceptable.rb, line 306 def before_callback_halted? !!@before_callback_halted end
Determine if the child should fire the callback.
@example Should the child fire the callback?
document.cascadable_child?(:update, doc)
@param [ Symbol ] kind The type of callback. @param [ Document
] child The child document.
@return [ true | false ] If the child should fire the callback.
# File lib/mongoid/interceptable.rb, line 346 def cascadable_child?(kind, child, association) return false if kind == :initialize || kind == :find || kind == :touch return false if kind == :validate && association.validate? child.callback_executable?(kind) ? child.in_callback_state?(kind) : false end
Get all the child embedded documents that are flagged as cascadable.
@example Get all the cascading children.
document.cascadable_children(:update)
@param [ Symbol ] kind The type of callback.
@return [ Array<Document> ] The children.
# File lib/mongoid/interceptable.rb, line 318 def cascadable_children(kind, children = Set.new) embedded_relations.each_pair do |name, association| next unless association.cascading_callbacks? without_autobuild do delayed_pulls = delayed_atomic_pulls[name] delayed_unsets = delayed_atomic_unsets[name] children.merge(delayed_pulls) if delayed_pulls children.merge(delayed_unsets) if delayed_unsets relation = send(name) Array.wrap(relation).each do |child| next if children.include?(child) children.add(child) if cascadable_child?(kind, child, association) child.send(:cascadable_children, kind, children) end end end children.to_a end
Get the name of the callback that the child should fire. This changes depending on whether or not the child is new. A persisted parent with a new child would fire :update from the parent, but needs to fire :create on the child.
@example Get the callback type.
document.child_callback_type(:update, doc)
@param [ Symbol ] kind The type of callback. @param [ Document
] child The child document
@return [ Symbol ] The name of the callback.
# File lib/mongoid/interceptable.rb, line 364 def child_callback_type(kind, child) if kind == :update return :create if child.new_record? return :destroy if child.flagged_for_destroy? kind else kind end end
Compile the callback chain.
This method hides the differences between ActiveSupport implementations before and after 7.1.
@param [ ActiveSupport::Callbacks::CallbackChain ] chain The callback chain. @param [ Symbol | nil ] type The type of callback chain to compile.
@return [ ActiveSupport::Callbacks::CallbackSequence ] The compiled callback sequence.
# File lib/mongoid/interceptable.rb, line 428 def compile_callbacks(chain, type = nil) if chain.method(:compile).arity == 0 # ActiveSupport < 7.1 chain.compile else # ActiveSupport >= 7.1 chain.compile(type) end end
We need to hook into this for autosave, since we don’t want it firing if the before callbacks were halted.
@api private
@example Hook into the halt.
document.halted_callback_hook(filter)
@param [ Symbol ] filter The callback that halted. @param [ Symbol ] name The name of the callback that was halted
(requires Rails 6.1+)
# File lib/mongoid/interceptable.rb, line 385 def halted_callback_hook(filter, name = nil) @before_callback_halted = true end
Run only the callbacks for the target location (before, after, around) and kind (save, update, create).
@example Run the targeted callbacks.
model.run_targeted_callbacks(:before, :save)
@param [ Symbol ] place The time to run, :before, :after, :around. @param [ Symbol ] kind The type of callback, :save, :create, :update.
@return [ Object
] The result of the chain execution.
# File lib/mongoid/interceptable.rb, line 399 def run_targeted_callbacks(place, kind) name = "_run__#{place}__#{kind}__callbacks" unless respond_to?(name) chain = ActiveSupport::Callbacks::CallbackChain.new(name, {}) send("_#{kind}_callbacks").each do |callback| chain.append(callback) if callback.kind == place end self.class.send :define_method, name do env = ActiveSupport::Callbacks::Filters::Environment.new(self, false, nil) sequence = compile_callbacks(chain) sequence.invoke_before(env) env.value = !env.halted sequence.invoke_after(env) env.value end self.class.send :protected, name end send(name) end