File Coverage

blib/lib/Dancer2/Plugin.pm
Criterion Covered Total %
statement 223 228 97.8
branch 61 80 76.2
condition 15 23 65.2
subroutine 49 54 90.7
pod 6 24 25.0
total 354 409 86.5


line stmt bran cond sub pod time code
1             package Dancer2::Plugin;
2             # ABSTRACT: base class for Dancer2 plugins
3             $Dancer2::Plugin::VERSION = '2.1.0';
4 38     38   1652573 use strict;
  38         2933  
  38         6878  
5 38     38   7488 use warnings;
  38         110  
  38         6469  
6              
7 38     38   7176 use Moo;
  38         107516  
  38         292  
8 38     38   31658 use Carp;
  38         489  
  38         2188  
9 38     38   13013 use List::Util qw/ reduce /;
  38         83  
  38         6482  
10 24     24   6695 use Module::Runtime 'require_module';
  24         26487  
  24         255  
11 24     24   16696 use Attribute::Handlers;
  24         147422  
  24         223  
12 24     24   1158 use Scalar::Util;
  24         62  
  24         1601  
13 24     24   8336 use Ref::Util qw;
  24         32683  
  24         32272  
14              
15             our $CUR_PLUGIN;
16              
17             extends 'Exporter::Tiny';
18              
19             with 'Dancer2::Core::Role::Hookable';
20              
21             has app => (
22             is => 'ro',
23             weak_ref => 1,
24             required => 1,
25             );
26              
27             has config => (
28             is => 'ro',
29             lazy => 1,
30             default => sub {
31             my $self = shift;
32             my $config = $self->app->config;
33             my $package = ref $self; # TODO
34             $package =~ s/Dancer2::Plugin:://;
35             $config->{plugins}{$package} || {};
36             },
37             );
38              
39             my $_keywords = {};
40 0     0 1 0 sub keywords { $_keywords }
41              
42             my $REF_ADDR_REGEX = qr{
43             [A-Za-z0-9\:\_]+
44             =HASH
45             \(
46             ([0-9a-fx]+)
47             \)
48             }x;
49             my %instances;
50              
51             # backwards compatibility
52             our $_keywords_by_plugin = {};
53              
54             has '+hooks' => (
55             default => sub {
56             my $plugin = shift;
57             my $name = 'plugin.' . lc ref $plugin;
58             $name =~ s/Dancer2::Plugin:://i;
59             $name =~ s/::/_/g;
60              
61             +{
62             map { join( '.', $name, $_ ) => [] }
63             @{ $plugin->ClassHooks }
64             };
65             },
66             );
67              
68             sub add_hooks {
69 7     7 0 17 my $class = shift;
70 7         13 push @{ $class->ClassHooks }, @_;
  7         216  
71             }
72              
73             sub execute_plugin_hook {
74 1     1 0 5 my ( $self, $name, @args ) = @_;
75 1         3 my $plugin_class = ref $self;
76              
77 1 50       19 $self->isa('Dancer2::Plugin')
78             or croak "Cannot call plugin hook ($name) from outside plugin";
79 1         5 $plugin_class =~ s/^Dancer2::Plugin:://; # short names
80              
81 1         6 my $full_name = 'plugin.' . lc($plugin_class) . ".$name";
82 1         7 $full_name =~ s/::/_/g;
83              
84 1         37 $self->app->execute_hook( $full_name, @args );
85             }
86              
87             sub find_plugin {
88 2     2 0 1399 my ( $self, $name ) = @_;
89 2         21 return $self->app->find_plugin($name);
90             }
91              
92             # both functions are there for D2::Core::Role::Hookable
93             # back-compatibility. Aren't used
94 0     0 0 0 sub supported_hooks { [] }
95 20   100 20 0 355 sub hook_aliases { $_[0]->{'hook_aliases'} ||= {} }
96              
97             ### has() STUFF ########################################
98              
99             # our wrapping around Moo::has, done to be able to intercept
100             # both 'from_config' and 'plugin_keyword'
101             sub _p2_has {
102 23     23   1213 my $class = shift;
103 23         119 $class->_p2_has_from_config( $class->_p2_has_keyword( @_ ) );
104             };
105              
106             sub _p2_has_from_config {
107 23     23   73 my( $class, $name, %args ) = @_;
108              
109 23 100       156 my $config_name = delete $args{'from_config'}
110             or return ( $name, %args );
111              
112 8         19 $args{lazy} = 1;
113              
114 8 100       43 if ( is_coderef($config_name) ) {
115 2   66     11 $args{default} ||= $config_name;
116 2         3 $config_name = 1;
117             }
118              
119 8 100       38 $config_name = $name if $config_name eq '1';
120 8   66 1   81 my $orig_default = $args{default} || sub{};
121             $args{default} = sub {
122 8     8   150 my $plugin = shift;
123 8         311 my $value = reduce { eval { $a->{$b} } } $plugin->config, split /\./, $config_name;
  9         112  
  9         40  
124 8 100       89 return defined $value ? $value: $orig_default->($plugin);
125 8         69 };
126              
127 8         56 return $name => %args;
128             }
129              
130             sub _p2_has_keyword {
131 23     23   85 my( $class, $name, %args ) = @_;
132              
133 23 100       96 if( my $keyword = delete $args{plugin_keyword} ) {
134              
135 8 100       26 $keyword = $name if $keyword eq '1';
136              
137 6     6   157 $class->keywords->{$_} = sub { (shift)->$name(@_) }
138 8 100       194 for ref $keyword ? @$keyword : $keyword;
139             }
140              
141 23         154 return $name => %args;
142             }
143              
144             ### ATTRIBUTE HANDLER STUFF ########################################
145              
146             # :PluginKeyword shenanigans
147              
148             sub PluginKeyword :ATTR(CODE,BEGIN) {
149 6     6 0 18 my( $class, $sym_ref, $code, undef, $args ) = @_;
150              
151             # importing at BEGIN stage doesn't work with 5.10 :-(
152 6 50       19 return unless ref $sym_ref;
153              
154 6         10 my $func_name = *{$sym_ref}{NAME};
  6         10  
155              
156 6 100       21 $args = join '', @$args if is_arrayref($args);
157              
158 6   66     40 for my $name ( split ' ', $args || $func_name ) {
159 7         165 $class->keywords->{$name} = $code;
160             }
161              
162 24     24   1155 }
  24         53  
  24         513  
