File Coverage

blib/lib/Dancer2/Plugin.pm
Criterion Covered Total %
statement 251 259 96.9
branch 61 80 76.2
condition 15 22 68.1
subroutine 66 71 92.9
pod 6 24 25.0
total 399 456 87.5


line stmt bran cond sub pod time code
1             package Dancer2::Plugin;
2             # ABSTRACT: base class for Dancer2 plugins
3             $Dancer2::Plugin::VERSION = '1.0.0';
4 36     36   737806 use strict;
  36         189  
  36         715  
5 36     36   5931 use warnings;
  36         102  
  36         968  
6              
7 36     36   6192 use Moo;
  36         79180  
  36         191  
8 36     36   23188 use Carp;
  36         87  
  36         1562  
9 21     36   146 use List::Util qw/ reduce /;
  36         9786  
  36         2007  
10 36     22   9514 use Module::Runtime 'require_module';
  22         19500  
  22         199  
11 22     22   12833 use Attribute::Handlers;
  22         88125  
  22         124  
12 22     22   872 use Scalar::Util;
  22         52  
  22         990  
13 22     22   5573 use Ref::Util qw<is_arrayref is_coderef>;
  22         17873  
  22         24227  
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         15 push @{ $class->ClassHooks }, @_;
  7         130  
71             }
72              
73             sub execute_plugin_hook {
74 1     1 0 5 my ( $self, $name, @args ) = @_;
75 1         4 my $plugin_class = ref $self;
76              
77 1 50       15 $self->isa('Dancer2::Plugin')
78             or croak "Cannot call plugin hook ($name) from outside plugin";
79 1         2 $plugin_class =~ s/^Dancer2::Plugin:://; # short names
80              
81 1         9 my $full_name = 'plugin.' . lc($plugin_class) . ".$name";
82 1         6 $full_name =~ s/::/_/g;
83              
84 1         240 $self->app->execute_hook( $full_name, @args );
85             }
86              
87             sub find_plugin {
88 2     2 0 1301 my ( $self, $name ) = @_;
89 2         16 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 308 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   925 my $class = shift;
103 23         110 $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       119 my $config_name = delete $args{'from_config'}
110             or return ( $name, %args );
111              
112 8         19 $args{lazy} = 1;
113              
114 8 100       23 if ( is_coderef($config_name) ) {
115 2   66     9 $args{default} ||= $config_name;
116 2         3 $config_name = 1;
117             }
118              
119 8 100       23 $config_name = $name if $config_name eq '1';
120 8   100 1   40 my $orig_default = $args{default} || sub{};
121             $args{default} = sub {
122 8     8   141 my $plugin = shift;
123 8         234 my $value = reduce { eval { $a->{$b} } } $plugin->config, split /\./, $config_name;
  9         101  
  9         26  
124 8 100       91 return defined $value ? $value: $orig_default->($plugin);
125 8         33 };
126              
127 8         40 return $name => %args;
128             }
129              
130             sub _p2_has_keyword {
131 23     23   77 my( $class, $name, %args ) = @_;
132              
133 23 100       78 if( my $keyword = delete $args{plugin_keyword} ) {
134              
135 8 100       25 $keyword = $name if $keyword eq '1';
136              
137 6     6   135 $class->keywords->{$_} = sub { (shift)->$name(@_) }
138 8 100       199 for ref $keyword ? @$keyword : $keyword;
139             }
140              
141 23         136 return $name => %args;
142             }
143              
144             ### ATTRIBUTE HANDLER STUFF ########################################
145              
146             # :PluginKeyword shenanigans
147              
148             sub PluginKeyword :ATTR(CODE,BEGIN) {
149 7     6 0 32 my( $class, $sym_ref, $code, undef, $args ) = @_;
150              
151             # importing at BEGIN stage doesn't work with 5.10 :-(
152 7 50       263 return unless ref $sym_ref;
153              
154 6         10 my $func_name = *{$sym_ref}{NAME};
  6         14  
155              
156 6 100       20 $args = join '', @$args if is_arrayref($args);
157              
158 6   66     34 for my $name ( split ' ', $args || $func_name ) {
159 7         127 $class->keywords->{$name} = $code;
160             }
161              
162 22     22   198 }
  21         71  
  22         725  
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   18108 my( $class, $name, $args, $global ) = @_;
178              
179 66         161 my $caller = $global->{into};
180              
181 66 100       262 $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     407 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     469 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   93 my( $class, $caller, $global ) = @_;
198              
199 29         71 $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     1736 my $app = eval("${caller}::app()") || eval { $caller->dsl->app } ## no critic qw(BuiltinFunctions::ProhibitStringyEval)
206             or return; ## no critic
207              
208 29 50       223 return unless $app->can('with_plugin');
209              
210 29         199 my $plugin = $app->with_plugin( '+' . $class );
211 29         104 $global->{'plugin'} = $plugin;
212              
213 29 50       210 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         78 foreach my $hook ( @{ $plugin->ClassHooks } ) {
  29         588  
225 7         56 my $full_name = 'plugin.' . lc($class) . ".$hook";
226 7         29 $full_name =~ s/Dancer2::Plugin:://i;
227 7         19 $full_name =~ s/::/_/g;
228              
229             # this adds it to the plugin
230 7         91 $plugin->hook_aliases->{$hook} = $full_name;
231              
232             # this adds it to the app
233 7         54 $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         189 @{ $plugin->app->hooks }{ keys %{ $plugin->hooks } } =
  7         298  
239 7         15 values %{ $plugin->hooks };
  7         143  
240             }
241             }
242              
243             {
244             # get the reference
245 29         64 my ($plugin_addr) = "$plugin" =~ $REF_ADDR_REGEX;
  29         121  
  29         233  
246              
247 29     5   203 $instances{$plugin_addr}{'config'} = sub { $plugin->config };
  5         110  
248 29         153 $instances{$plugin_addr}{'app'} = $plugin->app;
249              
250 29         123 Scalar::Util::weaken( $instances{$plugin_addr}{'app'} );
251              
252             ## no critic
253 21     21   19792 no strict 'refs';
  21         81  
  21         849  
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 21     21   158 no warnings 'redefine';
  21         63  
  21         15967  
262 29         323 *{"${class}::plugin_setting"} = sub {
263 5     5   65 my ($plugin_addr) = "$CUR_PLUGIN" =~ $REF_ADDR_REGEX;
264              
265 5 50       18 $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         23 $name =~ s/^Dancer2::Plugin:://g;
274              
275 5         15 my $plugin_inst = $instances{$plugin_addr};
276 5         17 my $plugin_config = $plugin_inst->{'config'}->();
277 5         110 my $app_plugin_config = $plugin_inst->{'app'}->config->{'plugins'}{$name};
278              
279 5 50       37 return { %{ $plugin_config || {} }, %{ $app_plugin_config || {} } };
  5 50       24  
  5         41  
280 29         130 };
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         222 *{"${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   82 if ( $_[0]->isa('Dancer2::Plugin') ) {
292             # this means it's probably our hook, we need to verify it
293 2         6 my ( $plugin_self, $hook_name, @args ) = @_;
294              
295 2         6 my $plugin_class = lc $class;
296 2         8 $plugin_class =~ s/^dancer2::plugin:://;
297 2         6 $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       32 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       38 $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         4 for @{ $plugin->hooks->{$hook_name} };
  2         34  
318              
319 2         11 return;
320             }
321              
322 3         70 return $plugin->app->execute_hook(@_);
323 29         174 };
324             }
325              
326 29         115 local $CUR_PLUGIN = $plugin;
327 29         78 $_->($plugin) for @{ $plugin->_DANCER2_IMPORT_TIME_SUBS() };
  29         710  
