在插件中覆盖现有的Discourse方法

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.
[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 :slight_smile:

35 个赞

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 个赞

@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 个赞

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 个赞

有什么覆盖模块类的技巧吗?我想对 GroupGuardian 做一些更改(为特殊类型的组添加一些特殊条件)。

谢谢。

1 个赞

您可以重新定义模块并重新定义函数。如果您想保留对旧方法的访问权限,请使用 alias_method,如下所示:RubyDoc.info: Method: Module#alias_method – Documentation for core (3.4.3) – RubyDoc.info

3 个赞

刚开始接触 discourse 和 Rails 开发。我正在本地使用 Dev Container 环境(在 VS Code 中)。指南和文档很有帮助。

我想知道是否有人能提供关于如何覆盖核心 discourse 类的方法,特别是在本地开发环境中使其持久化。

在我的插件中,我试图覆盖核心 discourse TopicEmbed 类中的一个方法。(使用 @angus 上面很好地记录的通用方法。)它在我重建和重新加载 VS Code 时工作一次,但在后续的 http 请求中,我的覆盖从未被调用。

我的覆盖定义在 /plugins/my-plugin/app/models/override.rb 中,我使用 require_relative 将此文件包含在我的 plugin.rb 中。

#override.rb:
class ::TopicEmbed

  # 一个将预置到 TopicEmbed.singleton_class 的模块
  module TopicEmbedOverrideModule
    # TopicEmbed 中的方法
    def first_paragraph_from(html)
      Rails.logger.info(“我的覆盖正在发生! ”)

      # 继续执行 TopicEmbed 提供的原始实现。
      super

    end
  end

  # 在此处进行预置
  singleton_class.prepend TopicEmbedOverrideModule
end

我怀疑我的持久化挑战可能与我的开发环境以及 Ruby 代码的编译/缓存方式有关。
我也尝试了 rm -rf tmp; bin/ember-cli -ubundle exec rake tmp:cache:clear