NAME

MooX::PluginKit - A comprehensive plugin system.

INTRODUCTION

PluginKit provides a simple interface for creating plugins and consuming those plugins.

PluginKit is comprised of two main pieces: the plugins, and the classes which consume the plugins. A plugin is just a regular old Moo::Role with some extra (optional) metadata, and the consumers of plugins are regular old Moo classes.

But, what makes this all interesting and useful is the intersection of the two primary features provided by this module.

  • Plugins are contextual, in that they may choose which classes they apply to.

  • Plugins may include other plugins.

This means that you can make groups of plugins which apply to various classes in a hierarchy.

IN THE REAL WORLD

What fun would a fancy idea be if it had no real world applications!

Here's some real-world plugins/things done-right and done-wrong and how PluginKit could play a role in them.

NOTE: This section is purely the opinion of the author. If you disagree or have better examples/wording/ideas, please let him know!

Catalyst

Catalyst could benefit from this module. Currently extending the context class, the request class, and the response class all require separate directives manually entered in by the end-user of Catalyst. Its really a lot of mess when a feature is implemented by, for example, both a request and response plugin. If you forget one, the whole thing breaks. Here's a fake example of how things currently look:

use Catalyst (
  SomeContextPlugin
);
use CatalystX::RoleApplicator;
__PACKAGE__->apply_request_class_roles(
  SomeRequestPlugin
);
__PACKAGE__->apply_response_class_roles(
  SomeResponsePlugin
);
# Note, there is also apply_engine_class_roles, apply_dispatcher_class_roles
# and apply_stats_class_roles.

Oh my! Let's say we start off by using a PluginKit plugin's ability to declare which classes they belong to. Then we could just do this:

use Catalyst (
  SomeContextPlugin
  SomeRequestPlugin
  SomeResponsePlugin
);

And then one step further if those three plugins, when used together, comprised a single feature then we could just:

use Catalyst (
  SomePlugin
);

If Catalyst used PluginKit then all the user would have to do is declare that they use the root plugin and the rest of the plugins would automatically find their way into the appropriate classes.

Starch

Starch is the inspiration for this module. It has a home-grown plugin system very similar, but inferior, to PluginKit. Starch has complex plugins which alter the behavior of various systems (classes) within Starch. For a simple example, Starch::Plugin::Trace injects around() modifiers within 3 different classes at object construction time. And all a user need to do is:

my $starch = Starch->new( plugins=>['::Trace'] );

Without a plugin system to hide away these complexities the user would have been exposed to implementation details and would leave gaping holes for humans to make errors.

Test::WWW::Mechanize::PSGI

Yay! Fake a web server with Test::WWW::Mechanize::PSGI! Thats awesome. But, uh, what if you want the ::PSGI but don't need the Test::. What if you don't want the ::Mechanize:: and want just LWP::UserAgent::PSGI? Wouldn't it be nice if you could just do:

my $ua = LWP::UserAgent->new( plugins=>['::PSGI', '::Test'] );

PluginKit would make this super simple to support. Also there is a god awful number CPAN module subclasses and subclasses of those subclasses on CPAN. Its a world of hurt with no flexibility. It has made me cry a few times.

CREATING PLUGINS

Basics

The most minimal plugin is a Moo::Role:

package MyApp::Plugin::Foo;
use Moo::Role;

But if that is all you are doing then you're just using PluginKit as a tool to apply roles at run-time. That's cool and all, but PluginKit can do so much more.

Bundling

Let's include another plugin in this plugin:

use MooX::PluginKit::Plugin;
plugin_includes 'MyApp::Plugin::Foo::Bar';

We could also write that using a relative (to the including plugin) plugin name:

plugin_includes '::Bar';

plugin_includes takes a list, so you may include multiple plugins. Gnarly, we can package together plugins! Yes, you could get the same effect with a simple "with" in Moo, but then you wouldn't get the contextual nature of PluginKit plugins as described next.

Contextual

Take everything you learned so far and throw this awesome bomb at it:

plugin_applies_to 'MyApp::SomeClass';

Did you hear the mic drop? Maybe not, so let me explain it for ya. You can create groups of plugins (even groups of groups of groups of plugins) and each plugin, at any level, can declare what kinds of classes it (and its included plugins) applies to. This means you can tell your end-user "use plugin X" and behind the scenes they could potentially be using dozens of plugins applied dynamically to dozens of classes. This makes something that is normally hard and complex for the end-user something that only the plugin author needs to deal with and can tightly control.

