Search code examples
ruby-on-railsrubydeviseruby-on-rails-5

Best practise creating a shared method between devise SessionsController and RegistrationsController


There are 3 ways to get in my applications with an invite token:

  • You're already logged in
  • You have an account, but not logged in
  • You need to register

Now I'm interested how to handle the last 2 situations in combination with Devise without having to repeat the same functions.

Controller overrides are handled from the routes.rb:

devise_for :users, controllers: {
      sessions: 'users/sessions',
      registrations: 'users/registrations'
  }

Overriding the after_sign_in/up_path for Sessions and Regitrations:

class Users::SessionsController < Devise::SessionsController
  protected
  def after_sign_in_path(resource)
    handle_invite
    super(resource)
  end
end
class Users::RegistrationsController < Devise::RegistrationsController
  protected
  def after_sign_up_path_for(resource)
    handle_invite
    super(resource)
  end
end

Where should I place the handle_invite method?

I'm looking for a solution that I can put the method in my UsersController, because that seems to be the right place to put it.:

class UsersController < ApplicationController
  private
  def handle_invite
    # Some code getting the token and adding the user to a group
  end
end

I thought this should work, because it seems that Devise inherits this controller:

class Users::SessionsController < Devise::SessionsController; end
class Devise::SessionsController < DeviseController; end
class DeviseController < Devise.parent_controller.constantize; end

So I expected Devise.parent_controller.constantize to represent UsersController, but for some reason handle_invite can't be called from the child controllers.


Solution

  • If you want to use classical inheritance you would have to actually place that class in the inheritance chain by configuring Devise.parent_controller while not breaking the rest of the chain.

    Ruby does not have multiple inheritance. A class may only inherit from a single parent class.

    # config/initializers/devise.rb
    config.parent_controller = 'UsersController'
    
    class UsersController < DeviseController
      private
      def handle_invite
        # Some code getting the token and adding the user to a group
      end
    end
    

    But that's not really the best way to solve it since Ruby has horizontal inheritance through modules:

    # app/controllers/concerns/invitable.rb
    module Invitable
      private
    
      def handle_invite
        # Some code getting the token and adding the user to a group
      end
    
      def after_sign_in_path(resource)
        handle_invite
        super(resource)
      end
    end
    
    # Do not use the scope resolution operator (::) for namespace definition!
    # See https://github.com/rubocop-hq/ruby-style-guide#namespace-definition
    module Users
      class SessionsController < ::Devise::SessionsController
        include Invitable
      end 
    end
    
    module Users
      class RegistrationsController < ::Devise::SessionsController
        include Invitable
      end 
    end
    

    This is known as a module mixin. In other languages like Java or PHP it would be called a trait. The module encapsulates a set of methods that can be included in any class and you can also mix modules into other modules.

    In Rails lingo module mixins are called concerns - this really just wraps a convention that the app/controllers/concerns and app/models/concerns directories are added to the autoload roots. Which means that rails will look for constants in the top level namespace there.

    This is also loosely connected to ActiveSupport::Concern which is syntactic sugar for common ruby idioms. But there is no need to use it unless you're actually using its features.