module Mongoid::Interceptable

This module contains all the callback hooks for Mongoid.

Constants

CALLBACKS

Attributes

before_callback_halted[RW]

Public Instance Methods

_mongoid_run_child_after_callbacks(callback_list: []) click to toggle source

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
_mongoid_run_child_before_callbacks(kind, children: [], callback_list: []) click to toggle source

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
_mongoid_run_child_callbacks(kind, children: nil, &block) click to toggle source

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
_mongoid_run_child_callbacks_with_around(kind, children: nil, &block) click to toggle source

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
_mongoid_run_child_callbacks_without_around(kind, children: nil, &block) click to toggle source

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
callback_executable?(kind) click to toggle source

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
in_callback_state?(kind) click to toggle source

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
pending_callbacks() click to toggle source

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
pending_callbacks=(value) click to toggle source

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_after_callbacks(*kinds) click to toggle source

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_before_callbacks(*kinds) click to toggle source

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_callbacks(kind, with_children: true, skip_if: nil, &block) click to toggle source

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.
Calls superclass method
# 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_pending_callbacks() click to toggle source

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

before_callback_halted?() click to toggle source

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
cascadable_child?(kind, child, association) click to toggle source

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
cascadable_children(kind, children = Set.new) click to toggle source

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
child_callback_type(kind, child) click to toggle source

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_callbacks(chain, type = nil) click to toggle source

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
halted_callback_hook(filter, name = nil) click to toggle source

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_targeted_callbacks(place, kind) click to toggle source

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