Search code examples
ruby-on-railsrubycallbackactivesupportactioncontroller

Register a different type of callback in ApplicationController


I need more fine grain control on the order of callbacks in my controllers. Currently Rails only let you use append|prepend_before|after_action, but this is just extremely bad if you want to add a module with its dedicated callbacks.

I am trying to understand how AbstractController::Callbacks work, and I'm trying to register a new type of callback, that would be executed at a specific moment, taking advantage of Rail's controllers syntax for adding a callback (only/except + list of actions, etc.).

You can think of it as a custom Access Control feature, but this question isn't about access control, please refrain answerbombing with gems like Cancan.

class ApplicationController
  include xxx
  include MyModuleWithCallbacks
  include yyy
  ...
end

class MyController < ApplicationController
  prepend_before_action :something_before_my_callbacks
  my_callback_list :custom_callback, only: [:index, :show]
  before_action :something_after_my_callbacks
  # Goal : the order of above callbacks should NOT matter, my_callback does not depend on ActionController process_action callback list
end

module MyModuleWithCallbacks
  extend ActiveSupport::Concern
  extend AbstractController::Callbacks

  included do
    around_action :perform_if_condition

    def perform_if_condition
      run_callbacks :my_callback_list do 
        if my_callbacks_went_good?
          yield # And run the controller's before_callbacks
        else
          # log, render error page, etc.
        end
      end
    end

  # This is the hard part register the callback, I tried
  class_methods do
    define_method :my_callback_list do |*names, &blk|
      _insert_callbacks(names, blk) do |name, options|
        set_callback(:my_callback_list, :before, name, options)
      end
    end
  end

The current error is

undefined method `_my_callbacks_list_callbacks' for PublicController:Class

I am taking my inspiration from the source code of AbstractController::Callbacks but I'm not sure I understand what's going on there ^^"


Solution

  • I saw some upvotes so here is my current solution :

    With the example of a very lightweight Access control method, the original name of my_callback was access_control

    # controllers/concerns/access_control.rb
        module AccessControl
          extend ActiveSupport::Concern
          class_methods do
            define_method :my_callback do |*names, &blk|
              _insert_callbacks(names, blk) do |name, options|
                set_callback(:my_callback, :before, name, options)
              end
            end
          end
    
          included do
    
            define_callbacks :my_callback
    
            def perform_if_access_granted
              run_callbacks :my_callback do
                if @access_denied and not @access_authorized and not god_mode?
                  @request_authentication = true unless user_signed_in?
                  render(
                    file: File.join(Rails.root, 'app/views/errors/403.html'),
                    status: 403,
                    layout: 'error')
                else
                  yield
                end
              end
            end
    

    Then in your other controllers that include the module (again with Access control example)

    # controllers/your_controller.rb
    class YourController < SeekerController
      your_callback do
        set_entity
        allow_access_to_entity(@entity)
      end