59 votes

STI, un contrôleur

Je suis nouveau sur les rails et je suis un peu coincé avec ce problème de conception, qui pourrait être facile à résoudre, mais je n'arrive à rien : J'ai deux types d'annonces différentes : les faits marquants et les bonnes affaires. Les deux ont les mêmes attributs : titre, description et une image (avec trombone). Ils ont également le même type d'actions à appliquer sur eux : index, nouveau, modifier, créer, mettre à jour et détruire.

J'ai mis une STI comme ça :

Modèle d'annonce : ad.rb

class Ad < ActiveRecord::Base
end

Modèle Bargain : bargain.rb

class Bargain < Ad
end

Modèle de mise en évidence : highlight.rb

class Highlight < Ad
end

Le problème est que j'aimerais avoir un seul contrôleur ( AdsController ) qui exécute les actions que j'ai dites sur les bonnes affaires ou les faits marquants en fonction de l'URL, disons www.foo.com/bargains [/...] ou www.foo.com/highlights [/...].

Par exemple :

  • GET www.foo.com/highlights => une liste de toutes les annonces qui sont en surbrillance.
  • GET www.foo.com/highlights/new => formulaire pour créer une nouvelle mise en évidence etc...

Comment puis-je faire ça ?

Merci !

106voto

fl00r Points 41855

Premièrement. Ajoutez de nouvelles routes :

resources :highlights, :controller => "ads", :type => "Highlight"
resources :bargains, :controller => "ads", :type => "Bargain"

Et corriger certaines actions dans AdsController . Par exemple :

def new
  @ad = Ad.new()
  @ad.type = params[:type]
end

Pour une meilleure approche de ce travail de contrôleur, regardez ce commentaire

C'est tout. Maintenant vous pouvez aller à localhost:3000/highlights/new et de nouvelles Highlight sera initialisé.

L'action de l'index peut ressembler à ceci :

def index
  @ads = Ad.where(:type => params[:type])
end

Ir a localhost:3000/highlights et la liste des points forts apparaîtra.
De même pour les bonnes affaires : localhost:3000/bargains

etc.

URLS

<%= link_to 'index', :highlights %>
<%= link_to 'new', [:new, :highlight] %>
<%= link_to 'edit', [:edit, @ad] %>
<%= link_to 'destroy', @ad, :method => :delete %>

pour être polymorphe :)

<%= link_to 'index', @ad.class %>

0 votes

Oui !! Merci, ça marche super ! Je savais qu'il y avait un moyen de faire cela d'une manière DRY et RESTful. Le seul problème que j'ai maintenant est comment définir les chemins et les urls correctement, en fonction du type d'annonce. Je suppose que je peux le faire en vérifiant simplement la valeur du type, mais je ne trouve pas cela très intelligent... des idées ?

0 votes

Bon, j'ai fini, j'ai utilisé chemin_polymorphe pour configurer les chemins dans les vues ainsi que l'idée d'Alan (juste en dessous) d'utiliser la bonne classe en fonction de l'URL (highlights ou bargains)... Je ne suis pas entièrement convaincu, mais cela fonctionne et semble correct.

0 votes

Hmmm... je ne sais pas... quand vous avez un seul contrôleur qui utilise les mêmes vues, dans ce cas, ads_controller, vous ne devriez pas spécifier :highlights, ou :bargains. La façon dont je l'ai fait : link_to "new", new_polymorphic_path(@ad.class)

64voto

Alan Peabody Points 2280

Fl00r a une bonne solution, cependant je ferais un ajustement.

Cela peut être nécessaire ou non dans votre cas. Cela dépend du comportement qui change dans vos modèles STI, en particulier les validations et les crochets de cycle de vie.

Ajoutez une méthode privée à votre contrôleur pour convertir votre paramètre de type en la constante de classe réelle que vous souhaitez utiliser :

def ad_type
  params[:type].constantize
end

Ce qui précède est toutefois peu sûr. Ajoutez une liste blanche de types :

def ad_types
  [MyType, MyType2]
end

def ad_type
  params[:type].constantize if params[:type].in? ad_types
end

Pour en savoir plus sur la méthode de constanciation des rails, cliquez ici : http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-constantize

Ensuite, dans les actions du contrôleur, vous pouvez faire :

def new
  ad_type.new
end

def create
  ad_type.new(params)
  # ...
end

def index
  ad_type.all
end

Et maintenant vous utilisez la classe réelle avec le comportement correct au lieu de la classe parent avec le type d'attribut défini.

0 votes

Pour info, j'ai dû définir la méthode de création comme suit : ad_type.new(params[params[:type].downcase])

0 votes

Je pense que votre méthode devrait être mise à jour pour params[:type].underscore . Exemple : si votre type est BrandNewType alors vous devez vous référer à params['brand_new_type'] pas params['brandnewtype']

2 votes

N'utilisez pas cette méthode telle quelle dans un code de production. Cela n'est peut-être pas évident au premier abord, mais par défaut, cette méthode Ad#ad_type fait implicitement confiance aux données fournies par l'utilisateur, enfreignant ainsi une règle de base de la sécurité des applications Web. Imaginez une requête POST vers /ads?type=User avec une charge utile comprenant is_admin=1 (ou ce qui est applicable au mécanisme d'authentification utilisé). L'utilisateur attaquant dispose maintenant de son propre compte utilisateur avec des droits d'administrateur ! (Techniquement, ce vecteur d'attaque pourrait être contrecarré en modifiant ou en supprimant la valeur par défaut de l'option ads des routes, mais une nouvelle route plus tard pourrait involontairement ouvrir le trou à nouveau).