328              
329 29         84 map { [ $_ => {plugin => $plugin} ] } keys %{ $plugin->keywords };
  50         274  
  29         646  
330             }
331              
332             # turns the caller namespace into
333             # a D2P2 class, with exported keywords
334             sub _exporter_plugin {
335 37     37   74 my $caller = shift;
336 37         174 require_module('Dancer2::Core::DSL');
337 37         833 my $keywords_list = join ' ', keys %{ Dancer2::Core::DSL->dsl_keywords };
  37         200  
338              
339 21 50   21 0 195 eval <<"END"; ## no critic
  21 50   21 0 48  
  21 100   21 0 93  
  21 50   21 1 10251  
  21     21 1 76  
  21     29 1 519  
  21     15 1 126  
  21     26 1 45  
  21     9 0 157  
  21     110 0 5116  
  21     20 0 57  
  21     4 0 189  
  6     9   2931  
  21     5   16934  
  21     11   64  
  21     0   4886  
  37     0   4171  
  29     0   197  
  33         249  
  10         116  
  107         9023  
  24         2641  
  4         54  
  3         11  
  9         1068  
  6         18  
  6         32  
  6         61  
  6         138  
  5         11  
  5         7  
  5         8  
  5         29  
  2         3680  
  8         2974  
  0            
  0            
  0            
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   4 0 7597 $no_dsl->{$caller} or eval <<"END"; ## no critic
  4     4 0 55  
  5     2 0 267  
  4     1 0 54  
  3         31  
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       171 die $@ if $@;
463              
464 37         124 my $app_dsl_cb = _find_consumer();
465              
466 37 100       120 if ( $app_dsl_cb ) {
467 19         66 my $dsl = $app_dsl_cb->();
468              
469             {
470             ## no critic qw(TestingAndDebugging::ProhibitNoWarnings)
471 21     21   173 no strict 'refs';
  21         61  
  21         788  
  19         32  
472 21     21   129 no warnings 'redefine';
  21         81  
  21         6017  
473 19     6   78 *{"${caller}::dsl"} = sub {$dsl};
  19         88  
  6         2692  
474             }
475             }
476              
477 37         95 return map { [ $_ => { class => $caller } ] }
  74         477  
478             qw/ plugin_keywords plugin_hooks /;
479             }
480              
481             sub _find_consumer {
482 50     48   87 my $class;
483              
484             ## no critic qw(ControlStructures::ProhibitCStyleForLoops)
485 50         260 for ( my $i = 1; my $caller = caller($i); $i++ ) {
486 300 100       2005 $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 53         3373 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 15     10 0 58 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 15         335 my $_DANCER2_IMPORT_TIME_SUBS = $plugin_module->_DANCER2_IMPORT_TIME_SUBS;
513             unshift(@$_DANCER2_IMPORT_TIME_SUBS, sub {
514 14     11   43 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 14 50       50 $app_dsl_cb
519             or Carp::croak(
520             'I could not find a Dancer App for this plugin');
521              
522 14         41 my $dsl = $app_dsl_cb->();
523              
524 14         25 foreach my $keyword ( keys %{ $dsl->dsl_keywords} ) {
  14         77  
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 21     21   182 no strict 'refs';
  21         56  
  21         6929  
532             $plugin_module->can($keyword)
533 742         2562 or *{"${plugin_module}::$keyword"} = sub {
534             $_[0]
535 10 100   8   1548 ? do {
536 9         108 my $cb = shift()->app->name->can($keyword);
537 9         88 $cb->(@_);
538             }
539             : $app_dsl_cb->(@_);
540 893 100       6512 };
541             }
542 14         336 });
543             }
544              
545             sub _exporter_expand_sub {
546 126     124   7796 my( $plugin, $name, $args, $global ) = @_;
547 126         305 my $class = $args->{class};
548              
549 126 100       438 return _exported_plugin_keywords($plugin,$class)
550             if $name eq 'plugin_keywords';
551              
552 89 100       330 return _exported_plugin_hooks($class)
553             if $name eq 'plugin_hooks';
554              
555 51 50       407 $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 51         98 my $p = $args->{plugin};
561 51         1118 my $sub = $p->keywords->{$name};
562             return $name => sub(@) {
563             # localize the plugin so we can get it later
564 42     41   15329 local $CUR_PLUGIN = $p;
565 42         459 $sub->($p,@_);
566             }
567 51         383 }
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 21     21   165 no strict 'refs';
  21         76  
  21         718  
583 21     21   135 no warnings 'redefine';
  21         57  
  21         7736  
584             my $orig_cb = Dancer2::Core::App->can('add_hook');
585             $orig_cb and *{'Dancer2::Core::App::add_hook'} = sub {
586 13     12   420 my ( $app, $hook ) = @_;
587              
588 13 100       86 my $hook_code = Scalar::Util::blessed($hook) ? $hook->code : $hook->{code};
589 13         39 my $plugin = $CUR_PLUGIN;
590              
591             $hook->{'code'} = sub {
592 18     18   49 local $CUR_PLUGIN = $plugin;
593 18         63 $hook_code->(@_);
594 12         58 };
595              
596 12         286 $orig_cb->(@_);
597             };
598             }
599              
600              
601             # define the exported 'plugin_keywords'
602             sub _exported_plugin_keywords{
603 37     37   105 my( $plugin, $class ) = @_;
604              
605             return plugin_keywords => sub(@) {
606 13     13   4004 while( my $name = shift @_ ) {
607             ## no critic
608 20 50       1060 my $sub = is_coderef($_[0])
    100          
609             ? shift @_
610             : eval '\&'.$class."::" . ( ref $name ? $name->[0] : $name );
611 20 50       483 $class->keywords->{$_} = $sub for ref $name ? @$name : $name;
612             }
613             }
614 37         341 }
615              
616             sub _exported_plugin_hooks {
617 37     37   83 my $class = shift;
618 7     7   804 return plugin_hooks => sub (@) { $class->add_hooks(@_) }
619 37         283 }
620              
621             1;
622              
623             __END__
624              
625             =pod
626              
627             =encoding UTF-8
628              
629             =head1 NAME
630              
631             Dancer2::Plugin - base class for Dancer2 plugins
632              
633             =head1 VERSION
634              
635             version 1.0.0
636              
637             =head1 SYNOPSIS
638              
639             The plugin itself:
640              
641             package Dancer2::Plugin::Polite;
642              
643             use strict;
644             use warnings;
645              
646             use Dancer2::Plugin;
647              
648             has smiley => (
649             is => 'ro',
650             default => sub {
651             $_[0]->config->{smiley} || ':-)'
652             }
653             );
654              
655             plugin_keywords 'add_smileys';
656              
657             sub BUILD {
658             my $plugin = shift;
659              
660             $plugin->app->add_hook( Dancer2::Core::Hook->new(
661             name => 'after',
662             code => sub { $_[0]->content( $_[0]->content . " ... please?" ) }
663             ));
664              
665             $plugin->app->add_route(
666             method => 'get',
667             regexp => '/goodbye',
668             code => sub {
669             my $app = shift;
670             'farewell, ' . $app->request->params->{name};
671             },
672             );
673              
674             }
675              
676             sub add_smileys {
677             my( $plugin, $text ) = @_;
678              
679             $text =~ s/ (?<= \. ) / $plugin->smiley /xeg;
680              
681             return $text;
682             }
683              
684             1;
685              
686             then to load into the app:
687              
688             package MyApp;
689              
690             use strict;
691             use warnings;
692              
693             use Dancer2;
694              
695             BEGIN { # would usually be in config.yml
696             set plugins => {
697             Polite => {
698             smiley => '8-D',
699             },
700             };
701             }
702              
703             use Dancer2::Plugin::Polite;
704              
705             get '/' => sub {
706             add_smileys( 'make me a sandwich.' );
707             };
708              
709             1;
710              
711             =head1 DESCRIPTION
712              
713             =head2 Writing the plugin
714              
715             =head3 C<use Dancer2::Plugin>
716              
717             The plugin must begin with
718              
719             use Dancer2::Plugin;
720              
721             which will turn the package into a L<Moo> class that inherits from L<Dancer2::Plugin>. The base class provides the plugin with
722             two attributes: C<app>, which is populated with the Dancer2 app object for which
723             the plugin is being initialized for, and C<config> which holds the plugin
724             section of the application configuration.
725              
726             =head3 Modifying the app at building time
727              
728             If the plugin needs to tinker with the application -- add routes or hooks, for example --
729             it can do so within its C<BUILD()> function.
730              
731             sub BUILD {
732             my $plugin = shift;
733              
734             $plugin->app->add_route( ... );
735             }
736              
737             =head3 Adding keywords
738              
739             =head4 Via C<plugin_keywords>
740              
741             Keywords that the plugin wishes to export to the Dancer2 app can be defined via the C<plugin_keywords> keyword:
742              
743             plugin_keywords qw/
744             add_smileys
745             add_sad_kitten
746             /;
747              
748             Each of the keyword will resolve to the class method of the same name. When invoked as keyword, it'll be passed
749             the plugin object as its first argument.
750              
751             sub add_smileys {
752             my( $plugin, $text ) = @_;
753              
754             return join ' ', $text, $plugin->smiley;
755             }
756              
757             # and then in the app
758              
759             get '/' => sub {
760             add_smileys( "Hi there!" );
761             };
762              
763             You can also pass the functions directly to C<plugin_keywords>.
764              
765             plugin_keywords
766             add_smileys => sub {
767             my( $plugin, $text ) = @_;
768              
769             $text =~ s/ (?<= \. ) / $plugin->smiley /xeg;
770              
771             return $text;
772             },
773             add_sad_kitten => sub { ... };
774              
775             Or a mix of both styles. We're easy that way:
776              
777             plugin_keywords
778             add_smileys => sub {
779             my( $plugin, $text ) = @_;
780              
781             $text =~ s/ (?<= \. ) / $plugin->smiley /xeg;
782              
783             return $text;
784             },
785             'add_sad_kitten';
786              
787             sub add_sad_kitten {
788             ...;
789             }
790              
791             If you want several keywords to be synonyms calling the same
792             function, you can list them in an arrayref. The first
793             function of the list is taken to be the "real" method to
794             link to the keywords.
795              
796             plugin_keywords [qw/ add_smileys add_happy_face /];
797              
798             sub add_smileys { ... }
799              
800             Calls to C<plugin_keywords> are cumulative.
801              
802             =head4 Via the C<:PluginKeyword> function attribute
803              
804             For perl 5.12 and higher, keywords can also be defined by adding the C<:PluginKeyword> attribute
805             to the function you wish to export.
806              
807             For Perl 5.10, the export triggered by the sub attribute comes too late in the
808             game, and the keywords won't be exported in the application namespace.
809              
810             sub foo :PluginKeyword { ... }
811              
812             sub bar :PluginKeyword( baz quux ) { ... }
813              
814             # equivalent to
815              
816             sub foo { ... }
817             sub bar { ... }
818              
819             plugin_keywords 'foo', [ qw/ baz quux / ] => \&bar;
820              
821             =head4 For an attribute
822              
823             You can also turn an attribute of the plugin into a keyword.
824              
825             has foo => (
826             is => 'ro',
827             plugin_keyword => 1, # keyword will be 'foo'
828             );
829              
830             has bar => (
831             is => 'ro',
832             plugin_keyword => 'quux', # keyword will be 'quux'
833             );
834              
835             has baz => (
836             is => 'ro',
837             plugin_keyword => [ 'baz', 'bazz' ], # keywords will be 'baz' and 'bazz'
838             );
839              
840             =head3 Accessing the plugin configuration
841              
842             The plugin configuration is available via the C<config()> method.
843              
844             sub BUILD {
845             my $plugin = shift;
846              
847             if ( $plugin->config->{feeling_polite} ) {
848             $plugin->app->add_hook( Dancer2::Core::Hook->new(
849             name => 'after',
850             code => sub { $_[0]->content( $_[0]->content . " ... please?" ) }
851             ));
852             }
853             }
854              
855             =head3 Getting default values from config file
856              
857             Since initializing a plugin with either a default or a value passed via the configuration file,
858             like
859              
860             has smiley => (
861             is => 'ro',
862             default => sub {
863             $_[0]->config->{smiley} || ':-)'
864             }
865             );
866              
867             C<Dancer2::Plugin> allows for a C<from_config> key in the attribute definition.
868             Its value is the plugin configuration key that will be used to initialize the attribute.
869              
870             If it's given the value C<1>, the name of the attribute will be taken as the configuration key.
871              
872             Nested hash keys can also be referred to using a dot notation.
873              
874             If the plugin configuration has no value for the given key, the attribute default, if specified, will be honored.
875              
876             If the key is given a coderef as value, it's considered to be a C<default> value combo:
877              
878             has foo => (
879             is => 'ro',
880             from_config => sub { 'my default' },
881             );
882              
883              
884             # equivalent to
885             has foo => (
886             is => 'ro',
887             from_config => 'foo',
888             default => sub { 'my default' },
889             );
890              
891             For example:
892              
893             # in config.yml
894              
895             plugins:
896             Polite:
897             smiley: ':-)'
898             greeting:
899             casual: Hi!
900             formal: How do you do?
901              
902              
903             # in the plugin
904              
905             has smiley => ( # will be ':-)'
906             is => 'ro',
907             from_config => 1,
908             default => sub { ':-(' },
909             );
910              
911             has casual_greeting => ( # will be 'Hi!'
912             is => 'ro',
913             from_config => 'greeting.casual',
914             );
915              
916             has apology => ( # will be 'sorry'
917             is => 'ro',
918             from_config => 'apology',
919             default => sub { 'sorry' },
920             )
921              
922             has closing => ( # will be 'See ya!'
923             is => 'ro',
924             from_config => sub { 'See ya!' },
925             );
926              
927             =head3 Config becomes immutable
928              
929             The plugin's C<config> attribute is loaded lazily on the first call to
930             C<config>. After this first call C<config> becomes immutable so you cannot
931             do the following in a test:
932              
933             use Dancer2;
934             use Dancer2::Plugin::FooBar;
935              
936             set plugins => {
937             FooBar => {
938             wibble => 1, # this is OK
939             },
940             };
941              
942             flibble(45); # plugin keyword called which causes config read
943            
944             set plugins => {
945             FooBar => {
946             wibble => 0, # this will NOT change plugin config
947             },
948             };
949              
950             =head3 Accessing the parent Dancer app
951              
952             If the plugin is instantiated within a Dancer app, it'll be
953             accessible via the method C<app()>.
954              
955             sub BUILD {
956             my $plugin = shift;
957              
958             $plugin->app->add_route( ... );
959             }
960              
961             To use Dancer's DSL in your plugin:
962              
963             $self->dsl->debug( “Hi! I’m logging from your plugin!” );
964              
965             See L<Dancer2::Manual/"DSL KEYWORDS"> for a full list of Dancer2 DSL.
966              
967             =head2 Using the plugin within the app
968              
969             A plugin is loaded via
970              
971             use Dancer2::Plugin::Polite;
972              
973             The plugin will assume that it's loading within a Dancer module and will
974             automatically register itself against its C<app()> and export its keywords
975             to the local namespace. If you don't want this to happen, specify that you
976             don't want anything imported via empty parentheses when C<use>ing the module:
977              
978             use Dancer2::Plugin::Polite ();
979              
980             =head2 Plugins using plugins
981              
982             It's easy to use plugins from within a plugin:
983              
984             package Dancer2::Plugin::SourPuss;
985            
986             use Dancer2::Plugin;
987             use Dancer2::Plugin::Polite;
988            
989             sub my_keyword { my $smiley = smiley(); }
990              
991             1;
992              
993             This does not export C<smiley()> into your application - it is only available
994             from within your plugin. However, from the example above, you can wrap
995             DSL from other plugins and make it available from your plugin.
996              
997             =head2 Utilizing other plugins
998              
999             You can use the C<find_plugin> to locate other plugins loaded by the user,
1000             in order to use them, or their information, directly:
1001              
1002             # MyApp.pm
1003             use Dancer2;
1004             use Dancer2::Plugin::Foo;
1005             use Dancer2::Plugin::Bar;
1006              
1007             # Dancer2::Plugin::Bar;
1008             ...
1009              
1010             sub my_keyword {
1011             my $self = shift;
1012             my $foo = $self->find_plugin('Dancer2::Plugin::Foo')
1013             or $self->dsl->send_error('Could not find Foo');
1014              
1015             return $foo->foo_keyword(...);
1016             }
1017              
1018             =head2 Hooks
1019              
1020             New plugin hooks are declared via C<plugin_hooks>.
1021              
1022             plugin_hooks 'my_hook', 'my_other_hook';
1023              
1024             Hooks are prefixed with C<plugin.plugin_name>. So the plugin
1025             C<my_hook> coming from the plugin C<Dancer2::Plugin::MyPlugin> will have the hook name
1026             C<plugin.myplugin.my_hook>.
1027              
1028             Hooks are executed within the plugin by calling them via the associated I<app>.
1029              
1030             $plugin->execute_plugin_hook( 'my_hook' );
1031              
1032             You can also call any other hook if you provide the full name using the
1033             C<execute_hook> method:
1034              
1035             $plugin->app->execute_hook( 'core.app.route_exception' );
1036              
1037             Or using their alias:
1038              
1039             $plugin->app->execute_hook( 'on_route_exception' );
1040              
1041             B<Note:> If your plugin consumes a plugin that declares any hooks, those hooks
1042             are added to your application, even though DSL is not.
1043              
1044             =head2 Writing Test Gotchas
1045              
1046             =head3 Constructor for Dancer2::Plugin::Foo has been inlined and cannot be updated
1047              
1048             You'll usually get this one because you are defining both the plugin and app
1049             in your test file, and the runtime creation of Moo's attributes happens after
1050             the compile-time import voodoo dance.
1051              
1052             To get around this nightmare, wrap your plugin definition in a C<BEGIN> block.
1053              
1054             BEGIN {
1055             package Dancer2::Plugin::Foo;
1056              
1057             use Dancer2::Plugin;
1058              
1059             has bar => (
1060             is => 'ro',
1061             from_config => 1,
1062             );
1063              
1064             plugin_keywords qw/ bar /;
1065              
1066             }
1067              
1068             {
1069             package MyApp;
1070              
1071             use Dancer2;
1072             use Dancer2::Plugin::Foo;
1073              
1074             bar();
1075             }
1076              
1077             =head3 You cannot overwrite a locally defined method (bar) with a reader
1078              
1079             If you set an object attribute of your plugin to be a keyword as well, you need
1080             to call C<plugin_keywords> after the attribute definition.
1081              
1082             package Dancer2::Plugin::Foo;
1083              
1084             use Dancer2::Plugin;
1085              
1086             has bar => (
1087             is => 'ro',
1088             );
1089              
1090             plugin_keywords 'bar';
1091              
1092             =head1 AUTHOR
1093              
1094             Dancer Core Developers
1095              
1096             =head1 COPYRIGHT AND LICENSE
1097              
1098             This software is copyright (c) 2023 by Alexis Sukrieh.
1099              
1100             This is free software; you can redistribute it and/or modify it under
1101             the same terms as the Perl 5 programming language system itself.
1102              
1103             =cut