cross
New Foswiki release 2.1.6 is available with important security fixes.
Sourceforge foswiki email lists being discontinued. Subscribe to the new Foswiki announce and discuss lists at MailingLists

Feature Proposal: The new OO foswiki needs new OO plugins.

Motivation

Foswiki's current plugin/extension subsystem is outdated, limited, and hardly comply with OO approach. We need a new model which would ease development of complicated plugins and let us extend the limits.

Description and Documentation

This proposal is pure draft and not intended to finalize what I was thinking about for some time now but rather to initiate a discussion. It may contain some controversial or not thoroughly thought out ideas. But the main one which is the corner stone of the whole concept is: the new plugin is an object.

Naming

This is the least important subject but in a way it may impact some future organizational matters. The subject is contained in one simple question:

How do we name the new subsystem?

Initially I thought it would be called 'plugins'. But that may confuse end-users and complicate things. Therefore the new subsystem better get a new name. Considering that it is planned to be able to extend the core in any possible way the name must reflect this fact. Unfortunately, any possible naming option I considered is already used by Foswiki. For this reason I will continue to use the 'plugin' name in a hope that it is 100% clear that it has little in common with the current subsystem.

Functionality

Plugins must be able to take care of:

  • Declaring tags/macros.
  • Processing named callbacks.
  • Redefining some key methods of the core.
  • Providing new functionality by replacing some core classes.

Implementations details.

This section contains examples with sample keywords/modifiers whose names are not final and most likely will change in the final specification.

What new plugin is

A new plugin is an object of class inheriting from Foswiki::NewPlugin ('NewPlugin' will be replaced with the new subsystem name) and extending Foswiki's core functionality by adding new features or redefining some of them.

Plugins are loaded as early as possible. The particular meaning of this statement is yet to be determined.

Considering OO-nature of the new plugins an additional plugin set could be created any time during application lifetime. A correctly coded plugin would be well-encapsulated and won't break this feature. This way a parallel independent processing of topics might be implemented.

Execution flow

New plugin objects are forming a queue. The queue define the order they're getting called upon processing. The order is defined by the following factors (factor's priority is increasing from the first to the last):

  1. Alphabet sorting
  2. User-defined order
  3. Plugin-defined dependecies

Dependencies could be declared using modifiers like 'plugin_after' or 'plugin_before':

package Foswiki::NewPlugin::SomePlugin;

use Moo;
extends qw(Foswiki::NewPlugin);

plugin_after 'Foswiki::NewPlugin::MainOne';

package Foswiki::NewPlugin::Important;

use Moo;
extends qw(Foswiki::NewPlugin);

plugin_before 'Foswiki::NewPlugin::SomePlugin';

If for a reason wiki admin would manually define plugin order as:

...,Foswiki::NewPlugin::SomePlugin,...,Foswiki::NewPlugin::MainOne,...

the core will rearrange it to make MainOne actually preceed SomePlugin. In a simple case like this MainOne would be relocated right before SomePlugin.

Note In this example nothing can bu guaranteed as to mutual location of Foswiki::NewPlugin::MainOne and Foswiki::NewPlugin::Important unless the latter is explicitly defined by admin in the plugin order configuration.

It is possible that dependencies might form a circular chain. For this matter a warning would be generated and the dependecy ordering would start with the first plugin the chain the core loaded. For example, if there are plugins P1, P2, P3, P4 are forming such chain (P1 depends upon P2, P2 upon P3, P3 upon P4, P4 upon P1) and wiki admin defines the order as P5,P3,P1,P2,P4,P6,P7 then P3 will be considered the the chain starter and the final order will be P5,P2,P1,P4,P3,P6,P7. Though such a situation could and should be prevented at the stage of uploading a plugin to the server and a warning returned back to the plugin developer.

In a most simple case plugins are called one after another in the order defined by the rules above. But sometimes a plugin might generate a special exception which would change the order or stop the flow. Exceptions responsible for this are grouped under Foswiki::Exception::Flow node. For example, if a plugin decides that it must be the last one executed then it could raise Foswiki::Exception::Flow::Last. This would prevent the original method from being called too. Though Foswiki::Exception::Flow::Skip would cause the core to skip all remaining plugins and call the original method right after the current plugin call. And Foswiki::Exception::Flow::Restart would restart the flow from the beginning preserving plugin parameters hash (see below) or any other state changes done by the plugins so far.

