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 8
    %i[
      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 ]


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

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: %i[kind name]

  attr_accessor :before_callback_halted
end

Instance Attribute Details

#before_callback_halted?true | false (readonly, private)

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 304

def before_callback_halted?
  !!@before_callback_halted
end

#pending_callbacksArray<Symbol> (rw)

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 259

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

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

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 273

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 247

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

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 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 ]
  end
  callback_list
end

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

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 148

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)

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 169

def _mongoid_run_child_callbacks_with_around(kind, children: nil, &block)
  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)

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 202

def _mongoid_run_child_callbacks_without_around(kind, children: nil, &block)
  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 60

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 346

def cascadable_child?(kind, child, association)
  return false if %i[initialize find touch].include?(kind)
  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 316

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 365

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 430

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)

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)

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

[ GitHub ]

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

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 73

def in_callback_state?(kind)
  %i[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 88

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 105

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 125

def run_callbacks(kind, with_children: true, skip_if: nil, &block)
  return block&.call if skip_if&.call

  if with_children
    cascadable_children(kind).each do |child|
      return false if child.run_callbacks(child_callback_type(kind, child), with_children: with_children) == false
    end
  end
  if callback_executable?(kind)
    super(kind, &block)
  else
    true
  end
end

#run_pending_callbacks

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 282

def run_pending_callbacks
  pending_callbacks.each do |cb|
    if %i[apply_defaults apply_post_processed_defaults].include?(cb)
      send(cb)
    else
      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 401

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