Skip to content

Patches and cross-plugin extensions

Use a patch to change a class owned by core, an engine or another plugin. Keep the patch in the plugin that needs it, for example in your modification_<name> plugin. Do not edit the class itself: updates overwrite it.

Do not add if Redmine::Plugin.installed?(:my_plugin) branches to the extended class. A patch in my_plugin has these advantages:

  • It loads only when my_plugin is installed and active.
  • The code stays in the plugin that owns the feature.
  • The extended component does not depend on an optional plugin.
  • Removing the plugin removes the behaviour.

Write and register a patch

Put the patch into patches/ of your plugin. Easy8 requires every .rb file under patches/ at boot, so the file must register the patch itself. Pass the class and the patching module as strings.

plugins/easyproject/easy_plugins/modification_acme/patches/models/issue_patch.rb
module ModificationAcme
  module IssuePatch
    def some_method
      super

      do_something_for_acme
    end
  end
end

EasyPatchManager.register_model_patch "Issue", "ModificationAcme::IssuePatch", prepend: true

With prepend: true the module is prepended, so its methods can call super. Without it the module is included, which suits patches that add methods or use an included(base) hook with base.class_eval.

Choose the registration method by the kind of class:

Method Class
register_model_patch Models
register_controller_patch Controllers
register_helper_patch Helpers
register_concern_patch Concerns
register_other_patch Other classes, for example Redmine::FieldFormat::StringFormat
register_patch_to_be_first A patch that must be applied before the others

Options: prepend:, if: (a proc, the patch is applied only when it returns true), first:, last:, before: and after: (order against another patch). Pass an array of class names to patch several classes with one module.

What belongs in the original component

Keep code in the original component only when the behaviour is generic and should exist without the extending plugin. If several plugins need the same extension, add a hook or another explicit extension point to the original component and implement the plugin-specific part in the plugin.

For views, prefer view hooks for small additions. A template or partial override is acceptable for a large rewrite.

Patches in a RYS engine

A RYS engine registers patches with Rys::Patcher. Use this only to maintain an existing engine, see Get started with RYS.

Rys::Patcher.add("IssuesController") do
  # nothing: applied after Redmine plugins are loaded
  # :earlier_to_prepare: applied as soon as possible
  where :earlier_to_prepare

  # Apply once, for classes that are not reloaded (like String)
  apply_only_once!

  # Apply only if the condition is met
  apply_if { CONDITION }

  # Apply only if these Redmine plugins are installed
  apply_if_plugins :easy_crm_cases

  included do
    before_action :add_flash_notice, only: [:show]
  end

  instance_methods do
    def show
      flash.now[:notice] = "Hello"
      super
    end
  end

  instance_methods(feature: "issue.show") do
    # Applied only if the RYS feature is active, must call super
  end

  class_methods do
    # Class methods
  end
end

The engine's patches/ directory is registered by include ::Rys::EngineExtensions. For another directory, add it with Rys::Patcher.paths << PATH_TO_DIRECTORY. Rys patches are prepended, so they can call super. Do not mix them with alias_method_chain.