Note The flow control model presumes that plguins act in cooperative manner.

Tags/macros

A plugin must handle a macro by implementing a class with role Foswiki::Macro. The role demands a class to have expand() method which would perform actual macro expansion. Sample implementation can be found in Item13897 development branch, in core/lib/Foswiki/Macros/IF.pm and few other core macros.

In a complicated case when result of macro expansion depends upon plugin's state macro must be registered as a pre-instantiated object with a reference to the parent plugin.

Named callbacks.

This is what is currently called 'handlers'. In the Item13897 branch there is a frame implementation of callbacks by Foswiki::Aux::Callbacks role.

Callback is a closure called on behalf of a core code by a registered callback name. What makes it different from currently implemented handlers is the ability to supply the closure with parameters from both the code which is registering the callback and from the code calling it.

For example, for a callback registered by Unit::TestApp class using the following call:

    my $cbData = { app => $this, };
    weaken( $cbData->{app} );
    foreach my $cbName ( keys %{ $this->callbacks } ) {
        $this->registerCallback( $cbName, $this->callbacks->{$cbName},
            $cbData );
    }

and called by Foswiki::App::handleRequest method:

    $this->callback( 'handleRequestException', { exception => $e, } );

the closure would get the following arguments:

    my $obj = shift;
    my %args = @_;

    my $app = $args{data}{app};
    my $exception = $args{params}{exception};

Actually, in this example $obj and $app are the same object because a callback's first argument is the object which initiated the call.

The current callback implementation is experimental and limited in the way that a callback can be registered on a particular object only. There is no support neither for application- nor for class-wide registration.

Method redefinition.

This functionality isn't implemented and pretty much questionable.

A class method could be declared as pluggable; in other words a plugin may claim that it will take care of this method. This is similar to callbacks except that a plugin can completely replace the method and implement different functionality. Here is what it may look like:

package Foswiki::CoreModule;

use Moo;
with 'Foswiki::NewPlugins::Pluggable';

...

pluggable redefinable => sub {
    # Here is something we do by default.
};

package Foswiki::NewPlugin::SomePlugin;

use Moo;
extends qw(Foswiki::NewPlugin);

...

prefix_method 'Foswiki::CoreModule::redefinable' => sub {
    my $this = shift; # Plugin object.
    my $coreObject = shift; # A instance of Foswiki::CoreModule
    my %params = @_;

    my @args = @{ $params{args} };
    
    # Here we do something.

    if ($thisIsFinal) {
        Foswiki::Exception::Flow::Last->throw(
            text => "The reason why the remaing plugins must not have their chance to run",
            rc => $returnValue,
        );
    }

    if ($weWantToOverrideTheOriginalMethod) {
        Foswiki::Exception::Flow::NoOrig->throw(
            rc => $returnValue,
        );
    }

    # This is what's gonna be passed over to the original method if it gets called.
    $params{args} = \@modifiedArgs;
};

In the above example prefix_method acts similarly to before modifier of Moo/Moose except that it gives a plugin more control of the execution flow. A plugin can influence the flow in the following ways:

  • redefinition of arguments passed over to the remaining plugins and the original method by redefining key args of the parameters hash
  • avoiding the original method from being called by setting raising Foswiki::Exception::Flow::NoOrig and setting it's rc parameter to the desired return value. Note this would let run the all the remaining plugins.
  • stop the execution flow by rasing Foswiki::Exception::Flow::Last. It is similar to the previous case except that the remaining plugins won't get their chance to run.
  • restart the flow with Foswiki::Exception::Flow::Restart. Any changes to the parameters hash are preserved. This would cause the whole execution chain to rerun in a different state.
  • skip all remaining plugins by raising Foswiki::Exception::Flow::Skip and pass the control to the original method.

Other ways might be developed over the time if necessary.

Class redefinition

