Override existing Discourse methods in plugins

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개의 좋아요

안녕하세요, 이 코드 부분에 대해 궁금한 점이 있습니다. 왜 이 require_dependency가 필요한가요? 없어도 코드가 정상적으로 동작하는 것 같습니다.

네, 맞습니다. 해당 게시글 이후 Discourse의 rails 사용 방식이 업데이트되면서 require_dependency가 더 이상 필요하지 않게 되었습니다. 해당 부분을 반영하여 게시글을 수정할 수 없는 상황입니다. 추가 정보는 아래를 참고하세요:

3개의 좋아요

모듈 클래스를 오버라이드하는 방법에 대한 팁이 있을까요? GroupGuardian에 몇 가지 변경 사항(특정 유형의 그룹을 위한 특별한 조건)을 적용하고 싶습니다.

감사합니다.

1개의 좋아요

모듈과 함수를 다시 정의하면 됩니다. 기존 메서드에 대한 접근을 유지하려면 alias_method를 이렇게 사용하세요: RubyDoc.info: Method: Module#alias_method – Documentation for core (4.0.0) – RubyDoc.info

3개의 좋아요

Discourse와 Rails 개발은 처음입니다. 로컬에서 Dev Container 환경(VS Code)을 사용하고 있습니다.

가이드와 문서들이 많은 도움이 되었습니다.

코어 Discourse 클래스를 오버라이드하는 방법에 대해, 특히 로컬 개발 환경에서 이를 유지(persist)시키는 방법에 대한 팁이 있는지 궁금합니다.

플러그인에서 코어 Discourse의 TopicEmbed 클래스의 메서드를 오버라이드하려고 합니다. (위에서 @angus 님이 잘 문서화한 일반적인 접근 방식을 사용 중입니다.) VS Code를 재빌드하고 재시작하면 한 번은 작동하지만, 이후의 HTTP 요청에서는 제 오버라이드가 절대 호출되지 않습니다.

제 오버라이드는 \/plugins/my-plugin/app/models/override.rb`파일에 정의되어 있으며,`require_relative`를 사용하여 이 파일을 `plugin.rb``에 포함시키고 있습니다.

#override.rb:
class ::TopicEmbed

  # TopicEmbed.singleton_class에 prepend될 모듈
  module TopicEmbedOverrideModule
    # TopicEmbed의 메서드
    def first_paragraph_from(html)
      Rails.logger.info("my override is happening! ")

      # TopicEmbed에서 제공되는 원래 구현을 계속합니다.
      super

    end
  end

  # 여기서 prepend 수행
  singleton_class.prepend TopicEmbedOverrideModule
end

이 지속성(persistence) 문제는 제 개발 환경과 Ruby 코드의 컴파일/캐싱 방식 때문일 것이라고 추측합니다.
rm -rf tmp; bin/ember-cli -u와 bundle exec rake tmp:cache:clear도 시도해 보았습니다.

싱글턴 클래스에서 이렇게 해서 동작하도록 만들었습니다:

# my overrides.rb

# TopicEmbed.singleton_class에 추가될 모듈
module TopicEmbedOverrides

  # parse_html 메서드 오버라이드
  def parse_html(html, url) # 참고: 여기서는 self를 사용하지 않습니다.
    # 여기에 제 새로운 코드
    # 그 다음 원래 구현 실행
    super
  end

end

# 여기서 오버라이드 수행
class ::TopicEmbed
  singleton_class.prepend TopicEmbedOverrides
end