123456789_123456789_123456789_123456789_123456789_

Module: Mongoid::Interceptable

Relationships & Source Files
Extension / Inclusion / Inheritance Descendants
Included In:
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, ActiveSupport::Concern
Defined in: lib/mongoid/interceptable.rb

Overview

This module contains all the callback hooks for ::Mongoid.

Constant Summary

  • CALLBACKS =
    # File 'lib/mongoid/interceptable.rb', line 10
    [
      :after_build,
      :after_create,
      :after_destroy,
      :after_find,
      :after_initialize,
      :after_save,
      :after_touch,
      :after_update,
      :after_upsert,
      :after_validation,
      :around_create,
      :around_destroy,
      :around_save,
      :around_update,
      :around_upsert,
      :before_create,
      :before_destroy,
      :before_save,
      :before_update,
      :before_upsert,
      :before_validation,
    ].freeze

Instance Attribute Summary

Instance Method Summary

DSL Calls

included

[ GitHub ]


34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/mongoid/interceptable.rb', line 34

included do
  extend ActiveModel::Callbacks
  include ActiveModel::Validations::Callbacks

  define_model_callbacks :build, :find, :initialize, :touch, only: :after
  define_model_callbacks :create, :destroy, :save, :update, :upsert

  # This callback is used internally by Mongoid to save association
  # targets for referenced associations after the parent model is persisted.
  #
  # @api private
  define_model_callbacks :persist_parent

  define_callbacks :commit, :rollback,
                   only: :after,
                   scope: [:kind, :name]

  attr_accessor :before_callback_halted
end

Instance Attribute Details

#before_callback_halted?true | false (readonly, private)

This method is for internal use only.

We need to hook into this for autosave, since we don’t want it firing if the before callbacks were halted.

Examples:

Was a before callback halted?

document.before_callback_halted?

Returns:

  • (true | false)

    If a before callback was halted.

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 301

def before_callback_halted?
  !!@before_callback_halted
end

#pending_callbacksArray<Symbol> (rw)

This method is for internal use only.

Returns the stored callbacks to be executed later.

Returns:

  • (Array<Symbol>)

    Method symbols of the stored pending callbacks.

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 256

def pending_callbacks
  @pending_callbacks ||= [].to_set
end

#pending_callbacks=(value) ⇒ Array<Symbol> (rw)

This method is for internal use only.

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.

Parameters:

  • value (Array<Symbol>)

    Method symbols of the pending callbacks to store.

Returns:

  • (Array<Symbol>)

    Method symbols of the stored pending callbacks.

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 270

def pending_callbacks=(value)
  @pending_callbacks = value
end

Instance Method Details

#_mongoid_run_child_after_callbacks(callback_list: [])

Execute the after callbacks.

Parameters:

  • callback_list (Array<ActiveSupport::Callbacks::CallbackSequence, ActiveSupport::Callbacks::Filters::Environment>)

    List of pairs of callback sequence and environment.

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 244

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: [])

This method is for internal use only.

Execute the before callbacks of given kind for embedded documents.

Parameters:

  • kind (Symbol)

    The type of callback to execute.

  • children (Array<Document>)

    Children to execute callbacks on.

  • callback_list (Array<ActiveSupport::Callbacks::CallbackSequence, ActiveSupport::Callbacks::Filters::Environment>)

    List of pairs of callback sequence and environment. This list will be later used to execute after callbacks in reverse order.

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 220

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)

This method is for internal use only.

Run the callbacks for embedded documents.

Parameters:

  • kind (Symbol)

    The type of callback to execute.

  • children (Array<Document>)

    Children to execute callbacks on. If nil, callbacks will be executed on all cascadable children of the document.

[ GitHub ]

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

Note:

This method is prone to stack overflow errors if the document has a large number of embedded documents. It is recommended to avoid using around callbacks for embedded documents until a proper solution is implemented.

Execute the callbacks of given kind for embedded documents including around callbacks.

Parameters:

  • kind (Symbol)

    The type of callback to execute.

  • children (Array<Document>)

    Children to execute callbacks on. If nil, callbacks will be executed on all cascadable children of the document.

    @api private

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 175

def _mongoid_run_child_callbacks_with_around(kind, children: nil, &block)
  child, *tail = (children || cascadable_children(kind))
  with_children = !Mongoid::Config.prevent_multiple_calls_of_embedded_callbacks
  if child.nil?
    block&.call
  elsif tail.empty?
    child.run_callbacks(child_callback_type(kind, child), with_children: with_children, &block)
  else
    child.run_callbacks(child_callback_type(kind, child), with_children: with_children) do
      _mongoid_run_child_callbacks_with_around(kind, children: tail, &block)
    end
  end
end

#_mongoid_run_child_callbacks_without_around(kind, children: nil, &block)

This method is for internal use only.

Execute the callbacks of given kind for embedded documents without around callbacks.

