File Coverage

blib/lib/Dancer2/Plugin.pm
Criterion Covered Total %
statement 224 229 97.8
branch 61 80 76.2
condition 15 23 65.2
subroutine 49 54 90.7
pod 6 24 25.0
total 355 410 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.0.1';
4 37     39   1462935 use strict;
  37         94  
  37         970  
5 37     37   7545 use warnings;
  37         81  
  37         11916  
6              
7 37     37   10857 use Moo;
  37         94489  
  37         381  
8 37     37   28625 use Carp;
  37         97  
  37         2088  
9 37     37   13635 use List::Util qw/ reduce /;
  37         77  
  37         6322  
10 23     23   5789 use Module::Runtime 'require_module';
  23         23213  
  23         182  
11 23     23   14589 use Attribute::Handlers;
  23         128044  
  23         173  
12 23     23   1245 use Scalar::Util;
  23         60  
  23         1315  
13 23     23   6627 use Ref::Util qw;
  23         28020  
  23         29404  
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 250 my $class = shift;
70 7         18 push @{ $class->ClassHooks }, @_;
  7         169  
71             }
72              
73             sub execute_plugin_hook {
74 1     1 0 4 my ( $self, $name, @args ) = @_;
75 1         3 my $plugin_class = ref $self;
76              
77 1 50       17 $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         8 $full_name =~ s/::/_/g;
83              
84 1         35 $self->app->execute_hook( $full_name, @args );
85             }
86              
87             sub find_plugin {
88 2     2 0 1972 my ( $self, $name ) = @_;
89 2         20 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 386 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   1120 my $class = shift;
103 23         146 $class->_p2_has_from_config( $class->_p2_has_keyword( @_ ) );
104             };
105              
106             sub _p2_has_from_config {
107 23     23   74 my( $class, $name, %args ) = @_;
108              
109 23 100       161 my $config_name = delete $args{'from_config'}
110             or return ( $name, %args );
111              
112 8         18 $args{lazy} = 1;
113              
114 8 100       25 if ( is_coderef($config_name) ) {
115 2   66     10 $args{default} ||= $config_name;
116 2         3 $config_name = 1;
117             }
118              
119 8 100       53 $config_name = $name if $config_name eq '1';
120 8   66 1   43 my $orig_default = $args{default} || sub{};
121             $args{default} = sub {
122 8     8   124 my $plugin = shift;
123 8         231 my $value = reduce { eval { $a->{$b} } } $plugin->config, split /\./, $config_name;
  9         102  
  9         32  
124 8 100       114 return defined $value ? $value: $orig_default->($plugin);
125 8         39 };
126              
127 8         51 return $name => %args;
128             }
129              
130             sub _p2_has_keyword {
131 23     23   376 my( $class, $name, %args ) = @_;
132              
133 23 100       110 if( my $keyword = delete $args{plugin_keyword} ) {
134              
135 8 100       33 $keyword = $name if $keyword eq '1';
136              
137 6     6   170 $class->keywords->{$_} = sub { (shift)->$name(@_) }
138 8 100       220 for ref $keyword ? @$keyword : $keyword;
139             }
140              
141 23         419 return $name => %args;
142             }
143              
144             ### ATTRIBUTE HANDLER STUFF ########################################
145              
146             # :PluginKeyword shenanigans
147              
148             sub PluginKeyword :ATTR(CODE,BEGIN) {
149 6     6 0 21 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         8 my $func_name = *{$sym_ref}{NAME};
  6         13  
155              
156 6 100       33 $args = join '', @$args if is_arrayref($args);
157              
158 6   66     31 for my $name ( split ' ', $args || $func_name ) {
159 7         160 $class->keywords->{$name} = $code;
160             }
161              
162 23     23   1087 }
  23         76  
  23         488  
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 66     66   25565 my( $class, $name, $args, $global ) = @_;
178              
179 66         180 my $caller = $global->{into};
180              
181 66 100       899 $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 66 100 100     501 return _exporter_plugin($caller)
185             if $name eq 'plugin' or $name eq 'no_dsl';
186              
187             return _exporter_app($class,$caller,$global)
188 29 50 33     708 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 29     29   102 my( $class, $caller, $global ) = @_;
198              
199 29         409 $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 29 50 66     2638 my $app = eval("${caller}::app()") || eval { $caller->dsl->app } ## no critic qw(BuiltinFunctions::ProhibitStringyEval)
206             or return; ## no critic
207              
208 29 50       724 return unless $app->can('with_plugin');
209              
210 29         223 my $plugin = $app->with_plugin( '+' . $class );
211 29         108 $global->{'plugin'} = $plugin;
212              
213 29 50       252 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 29         60 foreach my $hook ( @{ $plugin->ClassHooks } ) {
  29         731  
225 7         46 my $full_name = 'plugin.' . lc($class) . ".$hook";
226 7         52 $full_name =~ s/Dancer2::Plugin:://i;
227 7         24 $full_name =~ s/::/_/g;
228              
229             # this adds it to the plugin
230 7         46 $plugin->hook_aliases->{$hook} = $full_name;
231              
232             # this adds it to the app
233 7         63 $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         220 @{ $plugin->app->hooks }{ keys %{ $plugin->hooks } } =
  7         357  
239 7         13 values %{ $plugin->hooks };
  7         216  
240             }
241             }
242              
243             {
244             # get the reference
245 29         85 my ($plugin_addr) = "$plugin" =~ $REF_ADDR_REGEX;
  29         115  
  29         554  
246              
247 29     5   263 $instances{$plugin_addr}{'config'} = sub { $plugin->config };
  5         176  
248 29         214 $instances{$plugin_addr}{'app'} = $plugin->app;
249              
250 29         103 Scalar::Util::weaken( $instances{$plugin_addr}{'app'} );
251              
252             ## no critic
253 22     22   22503 no strict 'refs';
  22         70  
  22         1077  
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 22     22   130 no warnings 'redefine';
  22         43  
  22         20935  
262 29         501 *{"${class}::plugin_setting"} = sub {
263 5     5   102 my ($plugin_addr) = "$CUR_PLUGIN" =~ $REF_ADDR_REGEX;
264              
265 5 50       26 $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         14 my $name = ref $CUR_PLUGIN;
273 5         24 $name =~ s/^Dancer2::Plugin:://g;
274              
275 5         18 my $plugin_inst = $instances{$plugin_addr};
276 5         24 my $plugin_config = $plugin_inst->{'config'}->();
277 5         176 my $app_plugin_config = $plugin_inst->{'app'}->config->{'plugins'}{$name};
278              
279 5 50       50 return { %{ $plugin_config || {} }, %{ $app_plugin_config || {} } };
  5 50       26  
  5         62  
280 29         167 };
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 29         261 *{"${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   114 if ( $_[0]->isa('Dancer2::Plugin') ) {
292             # this means it's probably our hook, we need to verify it
293 2         8 my ( $plugin_self, $hook_name, @args ) = @_;
294              
295 2         9 my $plugin_class = lc $class;
296 2         9 $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       50 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         48  
318              
319 2         17 return;
320             }
321              
322 3         103 return $plugin->app->execute_hook(@_);
323 29         299 };
324             }
325              
326 29         81 local $CUR_PLUGIN = $plugin;
327 29         77 $_->($plugin) for @{ $plugin->_DANCER2_IMPORT_TIME_SUBS() };
  29         869  
