MooX::PluginKit - A comprehensive plugin system.
package MyApp::Plugin::LogEngine; use Moo::Role; use MooX::PluginKit::Plugin; plugin_applies_to 'MyApp::Engine'; before start => sub{ print "Starting app.\n" }; after stop => sub{ print "Stopped app.\n" }; package MyApp::Engine; use Moo; sub start { ... } sub stop { ... } package MyApp; use Moo; use MooX::PluginKit::Consumer; has_pluggable_object engine => ( class => 'MyApp::Engine', default => sub{ {} }, ); my $app = MyApp->new( plugins => ['LogEngine'], ); $app->engine->start(); # Prints: Starting app.
PluginKit provides a simple interface for creating plugins and consuming those plugins at run-time.
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.
The most minimal plugin is a Moo::Role:
package MyApp::Plugin::Foo; use Moo::Role;
This sort of plugin will apply to any class.
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.
plugin_includes
plugin_applies_to 'MyApp::SomeClass';
This declares that this plugins only applies to the specified class (or subclasses of it). This is where PluginKit's power really shines as it allows plugin authors to transparently decide how and where plugins get applied and plugin users to not need to know the intricate details.
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.
plugin_applies_to
Read more about implementing plugins at MooX::PluginKit::Plugin.
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.
MooX::PluginKit::Consumer, when used sets the subclass, applies a role, and exports some candy functions for building your plugin consuming class.
use
A class which accepts a plugins argument looks like this:
plugins
package MyApp; use Moo; use MooX::PluginKit::Consumer;
This class now supports the plugins argument when calling new(), like so:
new()
my $app = MyApp->new( plugins=>[...] );
has_pluggable_object engine => ( class => 'MyApp::Enging', );
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
has_pluggable_object engine => ( class => 'MyApp::Engine', default => sub{ {} }, );
See more at "has_pluggable_object" in MooX::PluginKit::Consumer.
has_pluggable_class response_class => ( default => 'MyApp::Response', );
This is useful for when you want to dynamically create instances of a class which supports plugins. The value of this attribute will be the composed class name with all plugins applied.
See more at "has_pluggable_class" in MooX::PluginKit::Consumer.
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. This default behavior can be changed:
::
MyApp
::Foo
MyApp::Foo
plugin_namespace 'MyApp::Plugin';
Now if the user specified ::Foo as a plugin it would resolve to MyApp::Plugin::Foo.
MyApp::Plugin::Foo
See more at "plugin_namespace" in MooX::PluginKit::Consumer.
Behind the scenes there is a factory object which does all the heavy lifting of this library. This factory can be accessed as the plugin_factory attribute on consumer classes or an instance of the factory class may be created directly.
plugin_factory
See more at MooX::PluginKit::Factory.
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.
predicate
clearer
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.
Method::Generator::Constructor
around()
new
It would be nice to find a fix for this as I expect it might bite someone.
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.
Aran Clary Deltac <bluefeet@gmail.com>
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.
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
To install MooX::PluginKit, copy and paste the appropriate command in to your terminal.
cpanm
cpanm MooX::PluginKit
CPAN shell
perl -MCPAN -e shell install MooX::PluginKit
For more information on module installation, please visit the detailed CPAN module installation guide.