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-levelTopicQuery
class to override, and not some modulized version of it (likeBabble::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 justprepend
.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.
[7] pry(main)> Topic.new.method(:best_post).source_location
=> ["/Users/gdpelican/workspace/discourse/app/models/topic.rb", 282]
( ^^ this is saying that the best_post
method on a new topic is defined in /app/models/topic.rb, on line 282)
Alright, that’s all I’ve got. Let me know if I should correct, expand, or clarify anything