This is the most powerful and the most controversial method. In the new OO model a new object must be created using Foswiki::App::create() method. Though currently it is merely a handy shortcut to avoid specifying app constructor parameter every time we create a new object with Foswiki::AppObject role. But it may get additional functionality of mapping standard class names into those provided by plugins. For example, we have a plugin with the following declaration:

package Foswiki::NewPlugin::Mighty;

use Moo;
extends qw(Foswiki::NewPlugin);

replace_class 'Foswiki::Meta' => 'Foswiki::NewPlugin::Mighty::Meta';

package Foswiki::NewPlugin::Mighty::Meta;

use Moo;
# Note that there is no extends keyword.

1;

What happens then is when a new Foswiki::Meta instance gets created:

    my $topicObject = $app->create('Foswiki::Meta', web => $web, topic => $topic);

the create() method maps 'Foswiki::Meta' into the replacement class name and creates a new object using this class. It means that ref($topicObject) eq 'Foswiki::NewPlugin::Mighty::Meta' in the latter example.

There might be few unrelated plugins wanting to override same base class the replacement classes must not use 'extends' keyword because their inheritance order would be defined by the application depending on the plugins execution flow order. So, if Foswiki::NewPlugin::AllMighty has precedence over Foswiki::NewPlugin::Mighty then the inheritance order will be defined as:

Foswiki::Meta <- Foswiki::NewPlugin::Mighty <- Foswiki::NewPlugin::AllMighty

There is a big controversy in this method related to limitations of Moo. Unlike Moose Moo doesn't use meta data to store class/object properties. Yet, the first time a class gets instantiated Moo pregenerates a constructor for it with hardcoded inheritance order. If we put this together with future support of virtual hosting where different hosts would have different plugins loaded; and then if we add delayed response to all this then we get into a situation where two applications sharing same address space may need different inheritance for same class. Taking the example above, if application 1 do use Foswiki::NewPlugin::Mighty but application 2 doesn't then it would not be possible to satisfy both of them.

By the time of writing this paragraph I'm not sure if Moose would let us get around the issue. But from old times when I was first playing with Moose there is something telling me that if a Moose class isn't finalized then it shall be possible to change inheritance order any time we want.

Another way to deal with this issue would be to disable chained overriding. Any replacement would then be inheriting exclusively from the class it is replacing. The create() method will use the class registered first within the particular application instance. In other words, the above examples would generate following inheritance orders:

Foswiki::Meta <- Foswiki::NewPlugin::Mighty::Meta
Foswiki::Meta <- Foswiki::NewPlugin::AllMighty::Meta

Assuming that application 1 places Foswiki::NewPlugin::AllMighty before Foswiki::NewPlugin::Mighty in the plugin order; and that application 2 doesn't use Foswiki::NewPlugin::AllMighty. Then call $app->create('Foswiki::Meta') would instantiate =Foswiki::NewPlugin::AllMighty::Meta for application 1 and Foswiki::NewPlugin::Mighty::Meta for application 2.

Whether this technique has any future or not is yet to be decided. The shortcoming described above can be workarounded with =around=-like modifier used to declare all methods of the replacement class. But this approach has it's own disadvantages and I'm not sure if there is any value in it.

Action registering

In addition to what's been already implemented in Item14152 branch it just recently came across my mind that one more feature could be provided by the new extension model. An extension could declare its own action handler. So, let's say, JsonRpcContrib may have just a single line of code:

actionHandler jsonrpc => sub {};

or

actionHandler jsonrpc => \&jsonActionHandler;

In scriptless environments like mod_perl or PSGI this would simply add an entry to the LSC SwitchBoard key. For CGI a special kind of universal action handling script can take care of supporting this functionality.

Examples

Impact

%WHATDOESITAFFECT%
edit

Implementation

-- Contributors: VadimBelman - 08 Aug 2016

Discussion

Above proposal offers a lot of dynamics. All of them seem to be going on while preparing an $app object before it actually is able to start processing the request. These dynamics don't seem to be happening during compile time (right?). Instead they need to be resolved dynamically based on various concepts and constraints. This for each single request coming in...

