Remplacer les méthodes Discourse existantes dans les plugins

J’ai récemment rencontré de nombreuses situations où il fallait remplacer des méthodes Ruby existantes provenant de plugins, et j’ai pensé partager mes meilleures pratiques ici.

Remplacement d’une méthode d’instance

class ::TopicQuery
  module BabbleDefaultResults
    def default_results(options={})
      super(options).where('archetype <> ?', Archetype.chat)
    end
  end
  prepend BabbleDefaultResults
end
  • Ici, je supprime les sujets de discussion d’une méthode d’instance qui retourne une liste de sujets.
  • Le nom du module BabbleDefaultResults peut être celui que vous souhaitez ; je le fais généralement correspondre au nom de la méthode, plus le nom de mon plugin, pour minimiser les risques de conflit de noms (bien qu’ils soient déjà très faibles).
  • Module#prepend est super cool et vous devriez le connaître si vous écrivez des plugins pour n’importe quoi en Ruby. Notez que c’est le fait que nous préposons un module qui nous permet d’appeler super à l’intérieur de la méthode de remplacement.
  • PS, appelez toujours super ! Cela rend votre plugin beaucoup moins susceptible de casser lorsque l’implémentation sous-jacente change. À moins que vous ne soyez vraiment, vraiment sûr que votre fonctionnalité remplace tout dans la méthode sous-jacente, vous voulez appeler super et modifier les résultats à partir de là, afin que les modifications de cette méthode dans le cœur de Discourse ne fassent pas casser votre plugin plus tard.
  • Le :: dans ::TopicQuery garantit que je me réfère à la classe TopicQuery de niveau supérieur à remplacer, et non à une version modularisée de celle-ci (comme Babble::TopicQuery).
  • Cela peut être ajouté directement dans plugin.rb tel quel, ou si votre plugin est volumineux, vous pouvez envisager de séparer chaque remplacement dans un fichier distinct.

Remplacement d’une méthode de classe

class ::Topic
  module BabbleForDigest
    def for_digest(user)
      super(user).where('archetype <> ?', Archetype.chat)
    end
  end
  singleton_class.prepend BabbleForDigest
end
  • Ici, je prends une méthode self.for_digest existante sur la classe Topic et je supprime les sujets de discussion du résultat.
  • Très similaire au remplacement de méthode d’instance, notez la différence : nous appelons singleton_class.prepend au lieu de simplement prepend. singleton_class est une façon légèrement étrange de dire « je veux ajouter cela au niveau de la classe, pas au niveau de l’instance », lecture complémentaire si vous cherchez un trou de lapin lié à Ruby.

Remplacement d’une portée (scope)

class ::Topic
  @@babble_listable_topics = method(:listable_topics).clone
  scope :listable_topics, ->(user) {
    @@babble_listable_topics.call(user).where('archetype <> ?', Archetype.chat)
  }
end
  • Celui-ci est un peu délicat car les portées ne fonctionnent pas bien avec super (ou, du moins, je n’ai pas réussi à les faire fonctionner). Au lieu de cela, nous prenons une définition de méthode existante, la clonons, la stockons, puis l’appelons plus tard.
  • Encore une fois, @@babble_listable_topics peut être n’importe quoi, mais utiliser le nom de votre plugin comme espace de noms est probablement une bonne idée.
  • Plus d’informations sur la fonction method, qui est aussi super cool, bien que les moments où vous en auriez vraiment besoin soient assez rares. Bonus : fait amusant gratuit lié à cela ; lors du débogage, si vous avez du mal à comprendre quel code est exécuté pour un appel de méthode particulier (généralement « Quelle gemme définit cette méthode ? »), vous pouvez utiliser source_location pour obtenir la ligne exacte de code source où la méthode est définie.
[7] pry(main)> Topic.new.method(:best_post).source_location
=> ["/Users/gdpelican/workspace/discourse/app/models/topic.rb", 282]

( ^^ cela indique que la méthode best_post sur un nouveau sujet est définie dans /app/models/topic.rb, à la ligne 282)

