I’ve been running into a bunch of instances recently of needing to override existing ruby methods from plugins, and thought I’d share my best practices here.
Overriding an instance method
class ::TopicQuery
module BabbleDefaultResults
def default_results(options={})
super(options).where('archetype <> ?', Archetype.chat)
end
end
prepend BabbleDefaultResults
end
Here I’m removing chat topics from an instance method which is returning a list of topics.
The module name BabbleDefaultResults can be anything you want; I usually make it match the name of the method, plus my plugin name, to minimize any name conflict risks (although they’re already quite low)
Module#prepend is super cool and you should know about it if you’re writing plugins for anything in ruby. Note that it’s the fact that we’re prepending a module which allows us to call super inside the override method.
PS, always call super! This makes your plugin far less likely to break when the underlying implementation changes. Unless you’re really, really sure that your functionality completely replaces everything in the underlying method, you want to call super and modify the results from there, so that changes to this method in Discourse core don’t make your plugin break later.
The :: in ::TopicQuery is ensuring that I’m referring to the top-level TopicQuery class to override, and not some modulized version of it (like Babble::TopicQuery)
This can go straight into plugin.rb as is, or if your plugin is large you can consider separating each override out into a separate file.
Overriding a class method
class ::Topic
module BabbleForDigest
def for_digest(user)
super(user).where('archetype <> ?', Archetype.chat)
end
end
singleton_class.prepend BabbleForDigest
end
Here I’m taking an existing self.for_digest method on the Topic class, and removing chat topics from the result
Very similar to the instance method override, note the difference being that we’re calling singleton_class.prepend instead of just prepend. singleton_class is a mildly weird way of saying ‘I want to append this to the class level, not the instance level’, further reading if you’re looking for a ruby-related rabbit hole.
Overriding a scope
class ::Topic
@@babble_listable_topics = method(:listable_topics).clone
scope :listable_topics, ->(user) {
@@babble_listable_topics.call(user).where('archetype <> ?', Archetype.chat)
}
end
This one’s a little bit tricky because scopes don’t play well with super (or, at least, I couldn’t get them to). So instead, we’re taking an existing method definition, cloning it, storing it, and then calling it later.
Again, @@babble_listable_topics can be anything you’d like, but using your plugin name as a namespacer is probably a good idea.
More on the method function, which is also super cool, although the times when you’d really need it are pretty few and far between. Bonus free fun fact related to that; when debugging, if you’re having trouble figuring out what code is getting run for a particular method call (usually “Which gem is defining this method?”), you can use source_location to get the exact line of source code where the method is defined.
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:
New to discourse and Rails development.I’m using the Dev Container environment (in VS Code) locally.The guides and docs have been helpful.
I was wondering if anyone has any tips on how to override core discourse classes, specifically getting it to persist in a local development environment.
In my plugin, I am trying to override a method in the core discourse TopicEmbed class. (using the general approach nicely documented by @angus above.) It works once when I rebuild and reload VS Code, but on subsequent http requests my override is never invoked.
My override is defined in `/plugins/my-plugin/app/models/override.rb` and I use `require_relative` to include this file in my `plugin.rb`.
#override.rb:
class ::TopicEmbed
# a module that will be prepended into TopicEmbed.singleton_class
module TopicEmbedOverrideModule
# method in TopicEmbed
def first_paragraph_from(html)
Rails.logger.info(“my override is happening! ”)
# continue with the original implementation provided by TopicEmbed.
super
end
end
# do the prepend here
singleton_class.prepend TopicEmbedOverrideModule
end
I suspect this my persistence challenge may be due to my dev environment and how ruby code is compiled/cached.
I also tried rm -rf tmp; bin/ember-cli -uand bundle exec rake tmp:cache:clear.