Plugin ordering as in "Execition flow" is problematic and always was. It does not matter whether a plugin order is specified explicitly as part of a configuration setting or by code pragmas. These dependencies are bound to cost, presumably be suboptimal in many cases and not desirable in the first place. Whenever you extend Foswiki, try to avoid any requirement on a specific plugin order. Or put the other way around: if you are trying to solve a problem that requires a linear execution order of some plugins, then the calling code should make use of other plugin's internal services explicitly or the problem should actually be solved in a single class of its own.

A plugin order is only required when processing callbacks or building up inheritance, but not for macros. Their execution flow is defined by the parser and is independent of the plugin they were defined in.

We do have a couple of alternative mechanisms to redefine code components. Of course, these are less flexible and less general as an approach. However I am not that convinced yet we need this level of control to redefine classes or single methods within in a dynamic fashion.

There is one important aspect missing here which might not be related to plugins by themselves but in the way they interact with each other: plugins do not share a parse tree at the moment. This perspective offers quite a different view on the "Execution flow", such as macros listening on tree changes.

There is loads of conceptual cruft in that area that we really should throw out as we are at it, such as commonTagsHandler.

-- MichaelDaum - 09 Aug 2016

There's a lesson to be learned here from the Android component architecture.

Android "components" can communicate through a mechanism called "Broadcasting" - which is very similar to the event mechanism in Javascript i.e. a component registers a "Broadcast Receiver" (event handler) that receives "Intents" (events) and acts on them. The order in which broadcasts are sent to components is determined by how the handlers are registered.

Components register broadcast receivers either as "registered" or "ordered registered". "Registered" receivers receive broadcasts asynchronously, and no dependencies are supported between them, while "ordered registered" receivers are notified in order. The result of processing a a broadcast in an ordered receiver can be passed on to the next receiver in the order.

Notification is expected to be handled fast - if an ordered component wants to trigger some high-cost processing, it has to do so by spawning an asynchronous thread.

This general architecture can underlie all the functions of a Foswiki "Component", whether it is listening to initialisation events, or listening to to SAX-like parse events, or tree-change events.

-- Main.CrawfordCurrie - 09 Aug 2016 - 16:14

Then we'd then need to adopt Promises too. Callbacks have basically been deprecated in modern javascript apis in favor of promises. These allow to serialize dependencies even though code is executed asynchronously. I am not necessarily advocating for a event driven Foswiki kernel. However that's where the argument is heading towards: dependencies among events, that's done using promises in javascript.

-- MichaelDaum - 09 Aug 2016

First of all, what I forgot to point at is that I wanna let the new plugins to customize Foswiki up to complete rewrite of some components.

Dynamics: I don't think there gonna be a lot of action. Most likely no more, than Foswiki::Plugins currently does. Plus, some of it might be avoided by caching the results of ordering/inheriting. The cache could be rebuilt by configure whenever possible.

Of course, ordering is not necessary for registered macros. Sorry if that wasn't clear. smile But otherwise as we cannot afford huge core rewrite. Foswiki still suffering from procedural style legacy.

Class inheritance came to my mind when I was thinking about different storage support as well as other kind of backends. For example, store attribute of application is currently initialized using configuration key {Store}{Implementation}:

    default => sub {
        my $storeClass = $Foswiki::cfg{Store}{Implementation}
          || 'Foswiki::Store::PlainFile';
        ASSERT( $storeClass, "Foswiki::store base class is not defined" )
          if DEBUG;
        return $_[0]->create($storeClass);
    },

But if we replace it with simple:

    default => sub { $_[0]->create('Foswiki::Store'); },

Then a plugin may claim Foswiki::Store implementation completely transparently to the core. I'm not even sure if any kind of event-driven or asynchronous approach would do well for this. Yes, in a way it'd be great to throw an event of 'Save' kind and have it processed by two different plugins and have a page duplicated onto two different storages. But then I foresee more problems then solutions here.

On other hand, an event-driven core is the way to a distributed one – in other words, it's the way for clusterisation support. But do we have resources to step onto this way? As a matter of fact it means to replace our currently monolithic core with a kind of micro-kernel.

A thing I didn't understand is about the parse tree and plugins sharing it. Michael, may I ask you to get deeper into this?

-- VadimBelman - 09 Aug 2016