Is it a good idea to write a dozen plugins and apply them to a dozen classes? Probably not! Would it be fun to write a dozen plugins and dynamically apply them to a dozen classes at run time without the user's knowledge? Heck ya!

Note that when you specify the plugin_applies_to you can provide a package name, a regex, an array ref of method names (aka duck type), or a custom subroutine reference.

Read more about implementing plugins at MooX::PluginKit::Plugin.

CONSUMING PLUGINS

You've got a few options here, but the typical way to consume plugins involves enabling it on the class people use as the main entry point to your library.

Plugins Argument

MooX::PluginKit::Consumer, when used sets the subclass, applies a role, and exports some candy functions to make creating a plugin consuming class simple as pie.

To make your class accept a plugins argument it goes something like this (well, actually, exactly like this):

package MyApp;
use Moo;
use MooX::PluginKit::Consumer;

This class now supports the plugins argument when calling new(), like so:

my $app = MyApp->new( plugins=>[...] );

Object Attributes

But, there is more! Your objects often refer to other objects, right? Those other objects shouldn't be left out of the plugin goodness, so open up your arms and hug them in!

has_pluggable_object foo => (
  class => 'MyApp::Foo',
);

has_pluggable_object takes many of the same arguments as "has" in Moo. When setup like above, rather than passing an object as the argument you'd pass a hashref which will be automatically coerced into an object with all relevant plugins applied. If you'd like to default the object you can with something like this:

has_pluggable_object foo => (
  class   => 'MyApp::Foo',
  default => sub{ {} },
);

See more at "has_pluggable_object" in MooX::PluginKit::Consumer.

Relative Plugin Namespace

If your user specifies a plugin starting with :: that means the plugin is relative. By default it will be relative to your consuming class name, so if your class is MyApp and the user wants to apply the ::Foo plugin then that will resolve to the MyApp::Foo plugin. Unlike many things in life, you can change this:

plugin_namespace 'MyApp::Plugin';

Now if the user specified ::Foo as a plugin it would resolve to MyApp::Plugin::Foo.

See more at "plugin_namespace" in MooX::PluginKit::Consumer.

The Factory

Alternatively you are welcome to use MooX::PluginKit::Factory directly. It is a more direct and lower level interface to popping out objects with plugins applied, so its considered a power-user tool.

TODO

Use Coercion

The "has_pluggable_object" in MooX::PluginKit::Consumer function jumps through a bunch of hoops due to the fact that "coerce" in Moo subroutines do not get access to the instance that the value is being set on. Due to this we create two accessors, one which acts as the writer, and the other which acts as the object builder and reader.

This design makes it difficult to support common "has" in Moo arguments such as predicate and clearer, etc. For now the design of has_pluggable_object has been limited somewhat so that we don't have to come back later and make backwards-incompatible changes.

Cleanly Alter Constructor

Its totally funky that MooX::PluginKit::Consumer sets MooX::PluginKit::ConsumerBase as the base class. This is only done because when calling new with plugins changes the class name that new is being called on, which means we need to change the behavior of new itself to return the object blessed into a different package than it was called with.

The problem is that Method::Generator::Constructor, a part of Moo, throws exceptions if you try to alter the behavior of new with an around() modifier or somesuch. So, to circumvent these exceptions we use a non-Moo parent class with a custom new, but then Moo gets into this mode where it acts slightly differently because its inheriting from a non-Moo class. For example, when inheriting from a non-Moo class in Moo you don't get a BUILDARGS. Despite that, BUILDARGS support has been shimmed in, but there may be other non-Moo Moo issues.

It would be nice to find a fix for this as I expect it might bite someone.

Document Core Library

The MooX::PluginKit::Core library contains a bunch of functions for low-level interaction with plugins and consumers. This API should be formalized with documentation, once it is in a final state that can be relied on to not change much. For now, don't use anything in there directly.

AUTHOR

Aran Clary Deltac <bluefeet@gmail.com>

ACKNOWLEDGEMENTS

Thanks to ZipRecruiter for encouraging their employees to contribute back to the open source ecosystem. Without their dedication to quality software development this distribution would not exist.

LICENSE

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.