File Coverage

blib/lib/Dancer2/Plugin.pm
Criterion Covered Total %
statement 255 260 98.0
branch 61 80 76.2
condition 15 22 68.1
subroutine 66 71 92.9
pod 6 24 25.0
total 403 457 88.1


line stmt bran cond sub pod time code
1             package Dancer2::Plugin;
2             # ABSTRACT: base class for Dancer2 plugins
3             $Dancer2::Plugin::VERSION = '0.400001';
4 36     36   629437 use strict;
  36         157  
  36         1925  
5 36     36   5624 use warnings;
  36         86  
  36         888  
6              
7 36     36   5217 use Moo;
  36         68599  
  36         195  
8 36     36   20918 use Carp;
  36         116  
  36         1445  
9 21     36   124 use List::Util qw/ reduce /;
  36         9192  
  36         2777  
10 36     22   7827 use Module::Runtime 'require_module';
  22         16877  
  22         130  
11 22     22   11213 use Attribute::Handlers;
  22         76412  
  22         111  
12 22     22   813 use Scalar::Util;
  22         51  
  22         897  
13 22     22   4843 use Ref::Util qw<is_arrayref is_coderef>;
  22         15225  
  22         20914  
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 16 my $class = shift;
70 7         12 push @{ $class->ClassHooks }, @_;
  7         124  
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       12 $self->isa('Dancer2::Plugin')
78             or croak "Cannot call plugin hook ($name) from outside plugin";
79 1         4 $plugin_class =~ s/^Dancer2::Plugin:://; # short names
80              
81 1         5 my $full_name = 'plugin.' . lc($plugin_class) . ".$name";
82 1         5 $full_name =~ s/::/_/g;
83              
84 1         20 $self->app->execute_hook( $full_name, @args );
85             }
86              
87             sub find_plugin {
88 2     2 0 994 my ( $self, $name ) = @_;
89 2         15 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 338 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   931 my $class = shift;
103 23         102 $class->_p2_has_from_config( $class->_p2_has_keyword( @_ ) );
104             };
105              
106             sub _p2_has_from_config {
107 23     23   65 my( $class, $name, %args ) = @_;
108              
109 23 100       129 my $config_name = delete $args{'from_config'}
110             or return ( $name, %args );
111              
112 8         18 $args{lazy} = 1;
113              
114 8 100       21 if ( is_coderef($config_name) ) {
115 2   66     9 $args{default} ||= $config_name;
116 2         4 $config_name = 1;
117             }
118              
119 8 100       23 $config_name = $name if $config_name eq '1';
120 8   100 1   1239 my $orig_default = $args{default} || sub{};
121             $args{default} = sub {
122 8     8   104 my $plugin = shift;
123 8         254 my $value = reduce { eval { $a->{$b} } } $plugin->config, split /\./, $config_name;
  9         87  
  9         30  
124 8 100       98 return defined $value ? $value: $orig_default->($plugin);
125 8         40 };
126              
127 8         41 return $name => %args;
128             }
129              
130             sub _p2_has_keyword {
131 23     23   81 my( $class, $name, %args ) = @_;
132              
133 23 100       74 if( my $keyword = delete $args{plugin_keyword} ) {
134              
135 8 100       23 $keyword = $name if $keyword eq '1';
136              
137 6     6   110 $class->keywords->{$_} = sub { (shift)->$name(@_) }
138 8 100       179 for ref $keyword ? @$keyword : $keyword;
139             }
140              
141 23         131 return $name => %args;
142             }
143              
144             ### ATTRIBUTE HANDLER STUFF ########################################
145              
146             # :PluginKeyword shenanigans
147              
148             sub PluginKeyword :ATTR(CODE,BEGIN) {
149 7     6 0 17 my( $class, $sym_ref, $code, undef, $args ) = @_;
150              
151             # importing at BEGIN stage doesn't work with 5.10 :-(
152 7 50       237 return unless ref $sym_ref;
153              
154 6         9 my $func_name = *{$sym_ref}{NAME};
  6         11  
155              
156 6 100       16 $args = join '', @$args if is_arrayref($args);
157              
158 6   66     28 for my $name ( split ' ', $args || $func_name ) {
159 7         111 $class->keywords->{$name} = $code;
160             }
161              
162 22     22   174 }
  21         55  
  22         821  
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   17904 my( $class, $name, $args, $global ) = @_;
178              
179 66         158 my $caller = $global->{into};
180              
181 66 100       244 $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     411 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     446 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   86 my( $class, $caller, $global ) = @_;
198              
199 29         77 $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     1676 my $app = eval("${caller}::app()") || eval { $caller->dsl->app } ## no critic qw(BuiltinFunctions::ProhibitStringyEval)
206             or return; ## no critic
207              
208 29 50       232 return unless $app->can('with_plugin');
209              
210 29         178 my $plugin = $app->with_plugin( '+' . $class );
211 29         94 $global->{'plugin'} = $plugin;
212              
213 29 50       200 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         585  
225 7         46 my $full_name = 'plugin.' . lc($class) . ".$hook";
226 7         29 $full_name =~ s/Dancer2::Plugin:://i;
227 7         21 $full_name =~ s/::/_/g;
228              
229             # this adds it to the plugin
230 7         76 $plugin->hook_aliases->{$hook} = $full_name;
231              
232             # this adds it to the app
233 7         44 $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         167 @{ $plugin->app->hooks }{ keys %{ $plugin->hooks } } =
  7         322  