One other thing to think about. Sometimes you want to replace a method or class. But in the area of authentication and mappers, a hierarchy of would be desirable. Try LDAP 1, try LDAP 2, fall back to local password, fall back to base mapper. Our current implementation can only have the fallback to base mapper, nothing more complex.

I suppose even with the Store there might be cases where multiple stores would be desirable.

-- GeorgeClark - 09 Aug 2016

Vadmi, a parse tree would be a data structure being build up starting from TML and templates which finally is linearized into its final form to be transmitted to the browser. The final form would be HTML or any other requested content type. The problem with Foswiki's current approach is that each extension and component has to make up its own mind about the source data structure: there is no shared parse tree.

Makros are parsed properly. For them, there is a parse tree being processed implicitly while traversing them inside-out-left-to-right. A more broader approach would encompass all other structured data as well: typographic markup but also named sections, noautolink, sticky, literal areas and - last but not least - (H)TML tables.

Besides the core's internal table parser all table plugins analyze TML to find the same table yet again. This is duplicate code and wasted CPU time. Let alone their findings are not shared with other plugins. They too would have to parse TML partially again. While we do have Foswiki::Tables::Reader, it is instantiated multiple times on the same content. This reader has to read the same structure multiple times just because there are multiple plugins being interested in the same table.

This is a conceptually weak.

Better would be to build up a full parse tree and cache it for further manipulation in a shared closure.

Problems don't stop here: have a simple counter on how often the core calls Foswiki::Meta::expandMacros() and Foswiki::Meta::renderText(). This happens far too often for small bits of content being build up during a request and all too often on the same fragments again and again just to make sure there are no macros and TML left unparsed.

Basically, the approach to plugin interaction could be centered around a shared closure with its most important data structure being a parse tree of content being build up so far.

Hm, maybe parse trees are more a kind of object that the store delivers in a cached fashion. There is a preliminary work inside Foswiki that goes in that direction: the Foswiki::MetaCache. However, this code is not part of the store but tightly embedded into Foswiki::Search. It actually is only used there and nowhere else. I've been going down this rabbit hole at Tasks.Item14067 just to find out how badly broken and a misconception it is like it is....

-- MichaelDaum - 10 Aug 2016

Hm, this is a lot you want! Writing a full fledged parser for TML and is something I wanted too. Unfortunately, it's almost 20 years ago as I wrote my last grammar parser... But from that experience I clearly remember that we hardly can have a BNF parser for Foswiki syntax. And this is where my internal Lavr steps in and claims that some plugins would break! wink For example, some are using syntax like:

%MACRO{...}%
...
%ENDMACRO%

Where actually ENDMACRO can be any word. Others are parsing their parameters manually and may not follow common Foswiki parameter syntax. And so on...

Generally it is possible to provide some level of support for any non-standard syntax by allowing a plugin to provide a set of flags/parameters for a macro it registers. Something like:

    $this->registerTAGHandler(
        name => 'MACRO',
        isBlock => 1,
        closingMacro => 'ENDMACRO',
        rawParamStr => 1,
        handler => sub { ... },
    );

But it means that storage cannot provide you a parsed tree. Actually, it shall not anyway. If we think in terms of microservice approach then storage is a clear candidate to get it's own service. In this case it's job would be only to send back topic text and metadata.

-- VadimBelman - 10 Aug 2016

Re: event model in JS - yes, Michael, Promises are needed.

Re: "Better would be to build up a full parse tree and cache it for further manipulation in a shared closure"

Ouch. Major ouch. The strength (and weakness) of TML is the dynamic nature of the markup. A macro expansion might in turn expand to markup that is well structured (totally contained within the macro container) - IF you are really lucky. If you are not so lucky, it can expand to TML fragments that only become interesting when they are combined with other fragments - the classic %%A%%B%% problem. Further, plugin callbacks, when processed, can completely transform the input text, in an entirely unpredictable way - for example, based on time, on data coming from an external source, even on user input. To be useful your parse tree would basically have to record an executable model of the expansion - which is pretty much the same thing as simply re-running it (which is effectively what JS does. In most apps, the static DOM parse is then heavily mutated by context-sensitive event callbacks. It works for JS because the basic DOM parse tree captures 99% of the final HTML. In Foswiki, a static parse will capture more like 9%)