12voto

Andrew Lank Points 705

Je voulais simplement inclure ce lien car il y a un certain nombre d'astuces intéressantes toutes liées à ce sujet.

Alex Reisner - L'héritage d'une seule table dans Rails

0 votes

J'ai écrit un article sur STI dans Rails 3 basé en partie sur l'article de Reisner. Mon article traite de l'utilisation d'un contrôleur unique pour servir plusieurs classes STI. christopherbloom.com/2012/02/01/notes-on-sti-in-rails-3-0

3 votes

1voto

genkilabs Points 1565

Je sais que c'est une vieille question mais voici un modèle que j'aime bien et qui inclut les réponses de @flOOr et @Alan_Peabody. (Testé dans Rails 4.2, fonctionne probablement dans Rails 5)

Dans votre modèle, créez votre liste blanche au démarrage. Dans dev, elle doit être chargée de manière anticipée.

class Ad < ActiveRecord::Base
    Rails.application.eager_load! if Rails.env.development?
    TYPE_NAMES = self.subclasses.map(&:name)
    #You can add validation like the answer by @dankohn
end

Nous pouvons maintenant faire référence à cette liste blanche dans n'importe quel contrôleur pour construire la portée correcte, ainsi que dans une collection pour une sélection :type sur un formulaire, etc.

class AdsController < ApplicationController
    before_action :set_ad, :only => [:show, :compare, :edit, :update, :destroy]

    def new
        @ad = ad_scope.new
    end

    def create
        @ad = ad_scope.new(ad_params)
        #the usual stuff comes next...
    end

    private
    def set_ad
        #works as normal but we use our scope to ensure subclass
        @ad = ad_scope.find(params[:id])
    end

    #return the scope of a Ad STI subclass based on params[:type] or default to Ad
    def ad_scope
        #This could also be done in some kind of syntax that makes it more like a const.
        @ad_scope ||= params[:type].try(:in?, Ad::TYPE_NAMES) ? params[:type].constantize : Ad
    end

    #strong params check works as expected
    def ad_params
        params.require(:ad).permit({:foo})
    end
end

Nous devons gérer nos formulaires car le routage doit être envoyé au contrôleur de la classe de base, malgré le :type réel de l'objet. Pour ce faire, nous utilisons "becomes" pour tromper le constructeur de formulaires et lui faire adopter un routage correct, et la directive :as pour forcer les noms d'entrée à être également la classe de base. Cette combinaison nous permet d'utiliser des routes non modifiées (ressources :ads) ainsi que la vérification forte des params sur les params[:ad] qui reviennent du formulaire.

#/views/ads/_form.html.erb
<%= form_for(@ad.becomes(Ad), :as => :ad) do |f| %>

0voto

dankohn Points 6999

[Réécriture avec une solution plus simple qui fonctionne parfaitement :]

Après avoir étudié les autres réponses, j'ai trouvé la solution suivante pour un contrôleur unique avec Single Table Inheritance qui fonctionne bien avec les Strong Parameters dans Rails 4.1. Le simple fait d'inclure :type comme paramètre autorisé provoquait un problème de ActiveRecord::SubclassNotFound erreur si un type non valide est saisi. De plus, le type n'est pas mis à jour car la requête SQL recherche explicitement l'ancien type. Au lieu de cela, :type doit être mis à jour séparément avec update_column s'il est différent de ce qui est actuellement défini et s'il s'agit d'un type valide. Notez également que j'ai réussi à DRYer toutes les listes de types.

# app/models/company.rb
class Company < ActiveRecord::Base
  COMPANY_TYPES = %w[Publisher Buyer Printer Agent]
  validates :type, inclusion: { in: COMPANY_TYPES,
    :message => "must be one of: #{COMPANY_TYPES.join(', ')}" }
end

Company::COMPANY_TYPES.each do |company_type|
  string_to_eval = <<-heredoc
    class #{company_type} < Company
      def self.model_name  # http://stackoverflow.com/a/12762230/1935918
        Company.model_name
      end
    end
  heredoc
  eval(string_to_eval, TOPLEVEL_BINDING)
end

Et dans le contrôleur :

  # app/controllers/companies_controller.rb
  def update
    @company = Company.find(params[:id])

    # This separate step is required to change Single Table Inheritance types
    new_type = params[:company][:type]
    if new_type != @company.type && Company::COMPANY_TYPES.include?(new_type)
      @company.update_column :type, new_type
    end

    @company.update(company_params)
    respond_with(@company)
  end

Et les routes :

# config/routes.rb
Rails.application.routes.draw do
  resources :companies
  Company::COMPANY_TYPES.each do |company_type|
    resources company_type.underscore.to_sym, type: company_type, controller: 'companies', path: 'companies'
  end
  root 'companies#index'

Enfin, je recommande d'utiliser l'option intervenants et la configuration de scaffolding pour utiliser un responders_controller, qui est compatible avec STI. La configuration pour scaffolding est la suivante :

# config/application.rb
    config.generators do |g|
      g.scaffold_controller "responders_controller"
    end

0 votes

J'aime le validateur. Jetez un coup d'oeil à ma variante qui construit la liste blanche sur la base de la sous-classe, et ajuste les formulaires au lieu de nécessiter des routes pour chaque type. C'est bien si vous avez une tonne de routes imbriquées et ne voulez pas un énorme bloc de quasi-duplicata pour chacune des nombreuses sous-classes.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X