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