I'm not saying it can't be done - we've discussed it in the past, and we know it's feasible - just that it's an enormous amount of work. You could reduce the workload by constraining TML (and plugins) to respect container boundaries, but that would be a fundamental change to the way Foswiki works.

The idea behind the tables parser is that of different views of the source text. A view is a static parse tree that focuses on a subset of the input that you are specifically interested in - in the case of the tables parser, it identifies and extracts table syntax from the raw text. The massive weakness with this idea is that it has to assume that all information necessary to build the parse tree is available from the source text, and as discussed above it may not be.

-- Main.CrawfordCurrie - 11 Aug 2016 - 09:25

Moving from plain text transformations towards tree transformations is indeed a major effort. However, I don't see a any other solution to preventing re-re-parsing the same text over and over again. The same problem does any wysiwyg editor have on the frontpage.

-- MichaelDaum - 11 Aug 2016

I'm not quite clear why you have a problem with re-parsing the text. Is it your perception that the parsing process is particularly slow? Because my feeling is that the parser is actually pretty good, and that the problem lies with the macros. Pre-compilation of templates, and pre-expansion of invariant content, are two relatively simple optimisations that have been going begging for a long time now.

-- Main.CrawfordCurrie - 11 Aug 2016 - 10:34

As usual, the discussion headed into quite different direction... The question of parsing TML/macros will eventually be answered in the most simple way: if we don't provide data backward compatibility then we cannot use it.

I'm afraid that efficient tree implementation is not possible. That wouldn't be a problem if macro-returned string is guaranteed to be self-contained and can simply be parsed into a valid subtree. But as soon as two adjacent macros output would require complete re-parse the final productivity gain becomes quite doubtful.

-- VadimBelman - 11 Aug 2016

I have refreshed my memory about Promises and their Perl implementation. I have big doubts about their usability in Foswiki core.

  • Promises are about asynchronous programming. Most of Foswiki internals are straightforward processing – more of a conveyor than McDonald's counter.
  • They're bound to event loops. I see no event loop in Foswiki. More than that, Foswiki app is a callback on HTTP server event loop.

The only limited use for them I see is in Foswiki::Net for speeding up parallel fetching from outside resources.

With those considerations in mind I would start implementing this proposal. It's just one thing I'd like to get discussed before proceeding: would we consider using the full power of Moose? The immediate outcome of this decision would be significant slowdown on startup for our command line utilities like configure and the test framework. On the bright side we might get more freedom of manipulations with a class metadata possibly allowing for per-application class inheritance chains. I'm still to investigate if Moose allows such things and therefore not sure if we'd ever need it.

-- VadimBelman - 16 Aug 2016

Would it only be a slowdown of CLI / shell scripts, or would it also impact plain old CGI? I'd be concerned about a CGI slowdown, as that's often the first user experience with Foswiki.

-- GeorgeClark - 16 Aug 2016

I think it would have some impact on CGI too. How significant would it be is yet to be determined. But in either case it seems that switching over to Moose would require some additional work on Moo/Moose compatibility. Moo not just simpler and faster but it implements some features not provided in Moose by default.

-- VadimBelman - 17 Aug 2016

I am far from understanding which specific class manipulation feature you get from Moose not available in Moo, nor do I see the need. But I am worried you are missing one thing: KISS - the simpler the better.

I am also worried that developing plugins becomes more complicated, more than needed. Could you please shed some light on this before going ahead? Maybe a new EmptyPlugin example helps. Thanks.

-- MichaelDaum - 17 Aug 2016

+1 for the example EmptyPlugin suggestion.

You might want to consider branching from Item13897 branch to an experimental branch for the plugin rewrite. Your new OO core is pretty stable now, and another major rewrite might be a bit of a setback. Could you preserve both the old Plugin model and the new OO one?

As for converting from Moo to Moose. That probably deserves its own discussion; What benefits does it bring vs. the current Moo based core, and why is it required.

-- GeorgeClark - 17 Aug 2016

The discussion is back on track and I like it. wink