328              
329 29         91 map { [ $_ => {plugin => $plugin} ] } keys %{ $plugin->keywords };
  50         386  
  29         855  
330             }
331              
332             # turns the caller namespace into
333             # a D2P2 class, with exported keywords
334             sub _exporter_plugin {
335 37     37   92 my $caller = shift;
336 37         283 require_module('Dancer2::Core::DSL');
337 37         1074 my $keywords_list = join ' ', keys %{ Dancer2::Core::DSL->dsl_keywords };
  37         336  
338              
339 37 50   27 0 5462 eval <<"END"; ## no critic
  24 50   26 0 287  
  24 50   22 1 51  
  24 50   22 1 136  
  24     21 1 10789  
  24     0 1 286  
  24     0 1 623  
  24     0 0 145  
  24       0 111  
  24       0 185  
  23       0 6573  
  23       0 55  
  23         259  
  23         20966  
  23         54  
  23         5658  
  2         4  
  2         4  
  2         15  
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 37 100     0 7511 $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 37 50       263 die $@ if $@;
463              
464 37         157 my $app_dsl_cb = _find_consumer();
465              
466 37 100       139 if ( $app_dsl_cb ) {
467 19         95 my $dsl = $app_dsl_cb->();
468              
469             {
470             ## no critic qw(TestingAndDebugging::ProhibitNoWarnings)
471 22     22   235 no strict 'refs';
  22         54  
  22         1220  
  19         41  
472 22     22   127 no warnings 'redefine';
  22         73  
  22         7593  
473 19     33   141 *{"${caller}::dsl"} = sub {$dsl};
  19         133  
  6         5086  
474             }
475             }
476              
477 37         100 return map { [ $_ => { class => $caller } ] }
  74         540  
478             qw/ plugin_keywords plugin_hooks /;
479             }
480              
481             sub _find_consumer {
482 75     65   828 my $class;
483              
484             ## no critic qw(ControlStructures::ProhibitCStyleForLoops)
485 65         4221 for ( my $i = 1; my $caller = caller($i); $i++ ) {
486 332 100       2309 $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 61         221 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 111     36 0 483442 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 35         4749 my $_DANCER2_IMPORT_TIME_SUBS = $plugin_module->_DANCER2_IMPORT_TIME_SUBS;
513             unshift(@$_DANCER2_IMPORT_TIME_SUBS, sub {
514 14     24   56 my $app_dsl_cb = _find_consumer();
515              
516             # Here we want to verify that "register_plugin" compat keyword
517             # was in fact only called from an app.
518 19 100       411874 $app_dsl_cb
519             or Carp::croak(
520             'I could not find a Dancer App for this plugin');
521              
522 18         907 my $dsl = $app_dsl_cb->();
523              
524 17         213 foreach my $keyword ( keys %{ $dsl->dsl_keywords} ) {
  16         424  
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 22     22   228 no strict 'refs';
  22         96  
  22         8250  
532             $plugin_module->can($keyword)
533 754         2751 or *{"${plugin_module}::$keyword"} = sub {
534             $_[0]
535 12 100       1554 ? do {
536 11         207 my $cb = shift()->app->name->can($keyword);
537 11         83 $cb->(@_);
538             }
539             : $app_dsl_cb->(@_);
540 906 100       6763 };
541             }
542 15         405 });
543             }
544              
545             sub _exporter_expand_sub {
546 125     233   16731 my( $plugin, $name, $args, $global ) = @_;
547 131         4023 my $class = $args->{class};
548              
549 128 100       584 return _exported_plugin_keywords($plugin,$class)
550             if $name eq 'plugin_keywords';
551              
552 91 100       802 return _exported_plugin_hooks($class)
553             if $name eq 'plugin_hooks';
554              
555 52 50       230 $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 52         144 my $p = $args->{plugin};
561 51         1702 my $sub = $p->keywords->{$name};
562             return $name => sub(@) {
563             # localize the plugin so we can get it later
564 42     66   469300 local $CUR_PLUGIN = $p;
565 45         1483 $sub->($p,@_);
566             }
567 51         484 }
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 22     22   182 no strict 'refs';
  22         57  
  22         976  
583 22     22   127 no warnings 'redefine';
  22         54  
  22         10248  
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   50         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 53     54   684 my( $plugin, $class ) = @_;
604              
605             return plugin_keywords => sub(@) {
606 30     41   476799 while( my $name = shift @_ ) {
607             ## no critic
608 37 100       1654 my $sub = is_coderef($_[0])
    100          
609             ? shift @_
610             : eval '\&'.$class."::" . ( ref $name ? $name->[0] : $name );
611 42 50       736 $class->keywords->{$_} = $sub for ref $name ? @$name : $name;
612             }
613             }
614 53         547 }
615              
616             sub _exported_plugin_hooks {
617 58     40   291 my $class = shift;
618 10     18   372700 return plugin_hooks => sub (@) { $class->add_hooks(@_) }
619 52         725 }
620              
621             1;
622              
623             __END__