239 7         16 values %{ $plugin->hooks };
  7         139  
240             }
241             }
242              
243             {
244             # get the reference
245 29         62 my ($plugin_addr) = "$plugin" =~ $REF_ADDR_REGEX;
  29         100  
  29         232  
246              
247 29     5   191 $instances{$plugin_addr}{'config'} = sub { $plugin->config };
  5         95  
248 29         157 $instances{$plugin_addr}{'app'} = $plugin->app;
249              
250 29         119 Scalar::Util::weaken( $instances{$plugin_addr}{'app'} );
251              
252             ## no critic
253 21     21   16466 no strict 'refs';
  21         53  
  21         818  
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   143 no warnings 'redefine';
  21         49  
  21         14501  
262 29         254 *{"${class}::plugin_setting"} = sub {
263 5     5   63 my ($plugin_addr) = "$CUR_PLUGIN" =~ $REF_ADDR_REGEX;
264              
265 5 50       17 $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         12 my $name = ref $CUR_PLUGIN;
273 5         17 $name =~ s/^Dancer2::Plugin:://g;
274              
275 5         12 my $plugin_inst = $instances{$plugin_addr};
276 5         15 my $plugin_config = $plugin_inst->{'config'}->();
277 5         91 my $app_plugin_config = $plugin_inst->{'app'}->config->{'plugins'}{$name};
278              
279 5 50       33 return { %{ $plugin_config || {} }, %{ $app_plugin_config || {} } };
  5 50       20  
  5         35  
280 29         144 };
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         186 *{"${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   78 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         4 my $plugin_class = lc $class;
296 2         7 $plugin_class =~ s/^dancer2::plugin:://;
297 2         5 $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       28 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         3 for @{ $plugin->hooks->{$hook_name} };
  2         27  
318              
319 2         8 return;
320             }
321              
322 3         69 return $plugin->app->execute_hook(@_);
323 29         168 };
324             }
325              
326 29         81 local $CUR_PLUGIN = $plugin;
327 29         55 $_->($plugin) for @{ $plugin->_DANCER2_IMPORT_TIME_SUBS() };
  29         649  
328              
329 29         81 map { [ $_ => {plugin => $plugin} ] } keys %{ $plugin->keywords };
  50         259  
  29         588  
330             }
331              
332             # turns the caller namespace into
333             # a D2P2 class, with exported keywords
334             sub _exporter_plugin {
335 37     37   78 my $caller = shift;
336 37         168 require_module('Dancer2::Core::DSL');
337 37         831 my $keywords_list = join ' ', keys %{ Dancer2::Core::DSL->dsl_keywords };
  37         206  
338              
339 21 50   21 0 154 eval <<"END"; ## no critic
  21 50   21 0 51  
  21 100   21 0 120  
  21 50   21 1 8355  
  21     21 1 52  
  21     26 1 536  
  21     19 1 127  
  21     25 1 42  
  21     12 0 129  
  21     106 0 4685  
  21     21 0 53  
  21     6 0 142  
  6     5   2593  
  21     6   15747  
  21     10   58  
  21     0   4195  
  37     0   3875  
  26     0   167  
  37         189  
  11         68  
  105         8309  
  24         1801  
  6         25  
  1         8  
  7         1721  
  4         16  
  4         23  
  4         47  
  4         121  
  3         8  
  3         7  
  3         6  
  3         22  
  3         5753  
  7         229  
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 {};
420              
421             sub plugin_args {
422             Carp::carp "Plugin DSL method 'plugin_args' is deprecated. "
423             . "Use '\\\@_' instead'.\n";
424              
425             \@_;
426             }
427             }
428             END
429              
430 37 100   5 0 8069 $no_dsl->{$caller} or eval <<"END"; ## no critic
  5     4 0 172  
  3     3 0 56  
  5     1 0 224  
  3         24  
  2         4  
  2         5  
  2         15  
  5         2998  
