Packages Tutorial

Monkey patching (coffee, rails)

Monkey patching is a big deal when it comes to compatibility and if you are a real hacker man then you want your add-on to run as smooth as possible on your target system.

The zammad codebase is changing very frequently, so it can easily happen that your changes get outdated and your package is the one which is making your zammad system to hell.

Patching rails

Imagine patching the ticket.rb:

https://github.com/zammad/zammad/blob/23a841fff42c2f3dfbbce467ec691e9068baf4a4/app/models/ticket.rb

IT HAS 1500 LINES CODE. FUCK. If you want to patch this file in your package then it might only take one stable update and your package ruins the system, because your package ticket.rb will of course win and the changes of your core will not get considered anymore.

In our example we might want to patch this function:

  def current_state_color
    return '#f35912' if escalation_at && escalation_at < Time.zone.now

    case state.state_type.name
    when 'new', 'open'
      return '#faab00'
    when 'closed'
      return '#38ad69'
    when 'pending reminder'
      return '#faab00' if pending_time && pending_time < Time.zone.now
    end

    '#000000'
  end

(https://github.com/zammad/zammad/blob/23a841fff42c2f3dfbbce467ec691e9068baf4a4/app/models/ticket.rb#L994-L1007)

To patch this, you could patch the hole file (which is bad) or you monkey patch like this:

config/initializers/example_ticket_patch.rb

ActiveSupport::Reloader.to_prepare do
  Ticket.class_eval do
    alias_method :current_state_color_original, :current_state_color

    def current_state_color
      result = current_state_color_original
      if result == '#000000'
        result = '#FFFFFF'
      end
      result
    end
  end
end

This example is the second-best case you can have. It is an appended monkey patch. This means the the original function is not even touched and your change only happens after the original function is run. Then you change something at the result based on the result of the original function and return your overwritten value.

The best case is of course if don’t patch any of the core files and work with the rails standard. E.g. events etc. There is a lot of stuff you can do in rails without event touching the zammad code base.

Patching coffee

Same stuff for the frontend. Checkout this file:

https://github.com/zammad/zammad/blob/23a841fff42c2f3dfbbce467ec691e9068baf4a4/app/assets/javascripts/app/lib/app_post/utils.coffee

1500 LINES of code as well. But you can also easy monkey patch it with appending a new file which is executed after the original file. E.g.:

  @icon: (name, className = '') ->
    return if !name

    # rtl support
    # ===========
    #
    # translates @Icon('arrow-{start}') to @Icon('arrow-left') on ltr and @Icon('arrow-right') on rtl
    dictionary =
      ltr:
        start: 'left'
        end: 'right'
      rtl:
        start: 'right'
        end: 'left'
    if name.indexOf('{') > 0 # only run through the dictionary when there is a {helper}
      for key, value of dictionary[App.i18n.dir()]
        name = name.replace("{#{key}}", value)

This function will return an icon. E.g. the zammad logo which is used at various places.

app/assets/javascripts/app/lib/app_post/utils_example_patch.coffee

class App.UtilsExamplePatch extends App.Utils
  @icon: (name, className = '') ->
    return if !name
    return '<img src="/assets/images/logo.svg" width="42" height="36">' if name is 'logo'
    return super

App.Utils = App.UtilsExamplePatch

CoffeeScript is old, but most of our old codebase is great for monkey patching at the moment. So in this example I do a prepending monkey patch which is returning a different value if the icon for the logo is asked. In all other cases, I do return the original value.

Important for the monkey patching here is also the execution order of the files, which is alphabetic. So make sure your file is handled after the original one.

2 Likes