Search code examples
ruby-on-railsrubymoduleprepend

Cannot use instance variables in prepended module


I want to be able to include my module in ActiveRecord::Base so that the has_folder_attachments method is available to my Rails AR classes.

I'm doing this to extend the original module's function to support AR hooks; however the variables @physical_path and @dice are both nil and I don't understand why.

module FolderAttachments
  module ClassMethods
    def has_folder_attachments(physical_path, excludes: [])
      @physical_path = physical_path
      super
    end 
  end  

  def self.prepended(base)
    class << base
      prepend ClassMethods
    end  
  end  

  attr_reader :physical_path
end

module ActiveRecord
  class Base
    prepend FolderAttachments

    attr_reader :dice

    # This should run after the module method
    def self.has_folder_attachments(*args)
      @dice = true
    end
  end
end

class Damned < ActiveRecord::Base
  has_folder_attachments :for_real
end

damn = Damned.new
puts damn.physical_path # => nil
puts damn.dice          # => nil

Solution

  • You are mixing instance and (meta)class context when using the two variables. Both variables are set their values in methods that are run in the class context (more precisely in the context of the metaclass). Thus, you cannot access these variables (and their attr_readers) in an instance context.

    For the attr_readers to work, you have to move them to the class context and access them from there:

    module FolderAttachments
      module ClassMethods
        ...
        attr_reader :physical_path
      end
    end
    
    module ActiveRecord
      class Base
        ...
        class << self
          attr_reader :dice
        end
      end
    end
    
    damn = Damned.new
    damn.class.physical_path # => :for_real
    damn.class.dice          # => true
    

    Or you may also add instance-level readers that delegate to the class-level readers so that you can access them also in instance context:

    module FolderAttachments
      module ClassMethods
        ...
        attr_reader :physical_path
      end
    
      def physical_path
        self.class.physical_path
      end
    end
    
    module ActiveRecord
      class Base
        ...
        class << self
          attr_reader :dice
        end
    
        def dice
         self.class.dice
        end
      end
    end
    
    damn = Damned.new
    damn.physical_path # => :for_real
    damn.dice          # => true