Überschreiben Sie bestehende Discourse-Methoden in Plugins

Ich habe kürzlich mehrere Fälle gehabt, in denen ich bestehende Ruby-Methoden von Plugins überschreiben musste, und möchte meine besten Praktiken hier teilen.

Überschreiben einer Instanzmethode

class ::TopicQuery
  module BabbleDefaultResults
    def default_results(options={})
      super(options).where('archetype <> ?', Archetype.chat)
    end
  end
  prepend BabbleDefaultResults
end
  • Hier entferne ich Chat-Themen aus einer Instanzmethode, die eine Liste von Themen zurückgibt.
  • Der Modulname BabbleDefaultResults kann beliebig sein; ich passe ihn normalerweise an den Methodennamen plus meinen Plugin-Namen an, um Namenskonflikte zu minimieren (obwohl diese bereits sehr gering sind).
  • Module#prepend ist super cool, und wenn Sie Plugins für irgendetwas in Ruby schreiben, sollten Sie damit vertraut sein. Beachten Sie, dass es die Tatsache ist, dass wir ein Modul prependen, die es uns ermöglicht, super innerhalb der überschreibenden Methode aufzurufen.
  • PS: Rufen Sie immer super auf! Dies macht Ihr Plugin viel weniger anfällig dafür, zu brechen, wenn die zugrunde liegende Implementierung sich ändert. Es sei denn, Sie sind wirklich, wirklich sicher, dass Ihre Funktionalität alles in der zugrunde liegenden Methode vollständig ersetzt, sollten Sie super aufrufen und die Ergebnisse von dort aus modifizieren, damit Änderungen an dieser Methode im Discourse-Kern Ihr Plugin später nicht zum Brechen bringen.
  • Das :: in ::TopicQuery stellt sicher, dass ich mich auf die Top-Level-Klasse TopicQuery beziehe, um sie zu überschreiben, und nicht auf eine modulare Version davon (wie Babble::TopicQuery).
  • Dies kann direkt so in plugin.rb eingefügt werden, oder wenn Ihr Plugin groß ist, können Sie jede Überschreibung in eine separate Datei auslagern.

Überschreiben einer Klassenmethode

class ::Topic
  module BabbleForDigest
    def for_digest(user)
      super(user).where('archetype <> ?', Archetype.chat)
    end
  end
  singleton_class.prepend BabbleForDigest
end
  • Hier nehme ich eine bestehende self.for_digest-Methode in der Topic-Klasse und entferne Chat-Themen aus dem Ergebnis.
  • Sehr ähnlich zur Überschreibung einer Instanzmethode, wobei der Unterschied darin besteht, dass wir singleton_class.prepend aufrufen anstatt nur prepend. singleton_class ist eine etwas seltsame Art zu sagen: „Ich möchte dies auf Klassenebene und nicht auf Instanzebene anhängen". Weitere Informationen, falls Sie nach einem ruby-bezogenen „Rabbit Hole" suchen.

Überschreiben eines Scopes

class ::Topic
  @@babble_listable_topics = method(:listable_topics).clone
  scope :listable_topics, ->(user) {
    @@babble_listable_topics.call(user).where('archetype <> ?', Archetype.chat)
  }
end
  • Dieser ist etwas knifflig, da Scopes nicht gut mit super funktionieren (oder zumindest konnte ich sie nicht dazu bringen). Stattdessen nehmen wir eine bestehende Methodendefinition, klonen sie, speichern sie und rufen sie später auf.
  • Auch hier kann @@babble_listable_topics beliebig sein, aber es ist wahrscheinlich eine gute Idee, Ihren Plugin-Namen als Namensraum zu verwenden.
  • Mehr zur method-Funktion, die auch super cool ist, obwohl die Fälle, in denen man sie wirklich braucht, eher selten sind. Bonus-Tipp: Beim Debuggen, wenn Sie Schwierigkeiten haben, herauszufinden, welcher Code für einen bestimmten Methodenaufruf ausgeführt wird (meistens: „Welches Gem definiert diese Methode?"), können Sie source_location verwenden, um die genaue Zeile des Quellcodes zu erhalten, in der die Methode definiert ist.
[7] pry(main)> Topic.new.method(:best_post).source_location
=> ["/Users/gdpelican/workspace/discourse/app/models/topic.rb", 282]

( ^^ dies bedeutet, dass die best_post-Methode auf einem neuen Topic in <root>/app/models/topic.rb auf Zeile 282 definiert ist.)

Alles, was ich zu sagen habe. Lassen Sie mich wissen, ob ich etwas korrigieren, erweitern oder klarstellen soll :slight_smile:

35 „Gefällt mir“

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 „Gefällt mir“

@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 „Gefällt mir“

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 „Gefällt mir“

Haben Sie Tipps zum Überschreiben von Modulklassen? Ich möchte einige Änderungen an GroupGuardian vornehmen (einige spezielle Bedingungen für eine spezielle Art von Gruppe).

Danke.

1 „Gefällt mir“

Sie können das Modul einfach neu definieren und die Funktion neu definieren. Verwenden Sie alias_method wie hier RubyDoc.info: Method: Module#alias_method – Documentation for core (3.4.3) – RubyDoc.info, wenn Sie Zugriff auf die alte Methode behalten möchten.

3 „Gefällt mir“

Neu bei Discourse und Rails-Entwicklung. Ich verwende die Dev Container-Umgebung (in VS Code) lokal. Die Anleitungen und Dokumentationen waren hilfreich.

Ich habe mich gefragt, ob jemand Tipps hat, wie man Kern-Discourse-Klassen überschreiben kann, insbesondere wie man sie in einer lokalen Entwicklungsumgebung persistent macht.

In meinem Plugin versuche ich, eine Methode in der Kern-Discourse-Klasse TopicEmbed zu überschreiben (unter Verwendung des allgemeinen Ansatzes, der von @angus oben gut dokumentiert wurde). Es funktioniert einmal, wenn ich VS Code neu baue und neu lade, aber bei nachfolgenden HTTP-Anfragen wird meine Überschreibung nie aufgerufen.

Meine Überschreibung ist in /plugins/my-plugin/app/models/override.rb definiert und ich verwende require_relative, um diese Datei in mein plugin.rb einzubinden.

#override.rb:
class ::TopicEmbed

  # ein Modul, das in TopicEmbed.singleton_class vorangestellt wird
  module TopicEmbedOverrideModule
    # Methode in TopicEmbed
    def first_paragraph_from(html)
      Rails.logger.info(“my override is happening! ”)

      # fahre mit der ursprünglichen Implementierung von TopicEmbed fort.
      super

    end
  end

  # führe das Prepend hier aus
  singleton_class.prepend TopicEmbedOverrideModule
end

Ich vermute, dass diese Herausforderung mit der Persistenz auf meine Entwicklungsumgebung und die Art und Weise, wie Ruby-Code kompiliert/gecacht wird, zurückzuführen sein könnte.
Ich habe auch rm -rf tmp; bin/ember-cli -u und bundle exec rake tmp:cache:clear versucht.

ich habe es auf diese Weise für eine Singleton-Klasse zum Laufen gebracht:

# my overrides.rb

# Ein Modul, das in die TopicEmbed.singleton_class vorangestellt wird
module TopicEmbedOverrides

  # parse_html Methode überschreiben
  def parse_html(html, url) # Hinweis: ich verwende hier kein self.
    # mein neuer Kram hier
    # dann die ursprüngliche Implementierung ausführen
    super
  end

end

# die Überschreibung hier durchführen
class ::TopicEmbed
  singleton_class.prepend TopicEmbedOverrides
end