Bon, c’est tout ce que j’ai. Faites-moi savoir si je dois corriger, développer ou clarifier quoi que ce soit :slight_smile:

35 « J'aime »

This is great, thanks for sharing James!

Overriding instance and class methods

My fav resource on this is: https://stackoverflow.com/a/4471202

I use basically the same structure, except I tend to seperate out the module and the prepend.

As you pointed out, this pattern is “super” :wink: useful when trying to avoid overriding core logic.

module InviteMailerEventExtension
  def send_invite(invite)
     ## stuff
     super(invite)
  end
end

require_dependency 'invite_mailer'
class ::InviteMailer
  prepend InviteMailerEventExtension
end

One small tip here is that when overriding private or protected methods, your overriding method also needs to be private or protected, e.g.

module UserNotificationsEventExtension
  protected def send_notification_email(opts)
    ## stuff
    super(opts)
  end
end
20 « J'aime »

@angus @gdpelican Thanks for this. This is great stuff. :smiley: . This would be really essential all the (especially newbies like me) plugin developers out there.

This is what I really really needed to be aware of. I use to think that if you override a method only to make a few changes to it, you’d probably copy the code to your new method and make changes to it which by the very thought of it sounded hacky.

3 « J'aime »

Hi, I was wondering about this part of the code? why is this require_dependency needed? It seems like the code is working without it as well.

Yes, indeed. Since that post, updates to Discourse’s use of rails have made require_dependency unecessary. I’m unable to edit the post to address that. See further:

3 « J'aime »

Des conseils pour remplacer les classes de modules ? Je veux apporter quelques modifications à GroupGuardian (des conditions spéciales pour un type de groupe spécial).

Merci.

1 « J'aime »

Vous pouvez simplement redéfinir le module et redéfinir la fonction. Utilisez alias_method comme ceci RubyDoc.info: Method: Module#alias_method – Documentation for core (3.4.3) – RubyDoc.info si vous souhaitez conserver l’accès à l’ancienne méthode.

3 « J'aime »

Nouveau dans le développement de Discourse et Rails. J’utilise l’environnement Dev Container (dans VS Code) localement. Les guides et la documentation ont été utiles.

Je me demandais si quelqu’un avait des conseils sur la façon de remplacer les classes principales de Discourse, en particulier pour que cela persiste dans un environnement de développement local.

Dans mon plugin, j’essaie de remplacer une méthode dans la classe principale de Discourse TopicEmbed. (en utilisant l’approche générale bien documentée par @angus ci-dessus.) Cela fonctionne une fois lorsque je reconstruis et recharge VS Code, mais lors des requêtes http suivantes, mon remplacement n’est jamais appelé.

Mon remplacement est défini dans /plugins/my-plugin/app/models/override.rb et j’utilise require_relative pour inclure ce fichier dans mon plugin.rb.

#override.rb :
class ::TopicEmbed

  # un module qui sera préfixé dans la singleton_class de TopicEmbed.
  module TopicEmbedOverrideModule
    # méthode dans TopicEmbed
    def first_paragraph_from(html)
      Rails.logger.info(“mon remplacement se produit ! ”)

      # continuer avec l'implémentation originale fournie par TopicEmbed.
      super

    end
  end

  # faire le préfixe ici
  singleton_class.prepend TopicEmbedOverrideModule
end

Je soupçonne que ce défi de persistance peut être dû à mon environnement de développement et à la façon dont le code ruby est compilé/mis en cache.
J’ai également essayé rm -rf tmp; bin/ember-cli -u et bundle exec rake tmp:cache:clear.

j’ai réussi à le faire fonctionner pour une classe singleton de cette façon :

# my overrides.rb

# Un module qui sera préfixé dans la singleton_class de TopicEmbed
module TopicEmbedOverrides

  # Surcharge la méthode parse_html
  def parse_html(html, url) # note : je n'utilise pas self. ici
    # mes nouvelles choses ici
    # puis exécute l'implémentation originale
    super
  end

end

# effectue la surcharge ici
class ::TopicEmbed
  singleton_class.prepend TopicEmbedOverrides
end