TWiki plugins operate using a sort of crude
C2Wiki:ObserverPattern, where unfortunately the coupling between the dispatcher (TWiki::Plugins) and the observer (TWiki::Plugin) is rather too tight for comfort. It's also specific to the concept of plugins being
rendering agents, and many authors (including me) have gone way beyond that simplistic model. I believe it's time we fixed this, and moved to a more open architecture.
To understand what I'm driving at, consider the list of events that plugins can currently respond to:
| Handler |
Context |
| afterAttachmentSaveHandler |
Save |
| afterCommonTagsHandler |
Render |
| afterEditHandler |
Edit |
| afterRenameHandler |
Rename |
| afterSaveHandler |
Save |
| beforeAttachmentSaveHandler |
Save |
| beforeCommonTagsHandler |
Render |
| beforeEditHandler |
Edit |
| beforeSaveHandler |
Save |
| commonTagsHandler |
Render |
| earlyInitPlugin |
god alone knows |
| initializeUserHandler |
Login |
| modifyHeaderHandler |
HTTP header construction (!) |
| mergeHandler |
Save |
| postRenderingHandler |
Render |
| preRenderingHandler |
Render |
| redirectCgiQueryHandler |
All |
| registrationHandler |
Register |
| renderFormFieldForEditHandler |
Render |
| renderWikiWordHandler |
Render |
Really quite a limited set. There are a lot more "events" in TWiki that a plugin is interested in, but can't see because no handlers have been defined. Because of their origins as callbacks injected into the rendering process, plugin handlers have a number of unfortunate problems:
- They are injected into the rendering process! There is no way (except DIY) to call them asynchronously. This is a pain for, for example, mailers that want to send out a notification on topic change, but don't need to hold up the view process to do so.
- They have to be passed, and receive in return, masses of data (the topic and meta). This involves a lot of copying around of data which is often simply not required.
- There is an implicit (and quite unclear to most people) linear call order. This is somewhat controllable via INSTALLEDPLUGINS, but the constraints are clumsy. You can't for example, say in the plugin "I don't care when I am called, as long as I am called before SpreadSheetPlugin". this has to be manually defined by the admin.
- There is no way for a plugin to trigger an event in another plugin.
- Handlers defined in TWiki::Plugins are always called, even when no-one is listening out for them. This is inefficient.
It would be better to move to an cleaner event driven model, following the lead TWiki once had, but has now lost to Joomla, Confluence, and most other CMS and Wiki implementations. Most programmers are familiar with MVC these days, so event handling hold no terrors. Any programmer who has coded
HTML or
JavaScript is also familiar with the concept.
So how would this work?
For example, the common tags handler from
CommentPlugin currently looks like this:
sub commonTagsHandler {
my ( $text, $topic, $web ) = @_;
require TWiki::Plugins::CommentPlugin::Comment;
if ($@) {
TWiki::Func::writeWarning( $@ );
return 0;
}
my $query = TWiki::Func::getCgiQuery();
return unless( defined( $query ));
return unless $_[0] =~ m/%COMMENT({.*?})?%/o;
# SMELL: Nasty, tacky way to find out where we were invoked from
my $scriptname = $ENV{'SCRIPT_NAME'} || '';
# SMELL: unreliable
my $previewing = ($scriptname =~ /\/(preview|gnusave|rdiff)/);
TWiki::Plugins::CommentPlugin::Comment::prompt( $previewing,
$_[0], $web, $topic );
}
Under an event dispatcher it would look like this (commonTagsHandler was always a bad name):
sub onRenderTextFeatures {
my $event = shift;
require TWiki::Plugins::CommentPlugin::Comment;
if ($@) {
TWiki::Func::writeWarning( $@ );
return 0;
}
my $query = $event->{query};
return unless( defined( $query ));
return unless $_[0] =~ m/%COMMENT({.*?})?%/o;
# SMELL: Nasty, tacky way to find out where we were invoked from
my $scriptname = $ENV{'SCRIPT_NAME'} || '';
# SMELL: unreliable
my $previewing = ($scriptname =~ /\/(preview|gnusave|rdiff)/);
TWiki::Plugins::CommentPlugin::Comment::prompt(
$previewing,
$event->{text},
$event->{web},
$event->{topic} );
}
But why? I hear you cry. Well, a couple of things. First off, in the core code, the existing call:
# Plugin Hook
$this->{plugins}->commonTagsHandler( $text, $theTopic, $theWeb, 0 );
changes to this:
$this->queueSynchronousEvent( 'renderTextFeatures' );
Secondly, an event model allows us to
queue events for later execution as well as dispatch them directly
$this->queueAsynchronousEvent( 'topicModified' );
The event dispatcher takes responsibility for prioritising and dispatching the events, so that it can handle more complex relationships between plugins (such as "call me before SpreadSheetPlugin"). It can even save events until after the page has already been rendered, or serialise then for later (e.g. a cron job might pick them up and execute them).
Another rationale is the potential for plugins to intercommunicate using events. For example, the ActionTrackerPlugin might want to use the MailerPlugin (aynchronously of course) to notify a user that a new action was just added for them.
TWiki::Func::queueEvent( 'sendMail', user => $user, text => "You have a new action...." );
Questions, comments, observations, better ideas?
--
Contributors: CrawfordCurrie - 05 Mar 2007
Discussion
Hi Crawford, I'm really surprised that this topic hasn't caught more traction since it was written. I've been watching it and waiting for the activity to pick up. Personally, I think you've got a fantastic idea. For me, the true power of TWiki is yet to be unlocked, and that will come when we start to capitalize on the opportunity of
MashUps between plugins. And that can't happen until the plugins have a reliable way of passing events back & forth to each other...
--
KeithHelfrich - 08 Mar 2007
+1 to this
--
RafaelAlvarez - 08 Mar 2007
I think you are on the right track.
--
ArthurClemens - 08 Mar 2007