Dist::Zilla::Plugin::Beam::Connector - Connect events to listeners in Dist::Zilla plugins.
version 0.001003
[Some::PluginA / PluginA] [Some::PluginB / PluginB] [Beam::Connector] ; PluginA emitting event 'foo' passes the event to PluginB on = plugin:PluginA#foo => plugin:PluginB#handle_foo on = plugin:PluginA#bar => plugin:PluginB#handle_bar ; Load 'beam.yml' as a Beam::Wire container container = beam.yml ; Handle Dist::Zilla plugin events with arbitrary classes ; loaded by Beam::Wire on = plugin:PluginA#foo => container:servicename#handle_foo on = plugin:PluginA#bar => container:otherservicename#handle_bar
This module aims to allow Dist::Zilla to use plugins using Beam::Event and Beam::Emitter, and perhaps reduce the need for massive amounts of composition and role application proliferating CPAN.
Dist::Zilla
Beam::Event
Beam::Emitter
CPAN
This is in lieu of a decent dependency injection system, and is presently relying on Dist::Zilla to load and construct the plugins itself, and then you just connect the plugins together informally, without necessitating each plugin be specifically tailored to the recipient.
Hopefully, this may also give scope for non-dzil plugins being loadable into memory some day, and allowing message passing of events to those plugins. ( Hence, the plugin: prefix )
dzil
plugin:
A Real World Example of what a future could look like?
[GatherDir] [Test::Compile] [Beam::Connector] on = plugin:GatherDir#collect => plugin:Test::Compile#generate_test
GatherDir in this example would build a mutable tree of files, attach them to an event ::GatherDir::Tree, and pass that event to Test::Compile#generate_test, which would then add ( or remove, or mutate ) any files in that tree.
GatherDir
::GatherDir::Tree
Test::Compile#generate_test
Tree state mutation then happens in order of prescription, in the order given by the various on declarations.
on
Thus, a single plugin can be in 2 places in the same logical stage.
[Beam::Connector] on = plugin:GatherDir#collect => plugin:Test::Compile#generate_test ; lots more collectors here on = plugin:GatherDir#collect => plugin:Test::Compile#finalize_test
Whereas presently, order of affect is either governed by:
phase - where you can add but not remove or mutate, mutate but not add or remove, remove, but not add or mutate
plugin order - where a single plugin cant be both early in a single phase and late
If that example is not convincing enough for you, consider all the different ways there are presently for implementing [MakeMaker]. If you're following the standard logic its fine, but as soon as you set out of the box, you have a few things you're going to have to do instead:
[MakeMaker]
Subclass MakeMaker in some way
MakeMaker
Re-implement MakeMaker in some way
Fuss a lot with phase ordering and then inject code in the File that MakeMaker generates.
File
These approaches all work, but they're an open door to everyone re-implementing the same thing thousands of times over.
[MakeMaker] [DynamicPrereqs] -phases = none [Beam::Connector] on = plugin:MakeMaker#collect_augments => plugin:DynamicPrereqs#inject_augments
MakeMaker here can just create an event, pass it to DynamicPrereqs, DynamicPrereqs can inject its desired content into the event, and then MakeMaker can integrate the injected events at "wherever" the right place for them is.
event
DynamicPrereqs
This is much superior to scraping the generated text file and injecting events at a given place based on a RegEx match.
RegEx
container
Allows loading an arbitrary Beam::Wire container specification, initializing the relevant objects lazily, and connecting them to relevant events emitted by dzil plugins.
Beam::Wire
[Beam::Connector] container = inc/dist_beam.yml
The value can be a path to any file name that Beam::Wire->new( file => ... ) understands, (which itself is any file name that Config::Any->load_files understands).
Beam::Wire->new( file => ... )
Config::Any->load_files
Items in loaded container can then be referred to by their identifiers to the on parameter in the form
container:${name}#${method}
For example:
[Beam::Connector] container = inc/dist_beam.yml on = plugin:GatherDir#gather_files => container:file_gatherer#on_gather_files
This would register the object called file_gatherer inside the container to be a recipient of any events called gather_files emitted by the plugin named GatherDir
file_gatherer
gather_files
Defines a connection between an event emitter and a listener.
The general syntax is:
on = emitterspec => listenerspec
Where emitterspec and listenerspec are of the form
emitterspec
listenerspec
objectnamespace:objectname#connector
objectnamespace
There are presently two defined object name-spaces.
plugin: Resolves objectname to a Dist::Zilla plugin by its name identifier
plugin
objectname
name
container: Resolves objectname to an explicitly named object inside an associated container
connector
For an emitter, the connector property identifies the name of the event that is expected to be emitted by that emitter
emitter
For a listener, the connector property identifies the name of a method that is expected to receive the event.
listener
method
Adding support for hookable events in new and existing Dist::Zilla plugins is relatively straight-forward, and uses Beam::Emitter
# Somewhere after `use Moose` with "Beam::Emitter";
And your class is now ready to broadcast events, and plugins are now able to hook events. Even though they don't exist yet.
But that's not very useful in itself. You need to find good places in your code to write events, and construct little bundles of state, "messages" to pass around, and perhaps, allow modifying.
You want to start off designing an event class that communicates the absolute minimum required to be useful.
Carrying too much state, or too much indirect state is the enemy.
For instance, it would generally be unwise to design an Event that you passed to something which carried a $zilla instance with it.
$zilla
You want to make it as obscure as possible who is even sending the event, as the contents of the event should be usable in total isolation, because you have no idea where your events are going to get sent ( because that is outside the scope of your plugin ), and receivers have no solid expectations of where events are going to come from ( because that is dictated by the connector ).
It is presently recommended you define these events inline somewhere, either in the plugin that emits them, or in some shared container.
The recommended namespace scheme to follow is:
Dist::Zilla::Event::
Preferably, structuring it similar to your plugin
Dist::Zilla::Plugin::Thing::Dooer Dist::Zilla::Event::Thing::Dooer::BeforeDoingThing
This I'm sure you'll agree is much nicer than
Dist::Zilla::Plugin::Thing::Dooer::BeforeDoingThingEvent # O_O Dist::Zilla::Plugin::BeforeDoingThingEvent # Not a plugin
It is also recommended to NOT index said Event packages at present, as that would encourage people depending on the events at some point, which for this system, is likely unwanted toxicity.
Only people emitting events should be caring about loading the class.
Events themselves are quite straight forward: They're just objects, objects extending Beam::Event.
This is an example event definition: It will communicate a file name it intends to prepend lines to and pass a mutable, empty array for the event handler to inject lines into.
package # hide from PAUSE Dist::Zilla::Event::Prepender::BeforePrepend; use Moose; # or Moo, both work extends "Beam::Event" has 'filename' => ( is => 'ro', isa => Str, required => 1, ); has 'lines' => ( is => 'rw', isa => ArrayRef[Str], lazy => 1, default => sub { [] }, ); __PACKAGE__->meta->make_immutable;
See Using Custom Events in Beam::Emitter for details.
Once you have an Event class designed, gluing it into your code is also quite simple:
# somewhere deep in your plugin my $event = $self->emit( 'before_append', # the "name" of the event, this corresponds to the "connector" # property in Beam::Connector class => 'Dist::Zilla::Event::Prepender::BeforePrepend', # The class to construct an instance of filename => 'lib/Foo.pm', # attribute property of the Event object. );
An instance of class is created with the defined name, and is passed in-order to all the objects who subscribed to the before_append event, and then returned once they're done.
class
before_append
And then you can extract any of the state in the passed object and use it to do your work.
Fortunately, the requirements for an Event Receiver is very low.
If you're using the Dist::Zilla::Plugin/plugin: approach, all that is required is
Dist::Zilla::Plugin
A Valid Dist::Zilla plugin that registers in $zilla->plugins
$zilla->plugins
Some method name of any description that can be passed an argument
For Example:
package My::Plugin; use Moose; with 'Dist::Zilla::Role::Plugin'; sub on_before_append { my ( $self, $event ) = @_; ... }
If you're using the Beam::Wire/container: approach, all that is required is:
container:
A named object
package My::Listener; sub new { bless {}, $_[0] } sub on_before_append { my ( $self, $event ) = @_; ... }
These listeners will do nothing on their own, but have events routed to them by relevant Beam configuration.
Beam
Your method will be called with one argument: The event.
sub on_whatever { my ( $self, $event ) = @_; }
What sort of events you receive of course depends on who sent them.
You can then filter them the same way as you would with any Perl Object, via ->isa etc,
->isa
sub on_whatever { my ( $self, $event ) = @_; if ( $event->isa('Dist::Zilla::Plugin::Prepender::AppenderEvent') ) { } }
But you can identify events by other means, via the ->name property.
->name
sub on_whatever { my ( $self, $event ) = @_; if ( q[before_append] eq $event->name ) ) { } }
You can then read the data of the event, or potentially modify it in-place, to communicate data back to the sender of the event.
sub on_whatever { my ( $self, $event ) = @_; if ( q[before_append] eq $event->name ) ) { push @{$event->lines}, 'use Moose;' if $event->filename =~ /\bMooseX\b/; # Rediculous example I know. } }
But you don't need to return anything from the sub, return values are entirely ignored.
sub
Beam::Event and Beam::Emitter have some tools for controlling intra-event flow, however, their usage is not 100% clear and their API may be subject to change in future.
So I have deleted the relevant instruction on this and it will be resurrected when I'm more sure about how it should be instructed.
Kent Fredric <kentnl@cpan.org>
This software is copyright (c) 2017 by Kent Fredric <kentfredric@gmail.com>.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.
To install Dist::Zilla::Plugin::Beam::Connector, copy and paste the appropriate command in to your terminal.
cpanm
cpanm Dist::Zilla::Plugin::Beam::Connector
CPAN shell
perl -MCPAN -e shell install Dist::Zilla::Plugin::Beam::Connector
For more information on module installation, please visit the detailed CPAN module installation guide.