File Coverage

blib/lib/MooX/Role/Pluggable.pm
Criterion Covered Total %
statement 240 327 73.3
branch 90 164 54.8
condition 14 29 48.2
subroutine 36 42 85.7
pod 16 16 100.0
total 396 578 68.5


line stmt bran cond sub pod time code
1             package MooX::Role::Pluggable;
2             $MooX::Role::Pluggable::VERSION = '1.001002';
3 2     2   18459 use Carp;
  2         4  
  2         144  
4 2     2   9 use strictures 2;
  2         13  
  2         70  
5              
6 2     2   423 use Scalar::Util 'blessed';
  2         2  
  2         125  
7 2     2   1051 use Try::Tiny;
  2         1859  
  2         136  
8              
9 2     2   1038 use Types::Standard -all;
  2         112744  
  2         27  
10              
11 2     2   57055 use MooX::Role::Pluggable::Constants;
  2         4  
  2         158  
12              
13              
14 2     2   8 use Moo::Role;
  2         3  
  2         18  
15              
16              
17             has __pluggable_opts => (
18             lazy => 1,
19             is => 'ro',
20             isa => Dict[
21             reg_prefix => Str,
22             ev_prefix => Str,
23             types => HashRef,
24             ],
25             builder => sub {
26             +{
27 2     2   1365 reg_prefix => 'plugin_',
28             ev_prefix => 'plugin_ev_',
29             types => +{ PROCESS => 'P', NOTIFY => 'N' },
30             },
31             },
32             );
33              
34             has __pluggable_loaded => (
35             lazy => 1,
36             is => 'ro',
37             isa => Dict[
38             ALIAS => HashRef,
39             OBJ => HashRef,
40             HANDLE => HashRef,
41             ],
42             builder => sub {
43             +{
44 2     2   1434 ALIAS => +{}, # Objs keyed by aliases
45             OBJ => +{}, # Aliases keyed by obj
46             HANDLE => +{}, # Type/event map hashes keyed by obj
47             },
48             },
49             );
50              
51             has __pluggable_pipeline => (
52             lazy => 1,
53             is => 'ro',
54             isa => ArrayRef,
55 2     2   1152 builder => sub { [] },
56             );
57              
58              
59             sub _pluggable_destroy {
60 2     2   9 my ($self) = @_;
61 2         7 $self->plugin_del($_) for $self->plugin_alias_list;
62             }
63              
64 0     0   0 sub _pluggable_event {
65             # This should be overriden to handle Pluggable events
66             # ( plugin_{added, removed, error} )
67             }
68              
69             sub _pluggable_init {
70 2     2   396 my ($self, %params) = @_;
71 2         15 $params{lc $_} = delete $params{$_} for keys %params;
72              
73 2 50       9 my $reg_prefix = defined $params{register_prefix} ?
74             $params{register_prefix} : $params{reg_prefix};
75 2 100       8 $self->__pluggable_opts->{reg_prefix} = $reg_prefix
76             if defined $reg_prefix;
77              
78 2 50       29 my $ev_prefix = defined $params{event_prefix} ?
79             $params{event_prefix} : $params{ev_prefix};
80 2 50       6 $self->__pluggable_opts->{ev_prefix} = $ev_prefix
81             if defined $ev_prefix;
82              
83 2 50       5 if (defined $params{types}) {
84             $self->__pluggable_opts->{types} =
85             ref $params{types} eq 'ARRAY' ?
86 2 100       41 +{ map {; $_ => $_ } @{ $params{types} } }
  0 50       0  
  0         0  
87             : ref $params{types} eq 'HASH' ?
88             $params{types}
89             : confess 'Expected ARRAY or HASH but got '.$params{types};
90             }
91              
92             $self
93 1         24 }
94              
95             sub _pluggable_process {
96 24     24   4143 my ($self, $type, $event, $args) = @_;
97              
98             # This is essentially the same logic as Object::Pluggable.
99             # Profiled, rewritten, and tightened up a bit;
100             #
101             # - Error handling is much faster as a normal sub
102             # Still need $self to dispatch _pluggable_event, but skipping method
103             # resolution and passing $self on the arg stack added a few hundred
104             # extra calls/sec, and override seems like an acceptable sacrifice
105             # Additionally our error handler is optimized
106             #
107             # - Do not invoke the regex engine at all, saving a fair bit of
108             # time; checking index() and applying substr() as needed to strip
109             # event prefixes is significantly quicker.
110             #
111             # - Conditionals have been optimized a bit.
112             #
113             # I'm open to other ideas . . .
114 24 100       58 unless (ref $args) {
115 1         10 confess 'Expected a type, event, and (possibly empty) args ARRAY'
116             }
117              
118 23         46 my $prefix = $self->__pluggable_opts->{ev_prefix};
119 23 100       596 substr($event, 0, length($prefix), '') if index($event, $prefix) == 0;
120              
121 23         47 my $meth = $self->__pluggable_opts->{types}->{$type} .'_'. $event;
122              
123 23         487 my ($retval, $self_ret, @extra) = EAT_NONE;
124              
125 23         20 local $@;
126 23 100       128 if ( $self->can($meth) ) {
    100          
127             # Dispatch to ourself
128 17         19 eval {;
129 17         56 $self_ret = $self->$meth($self, \(@$args), \@extra)
130             };
131 17         134 __plugin_process_chk($self, $self, $meth, $self_ret);
132             } elsif ( $self->can('_default') ) {
133             # Dispatch to _default
134 2         3 eval {;
135 2         7 $self_ret = $self->_default($self, $meth, \(@$args), \@extra)
136             };
137 2         13 __plugin_process_chk($self, $self, '_default', $self_ret);
138             }
139              
140 23 100       73 if (! defined $self_ret) {
    50          
    50          
    100          
141             # No-op.
142             } elsif ( $self_ret == EAT_PLUGIN ) {
143             # Don't plugin-process, just return EAT_NONE.
144             # (Higher levels like Emitter can still pick this up.)
145 0         0 return $retval
146             } elsif ( $self_ret == EAT_CLIENT ) {
147             # Plugin process, but return EAT_ALL after.
148 0         0 $retval = EAT_ALL
149             } elsif ( $self_ret == EAT_ALL ) {
150 15         31 return EAT_ALL
151             }
152              
153 8 50       18 if (@extra) {
154 0         0 push @$args, splice @extra, 0, scalar(@extra)
155             }
156              
157 8         20 my $handle_ref = $self->__pluggable_loaded->{HANDLE};
158 8         215 my $plug_ret;
159 8         9 PLUG: for my $thisplug (
160 8         22 grep {;
161 8 100 66     175 exists $handle_ref->{$_}->{$type}->{$event}
162             || exists $handle_ref->{$_}->{$type}->{all}
163             && $self != $_
164             } @{ $self->__pluggable_pipeline } ) {
165 6         8 undef $plug_ret;
166             # Using by_ref is nicer, but the method call is too much overhead.
167 6         14 my $this_alias = $self->__pluggable_loaded->{OBJ}->{$thisplug};
168              
169 6 100       173 if ( $thisplug->can($meth) ) {
    50          
170 5         5 eval {;
171 5         21 $plug_ret = $thisplug->$meth($self, \(@$args), \@extra)
172             };
173 5         39 __plugin_process_chk($self, $thisplug, $meth, $plug_ret, $this_alias);
174             } elsif ( $thisplug->can('_default') ) {
175 1         2 eval {;
176 1         9 $plug_ret = $thisplug->_default($self, $meth, \(@$args), \@extra)
177             };
178 1         8 __plugin_process_chk($self, $thisplug, '_default', $plug_ret, $this_alias);
179             }
180              
181 6 50       28 if (! defined $plug_ret) {
    100          
    50          
    50          
182             # No-op.
183             } elsif ($plug_ret == EAT_PLUGIN) {
184             # Stop plugin-processing.
185             # Return EAT_ALL if we previously had a EAT_CLIENT
186             # Return EAT_NONE otherwise
187 1         3 return $retval
188             } elsif ($plug_ret == EAT_CLIENT) {
189             # Set a pending EAT_ALL.
190             # If another plugin in the pipeline returns EAT_PLUGIN,
191             # we'll tell higher layers like Emitter to EAT_ALL
192 0         0 $retval = EAT_ALL
193             } elsif ($plug_ret == EAT_ALL) {
194 0         0 return EAT_ALL
195             }
196              
197 5 50       16 if (@extra) {
198 0         0 push @$args, splice @extra, 0, scalar(@extra);
199             }
200              
201             } # PLUG
202              
203             $retval
204 7         94 }
205              
206             sub __plugin_process_chk {
207             # Ugly as sin, but fast if there are no errors, which matters here.
208              
209 25 100 100 25   179 if ($@) {
    50 66        
      66        
      33        
210 1         2 chomp $@;
211 1         3 my ($self, $obj, $meth, undef, $src) = @_;
212              
213 1 50       3 my $e_src = defined $src ? "plugin '$src'" : 'self' ;
214 1         3 my $err = "$meth call on $e_src failed: $@";
215              
216 1         5 warn "$err\n";
217              
218 1         4 $self->_pluggable_event(
219             $self->__pluggable_opts->{ev_prefix} . "plugin_error",
220             $err,
221             $obj,
222             $e_src
223             );
224              
225             return
226 1         3 } elsif (! defined $_[3] ||
227             ( $_[3] != EAT_NONE && $_[3] != EAT_ALL &&
228             $_[3] != EAT_CLIENT && $_[3] != EAT_PLUGIN ) ) {
229              
230 0         0 my ($self, $obj, $meth, undef, $src) = @_;
231              
232 0 0       0 my $e_src = defined $src ? "plugin '$src'" : 'self' ;
233 0         0 my $err = "$meth call on $e_src did not return a valid EAT_ constant";
234              
235 0         0 warn "$err\n";
236              
237 0         0 $self->_pluggable_event(
238             $self->__pluggable_opts->{ev_prefix} . "plugin_error",
239             $err,
240             $obj,
241             $e_src
242             );
243              
244             return
245 0         0 }
246             }
247              
248              
249             ## Basic plugin manipulation (add/del/get/replace ...)
250              
251             sub plugin_add {
252 4     4 1 43247 my ($self, $alias, $plugin, @args) = @_;
253              
254 4 50 33     47 confess "Expected a plugin alias and object"
255             unless defined $alias and blessed $plugin;
256              
257 4         18 $self->plugin_pipe_push($alias, $plugin, @args)
258             }
259              
260             sub plugin_alias_list {
261 3     3 1 344 my ($self) = @_;
262 3         4 keys %{ $self->__pluggable_loaded->{ALIAS} }
  3         8  
263             }
264              
265             sub plugin_del {
266 5     5 1 74 my ($self, $alias_or_plug, @args) = @_;
267              
268 5 50       12 confess "Expected a plugin alias"
269             unless defined $alias_or_plug;
270              
271 5         13 scalar( $self->__plugin_pipe_remove($alias_or_plug, @args) )
272             }
273              
274             sub plugin_get {
275 2     2 1 13 my ($self, $item) = @_;
276              
277 2         5 my ($item_alias, $item_plug) = $self->__plugin_get_plug_any($item);
278              
279 2 50       58 unless (defined $item_plug) {
280 0         0 carp ($@ = "No such plugin: $item_alias");
281             return
282 0         0 }
283              
284 2 100       13 wantarray ? ($item_plug, $item_alias) : $item_plug
285             }
286              
287             sub plugin_replace {
288 1     1 1 408 my ($self, %params) = @_;
289 1         8 $params{lc $_} = delete $params{$_} for keys %params;
290              
291             # ->plugin_replace(
292             # old => $obj || $alias,
293             # alias => $newalias,
294             # plugin => $newplug,
295             # # optional:
296             # unregister_args => ARRAY
297             # register_args => ARRAY
298             # )
299              
300 1         3 for (qw/old alias plugin/) {
301 3 50       8 confess "Missing required param $_"
302             unless defined $params{$_}
303             }
304              
305 1         4 my ($old_alias, $old_plug)
306             = $self->__plugin_get_plug_any( $params{old} );
307              
308 1 50       33 unless (defined $old_plug) {
309 0         0 $@ = "No such plugin: $old_alias";
310 0         0 carp $@;
311             return
312 0         0 }
313              
314 0         0 my @unreg_args = ref $params{unregister_args} eq 'ARRAY' ?
315 1 50       5 @{ $params{unregister_args} } : () ;
316              
317 1         3 $self->__plug_pipe_unregister( $old_alias, $old_plug, @unreg_args );
318              
319 1         3 my ($new_alias, $new_plug) = @params{'alias','plugin'};
320              
321 0         0 return unless $self->__plug_pipe_register( $new_alias, $new_plug,
322             (
323             ref $params{register_args} eq 'ARRAY' ?
324 1 50       4 @{ $params{register_args} } : ()
    50          
325             ),
326             );
327              
328 1         1 for my $thisplug (@{ $self->__pluggable_pipeline }) {
  1         3  
329 1 50       25 if ($thisplug == $old_plug) {
330 1         2 $thisplug = $params{plugin};
331             last
332 1         1 }
333             }
334              
335             $old_plug
336 1         5 }
337              
338              
339             ## Event registration.
340              
341             sub subscribe {
342 5     5 1 44 my ($self, $plugin, $type, @events) = @_;
343              
344 5 50       12 confess "Cannot subscribe; event type $type not supported"
345             unless exists $self->__pluggable_opts->{types}->{$type};
346              
347 5 50       132 confess "Expected a plugin object, a type, and a list of events"
348             unless @events;
349              
350 5 50       19 confess "Expected a blessed plugin object" unless blessed $plugin;
351              
352 5   50     14 my $handles
353             = $self->__pluggable_loaded->{HANDLE}->{$plugin}->{$type}
354             ||= +{};
355              
356 5         180 for my $ev (@events) {
357 6 50       56 if (ref $ev eq 'ARRAY') {
358 0         0 $handles->{$_} = 1 for @$ev;
359             next
360 0         0 }
361 6         22 $handles->{$ev} = 1
362             }
363              
364             1
365 5         13 }
366              
367             sub unsubscribe {
368 0     0 1 0 my ($self, $plugin, $type, @events) = @_;
369              
370 0 0       0 confess "Cannot unsubscribe; event type $type not supported"
371             unless exists $self->__pluggable_opts->{types}->{$type};
372              
373 0 0 0     0 confess "Expected a blessed plugin obj, event type, and events to unsubscribe"
374             unless blessed $plugin and defined $type;
375              
376 0 0       0 confess "No events specified; did you mean to plugin_del instead?"
377             unless @events;
378              
379 0   0     0 my $handles =
380             $self->__pluggable_loaded->{HANDLE}->{$plugin}->{$type} || +{};
381              
382 0         0 for my $ev (@events) {
383 0 0       0 if (ref $ev eq 'ARRAY') {
384 0         0 for my $this_ev (@$ev) {
385 0 0       0 unless (delete $handles->{$this_ev}) {
386 0         0 carp "Nonexistant event $this_ev cannot be unsubscribed from";
387             }
388             }
389             } else {
390 0 0       0 unless (delete $handles->{$ev}) {
391 0         0 carp "Nonexistant event $ev cannot be unsubscribed from";
392             }
393             }
394              
395             }
396              
397             1
398 0         0 }
399              
400              
401             ## Pipeline methods.
402              
403             sub plugin_pipe_push {
404 4     4 1 7 my ($self, $alias, $plug, @args) = @_;
405              
406 4 50       15 if (my $existing = $self->__plugin_by_alias($alias) ) {
407 0         0 $@ = "Already have plugin $alias : $existing";
408 0         0 carp $@;
409             return
410 0         0 }
411              
412 4 50       131 return unless $self->__plug_pipe_register($alias, $plug, @args);
413              
414 4         5 push @{ $self->__pluggable_pipeline }, $plug;
  4         14  
415              
416 4         100 scalar @{ $self->__pluggable_pipeline }
  4         11  
417             }
418              
419             sub plugin_pipe_pop {
420 0     0 1 0 my ($self, @args) = @_;
421              
422 0 0       0 return unless @{ $self->__pluggable_pipeline };
  0         0  
423              
424 0         0 my $plug = pop @{ $self->__pluggable_pipeline };
  0         0  
425 0         0 my $alias = $self->__plugin_by_ref($plug);
426              
427 0         0 $self->__plug_pipe_unregister($alias, $plug, @args);
428              
429 0 0       0 wantarray ? ($plug, $alias) : $plug
430             }
431              
432             sub plugin_pipe_unshift {
433 1     1 1 3 my ($self, $alias, $plug, @args) = @_;
434              
435 1 50       4 if (my $existing = $self->__plugin_by_alias($alias) ) {
436 0         0 $@ = "Already have plugin $alias : $existing";
437 0         0 carp $@;
438             return
439 0         0 }
440              
441 1 50       34 return unless $self->__plug_pipe_register($alias, $plug, @args);
442              
443 1         2 unshift @{ $self->__pluggable_pipeline }, $plug;
  1         3  
444              
445 1         22 scalar @{ $self->__pluggable_pipeline }
  1         3  
446             }
447              
448             sub plugin_pipe_shift {
449 1     1 1 2 my ($self, @args) = @_;
450              
451 1 50       1 return unless @{ $self->__pluggable_pipeline };
  1         3  
452              
453 1         34 my $plug = shift @{ $self->__pluggable_pipeline };
  1         3  
454 1         23 my $alias = $self->__plugin_by_ref($plug);
455              
456 1         24 $self->__plug_pipe_unregister($alias, $plug, @args);
457              
458 1 50       5 wantarray ? ($plug, $alias) : $plug
459             }
460              
461             sub __plugin_pipe_remove {
462 5     5   6 my ($self, $old, @unreg_args) = @_;
463              
464 5         10 my ($old_alias, $old_plug) = $self->__plugin_get_plug_any($old);
465              
466 5 50       119 unless (defined $old_plug) {
467 0         0 $@ = "No such plugin: $old_alias";
468 0         0 carp $@;
469             return
470 0         0 }
471              
472 5         5 my $idx = 0;
473 5         6 for my $thisplug (@{ $self->__pluggable_pipeline }) {
  5         15  
474 6 100       111 if ($thisplug == $old_plug) {
475 5         4 splice @{ $self->__pluggable_pipeline }, $idx, 1;
  5         12  
476             last
477 5         94 }
478 1         2 ++$idx;
479             }
480              
481 5         13 $self->__plug_pipe_unregister( $old_alias, $old_plug, @unreg_args );
482              
483 5 50       38 wantarray ? ($old_plug, $old_alias) : $old_plug
484             }
485              
486             sub plugin_pipe_get_index {
487 9     9 1 343 my ($self, $item) = @_;
488              
489 9         15 my ($item_alias, $item_plug) = $self->__plugin_get_plug_any($item);
490              
491 9 50       264 unless (defined $item_plug) {
492 0         0 $@ = "No such plugin: $item_alias";
493 0         0 carp $@;
494 0         0 return -1
495             }
496              
497 9         9 my $idx = 0;
498 9         7 for my $thisplug (@{ $self->__pluggable_pipeline }) {
  9         21  
499 13 100       223 return $idx if $thisplug == $item_plug;
500 4         5 $idx++;
501             }
502              
503 0         0 return -1
504             }
505              
506             sub plugin_pipe_insert_before {
507 1     1 1 4 my ($self, %params) = @_;
508 1         8 $params{lc $_} = delete $params{$_} for keys %params;
509             # ->insert_before(
510             # before =>
511             # alias =>
512             # plugin =>
513             # register_args =>
514             # );
515              
516 1         3 for (qw/before alias plugin/) {
517 3 50       7 confess "Missing required param $_"
518             unless defined $params{$_}
519             }
520              
521 1         4 my ($prev_alias, $prev_plug)
522             = $self->__plugin_get_plug_any( $params{before} );
523              
524 1 50       33 unless (defined $prev_plug) {
525 0         0 $@ = "No such plugin: $prev_alias";
526 0         0 carp $@;
527             return
528 0         0 }
529              
530 1 50       4 if ( my $existing = $self->__plugin_by_alias($params{alias}) ) {
531 0         0 $@ = "Already have plugin $params{alias} : $existing";
532 0         0 carp $@;
533             return
534 0         0 }
535              
536 0         0 return unless $self->__plug_pipe_register(
537             $params{alias}, $params{plugin},
538             (
539             ref $params{register_args} eq 'ARRAY' ?
540 1 50       27 @{ $params{register_args} } : ()
    50          
541             )
542             );
543              
544 1         1 my $idx = 0;
545 1         2 for my $thisplug (@{ $self->__pluggable_pipeline }) {
  1         3  
546 2 100       27 if ($thisplug == $prev_plug) {
547 1         2 splice @{ $self->__pluggable_pipeline }, $idx, 0, $params{plugin};
  1         3  
548             last
549 1         20 }
550 1         2 $idx++;
551             }
552              
553             1
554 1         5 }
555              
556             sub plugin_pipe_insert_after {
557 1     1 1 6 my ($self, %params) = @_;
558 1         10 $params{lc $_} = delete $params{$_} for keys %params;
559              
560 1         6 for (qw/after alias plugin/) {
561 3 50       10 confess "Missing required param $_"
562             unless defined $params{$_}
563             }
564              
565 1         5 my ($next_alias, $next_plug)
566             = $self->__plugin_get_plug_any( $params{after} );
567              
568 1 50       49 unless (defined $next_plug) {
569 0         0 $@ = "No such plugin: $next_alias";
570 0         0 carp $@;
571             return
572 0         0 }
573              
574 1 50       4 if ( my $existing = $self->__plugin_by_alias($params{alias}) ) {
575 0         0 $@ = "Already have plugin $params{alias} : $existing";
576 0         0 carp $@;
577             return
578 0         0 }
579              
580 0         0 return unless $self->__plug_pipe_register(
581             $params{alias}, $params{plugin},
582             (
583             ref $params{register_args} eq 'ARRAY' ?
584 1 50       49 @{ $params{register_args} } : ()
    50          
585             ),
586             );
587              
588 1         1 my $idx = 0;
589 1         6 for my $thisplug (@{ $self->__pluggable_pipeline }) {
  1         3  
590 1 50       26 if ($thisplug == $next_plug) {
591 1         1 splice @{ $self->__pluggable_pipeline }, $idx+1, 0, $params{plugin};
  1         3  
592             last
593 1         24 }
594 0         0 $idx++;
595             }
596              
597             1
598 1         4 }
599              
600             sub plugin_pipe_bump_up {
601 1     1 1 485 my ($self, $item, $delta) = @_;
602              
603 1         6 my $idx = $self->plugin_pipe_get_index($item);
604 1 50       3 return -1 unless $idx >= 0;
605              
606 1   50     4 my $pos = $idx - ($delta || 1);
607              
608 1 50       3 unless ($pos >= 0) {
609 0         0 carp "Negative position ($idx - $delta is $pos), bumping to head"
610             }
611              
612 1         3 splice @{ $self->__pluggable_pipeline }, $pos, 0,
  1         21  
613 1         1 splice @{ $self->__pluggable_pipeline }, $idx, 1;
614              
615 1         20 $pos
616             }
617              
618             sub plugin_pipe_bump_down {
619 1     1 1 2 my ($self, $item, $delta) = @_;
620              
621 1         3 my $idx = $self->plugin_pipe_get_index($item);
622 1 50       4 return -1 unless $idx >= 0;
623              
624 1   50     3 my $pos = $idx + ($delta || 1);
625              
626 1 50       1 if ($pos >= @{ $self->__pluggable_pipeline }) {
  1         3  
627 0         0 carp "Cannot bump below end of pipeline, pushing to tail"
628             }
629              
630 1         3 splice @{ $self->__pluggable_pipeline }, $pos, 0,
  1         21  
631 1         22 splice @{ $self->__pluggable_pipeline }, $idx, 1;
632              
633 1         21 $pos
634             }
635              
636             sub __plug_pipe_register {
637 8     8   15 my ($self, $new_alias, $new_plug, @args) = @_;
638              
639             # Register this as a known plugin.
640             # Try to call $reg_prefix . "register"
641              
642 8         10 my ($retval, $err);
643 8         22 my $meth = $self->__pluggable_opts->{reg_prefix} . "register" ;
644              
645             try {
646 8     8   360 $retval = $new_plug->$meth( $self, @args )
647             } catch {
648 0     0   0 chomp;
649 0         0 $err = "$meth call on '$new_alias' failed: $_";
650 8         239 };
651              
652 8 50       137 unless ($retval) {
653 0         0 $err = "$meth call on '$new_alias' returned false";
654             }
655              
656 8 50       16 if ($err) {
657 0         0 $self->__plug_pipe_handle_err( $err, $new_plug, $new_alias );
658             return
659 0         0 }
660              
661 8         23 $self->__pluggable_loaded->{ALIAS}->{$new_alias} = $new_plug;
662 8         216 $self->__pluggable_loaded->{OBJ}->{$new_plug} = $new_alias;
663              
664 8         178 $self->_pluggable_event(
665             $self->__pluggable_opts->{ev_prefix} . "plugin_added",
666             $new_alias,
667             $new_plug
668             );
669              
670 8         39 $retval
671             }
672              
673             sub __plug_pipe_unregister {
674 7     7   13 my ($self, $old_alias, $old_plug, @args) = @_;
675              
676 7         9 my ($retval, $err);
677 7         20 my $meth = $self->__pluggable_opts->{reg_prefix} . "unregister" ;
678              
679             try {
680 7     7   276 $retval = $old_plug->$meth( $self, @args )
681             } catch {
682 0     0   0 chomp;
683 0         0 $err = "$meth call on '$old_alias' failed: $_";
684 7         237 };
685              
686 7 50       104 unless ($retval) {
687 0         0 $err = "$meth called on '$old_alias' returned false";
688             }
689              
690 7 50       72 if ($err) {
691 0         0 $self->__plug_pipe_handle_err( $err, $old_plug, $old_alias );
692             }
693              
694 7         20 delete $self->__pluggable_loaded->{ALIAS}->{$old_alias};
695             delete $self->__pluggable_loaded->{$_}->{$old_plug}
696 7         187 for qw/ OBJ HANDLE /;
697              
698 7         306 $self->_pluggable_event(
699             $self->__pluggable_opts->{ev_prefix} . "plugin_removed",
700             $old_alias,
701             $old_plug
702             );
703              
704 7         21 $retval
705             }
706              
707             sub __plug_pipe_handle_err {
708 0     0   0 my ($self, $err, $plugin, $alias) = @_;
709              
710 0         0 warn "$err\n";
711              
712 0         0 $self->_pluggable_event(
713             $self->__pluggable_opts->{ev_prefix} . "plugin_error",
714             $err,
715             $plugin,
716             $alias
717             );
718             }
719              
720             sub __plugin_by_alias {
721 7     7   13 my ($self, $item) = @_;
722              
723 7         25 $self->__pluggable_loaded->{ALIAS}->{$item}
724             }
725              
726             sub __plugin_by_ref {
727 1     1   2 my ($self, $item) = @_;
728              
729 1         3 $self->__pluggable_loaded->{OBJ}->{$item}
730             }
731              
732             sub __plugin_get_plug_any {
733 19     19   21 my ($self, $item) = @_;
734              
735 19 100       78 blessed $item ?
736             ( $self->__pluggable_loaded->{OBJ}->{$item}, $item )
737             : ( $item, $self->__pluggable_loaded->{ALIAS}->{$item} );
738             }
739              
740              
741             print
742             qq[ How can I run two separate process at the same time simultaneously?\n],
743             qq[ I'd use an operating system, and have it run them for me.\n]
744             unless caller;
745              
746             1;
747              
748             =pod
749              
750             =head1 NAME
751              
752             MooX::Role::Pluggable - Add a plugin pipeline to your cows
753              
754             =head1 SYNOPSIS
755              
756             # A simple pluggable dispatcher:
757             package MyDispatcher;
758             use Moo;
759             use MooX::Role::Pluggable::Constants;
760             with 'MooX::Role::Pluggable';
761              
762             sub BUILD {
763             my ($self) = @_;
764              
765             # (optionally) Configure our plugin pipeline
766             $self->_pluggable_init(
767             reg_prefix => 'Plug_',
768             ev_prefix => 'Event_',
769             types => {
770             NOTIFY => 'N',
771             PROCESS => 'P',
772             },
773             );
774             }
775              
776             around '_pluggable_event' => sub {
777             # This override redirects internal events (errors, etc) to ->process()
778             my ($orig, $self) = splice @_, 0, 2;
779             $self->process( @_ )
780             };
781              
782             sub process {
783             my ($self, $event, @args) = @_;
784              
785             # Dispatch to 'P_' prefixed "PROCESS" type handlers.
786             #
787             # _pluggable_process will automatically strip a leading 'ev_prefix'
788             # (see the call to _pluggable_init above); that lets us easily
789             # dispatch errors to our P_plugin_error handler below without worrying
790             # about our ev_prefix ourselves:
791             my $retval = $self->_pluggable_process( PROCESS =>
792             $event,
793             \@args
794             );
795              
796             unless ($retval == EAT_ALL) {
797             # The pipeline allowed the event to continue.
798             # A dispatcher might re-dispatch elsewhere, etc.
799             }
800             }
801              
802             sub shutdown {
803             my ($self) = @_;
804             # Unregister all of our plugins.
805             $self->_pluggable_destroy;
806             }
807              
808             sub P_plugin_error {
809             # Since we re-dispatched errors in our _pluggable_event handler,
810             # we could handle exceptions here and then eat them, perhaps:
811             my ($self, undef) = splice @_, 0, 2;
812              
813             # Arguments are references:
814             my $plug_err = ${ $_[0] };
815             my $plug_obj = ${ $_[1] };
816             my $error_src = ${ $_[2] };
817              
818             # ...
819            
820             EAT_ALL
821             }
822              
823              
824             # A Plugin object.
825             package MyPlugin;
826              
827             use MooX::Role::Pluggable::Constants;
828              
829             sub new { bless {}, shift }
830              
831             sub Plug_register {
832             my ($self, $core) = @_;
833              
834             # Subscribe to events:
835             $core->subscribe( $self, 'PROCESS',
836             'my_event',
837             'another_event'
838             );
839              
840             # Log that we're here, do some initialization, etc ...
841              
842             return EAT_NONE
843             }
844              
845             sub Plug_unregister {
846             my ($self, $core) = @_;
847             # Called when this plugin is unregistered
848             # ... do some cleanup, etc ...
849             return EAT_NONE
850             }
851              
852             sub P_my_event {
853             # Handle a dispatched "PROCESS"-type event:
854             my ($self, $core) = splice @_, 0, 2;
855              
856             # Arguments are references and can be modified:
857             my $arg = ${ $_[0] };
858              
859             # ... do some work ...
860              
861             # Return an EAT constant to control event lifetime
862             # EAT_NONE allows this event to continue through the pipeline
863             return EAT_NONE
864             }
865              
866             # An external package that interacts with our dispatcher;
867             # this is just a quick and dirty example to show external
868             # plugin manipulation:
869              
870             package MyController;
871             use Moo;
872              
873             has dispatcher => (
874             is => 'rw',
875             default => sub { MyDispatcher->new() },
876             );
877              
878             sub BUILD {
879             my ($self) = @_;
880             $self->dispatcher->plugin_add( 'MyPlugin',
881             MyPlugin->new()
882             );
883             }
884              
885             sub do_stuff {
886             my $self = shift;
887             $self->dispatcher->process( 'my_event', @_ )
888             }
889              
890             =head1 DESCRIPTION
891              
892             A L for turning instances of your class into pluggable objects.
893             Consumers of this role gain a plugin pipeline and methods to manipulate it,
894             as well as a flexible dispatch system (see L).
895              
896             The logic and behavior is based almost entirely on L
897             (see L).
898             Some methods are the same; implementation & interface differ and you
899             will still want to read thoroughly if coming from L.
900             Dispatch is significantly faster -- see L.
901              
902             It may be worth noting that this is nothing at all like the Moose
903             counterpart L. If the names confuse ... well, I
904             lacked for better ideas. ;-)
905              
906             If you're using L, also see L, which consumes
907             this role.
908              
909             =head2 Initialization
910              
911             =head3 _pluggable_init
912              
913             $self->_pluggable_init(
914             # Prefix for registration events.
915             # Defaults to 'plugin_' ('plugin_register' / 'plugin_unregister')
916             reg_prefix => 'plugin_',
917              
918             # Prefix for dispatched internal events
919             # (add, del, error, register, unregister ...)
920             # Defaults to 'plugin_ev_'
921             event_prefix => 'plugin_ev_',
922              
923             # Map type names to prefixes.
924             # Event types are arbitrary.
925             # Prefix is prepended when dispathing events of a particular type.
926             # Defaults to: { NOTIFY => 'N', PROCESS => 'P' }
927             types => {
928             NOTIFY => 'N',
929             PROCESS => 'P',
930             },
931             );
932              
933             A consumer can call B<_pluggable_init> to set up pipeline-related options
934             appropriately; this should be done prior to loading plugins or dispatching
935             to L. If it is not called, the defaults
936             (as shown above) are used.
937              
938             B<< types => >> can be either an ARRAY of event types (which will be used
939             as prefixes):
940              
941             types => [ qw/ IncomingEvent OutgoingEvent / ],
942              
943             ... or a HASH mapping an event type to a prefix:
944              
945             types => {
946             Incoming => 'I',
947             Outgoing => 'O',
948             },
949              
950             A '_' is automatically appended to event type prefixes when events are
951             dispatched via L; thus, an event destined for our
952             'Incoming' type shown above will be dispatched to appropriate C handlers:
953              
954             # Dispatched to 'I_foo' method in plugins registered for Incoming 'foo':
955             $self->_pluggable_process( Incoming => 'foo', 'bar', 'baz' );
956              
957             C/C are not automatically munged in any way.
958              
959             An empty string C/C is valid.
960              
961             =head3 _pluggable_destroy
962              
963             $self->_pluggable_destroy;
964              
965             Shuts down the plugin pipeline, unregistering/unloading all known plugins.
966              
967             =head3 _pluggable_event
968              
969             # In our consumer
970             sub _pluggable_event {
971             my ($self, $event, @args) = @_;
972             # Dispatch out, perhaps.
973             }
974              
975             C<_pluggable_event> is called for internal notifications, such as plugin
976             load/unload and error reporting (see L).
977              
978             It should be overriden in your consuming class to do something useful with
979             the dispatched event (and any other arguments passed in).
980              
981             The C<$event> passed will be prefixed with the configured B.
982              
983             Also see L.
984              
985             =head2 Registration
986              
987             A plugin is any blessed object that is registered with your Pluggable object
988             via L; during registration, plugins usually subscribe to some
989             events via L.
990              
991             See L regarding loading plugins.
992              
993             =head3 subscribe
994              
995             B
996              
997             $self->subscribe( $plugin_obj, $type, @events );
998              
999             Registers a plugin object to receive C<@events> of type C<$type>.
1000              
1001             This is frequently called from within the plugin's registration handler
1002             (see L):
1003              
1004             # In a plugin:
1005             sub plugin_register {
1006             my ($self, $core) = @_;
1007              
1008             $core->subscribe( $self, PROCESS =>
1009             qw/
1010             my_event
1011             another_event
1012             /
1013             );
1014              
1015             $core->subscribe( $self, NOTIFY =>
1016             'all'
1017             );
1018              
1019             EAT_NONE
1020             }
1021              
1022             Subscribe to B to receive all events. (It may be worth noting that
1023             subscribing a lot of plugins to 'all' events will
1024             cause a performance hit in L dispatch versus
1025             subscribing to specific events.)
1026              
1027             =head3 unsubscribe
1028              
1029             B
1030              
1031             The unregister counterpart to L; stops delivering
1032             specified events to a plugin.
1033              
1034             The plugin is still loaded and registered until L is called.
1035              
1036             Carries the same arguments as L.
1037              
1038             =head3 plugin_register
1039              
1040             B
1041              
1042             (Note that 'plugin_' is just a default register method prefix; it can be
1043             changed prior to loading plugins. See L for details.)
1044              
1045             The C method is called on a loaded plugin when it is
1046             added to the pipeline; it is passed the plugin object (C<$self>), the
1047             Pluggable object, and any arguments given to L (or similar
1048             registration methods).
1049              
1050             Normally one might call a L from here to start receiving
1051             events after load-time:
1052              
1053             sub plugin_register {
1054             my ($self, $core, @args) = @_;
1055             $core->subscribe( $self, 'NOTIFY', @events );
1056             EAT_NONE
1057             }
1058              
1059             =head3 plugin_unregister
1060              
1061             B
1062              
1063             (Note that 'plugin_' is just a default register method prefix; it can be
1064             changed prior to loading plugins. See L for details.)
1065              
1066             The unregister counterpart to L, called when the object
1067             is removed from the pipeline (via L or
1068             L).
1069              
1070             sub plugin_unregister {
1071             my ($self, $core) = @_;
1072             EAT_NONE
1073             }
1074              
1075             Carries the same arguments.
1076              
1077             =head2 Dispatch
1078              
1079             =head3 _pluggable_process
1080              
1081             my $eat = $self->_pluggable_process( $type, $event, \@args );
1082             return 1 if $eat == EAT_ALL;
1083              
1084             The C<_pluggable_process> method handles dispatching.
1085              
1086             If C<$event> is prefixed with our event prefix (see L),
1087             the prefix is stripped prior to dispatch (to be replaced with a type
1088             prefix matching the specified C<$type>).
1089              
1090             Arguments should be passed in as an ARRAY. During dispatch, references to
1091             the arguments are passed to subs following automatically-prepended objects
1092             belonging to the plugin and the pluggable caller, respectively:
1093              
1094             my @args = qw/baz bar/;
1095             $self->_pluggable_process( 'NOTIFY', 'foo', \@args );
1096              
1097             # In a plugin:
1098             sub N_foo {
1099             my ($self, $core) = splice @_, 0, 2;
1100             # Dereferenced expected scalars:
1101             my $baz = ${ $_[0] };
1102             my $bar = ${ $_[1] };
1103             }
1104              
1105             This allows for argument modification as an event is passed along the
1106             pipeline.
1107              
1108             Dispatch process for C<$event> 'foo' of C<$type> 'NOTIFY':
1109              
1110             - Prepend the known prefix for the specified type, and '_'
1111             'foo' -> 'N_foo'
1112             - Attempt to dispatch to $self->N_foo()
1113             - If no such method, attempt to dispatch to $self->_default()
1114             (The method we were attempting to call is prepended to arguments)
1115             - If the event was not eaten (see below), dispatch to plugins
1116              
1117             "Eaten" means a handler returned a EAT_* constant from
1118             L indicating that the event's lifetime
1119             should terminate.
1120              
1121             Specifically:
1122              
1123             B
1124              
1125             EAT_ALL: skip plugin pipeline, return EAT_ALL
1126             EAT_CLIENT: continue to plugin pipeline
1127             return EAT_ALL if plugin returns EAT_PLUGIN later
1128             EAT_PLUGIN: skip plugin pipeline entirely
1129             return EAT_NONE unless EAT_CLIENT was seen previously
1130             EAT_NONE: continue to plugin pipeline
1131              
1132             B
1133              
1134             EAT_ALL: skip further plugins, return EAT_ALL
1135             EAT_CLIENT: continue to next plugin, set pending EAT_ALL
1136             (EAT_ALL will be returned when plugin processing finishes)
1137             EAT_PLUGIN: return EAT_ALL if previous sub returned EAT_CLIENT
1138             else return EAT_NONE
1139             EAT_NONE: continue to next plugin
1140              
1141             This functionality (derived from L) provides
1142             fine-grained control over event lifetime.
1143              
1144             Higher layers can check for an C return value from
1145             _pluggable_process to determine whether to continue operating on a
1146             particular event
1147             (re-dispatch elsewhere, for example). Plugins can use 'EAT_CLIENT' to
1148             indicate that an event should be eaten after plugin processing
1149             is complete, 'EAT_PLUGIN' to stop plugin processing, and 'EAT_ALL'
1150             to indicate that the event should not be dispatched further.
1151              
1152             =head2 Plugin Management Methods
1153              
1154             Plugin pipeline manipulation methods will set C<$@>, C, and return
1155             empty list on error (unless otherwise noted). See L
1156             regarding errors raised during plugin registration and dispatch.
1157              
1158             =head3 plugin_add
1159              
1160             $self->plugin_add( $alias, $plugin_obj, @args );
1161              
1162             Add a plugin object to the pipeline. Returns the same values as
1163             L.
1164              
1165             =head3 plugin_del
1166              
1167             $self->plugin_del( $alias_or_plugin_obj, @args );
1168              
1169             Remove a plugin from the pipeline.
1170              
1171             Takes either a plugin alias or object. Returns the removed plugin object.
1172              
1173             =head3 plugin_get
1174              
1175             my $plug_obj = $self->plugin_get( $alias );
1176             my ($plug_obj, $plug_alias) = $self->plugin_get( $alias_or_plugin_obj );
1177              
1178             In scalar context, returns the plugin object belonging to the specified
1179             alias.
1180              
1181             In list context, returns the object and alias, respectively.
1182              
1183             =head3 plugin_alias_list
1184              
1185             my @loaded = $self->plugin_alias_list;
1186              
1187             Returns a list of loaded plugin aliases.
1188              
1189             =head3 plugin_replace
1190              
1191             $self->plugin_replace(
1192             old => $alias_or_plugin_obj,
1193             alias => $new_alias,
1194             plugin => $new_plugin_obj,
1195             # Optional:
1196             register_args => [ ],
1197             unregister_args => [ ],
1198             );
1199              
1200             Replace an existing plugin object with a new one.
1201              
1202             Returns the old (removed) plugin object.
1203              
1204             =head2 Pipeline methods
1205              
1206             =head3 plugin_pipe_push
1207              
1208             $self->plugin_pipe_push( $alias, $plugin_obj, @args );
1209              
1210             Add a plugin to the end of the pipeline. (Typically one would use
1211             L rather than calling this method directly.)
1212              
1213             =head3 plugin_pipe_pop
1214              
1215             my $plug = $self->plugin_pipe_pop( @unregister_args );
1216              
1217             Pop the last plugin off the pipeline, passing any specified arguments to
1218             L.
1219              
1220             In scalar context, returns the plugin object that was removed.
1221              
1222             In list context, returns the plugin object and alias, respectively.
1223              
1224             =head3 plugin_pipe_unshift
1225              
1226             $self->plugin_pipe_unshift( $alias, $plugin_obj, @args );
1227              
1228             Add a plugin to the beginning of the pipeline.
1229              
1230             Returns the total number of loaded plugins (or an empty list on failure).
1231              
1232             =head3 plugin_pipe_shift
1233              
1234             $self->plugin_pipe_shift( @unregister_args );
1235              
1236             Shift the first plugin off the pipeline, passing any specified args to
1237             L.
1238              
1239             In scalar context, returns the plugin object that was removed.
1240              
1241             In list context, returns the plugin object and alias, respectively.
1242              
1243             =head3 plugin_pipe_get_index
1244              
1245             my $idx = $self->plugin_pipe_get_index( $alias_or_plugin_obj );
1246             if ($idx < 0) {
1247             # Plugin doesn't exist
1248             }
1249              
1250             Returns the position of the specified plugin in the pipeline.
1251              
1252             Returns -1 if the plugin does not exist.
1253              
1254             =head3 plugin_pipe_insert_after
1255              
1256             $self->plugin_pipe_insert_after(
1257             after => $alias_or_plugin_obj,
1258             alias => $new_alias,
1259             plugin => $new_plugin_obj,
1260             # Optional:
1261             register_args => [ ],
1262             );
1263              
1264             Add a plugin to the pipeline after the specified previously-existing alias
1265             or plugin object. Returns boolean true on success.
1266              
1267             =head3 plugin_pipe_insert_before
1268              
1269             $self->plugin_pipe_insert_before(
1270             before => $alias_or_plugin_obj,
1271             alias => $new_alias,
1272             plugin => $new_plugin_obj,
1273             # Optional:
1274             register_args => [ ],
1275             );
1276              
1277             Similar to L, but insert before the specified
1278             previously-existing plugin, not after.
1279              
1280             =head3 plugin_pipe_bump_up
1281              
1282             $self->plugin_pipe_bump_up( $alias_or_plugin_obj, $count );
1283              
1284             Move the specified plugin 'up' C<$count> positions in the pipeline.
1285              
1286             Returns -1 if the plugin cannot be bumped up any farther.
1287              
1288             =head3 plugin_pipe_bump_down
1289              
1290             $self->plugin_pipe_bump_down( $alias_or_plugin_obj, $count );
1291              
1292             Move the specified plugin 'down' C<$count> positions in the pipeline.
1293              
1294             Returns -1 if the plugin cannot be bumped down any farther.
1295              
1296             =head2 Internal events
1297              
1298             These events are dispatched to L prefixed with our
1299             pluggable event prefix; see L.
1300              
1301             =head3 plugin_error
1302              
1303             Issued via L when an error occurs.
1304              
1305             The arguments are, respectively: the error string, the offending object,
1306             and a string describing the offending object ('self' or 'plugin' with name
1307             appended).
1308              
1309             =head3 plugin_added
1310              
1311             Issued via L when a new plugin is registered.
1312              
1313             Arguments are the new plugin alias and object, respectively.
1314              
1315             =head3 plugin_removed
1316              
1317             Issued via L when a plugin is unregistered.
1318              
1319             Arguments are the old plugin alias and object, respectively.
1320              
1321             =head2 Performance
1322              
1323             My motivation for writing this role was two-fold; I wanted
1324             L behavior but without screwing up my class inheritance,
1325             and I needed a little bit more juice out of the pipeline dispatch process for
1326             a fast-paced daemon.
1327              
1328             Dispatcher performance has been profiled and micro-optimized, but I'm most
1329             certainly open to further ideas ;-)
1330              
1331             Some L runs. 30000 L calls with 20 loaded
1332             plugins dispatching one argument to one handler that does nothing except
1333             return EAT_NONE:
1334              
1335             Rate object-pluggable moox-role-pluggable
1336             object-pluggable 6173/s -- -38%
1337             moox-role-pluggable 9967/s 61%
1338              
1339             Rate object-pluggable moox-role-pluggable
1340             object-pluggable 6224/s -- -38%
1341             moox-role-pluggable 10000/s 61% --
1342              
1343             Rate object-pluggable moox-role-pluggable
1344             object-pluggable 6383/s -- -35%
1345             moox-role-pluggable 9868/s 55%
1346              
1347             (Benchmark script is available in the C directory of the upstream
1348             repository; see L)
1349              
1350             =head1 AUTHOR
1351              
1352             Jon Portnoy
1353              
1354             Written from the ground up, but conceptually based entirely on
1355             L by BINGOS, HINRIK, APOCAL, japhy et al.
1356              
1357             =cut