To Clark: I thought about branching too. To my taste experimental branching off an experimental branch sounds a bit weird. But reasonable. Must complete one more Plack test suite as an example before switching to this task.

The old plugins are to be in place for as long as we need them. This actually puts me into the problem about naming the new subsystem. Once named in the experimental branch it would remain forever. And I'm still not sure how to call it.

To Michael: It looks like you're probably right. Moose doesn't provide the functionality I looked for. I knew it allows altering of superclasses but unfortunately it doesn't apply to around modifier. Similar functionality can be achieved by using roles, but this is what could be done with Moo too. I'll test it later.

What is related to a sample EmptyPlugin – I'm not sure I can provide it until the whole thing would get its final shape. But I'm gonna try explaining better what was on my mind when every feature was considered.

Back to more generic considerations. The proposed model doesn't make things complicated unless necessary. I don't focus on class replacement but consider it a very useful feature. Will get back to this later. Otherwise the proposal pretty much includes what we have now:

  • Registering custom tags.
  • Callbacks – similar to current plugin handlers but more flexible.

Use of modifier subs could even make plugin code more readable. For example, a custom tag could be registered like this:

macro 'CUSTOMACRO' => sub {
    my $this = shift;

    ...; # handle the macro
};

Or a simple callback could be registered by:

callback 'Foswiki::App::postConfig' => sub {
    my $this = shift;
    my %params = @_;

    ...;
};

The other two features which might be think of as complications though comes as a result to find solution for a really complicated task of redefining and extending some core functionality. Where it matters most are alternate stores for webs/topics, configuration, and static content. Some could think of alternate caching solution. Or providing some extra functionality to Foswiki::Meta.

Lets look at configuration. Is there any way to easily move it into a database? A way to have a plugin to implement this? No. Ok. Once there were an idea of having some kind of backend for this but that's it. What could be done within proposed model? We can use get/set methods on Foswiki::Config and make them the default way to access the config parameters. And read/write for caching the whole config and flushing the cache when needed. In this case all we need to allow a plugin implement it's own store for the config is to override these four methods. This is where pluggable methods comes handy.

The class replacement is for more complicated cases. In the example above I said nothing about %Foswiki::cfg which would for some time be existent for compatibility. It is currently serves as an alias for configuration object data attribute and is been set by assignGLOB() method which is not mentioned as a pluggable. A problem about aliasing to the data attribute is that if configuration is not prefetched from the DB then the global hash would be just empty. In this case it would be possible to inherit from the Foswiki::Config class, override the four methods above and additionally override the assignGLOB() method in a way that instead of aliasing to the attribute it becomes a tied hash working with config object set/get methods.

Additionally, we have a number of Implementation keys in the configuration. Those keys require manual definition by an admin using the configure tool. Instead, the admin may simply enable/disable corresponding plugins. Yet, for entities like Log stack of classes would allow simultaneous logging to several different destinations. For Store – using different stores for different webs.

And it is not that complex as it may seem. I would even put it that way: sometimes it's harder to get around limitations imposed by 'simple to use' subsystem which is only simple when one uses it for what it was designed for. Otherwise I see no complexity in having lines like these:

class_override 'Foswiki::Logger' => 'Foswiki::Contrib::MyPlugin::Logger';
class_inherit 'Foswiki::Config' => 'Foswiki::Contrib::MyPlugin::Config';

in a plugin module. Writing the classes is no more complex than writing any code.

After all, this is Perl, so – TMTOWTDI.

-- VadimBelman - 17 Aug 2016

Vadmin, please have a look at EmptyPlugin.pm. Could you please draft one to illustrate what you are aiming at?

-- MichaelDaum - 31 Aug 2016

I'm more like experimenting for now. The final thing might be somewhat different from the original plan due to efficiency matters. The draft would resemble this topic for now.

I hope to get a working example, not just a template.

-- VadimBelman - 31 Aug 2016
 
Topic revision: r25 - 14 Nov 2016, MichaelDaum - This page was cached on 22 Mar 2018 - 08:22.

The copyright of the content on this website is held by the contributing authors, except where stated elsewhere. See Copyright Statement. Creative Commons License