Best practices to handle routes for STI subclasses in rails

后端 未结 18 1316
面向向阳花
面向向阳花 2020-11-30 16:25

My Rails views and controllers are littered with redirect_to, link_to, and form_for method calls. Sometimes link_to and <

18条回答
  •  执念已碎
    2020-11-30 17:03

    Ok, Ive had a ton of frustration in this area of Rails, and have arrived at the following approach, perhaps this will help others.

    Firstly be aware that a number of solutions above and around the net suggest using constantize on client provided parameters. This is a known DoS attack vector as Ruby does not garbage collect symbols, thus allowing an attacker to create arbitrary symbols and consume available memory.

    I've implemented the approach below which supports instantiation of model subclasses, and is SAFE from the contantize problem above. It is very similar to what rails 4 does, but also allows more than one level of subclassing (unlike Rails 4) and works in Rails 3.

    # initializers/acts_as_castable.rb
    module ActsAsCastable
      extend ActiveSupport::Concern
    
      module ClassMethods
    
        def new_with_cast(*args, &block)
          if (attrs = args.first).is_a?(Hash)
            if klass = descendant_class_from_attrs(attrs)
              return klass.new(*args, &block)
            end
          end
          new_without_cast(*args, &block)
        end
    
        def descendant_class_from_attrs(attrs)
          subclass_name = attrs.with_indifferent_access[inheritance_column]
          return nil if subclass_name.blank? || subclass_name == self.name
          unless subclass = descendants.detect { |sub| sub.name == subclass_name }
            raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
          end
          subclass
        end
    
        def acts_as_castable
          class << self
            alias_method_chain :new, :cast
          end
        end
      end
    end
    
    ActiveRecord::Base.send(:include, ActsAsCastable)
    

    After trying various approaches for the 'sublclass loading in devlopment issue' many similar to whats suggested above, I found the only thing that worked reliably was to use 'require_dependency' in my model classes. This ensures that class loading works properly in development and causes no issues in production. In development, without 'require_dependency' AR wont know about all subclasses, which impacts the SQL emitted for matching on the type column. In addition without 'require_dependency' you can also end up in a situation with multiple versions of the model classes at the same time! (eg. this can happen when you change a base or intermediate class, the sub-classes don't always seem to reload and are left subclassing from the old class)

    # contact.rb
    class Contact < ActiveRecord::Base
      acts_as_castable
    end
    
    require_dependency 'person'
    require_dependency 'organisation'
    

    I also don't override model_name as suggested above because I use I18n and need different strings for the attributes of different subclasses, eg :tax_identifier becomes 'ABN' for Organisation, and 'TFN' for Person (in Australia).

    I also use route mapping, as suggested above, setting the type:

    resources :person, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Person.sti_name } }
    resources :organisation, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Organisation.sti_name } }
    

    In addition to the route mapping, I'm using InheritedResources and SimpleForm and I use the following generic form wrapper for new actions:

    simple_form_for resource, as: resource_request_name, url: collection_url,
          html: { class: controller_name, multipart: true }
    

    ... and for edit actions:

    simple_form_for resource, as: resource_request_name, url: resource_url,
          html: { class: controller_name, multipart: true }
    

    And to make this work, in my base ResourceContoller I expose InheritedResource's resource_request_name as a helper method for the view:

    helper_method :resource_request_name 
    

    If you're not using InheritedResources, then use something like the following in your 'ResourceController':

    # controllers/resource_controller.rb
    class ResourceController < ApplicationController
    
    protected
      helper_method :resource
      helper_method :resource_url
      helper_method :collection_url
      helper_method :resource_request_name
    
      def resource
        @model
      end
    
      def resource_url
        polymorphic_path(@model)
      end
    
      def collection_url
        polymorphic_path(Model)
      end
    
      def resource_request_name
        ActiveModel::Naming.param_key(Model)
      end
    end
    

    Always happy to hear others experiences and improvements.

提交回复
热议问题