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.
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” 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
@angus@gdpelican Thanks for this. This is great stuff. . 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.
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:
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).
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