Redmine Plugins #2 : patcher une classe de Redmine
ATTENTION: cet article n'est plus valable à partir de Redmine 2.x.
Il peut arriver qu’une classe de Redmine ne se comporte pas exactement comme vous le voudriez, ou que vous souhaitiez lui ajouter des propriétés.
C’est décrit en anglais sur la page Plugin Internals / Extending the Redmine Core du wiki officiel, qui renvoie vers la lecture de certains plugins d’Eric Davis pour des exemples.
Petit apparté, je partage assez l’analyse selon laquelle il est quasi inutile de maintenir une API pour surcharger les modèles / controlleurs. Cela dit, parfois les méthodes sont extrêmement longues et/ou sujettes à de fréquents changements. Toute surcharge dans un plugin induit donc un risque pour les futures versions du core…
Retour à nos moutons : admettons qu’on veuille ajouter au modèle Issue
une méthode d’instance whoami
qui retournerait “Je suis
le ticket #XXX”. Exemple bidon, c’est pour la science.
Si on applique ce que préconise Eric, ça donne quelque chose de ce genre :
require_dependency 'issue_patch' Issue.send(:include, IssuePatch) unless Issue.included_modules.include? IssuePatch require_dependency 'issue' def self.included(base) base.extend(ClassMethods) base.send(:include, InstanceMethods) base.class_eval do unloadable #permet de décharger la classe en mode dev end end #ici nos méthodes de classe module ClassMethods end #ici nos méthodes d'instance module InstanceMethods def whoami "Je suis le ticket ##{self.id}" end end
Classique, mais comme diraient certains amis “on voit pas trop ce que ça fait”.
Personnellement je préfère réouvrir la classe Issue, et ça a l’air de marcher tout aussi bien (en dev et en prod) :
config.to_prepare do require_dependency 'issue_patch' require_dependency 'issue' def whoami "Je suis le ticket ##{self.id}" end
Différences :
- utilisation de “config” au lieu de “Dispatcher” ; sans importance à mon avis. C’est discuté un peu ici.
- ré-ouverture de la classe plutôt qu’inclusion d’un module ; je trouve ça plus lisible pour ce coup-ci
Attention, je ne dis pas que ce que fait Eric fonctionne moins bien. Au contraire, c’est peut-être plus “propre”, mais n’étant pas un développeur confirmé, si je ne comprends pas au premier coup d’oeil ce que j’ai fait, j’ai plus de mal à maintenir mon code.
Au passage, c’est une mauvaise idée d’appeler son patch “lib/issue_patch.rb”. Si tout le monde fait ça, on ne pourra pas faire fonctionner 2 plugins qui patchent la même classe en même temps. Beurk. D’ailleurs, c’était le cas pour des plugins à moi, donc autant utiliser des noms a priori uniques : commit redmine_drafts/ec06b8