431             {
432             package $caller;
433              
434             # FIXME: AUTOLOAD might pick up on this
435             sub dancer_app {
436             Carp::carp "DEPRECATED: Plugin DSL method 'dancer_app'. "
437             . "Please use '\\\$self->app' instead'.\n";
438              
439             \$_[0]->app;
440             }
441              
442             # FIXME: AUTOLOAD might pick up on this
443             sub request {
444             Carp::carp "DEPRECATED: Plugin DSL method 'request'. "
445             . "Please use '\\\$self->app->request' instead'.\n";
446              
447             \$_[0]->app->request;
448             }
449              
450             # FIXME: AUTOLOAD might pick up on this
451             sub var {
452             Carp::carp "DEPRECATED: Plugin DSL method 'var'. "
453             . "Please use '\\\$self->app->request->var' instead'.\n";
454              
455             shift->app->request->var(\@_);
456             }
457              
458             # FIXME: AUTOLOAD might pick up on this
459             sub hook {
460             Carp::carp "DEPRECATED: Plugin DSL method 'hook'. "
461             . "Please use '\\\$self->app->add_hook' instead'.\n";
462              
463             shift->app->add_hook(
464             Dancer2::Core::Hook->new( name => shift, code => shift ) );
465             }
466              
467             }
468             END
469              
470 37 50       170 die $@ if $@;
471              
472 37         128 my $app_dsl_cb = _find_consumer();
473              
474 37 100       1271 if ( $app_dsl_cb ) {
475 19         73 my $dsl = $app_dsl_cb->();
476              
477             {
478             ## no critic qw(TestingAndDebugging::ProhibitNoWarnings)
479 21     21   159 no strict 'refs';
  21         45  
  21         938  
  19         34  
480 21     21   124 no warnings 'redefine';
  21         59  
  21         5622  
481 19     6   80 *{"${caller}::dsl"} = sub {$dsl};
  19         87  
  6         3088  
482             }
483             }
484              
485 37         91 return map { [ $_ => { class => $caller } ] }
  74         417  