163              
164             ## EXPORT STUFF ##############################################################
165              
166             # this @EXPORT will only be taken
167             # into account when we do a 'use Dancer2::Plugin'
168             # I.e., it'll only do its magic for the
169             # plugins themselves, not when they are
170             # called
171             our @EXPORT = qw/ :plugin /;
172              
173             # compatibility - it will be removed soon!
174             my $no_dsl = {};
175             my $exported_app = {};
176             sub _exporter_expand_tag {
177 69     69   26392 my( $class, $name, $args, $global ) = @_;
178              
179 69         228 my $caller = $global->{into};
180              
181 69 100       302 $name eq 'no_dsl' and $no_dsl->{$caller} = 1;
182             # no_dsl check here is for compatibility only
183             # it will be removed soon!
184 69 100 100     621 return _exporter_plugin($caller)
185             if $name eq 'plugin' or $name eq 'no_dsl';
186              
187             return _exporter_app($class,$caller,$global)
188 31 50 33     1003 if $name eq 'app' and $caller->can('app') and !$no_dsl->{$class};
      33        
189              
190 0         0 return;
191              
192             }
193              
194             # plugin has been called within a D2 app. Modify
195             # the app and export keywords
196             sub _exporter_app {
197 31     31   121 my( $class, $caller, $global ) = @_;
198              
199 31         99 $exported_app->{$caller} = 1;
200              
201             # The check for ->dsl->app is to handle plugins as well.
202             # Otherwise you can only import from a plugin to an app,
203             # but with this, you can import to anything
204             # that has a DSL with an app, which translates to "also plugins"
205 31 50 66     2851 my $app = eval("${caller}::app()") || eval { $caller->dsl->app } ## no critic qw(BuiltinFunctions::ProhibitStringyEval)
206             or return; ## no critic
207              
208 31 50       289 return unless $app->can('with_plugin');
209              
210 31         246 my $plugin = $app->with_plugin( '+' . $class );
211 31         135 $global->{'plugin'} = $plugin;
212              
213 31 50       797 return unless $class->can('keywords');
214              
215             # Add our hooks to the app, so they're recognized
216             # this is for compatibility so you can call execute_hook()
217             # without the fully qualified plugin name.
218             # The reason we need to do this here instead of when adding a
219             # hook is because we need to register in the app, and only now it
220             # exists.
221             # This adds a caveat that two plugins cannot register
222             # the same hook name, but that will be deprecated anyway.
223             {;
224 31         341 foreach my $hook ( @{ $plugin->ClassHooks } ) {
  31         999  
225 7         48 my $full_name = 'plugin.' . lc($class) . ".$hook";
226 7         46 $full_name =~ s/Dancer2::Plugin:://i;
227 7         23 $full_name =~ s/::/_/g;
228              
229             # this adds it to the plugin
230 7         47 $plugin->hook_aliases->{$hook} = $full_name;
231              
232             # this adds it to the app
233 7         59 $plugin->app->hook_aliases->{$hook} = $full_name;
234              
235             # copy the hooks from the plugin to the app
236             # this is in case they were created at import time
237             # rather than after
238 7         226 @{ $plugin->app->hooks }{ keys %{ $plugin->hooks } } =
  7         338  
239 7         15 values %{ $plugin->hooks };
  7         250  
240             }
241             }
242              
243             {
244             # get the reference
245 31         308 my ($plugin_addr) = "$plugin" =~ $REF_ADDR_REGEX;
  31         127  
  31         342  
246              
247 31     5   287 $instances{$plugin_addr}{'config'} = sub { $plugin->config };
  5         137  
248 31         592 $instances{$plugin_addr}{'app'} = $plugin->app;
249              
250 31         413 Scalar::Util::weaken( $instances{$plugin_addr}{'app'} );
251              
252             ## no critic
253 23     23   23633 no strict 'refs';
  23         71  
  23         1265  
254              
255             # we used a forward declaration
256             # so the compiled form "plugin_setting;" can be overridden
257             # with this implementation,
258             # which works on runtime ("plugin_setting()")
259             # we can't use can() here because the forward declaration will
260             # create a CODE stub
261 23     23   158 no warnings 'redefine';
  23         98  
  23         22624  
262 31         714 *{"${class}::plugin_setting"} = sub {
263 5     5   101 my ($plugin_addr) = "$CUR_PLUGIN" =~ $REF_ADDR_REGEX;
264              
265 5 50       21 $plugin_addr
266             or Carp::croak('Can\'t find originating plugin');
267              
268             # we need to do this because plugins might call "set"
269             # in order to change plugin configuration but it doesn't
270             # change the plugin object, it changes the app object
271             # so we merge them.
272 5         11 my $name = ref $CUR_PLUGIN;
273 5         19 $name =~ s/^Dancer2::Plugin:://g;
274              
275 5         13 my $plugin_inst = $instances{$plugin_addr};
276 5         18 my $plugin_config = $plugin_inst->{'config'}->();
277 5         107 my $app_plugin_config = $plugin_inst->{'app'}->config->{'plugins'}{$name};
278              
279 5 50       32 return { %{ $plugin_config || {} }, %{ $app_plugin_config || {} } };
  5 50       16  
  5         40  
280 31         396 };
281              
282             # FIXME:
283             # why doesn't this work? it's like it's already defined somewhere
284             # but i'm not sure where. seems like AUTOLOAD runs it.
285             #$class->can('execute_hook') or
286 31         421 *{"${class}::execute_hook"} = sub {
287             # this can also be called by App.pm itself
288             # if the plugin is a
289             # "candidate" for a hook
290             # See: App.pm "execute_hook" method, "around" modifier
291 5 100   5   88 if ( $_[0]->isa('Dancer2::Plugin') ) {
292             # this means it's probably our hook, we need to verify it
293 2         7 my ( $plugin_self, $hook_name, @args ) = @_;
294              
295 2         8 my $plugin_class = lc $class;
296 2         10 $plugin_class =~ s/^dancer2::plugin:://;
297 2         7 $plugin_class =~ s{::}{_}g;
298              
299             # you're either calling it with the full qualifier or not
300             # if not, use the execute_plugin_hook instead
301 2 50       48 if ( $plugin->hooks->{"plugin.$plugin_class.$hook_name"} ) {
302 0         0 Carp::carp("Please use fully qualified hook name or "
303             . "the method execute_plugin_hook");
304 0         0 $hook_name = "plugin.$plugin_class.$hook_name";
305             }
306              
307 2 50       50 $hook_name =~ /^plugin\.$plugin_class/
308             or Carp::croak('Unknown plugin called through other plugin');
309              
310             # now we can't really use the app to execute it because
311             # the "around" modifier is the one calling us to begin
312             # with, so we need to call it directly ourselves
313             # this is okay because the modifier is there only to
314             # call candidates, like us (this is in fact how and
315             # why we were called)
316             $_->( $plugin_self, @args )
317 2         5 for @{ $plugin->hooks->{$hook_name} };
  2         75  
318              
319 2         17 return;
320             }
321              
322 3         52 return $plugin->app->execute_hook(@_);
323 31         299 };
324             }
325              
326 31         335 local $CUR_PLUGIN = $plugin;
327 31         120 $_->($plugin) for @{ $plugin->_DANCER2_IMPORT_TIME_SUBS() };
  31         1229  
328              
329 31         96 map { [ $_ => {plugin => $plugin} ] } keys %{ $plugin->keywords };
  52         397  
  31         1091  
330             }
331              
332             # turns the caller namespace into
333             # a D2P2 class, with exported keywords
334             sub _exporter_plugin {
335 38     38   122 my $caller = shift;
336 38         230 require_module('Dancer2::Core::DSL');
337 38         1205 my $keywords_list = join ' ', keys %{ Dancer2::Core::DSL->dsl_keywords };
  38         315  
338              
339 38 50   25 0 5676 eval <<"END"; ## no critic
  24 50   26 0 256  
  24 50   23 1 54  
  24     23 1 142  
  24     22 1 11838  
  24     0 1 71  
  25     0 1 854  
  25     0 0 140  
  25       0 101  
  25       0 273  
  25       0 6578  
  25       0 59  
  25         277  
  25         23203  
  25         75  
  22         6258  
340             {
341             package $caller;
342             use Moo;
343             use Carp ();
344             use Attribute::Handlers;
345              
346             extends 'Dancer2::Plugin';
347              
348             our \@EXPORT = ( ':app' );
349              
350             around has => sub {
351             my( \$orig, \$name, \%args ) = \@_;
352              
353             if (ref \$name eq 'ARRAY'
354             && exists \$args{'plugin_keyword'}
355             && ref \$args{'plugin_keyword'} eq 'ARRAY') {
356              
357             Carp::croak('Setting "plugin_keyword" to an array is disallowed'
358             . ' when defining multiple attributes simultaneously');
359             }
360              
361             \$orig->( ${caller}->_p2_has( \$_, \%args) )
362             for ref \$name ? @\$name : \$name;
363             };
364              
365             sub PluginKeyword :ATTR(CODE,BEGIN) {
366             goto &Dancer2::Plugin::PluginKeyword;
367             }
368              
369             sub execute_plugin_hook {
370             goto &Dancer2::Plugin::execute_plugin_hook;
371             }
372              
373             my \$_keywords = {};
374             sub keywords { \$_keywords }
375              
376             my \$_ClassHooks = [];
377             sub ClassHooks { \$_ClassHooks }
378              
379             # this is important as it'll do the keywords mapping between the
380             # plugin and the app
381             sub register_plugin { Dancer2::Plugin::register_plugin(\@_) }
382              
383             sub register {
384             my ( \$keyword, \$sub ) = \@_;
385             \$_keywords->{\$keyword} = \$sub;
386              
387             \$keyword =~ /^[a-zA-Z_]+[a-zA-Z0-9_]*\$/
388             or Carp::croak(
389             "You can't use '\$keyword', it is an invalid name"
390             . " (it should match ^[a-zA-Z_]+[a-zA-Z0-9_]*\\\$ )");
391              
392              
393             grep +( \$keyword eq \$_ ), qw<$keywords_list>
394             and Carp::croak("You can't use '\$keyword', this is a reserved keyword");
395              
396             \$Dancer2::Plugin::_keywords_by_plugin->{\$keyword}
397             and Carp::croak("You can't use \$keyword, "
398             . "this is a keyword reserved by "
399             . \$Dancer2::Plugin::_keywords_by_plugin->{\$keyword});
400              
401             \$Dancer2::Plugin::_keywords_by_plugin->{\$keyword} = "$caller";
402              
403             # Exporter::Tiny doesn't seem to generate the subs
404             # in the caller properly, so we have to do it manually
405             {
406             no strict 'refs';
407             *{"${caller}::\$keyword"} = \$sub;
408             }
409             }
410              
411             my \@_DANCER2_IMPORT_TIME_SUBS;
412             sub _DANCER2_IMPORT_TIME_SUBS {\\\@_DANCER2_IMPORT_TIME_SUBS}
413             sub on_plugin_import (&) {
414             push \@_DANCER2_IMPORT_TIME_SUBS, \$_[0];
415             }
416              
417             sub register_hook { goto &plugin_hooks }
418              
419             sub plugin_setting { Carp::croak "DEPRECATED: Plugin DSL method 'plugin_setting'. "
420             . "Please use '\\\$self->config' instead\n" };
421              
422             sub plugin_args {
423             Carp::carp "Plugin DSL method 'plugin_args' is deprecated. "
424             . "Use '\\\@_' instead'.\n";
425              
426             \@_;
427             }
428             }
429             END
430              
431 38 100     0 7644 $no_dsl->{$caller} or eval <<"END"; ## no critic
          0    
          0    
          0    
432             {
433             package $caller;
434              
435             # FIXME: AUTOLOAD might pick up on this
436             sub dancer_app {
437             Carp::croak "DEPRECATED: Plugin DSL method 'dancer_app'. "
438             . "Please use '\\\$self->app' instead'.\n";
439             }
440              
441             # FIXME: AUTOLOAD might pick up on this
442             sub request {
443             Carp::croak "DEPRECATED: Plugin DSL method 'request'. "
444             . "Please use '\\\$self->app->request' instead'.\n";
445             }
446              
447             # FIXME: AUTOLOAD might pick up on this
448             sub var {
449             Carp::croak "DEPRECATED: Plugin DSL method 'var'. "
450             . "Please use '\\\$self->app->request->var' instead'.\n";
451             }
452              
453             # FIXME: AUTOLOAD might pick up on this
454             sub hook {
455             Carp::croak "DEPRECATED: Plugin DSL method 'hook'. "
456             . "Please use '\\\$self->app->add_hook' instead'.\n";
457             }
458              
459             }
460             END
461              
462 38 50       194 die $@ if $@;
463              
464             {
465             ## no critic qw(TestingAndDebugging::ProhibitNoWarnings)
466 23     23   196 no strict 'refs';
  23         75  
  23         1028  
  38         82  
467 23     23   126 no warnings 'redefine';
  23         108  
  23         10232  
468 38         328 *{"${caller}::dsl"} = sub {
469 6     35   4830 my $app_dsl_cb = _find_consumer($caller);
470 6 50       51 return $app_dsl_cb ? $app_dsl_cb->() : undef;
471 38         218 };
472             }
473              
474 38         136 return map { [ $_ => { class => $caller } ] }
  76         545  
475             qw/ plugin_keywords plugin_hooks /;
476             }
477              
478             sub _find_consumer {
479 46     33   518 my %skip = map +( $_ => 1 ), @_;
480 33         3461 my $class;
481              
482             ## no critic qw(ControlStructures::ProhibitCStyleForLoops)
483 45         270 for ( my $i = 1; my $caller = caller($i); $i++ ) {
484 108 100       320 next if $skip{$caller};
485 194 100       521645 next if eval { $caller->isa('Dancer2::Plugin') };
  110         7211  
486 58 100       375 $class = $caller->can('dsl')
487             and last;
488             }
489              
490             # If you use a Dancer2 plugin outside a Dancer App, this fails.
491             # It also breaks a bunch of the tests. -- SX
492             #$class
493             # or croak('Could not find Dancer2 app');
494              
495 21         235 return $class;
496             }
497              
498             # This has to be called for now at the end of every plugin package, in order to
499             # map the keywords of the associated app to the plugin, so that these keywords
500             # can be called from within the plugin code. This function is deprecated, as
501             # it's tied to the old plugin system. It's kept here for backcompat reason, but
502             # should go away with the old plugin system.
503             sub register_plugin {
504              
505 18     38 0 1575 my $plugin_module = caller(1);
506              
507             # if you ask yourself why we do the injection in the plugin
508             # module namespace every time the plugin is used, and not only
509             # once, it's because it can be used by different app that could
510             # have a different DSL with a different list of keywords.
511              
512 17         348 my $_DANCER2_IMPORT_TIME_SUBS = $plugin_module->_DANCER2_IMPORT_TIME_SUBS;
513             unshift(@$_DANCER2_IMPORT_TIME_SUBS, sub {
514 17     23   137 my $app_dsl_cb = _find_consumer($plugin_module);
515              
516             # Here we want to verify that "register_plugin" compat keyword
517             # was in fact only called from an app.
518 17 50       233 $app_dsl_cb
519             or Carp::croak(
520             'I could not find a Dancer App for this plugin');
521              
522 16         70 my $dsl = $app_dsl_cb->();
523              
524 16         47 foreach my $keyword ( keys %{ $dsl->dsl_keywords} ) {
  16         219  
525             # if not yet defined, inject the keyword in the plugin
526             # namespace, but make sure the code will always get the
527             # coderef from the right associated app, because one plugin
528             # can be used by multiple apps. Note that we remove the
529             # first parameter (plugin instance) from what we pass to
530             # the keyword implementation of the App
531 23     23   277 no strict 'refs';
  23         64  
  23         9123  
532             $plugin_module->can($keyword)
533 754         10303 or *{"${plugin_module}::$keyword"} = sub {
534             $_[0]
535 18 100       2246 ? do {
536 10         371 my $cb = shift()->app->name->can($keyword);
537 10         131 $cb->(@_);
538             }
539             : $app_dsl_cb->(@_);
540 907 100       7613 };
541             }
542 16         129 });
543             }
544              
545             sub _exporter_expand_sub {
546 131     244   10541 my( $plugin, $name, $args, $global ) = @_;
547 131         370 my $class = $args->{class};
548              
549 130 100       561 return _exported_plugin_keywords($plugin,$class)
550             if $name eq 'plugin_keywords';
551              
552 92 100       385 return _exported_plugin_hooks($class)
553             if $name eq 'plugin_hooks';
554              
555 54 50       227 $exported_app->{ $global->{'into'} }
556             or Carp::croak('Specific subroutines cannot be exported from plugin');
557              
558             # otherwise, we're exporting a keyword
559              
560 55         390868 my $p = $args->{plugin};
561 55         1694 my $sub = $p->keywords->{$name};
562             return $name => sub(@) {
563             # localize the plugin so we can get it later
564 46     68   376788 local $CUR_PLUGIN = $p;
565 45         516 $sub->($p,@_);
566             }
567 55         629 }
568              
569             # "There's a good reason for this, I swear!"
570             # -- Sawyer X
571             # basically, if someone adds a hook to the app directly
572             # that needs to access a DSL that needs the current object
573             # (such as "plugin_setting"),
574             # that object needs to be available
575             # So:
576             # we override App's "add_hook" to provide a register a
577             # different hook callback, that closes over the plugin when
578             # it's available, relocalizes it when the callback runs and
579             # after localizing it, calls the original hook callback
580             {
581             ## no critic;
582 23     23   185 no strict 'refs';
  23         58  
  23         1041  
583 23     23   170 no warnings 'redefine';
  23         59  
  23         10879  
584             my $orig_cb = Dancer2::Core::App->can('add_hook');
585             $orig_cb and *{'Dancer2::Core::App::add_hook'} = sub {
586             my ( $app, $hook ) = @_;
587              
588             my $hook_code = Scalar::Util::blessed($hook) ? $hook->code : $hook->{code};
589             my $plugin = $CUR_PLUGIN;
590              
591             $hook->{'code'} = sub {
592             local $CUR_PLUGIN = $plugin;
593             $hook_code->(@_);
594             };
595              
596             $orig_cb->(@_);
597             };
598             }
599              
600              
601             # define the exported 'plugin_keywords'
602             sub _exported_plugin_keywords{
603 52     56   664 my( $plugin, $class ) = @_;
604              
605             return plugin_keywords => sub(@) {
606 29     44   409222 while( my $name = shift @_ ) {
607             ## no critic
608 36 100       1761 my $sub = is_coderef($_[0])
    100          
609             ? shift @_
610             : eval '\&'.$class."::" . ( ref $name ? $name->[0] : $name );
611 43 50       1110 $class->keywords->{$_} = $sub for ref $name ? @$name : $name;
612             }
613             }
614 52         468 }
615              
616             sub _exported_plugin_hooks {
617 59     44   185 my $class = shift;
618 9     19   482075 return plugin_hooks => sub (@) { $class->add_hooks(@_) }
619 53         667 }
620              
621             1;
622              
623             __END__