Parameters:

  • kind (Symbol)

    The type of callback to execute.

  • children (Array<Document>)

    Children to execute callbacks on. If nil, callbacks will be executed on all cascadable children of the document.

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 198

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) ⇒ true | false

Is the provided type of callback executable by this document?

Examples:

Is the callback executable?

document.callback_executable?(:save)

Parameters:

  • kind (Symbol)

    The type of callback.

Returns:

  • (true | false)

    If the callback can be executed.

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 62

def callback_executable?(kind)
  respond_to?("_#{kind}_callbacks")
end

#cascadable_child?(kind, child, association) ⇒ true | false (private)

Determine if the child should fire the callback.

Examples:

Should the child fire the callback?

document.cascadable_child?(:update, doc)

Parameters:

  • kind (Symbol)

    The type of callback.

  • child (Document)

    The child document.

Returns:

  • (true | false)

    If the child should fire the callback.

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 341

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) ⇒ Array<Document> (private)

Get all the child embedded documents that are flagged as cascadable.

Examples:

Get all the cascading children.

document.cascadable_children(:update)

Parameters:

  • kind (Symbol)

    The type of callback.

Returns:

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 313

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) ⇒ Symbol (private)

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.

Examples:

Get the callback type.

document.child_callback_type(:update, doc)

Parameters:

  • kind (Symbol)

    The type of callback.

  • child (Document)

    The child document

Returns:

  • (Symbol)

    The name of the callback.

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 359

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) ⇒ ActiveSupport::Callbacks::CallbackSequence (private)

Compile the callback chain.

This method hides the differences between ::ActiveSupport implementations before and after 7.1.

Parameters:

  • chain (ActiveSupport::Callbacks::CallbackChain)

    The callback chain.

  • type (Symbol | nil) (defaults to: nil)

    The type of callback chain to compile.

Returns:

  • (ActiveSupport::Callbacks::CallbackSequence)

    The compiled callback sequence.

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 423

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) (private)

This method is for internal use only.

We need to hook into this for autosave, since we don’t want it firing if the before callbacks were halted.

Examples:

Hook into the halt.

document.halted_callback_hook(filter)

Parameters:

  • filter (Symbol)

    The callback that halted.

  • name (Symbol) (defaults to: nil)

    The name of the callback that was halted (requires ::Rails 6.1+)

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 380

def halted_callback_hook(filter, name = nil)
  @before_callback_halted = true
end

#in_callback_state?(kind) ⇒ true | false

Is the document currently in a state that could potentially require callbacks to be executed?

Examples:

Is the document in a callback state?

document.in_callback_state?(:update)

Parameters:

  • kind (Symbol)

    The callback kind.

Returns:

  • (true | false)

    If the document is in a callback state.

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 75

def in_callback_state?(kind)
  [ :create, :destroy ].include?(kind) || new_record? || flagged_for_destroy? || changed?
end

#run_after_callbacks(*kinds) ⇒ Object

Note:

::ActiveSupport does not allow this type of behavior by default, so ::Mongoid has to get around it and implement itself.

Run only the after callbacks for the specific event.

Examples:

Run only the after save callbacks.

model.run_after_callbacks(:save)

Parameters:

  • *kinds (Symbol...)

    The events that are occurring.

Returns:

  • (Object)

    The result of the chain executing.

[ GitHub ]

  
# 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) ⇒ Object

Note:

::ActiveSupport does not allow this type of behavior by default, so ::Mongoid has to get around it and implement itself.

Run only the before callbacks for the specific event.

Examples:

Run only the before save callbacks.

model.run_before_callbacks(:save, :create)

Parameters:

  • *kinds (Symbol...)

    The events that are occurring.

Returns:

  • (Object)

    The result of the chain executing.

[ GitHub ]

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

Run the callbacks for the document. This overrides active support’s functionality to cascade callbacks to embedded documents that have been flagged as such.

Examples:

Run the callbacks.

run_callbacks :save do
  save!
end

Parameters:

  • kind (Symbol)

    The type of callback to execute.

  • with_children (true | false)

    Flag specifies whether callbacks of embedded document should be run.

  • skip_if (Proc | nil)

    If this proc returns true, the callbacks will not be triggered, while the given block will be still called.

[ GitHub ]

  
# 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

This method is for internal use only.

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.

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 279

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

#run_targeted_callbacks(place, kind) ⇒ Object (private)

Run only the callbacks for the target location (before, after, around) and kind (save, update, create).

Examples:

Run the targeted callbacks.

model.run_targeted_callbacks(:before, :save)

Parameters:

  • place (Symbol)

    The time to run, :before, :after, :around.

  • kind (Symbol)

    The type of callback, :save, :create, :update.

Returns:

  • (Object)

    The result of the chain execution.

[ GitHub ]

  
# File 'lib/mongoid/interceptable.rb', line 394

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