486             qw/ plugin_keywords plugin_hooks /;
487             }
488              
489             sub _find_consumer {
490 53     48   97 my $class;
491              
492             ## no critic qw(ControlStructures::ProhibitCStyleForLoops)
493 53         337 for ( my $i = 1; my $caller = caller($i); $i++ ) {
494 302 100       2075 $class = $caller->can('dsl')
495             and last;
496             }
497              
498             # If you use a Dancer2 plugin outside a Dancer App, this fails.
499             # It also breaks a bunch of the tests. -- SX
500             #$class
501             # or croak('Could not find Dancer2 app');
502              
503 51         149 return $class;
504             }
505              
506             # This has to be called for now at the end of every plugin package, in order to
507             # map the keywords of the associated app to the plugin, so that these keywords
508             # can be called from within the plugin code. This function is deprecated, as
509             # it's tied to the old plugin system. It's kept here for backcompat reason, but
510             # should go away with the old plugin system.
511             sub register_plugin {
512              
513 13     10 0 44 my $plugin_module = caller(1);
514              
515             # if you ask yourself why we do the injection in the plugin
516             # module namespace every time the plugin is used, and not only
517             # once, it's because it can be used by different app that could
518             # have a different DSL with a different list of keywords.
519              
520 13         190 my $_DANCER2_IMPORT_TIME_SUBS = $plugin_module->_DANCER2_IMPORT_TIME_SUBS;
521             unshift(@$_DANCER2_IMPORT_TIME_SUBS, sub {
522 14     11   70 my $app_dsl_cb = _find_consumer();
523              
524             # Here we want to verify that "register_plugin" compat keyword
525             # was in fact only called from an app.
526 13 50       302 $app_dsl_cb
527             or Carp::croak(
528             'I could not find a Dancer App for this plugin');
529              
530 13         50 my $dsl = $app_dsl_cb->();
531              
532 13         48 foreach my $keyword ( keys %{ $dsl->dsl_keywords} ) {
  13         66  
533             # if not yet defined, inject the keyword in the plugin
534             # namespace, but make sure the code will always get the
535             # coderef from the right associated app, because one plugin
536             # can be used by multiple apps. Note that we remove the
537             # first parameter (plugin instance) from what we pass to
538             # the keyword implementation of the App
539 21     21   160 no strict 'refs';
  21         39  
  21         6114  
540             $plugin_module->can($keyword)
541 742         2537 or *{"${plugin_module}::$keyword"} = sub {
542             $_[0]
543 10 100   8   1319 ? do {
544 9         104 my $cb = shift()->app->name->can($keyword);
545 9         99 $cb->(@_);
546             }
547             : $app_dsl_cb->(@_);
548 893 100       6140 };
549             }
550 13         74 });
551             }
552              
553             sub _exporter_expand_sub {
554 127     124   7815 my( $plugin, $name, $args, $global ) = @_;
555 127         240 my $class = $args->{class};
556              
557 127 100       429 return _exported_plugin_keywords($plugin,$class)
558             if $name eq 'plugin_keywords';
559              
560 90 100       327 return _exported_plugin_hooks($class)
561             if $name eq 'plugin_hooks';
562              
563 53 50       179 $exported_app->{ $global->{'into'} }
564             or Carp::croak('Specific subroutines cannot be exported from plugin');
565              
566             # otherwise, we're exporting a keyword
567              
568 53         101 my $p = $args->{plugin};
569 53         1064 my $sub = $p->keywords->{$name};
570             return $name => sub(@) {
571             # localize the plugin so we can get it later
572 44     41   15260 local $CUR_PLUGIN = $p;
573 41         364 $sub->($p,@_);
574             }
575 53         314 }
576              
577             # "There's a good reason for this, I swear!"
578             # -- Sawyer X
579             # basically, if someone adds a hook to the app directly
580             # that needs to access a DSL that needs the current object
581             # (such as "plugin_setting"),
582             # that object needs to be available
583             # So:
584             # we override App's "add_hook" to provide a register a
585             # different hook callback, that closes over the plugin when
586             # it's available, relocalizes it when the callback runs and
587             # after localizing it, calls the original hook callback
588             {
589             ## no critic;
590 21     21   172 no strict 'refs';
  21         44  
  21         671  
591 21     21   119 no warnings 'redefine';
  21         51  
  21         6892  
592             my $orig_cb = Dancer2::Core::App->can('add_hook');
593             $orig_cb and *{'Dancer2::Core::App::add_hook'} = sub {
594 12     12   406 my ( $app, $hook ) = @_;
595              
596 12 100       93 my $hook_code = Scalar::Util::blessed($hook) ? $hook->code : $hook->{code};
597 12         31 my $plugin = $CUR_PLUGIN;
598              
599             $hook->{'code'} = sub {
600 18     18   38 local $CUR_PLUGIN = $plugin;
601 18         71 $hook_code->(@_);
602 12         70 };
603              
604 12         270 $orig_cb->(@_);
605             };
606             }
607              
608              
609             # define the exported 'plugin_keywords'
610             sub _exported_plugin_keywords{
611 37     37   100 my( $plugin, $class ) = @_;
612              
613             return plugin_keywords => sub(@) {
614 13     13   3494 while( my $name = shift @_ ) {
615             ## no critic
616 20 50       1996 my $sub = is_coderef($_[0])
    100          
617             ? shift @_
618             : eval '\&'.$class."::" . ( ref $name ? $name->[0] : $name );
619 20 50       446 $class->keywords->{$_} = $sub for ref $name ? @$name : $name;
620             }
621             }
622 37         1547 }
623              
624             sub _exported_plugin_hooks {
625 37     37   96 my $class = shift;
626 7     7   833 return plugin_hooks => sub (@) { $class->add_hooks(@_) }
627 37         243 }
628              
629             1;
630              
631             __END__
632              
633             =pod
634              
635             =encoding UTF-8
636              
637             =head1 NAME
638              
639             Dancer2::Plugin - base class for Dancer2 plugins
640              
641             =head1 VERSION
642              
643             version 0.400001
644              
645             =head1 SYNOPSIS
646              
647             The plugin itself:
648              
649             package Dancer2::Plugin::Polite;
650              
651             use strict;
652             use warnings;
653              
654             use Dancer2::Plugin;
655              
656             has smiley => (
657             is => 'ro',
658             default => sub {
659             $_[0]->config->{smiley} || ':-)'
660             }
661             );
662              
663             plugin_keywords 'add_smileys';
664              
665             sub BUILD {
666             my $plugin = shift;
667              
668             $plugin->app->add_hook( Dancer2::Core::Hook->new(
669             name => 'after',
670             code => sub { $_[0]->content( $_[0]->content . " ... please?" ) }
671             ));
672              
673             $plugin->app->add_route(
674             method => 'get',
675             regexp => '/goodbye',
676             code => sub {
677             my $app = shift;
678             'farewell, ' . $app->request->params->{name};
679             },
680             );
681              
682             }
683              
684             sub add_smileys {
685             my( $plugin, $text ) = @_;
686              
687             $text =~ s/ (?<= \. ) / $plugin->smiley /xeg;
688              
689             return $text;
690             }
691              
692             1;
693              
694             then to load into the app:
695              
696             package MyApp;
697              
698             use strict;
699             use warnings;
700              
701             use Dancer2;
702              
703             BEGIN { # would usually be in config.yml
704             set plugins => {
705             Polite => {
706             smiley => '8-D',
707             },
708             };
709             }
710              
711             use Dancer2::Plugin::Polite;
712              
713             get '/' => sub {
714             add_smileys( 'make me a sandwich.' );
715             };
716              
717             1;
718              
719             =head1 DESCRIPTION
720              
721             =head2 Writing the plugin
722              
723             =head3 C<use Dancer2::Plugin>
724              
725             The plugin must begin with
726              
727             use Dancer2::Plugin;
728              
729             which will turn the package into a L<Moo> class that inherits from L<Dancer2::Plugin>. The base class provides the plugin with
730             two attributes: C<app>, which is populated with the Dancer2 app object for which
731             the plugin is being initialized for, and C<config> which holds the plugin
732             section of the application configuration.
733              
734             =head3 Modifying the app at building time
735              
736             If the plugin needs to tinker with the application -- add routes or hooks, for example --
737             it can do so within its C<BUILD()> function.
738              
739             sub BUILD {
740             my $plugin = shift;
741              
742             $plugin->app->add_route( ... );
743             }
744              
745             =head3 Adding keywords
746              
747             =head4 Via C<plugin_keywords>
748              
749             Keywords that the plugin wishes to export to the Dancer2 app can be defined via the C<plugin_keywords> keyword:
750              
751             plugin_keywords qw/
752             add_smileys
753             add_sad_kitten
754             /;
755              
756             Each of the keyword will resolve to the class method of the same name. When invoked as keyword, it'll be passed
757             the plugin object as its first argument.
758              
759             sub add_smileys {
760             my( $plugin, $text ) = @_;
761              
762             return join ' ', $text, $plugin->smiley;
763             }
764              
765             # and then in the app
766              
767             get '/' => sub {
768             add_smileys( "Hi there!" );
769             };
770              
771             You can also pass the functions directly to C<plugin_keywords>.
772              
773             plugin_keywords
774             add_smileys => sub {
775             my( $plugin, $text ) = @_;
776              
777             $text =~ s/ (?<= \. ) / $plugin->smiley /xeg;
778              
779             return $text;
780             },
781             add_sad_kitten => sub { ... };
782              
783             Or a mix of both styles. We're easy that way:
784              
785             plugin_keywords
786             add_smileys => sub {
787             my( $plugin, $text ) = @_;
788              
789             $text =~ s/ (?<= \. ) / $plugin->smiley /xeg;
790              
791             return $text;
792             },
793             'add_sad_kitten';
794              
795             sub add_sad_kitten {
796             ...;
797             }
798              
799             If you want several keywords to be synonyms calling the same
800             function, you can list them in an arrayref. The first
801             function of the list is taken to be the "real" method to
802             link to the keywords.
803              
804             plugin_keywords [qw/ add_smileys add_happy_face /];
805              
806             sub add_smileys { ... }
807              
808             Calls to C<plugin_keywords> are cumulative.
809              
810             =head4 Via the C<:PluginKeyword> function attribute
811              
812             For perl 5.12 and higher, keywords can also be defined by adding the C<:PluginKeyword> attribute
813             to the function you wish to export.
814              
815             For Perl 5.10, the export triggered by the sub attribute comes too late in the
816             game, and the keywords won't be exported in the application namespace.
817              
818             sub foo :PluginKeyword { ... }
819              
820             sub bar :PluginKeyword( baz quux ) { ... }
821              
822             # equivalent to
823              
824             sub foo { ... }
825             sub bar { ... }
826              
827             plugin_keywords 'foo', [ qw/ baz quux / ] => \&bar;
828              
829             =head4 For an attribute
830              
831             You can also turn an attribute of the plugin into a keyword.
832              
833             has foo => (
834             is => 'ro',
835             plugin_keyword => 1, # keyword will be 'foo'
836             );
837              
838             has bar => (
839             is => 'ro',
840             plugin_keyword => 'quux', # keyword will be 'quux'
841             );
842              
843             has baz => (
844             is => 'ro',
845             plugin_keyword => [ 'baz', 'bazz' ], # keywords will be 'baz' and 'bazz'
846             );
847              
848             =head3 Accessing the plugin configuration
849              
850             The plugin configuration is available via the C<config()> method.
851              
852             sub BUILD {
853             my $plugin = shift;
854              
855             if ( $plugin->config->{feeling_polite} ) {
856             $plugin->app->add_hook( Dancer2::Core::Hook->new(
857             name => 'after',
858             code => sub { $_[0]->content( $_[0]->content . " ... please?" ) }
859             ));
860             }
861             }
862              
863             =head3 Getting default values from config file
864              
865             Since initializing a plugin with either a default or a value passed via the configuration file,
866             like
867              
868             has smiley => (
869             is => 'ro',
870             default => sub {
871             $_[0]->config->{smiley} || ':-)'
872             }
873             );
874              
875             C<Dancer2::Plugin> allows for a C<from_config> key in the attribute definition.
876             Its value is the plugin configuration key that will be used to initialize the attribute.
877              
878             If it's given the value C<1>, the name of the attribute will be taken as the configuration key.
879              
880             Nested hash keys can also be referred to using a dot notation.
881              
882             If the plugin configuration has no value for the given key, the attribute default, if specified, will be honored.
883              
884             If the key is given a coderef as value, it's considered to be a C<default> value combo:
885              
886             has foo => (
887             is => 'ro',
888             from_config => sub { 'my default' },
889             );
890              
891              
892             # equivalent to
893             has foo => (
894             is => 'ro',
895             from_config => 'foo',
896             default => sub { 'my default' },
897             );
898              
899             For example:
900              
901             # in config.yml
902              
903             plugins:
904             Polite:
905             smiley: ':-)'
906             greeting:
907             casual: Hi!
908             formal: How do you do?
909              
910              
911             # in the plugin
912              
913             has smiley => ( # will be ':-)'
914             is => 'ro',
915             from_config => 1,
916             default => sub { ':-(' },
917             );
918              
919             has casual_greeting => ( # will be 'Hi!'
920             is => 'ro',
921             from_config => 'greeting.casual',
922             );
923              
924             has apology => ( # will be 'sorry'
925             is => 'ro',
926             from_config => 'apology',
927             default => sub { 'sorry' },
928             )
929              
930             has closing => ( # will be 'See ya!'
931             is => 'ro',
932             from_config => sub { 'See ya!' },
933             );
934              
935             =head3 Config becomes immutable
936              
937             The plugin's C<config> attribute is loaded lazily on the first call to
938             C<config>. After this first call C<config> becomes immutable so you cannot
939             do the following in a test:
940              
941             use Dancer2;
942             use Dancer2::Plugin::FooBar;
943              
944             set plugins => {
945             FooBar => {
946             wibble => 1, # this is OK
947             },
948             };
949              
950             flibble(45); # plugin keyword called which causes config read
951            
952             set plugins => {
953             FooBar => {
954             wibble => 0, # this will NOT change plugin config
955             },
956             };
957              
958             =head3 Accessing the parent Dancer app
959              
960             If the plugin is instantiated within a Dancer app, it'll be
961             accessible via the method C<app()>.
962              
963             sub BUILD {
964             my $plugin = shift;
965              
966             $plugin->app->add_route( ... );
967             }
968              
969             To use Dancer's DSL in your plugin:
970              
971             $self->dsl->debug( “Hi! I’m logging from your plugin!” );
972              
973             See L<Dancer2::Manual/"DSL KEYWORDS"> for a full list of Dancer2 DSL.
974              
975             =head2 Using the plugin within the app
976              
977             A plugin is loaded via
978              
979             use Dancer2::Plugin::Polite;
980              
981             The plugin will assume that it's loading within a Dancer module and will
982             automatically register itself against its C<app()> and export its keywords
983             to the local namespace. If you don't want this to happen, specify that you
984             don't want anything imported via empty parentheses when C<use>ing the module:
985              
986             use Dancer2::Plugin::Polite ();
987              
988             =head2 Plugins using plugins
989              
990             It's easy to use plugins from within a plugin:
991              
992             package Dancer2::Plugin::SourPuss;
993            
994             use Dancer2::Plugin;
995             use Dancer2::Plugin::Polite;
996            
997             sub my_keyword { my $smiley = smiley(); }
998              
999             1;
1000              
1001             This does not export C<smiley()> into your application - it is only available
1002             from within your plugin. However, from the example above, you can wrap
1003             DSL from other plugins and make it available from your plugin.
1004              
1005             =head2 Utilizing other plugins
1006              
1007             You can use the C<find_plugin> to locate other plugins loaded by the user,
1008             in order to use them, or their information, directly:
1009              
1010             # MyApp.pm
1011             use Dancer2;
1012             use Dancer2::Plugin::Foo;
1013             use Dancer2::Plugin::Bar;
1014              
1015             # Dancer2::Plugin::Bar;
1016             ...
1017              
1018             sub my_keyword {
1019             my $self = shift;
1020             my $foo = $self->find_plugin('Dancer2::Plugin::Foo')
1021             or $self->dsl->send_error('Could not find Foo');
1022              
1023             return $foo->foo_keyword(...);
1024             }
1025              
1026             =head2 Hooks
1027              
1028             New plugin hooks are declared via C<plugin_hooks>.
1029              
1030             plugin_hooks 'my_hook', 'my_other_hook';
1031              
1032             Hooks are prefixed with C<plugin.plugin_name>. So the plugin
1033             C<my_hook> coming from the plugin C<Dancer2::Plugin::MyPlugin> will have the hook name
1034             C<plugin.myplugin.my_hook>.
1035              
1036             Hooks are executed within the plugin by calling them via the associated I<app>.
1037              
1038             $plugin->execute_plugin_hook( 'my_hook' );
1039              
1040             You can also call any other hook if you provide the full name using the
1041             C<execute_hook> method:
1042              
1043             $plugin->app->execute_hook( 'core.app.route_exception' );
1044              
1045             Or using their alias:
1046              
1047             $plugin->app->execute_hook( 'on_route_exception' );
1048              
1049             B<Note:> If your plugin consumes a plugin that declares any hooks, those hooks
1050             are added to your application, even though DSL is not.
1051              
1052             =head2 Writing Test Gotchas
1053              
1054             =head3 Constructor for Dancer2::Plugin::Foo has been inlined and cannot be updated
1055              
1056             You'll usually get this one because you are defining both the plugin and app
1057             in your test file, and the runtime creation of Moo's attributes happens after
1058             the compile-time import voodoo dance.
1059              
1060             To get around this nightmare, wrap your plugin definition in a C<BEGIN> block.
1061              
1062             BEGIN {
1063             package Dancer2::Plugin::Foo;
1064              
1065             use Dancer2::Plugin;
1066              
1067             has bar => (
1068             is => 'ro',
1069             from_config => 1,
1070             );
1071              
1072             plugin_keywords qw/ bar /;
1073              
1074             }
1075              
1076             {
1077             package MyApp;
1078              
1079             use Dancer2;
1080             use Dancer2::Plugin::Foo;
1081              
1082             bar();
1083             }
1084              
1085             =head3 You cannot overwrite a locally defined method (bar) with a reader
1086              
1087             If you set an object attribute of your plugin to be a keyword as well, you need
1088             to call C<plugin_keywords> after the attribute definition.
1089              
1090             package Dancer2::Plugin::Foo;
1091              
1092             use Dancer2::Plugin;
1093              
1094             has bar => (
1095             is => 'ro',
1096             );
1097              
1098             plugin_keywords 'bar';
1099              
1100             =head1 AUTHOR
1101              
1102             Dancer Core Developers
1103              
1104             =head1 COPYRIGHT AND LICENSE
1105              
1106             This software is copyright (c) 2023 by Alexis Sukrieh.
1107              
1108             This is free software; you can redistribute it and/or modify it under
1109             the same terms as the Perl 5 programming language system itself.
1110              
1111             =cut