Sobrescribir los métodos existentes de Discourse en plugins

He estado encontrando recientemente varios casos en los que necesito anular métodos de Ruby existentes desde plugins, y pensé que compartiría aquí mis mejores prácticas.

Anular un método de instancia

class ::TopicQuery
  module BabbleDefaultResults
    def default_results(options={})
      super(options).where('archetype <> ?', Archetype.chat)
    end
  end
  prepend BabbleDefaultResults
end
  • Aquí estoy eliminando los temas de chat de un método de instancia que devuelve una lista de temas.
  • El nombre del módulo BabbleDefaultResults puede ser el que quieras; por lo general, hago que coincida con el nombre del método más el nombre de mi plugin para minimizar cualquier riesgo de conflicto de nombres (aunque ya son bastante bajos).
  • Module#prepend es genial y deberías conocerlo si estás escribiendo plugins para cualquier cosa en Ruby. Ten en cuenta que es el hecho de que estamos anteponiendo un módulo lo que nos permite llamar a super dentro del método de anulación.
  • PS, ¡_siempre llama a super! Esto hace que tu plugin sea mucho menos propenso a romperse cuando cambie la implementación subyacente. A menos que estés realmente, muy seguro de que tu funcionalidad reemplaza todo en el método subyacente, quieres llamar a super y modificar los resultados desde allí, para que los cambios en este método en el núcleo de Discourse no hagan que tu plugin se rompa más adelante.
  • El :: en ::TopicQuery asegura que me refiero a la clase TopicQuery de nivel superior para anular, y no a alguna versión modularizada de ella (como Babble::TopicQuery).
  • Esto puede ir directamente en plugin.rb tal cual, o si tu plugin es grande, puedes considerar separar cada anulación en un archivo diferente.

Anular un método de clase

class ::Topic
  module BabbleForDigest
    def for_digest(user)
      super(user).where('archetype <> ?', Archetype.chat)
    end
  end
  singleton_class.prepend BabbleForDigest
end
  • Aquí estoy tomando un método existente self.for_digest en la clase Topic y eliminando los temas de chat del resultado.
  • Muy similar a la anulación del método de instancia, nota la diferencia: estamos llamando a singleton_class.prepend en lugar de simplemente prepend. singleton_class es una forma ligeramente extraña de decir ‘quiero añadir esto al nivel de clase, no al nivel de instancia’, lectura adicional si buscas un agujero de conejo relacionado con Ruby.

Anular un scope

class ::Topic
  @@babble_listable_topics = method(:listable_topics).clone
  scope :listable_topics, ->(user) {
    @@babble_listable_topics.call(user).where('archetype <> ?', Archetype.chat)
  }
end
  • Este es un poco complicado porque los scopes no funcionan bien con super (o, al menos, yo no pude hacer que lo hicieran). Así que, en su lugar, estamos tomando una definición de método existente, clonándola, almacenándola y luego llamándola más tarde.
  • Nuevamente, @@babble_listable_topics puede ser lo que quieras, pero usar el nombre de tu plugin como namespace probablemente sea una buena idea.
  • Más sobre la función method, que también es genial, aunque las veces en que realmente la necesitas son muy pocas. Dato divertido adicional relacionado con eso; al depurar, si tienes problemas para averiguar qué código se está ejecutando para una llamada de método en particular (usualmente “¿Qué gem está definiendo este método?”), puedes usar source_location para obtener la línea exacta del código fuente donde se define el método.
[7] pry(main)> Topic.new.method(:best_post).source_location
=> ["/Users/gdpelican/workspace/discourse/app/models/topic.rb", 282]

( ^^ esto indica que el método best_post en un nuevo tema está definido en /app/models/topic.rb, en la línea 282)

Bien, eso es todo lo que tengo. Avísame si debo corregir, ampliar o aclarar algo :slight_smile:

35 Me gusta

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 Me gusta

@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 Me gusta

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 Me gusta

¿Algún consejo para anular las clases de módulos? Quiero hacer algunos cambios en GroupGuardian (algunas condiciones especiales para un tipo especial de grupo).

Gracias.

1 me gusta

Puedes simplemente redefinir el módulo y la función. Usa alias_method como esto RubyDoc.info: Method: Module#alias_method – Documentation for core (3.4.3) – RubyDoc.info si quieres conservar el acceso al método antiguo.

3 Me gusta

Nuevo en el desarrollo de Discourse y Rails. Estoy usando el entorno Dev Container (en VS Code) localmente. Las guías y la documentación han sido útiles.

Me preguntaba si alguien tiene algún consejo sobre cómo anular las clases principales de Discourse, específicamente cómo hacer que persista en un entorno de desarrollo local.

En mi plugin, estoy intentando anular un método en la clase principal de Discourse TopicEmbed. (usando el enfoque general bien documentado por @angus arriba). Funciona una vez cuando reconstruyo y recargo VS Code, pero en solicitudes HTTP posteriores, mi anulación nunca se invoca.

Mi anulación está definida en /plugins/my-plugin/app/models/override.rb y uso require_relative para incluir este archivo en mi plugin.rb.

#override.rb:
class ::TopicEmbed

  # un módulo que se antepondrá a la singleton_class de TopicEmbed.
  module TopicEmbedOverrideModule
    # método en TopicEmbed
    def first_paragraph_from(html)
      Rails.logger.info(“¡mi anulación está sucediendo! ”)

      # continuar con la implementación original proporcionada por TopicEmbed.
      super

    end
  end

  # hacer el prepend aquí
  singleton_class.prepend TopicEmbedOverrideModule
end

Sospecho que este desafío de persistencia puede deberse a mi entorno de desarrollo y a cómo se compila/almacena en caché el código ruby.
También probé rm -rf tmp; bin/ember-cli -u y bundle exec rake tmp:cache:clear.

Lo hice funcionar para una clase singleton de esta manera:

# mis anulaciones.rb

# Un módulo que se antepondrá a la clase singleton de TopicEmbed
module TopicEmbedOverrides

  # Anular el método parse_html
  def parse_html(html, url) # nota: no uso self. aquí
    # mis cosas nuevas aquí
    # luego ejecutar la implementación original
    super
  end

end

# hacer la anulación aquí
class ::TopicEmbed
  singleton_class.prepend TopicEmbedOverrides
end