diff --git a/lib/Dancer2/Plugin.pm b/lib/Dancer2/Plugin.pm index 3c774f72e..0365c6896 100644 --- a/lib/Dancer2/Plugin.pm +++ b/lib/Dancer2/Plugin.pm @@ -1,423 +1,899 @@ package Dancer2::Plugin; -# ABSTRACT: Extending Dancer2's DSL with plugins +# ABSTRACT: base class for Dancer2 plugins + +use strict; +use warnings; + +use Moo; +use Carp; +use List::Util qw/ reduce /; +use Attribute::Handlers; +use Scalar::Util; + +extends 'Exporter::Tiny'; + +with 'Dancer2::Core::Role::Hookable'; + +has app => ( + is => 'ro', + required => 1, +); + +has config => ( + is => 'ro', + lazy => 1, + default => sub { + my $self = shift; + my $config = $self->app->config; + my $package = ref $self; # TODO + $package =~ s/Dancer2::Plugin:://; + $config->{plugins}{$package} || {}; + }, +); -use Moo::Role; -use Carp 'croak', 'carp'; -use Dancer2::Core::DSL; -use Scalar::Util qw(); - -# singleton for storing all keywords, -# their code and the plugin they come from my $_keywords = {}; +sub keywords { $_keywords } + +my $REF_ADDR_REGEX = qr{ + Dancer2::Plugin:: + [A-Za-z0-9\:\_]+ + =HASH + \( + ([0-9a-fx]+) + \) +}x; +my %instances; + +# backwards compatibility +our $_keywords_by_plugin = {}; + +has '+hooks' => ( + default => sub { + my $plugin = shift; + my $name = 'plugin.' . lc ref $plugin; + $name =~ s/Dancer2::Plugin:://i; + $name =~ s/::/_/g; -# singleton for storing all hooks and their aliases -my $_hooks = {}; + +{ + map { join( '.', $name, $_ ) => [] } + @{ $plugin->ClassHooks } + }; + }, +); -# singleton for applying code-blocks at import time -# so their code gets the callers DSL -my $_on_import = {}; +sub add_hooks { + my $class = shift; + push @{ $class->ClassHooks }, @_; +} -sub register { - my $plugin = caller; - my $caller = caller(1); - my ( $keyword, $code, $options ) = @_; - $options ||= { is_global => 1 }; +sub execute_plugin_hook { + my ( $self, $name, @args ) = @_; + my $plugin_class = ref $self; - $keyword =~ /^[a-zA-Z_]+[a-zA-Z0-9_]*$/ - or croak "You can't use '$keyword', it is an invalid name" - . " (it should match ^[a-zA-Z_]+[a-zA-Z0-9_]*\$ )"; + $plugin_class =~ s/^Dancer2::Plugin::// + or croak "Cannot call plugin hook ($name) from outside plugin"; - if (grep { $_ eq $keyword } - keys %{ Dancer2::Core::DSL->dsl_keywords } - ) - { - croak "You can't use '$keyword', this is a reserved keyword"; - } + my $full_name = 'plugin.' . lc($plugin_class) . ".$name"; + $full_name =~ s/::/_/g; - while ( my ( $plugin, $keywords ) = each %$_keywords ) { - if ( grep { $_->[0] eq $keyword } @$keywords ) { - croak "You can't use $keyword, " - . "this is a keyword reserved by $plugin"; - } + $self->app->execute_hook( $full_name, @args ); +} + +# both functions are there for D2::Core::Role::Hookable +# back-compatibility. Aren't used +sub supported_hooks { [] } +sub hook_aliases { $_[0]->{'hook_aliases'} ||= {} } + +### has() STUFF ######################################## + +# our wrapping around Moo::has, done to be able to intercept +# both 'from_config' and 'plugin_keyword' +sub _p2_has { + my $class = shift; + $class->_p2_has_from_config( $class->_p2_has_keyword( @_ ) ); +}; + +sub _p2_has_from_config { + my( $class, $name, %args ) = @_; + + my $config_name = delete $args{'from_config'} + or return ( $name, %args ); + + $args{lazy} = 1; + + if ( ref $config_name eq 'CODE' ) { + $args{default} ||= $config_name; + $config_name = 1; } - $_keywords->{$plugin} ||= []; - push @{ $_keywords->{$plugin} }, - [ $keyword, $code, $options ]; + $config_name = $name if $config_name eq '1'; + my $orig_default = $args{default} || sub{}; + $args{default} = sub { + my $plugin = shift; + my $value = reduce { eval { $a->{$b} } } $plugin->config, split '\.', $config_name; + return defined $value ? $value: $orig_default->($plugin); + }; + + return $name => %args; } -sub on_plugin_import(&) { - my $code = shift; - my $plugin = caller; - $_on_import->{$plugin} ||= []; - push @{ $_on_import->{$plugin} }, $code; +sub _p2_has_keyword { + my( $class, $name, %args ) = @_; + + if( my $keyword = delete $args{plugin_keyword} ) { + + $keyword = $name if $keyword eq '1'; + + $class->keywords->{$_} = sub { (shift)->$name(@_) } + for ref $keyword ? @$keyword : $keyword; + } + + return $name => %args; } -sub register_plugin { - my $plugin = caller; - my $caller = caller(1); - my %params = @_; +### ATTRIBUTE HANDLER STUFF ######################################## - # if the caller has no dsl method, we can't register the plugin - return if !$caller->can('dsl'); +# :PluginKeyword shenanigans - # the plugin consumes the DSL role - Moo::Role->apply_role_to_package( $plugin, 'Dancer2::Core::Role::DSL' ); +sub PluginKeyword :ATTR(CODE) { + my( $class, $sym_ref, $code, undef, $args ) = @_; + my $func_name = *{$sym_ref}{NAME}; - # bind all registered keywords to the plugin - my $dsl = $caller->dsl; - for my $k ( @{ $_keywords->{$plugin} } ) { - my ( $keyword, $code, $options ) = @{$k}; - { - no strict 'refs'; - *{"${plugin}::${keyword}"} = $dsl->_apply_prototype($code, $options); - } + $args = join '', @$args if ref $args eq 'ARRAY'; + + for my $name ( split ' ', $args || $func_name ) { + $class->keywords->{$name} = $code; } -# create the import method of the caller (the actual plugin) in order to make it -# imports all the DSL's keyword when it's used. - my $import = sub { - my $plugin = shift; +} - # caller(1) because our import method is wrapped, see below - my $caller = caller(1); +## EXPORT STUFF ############################################################## - for my $k ( @{ $_keywords->{$plugin} } ) { - my ( $keyword, $code, $options ) = @{$k}; - my $is_global = exists $options->{is_global} && $options->{is_global}; - $caller->dsl->register( $keyword, $is_global ); - } +# this @EXPORT will only be taken +# into account when we do a 'use Dancer2::Plugin' +# I.e., it'll only do its magic for the +# plugins themselves, not when they are +# called +our @EXPORT = qw/ :plugin /; - Moo::Role->apply_roles_to_object( $caller->dsl, $plugin ); - $caller->dsl->export_symbols_to($caller); - $caller->dsl->dancer_app->register_plugin( $caller->dsl ); - - # add hooks - my $current_hooks = [ $caller->dsl->supported_hooks ]; - my $current_aliases = $caller->dsl->hook_aliases; - for my $h ( keys %{ $_hooks->{$plugin} } ) { - push @$current_hooks, $h; - $current_aliases->{ $_hooks->{$plugin}->{$h} } = $h; - # If the hooks atttribute has already been constructed, - # add an entry so has_hook() finds these hooks. - $caller->dsl->hooks->{$h} = [] - if ! exists $caller->dsl->hooks->{$h}; - } - my $target = ref $caller->dsl; - { - no strict 'refs'; - no warnings 'redefine'; - *{"${target}::supported_hooks"} = sub {@$current_hooks}; - *{"${target}::hook_aliases"} = sub {$current_aliases}; - } +# compatibility - it will be removed soon! +my $no_dsl = {}; +sub _exporter_expand_tag { + my( $class, $name, $args, $global ) = @_; + + my $caller = $global->{into}; + + $name eq 'no_dsl' and $no_dsl->{$class} = 1; + # no_dsl check here is for compatibility only + # it will be removed soon! + return _exporter_plugin($caller) + if $name eq 'plugin' or $name eq 'no_dsl'; + + return _exporter_app($class,$caller,$global) + if $name eq 'app' and $caller->can('app') and !$no_dsl->{$class}; + + return; + +} + +# plugin has been called within a D2 app. Modify +# the app and export keywords +sub _exporter_app { + my( $class, $caller, $global ) = @_; + + my $app = eval "${caller}::app()" or return; ## no critic + + return unless $app->can('with_plugin'); + + ( my $short = $class ) =~ s/Dancer2::Plugin:://; + + my $plugin = $app->with_plugin( $short ); + $global->{plugin} = $plugin; + + return unless $class->can('keywords'); + + # Add our hooks to the app, so they're recognized + # this is for compatibility so you can call execute_hook() + # without the fully qualified plugin name. + # The reason we need to do this here instead of when adding a + # hook is because we need to register in the app, and only now it + # exists. + # This adds a caveat that two plugins cannot register + # the same hook name, but that will be deprecated anyway. + {; + foreach my $hook ( @{ $plugin->ClassHooks } ) { + my $full_name = 'plugin.' . lc($class) . ".$hook"; + $full_name =~ s/Dancer2::Plugin:://i; + $full_name =~ s/::/_/g; - for my $sub ( @{ $_on_import->{$plugin} } ) { - $sub->( $caller->dsl ); + # this adds it to the plugin + $plugin->hook_aliases->{$hook} = $full_name; + + # this adds it to the app + $plugin->app->hook_aliases->{$hook} = $full_name; + + # copy the hooks from the plugin to the app + # this is in case they were created at import time + # rather than after + @{ $plugin->app->hooks }{ keys %{ $plugin->hooks } } = + values %{ $plugin->hooks }; } - }; - my $app_caller = caller(); + } + { + # get the reference + my ($plugin_addr) = "$plugin" =~ $REF_ADDR_REGEX; + + $instances{$plugin_addr}{'config'} = sub { $plugin->config }; + $instances{$plugin_addr}{'app'} = $plugin->app; + + Scalar::Util::weaken( $instances{$plugin_addr}{'app'} ); + + ## no critic no strict 'refs'; + + # we used a forward declaration + # so the compiled form "plugin_setting;" can be overridden + # with this implementation, + # which works on runtime ("plugin_setting()") + # we can't use can() here because the forward declaration will + # create a CODE stub no warnings 'redefine'; - my $original_import = *{"${app_caller}::import"}{CODE}; - $original_import ||= sub { }; - *{"${app_caller}::import"} = sub { - $original_import->(@_); - $import->(@_); + *{"${class}::plugin_setting"} = sub { + my $cinfo = Carp::caller_info(1); + my ($plugin_addr) = $cinfo->{'sub_name'} =~ $REF_ADDR_REGEX; + return $instances{$plugin_addr}{'config'}->(); + }; + + # FIXME: + # why doesn't this work? it's like it's already defined somewhere + # but i'm not sure where. seems like AUTOLOAD runs it. + #$class->can('execute_hook') or + *{"${class}::execute_hook"} = sub { + my $level = -1; + my $plugin_addr; + while ( ++$level < 5 ) { + my $cinfo = Carp::caller_info($level); + ($plugin_addr) = $cinfo->{'sub_name'} =~ $REF_ADDR_REGEX; + + $plugin_addr and last; + } + + $plugin_addr + or Carp::croak('Can\'t find originating plugin in first 5 levels'); + # this can also be called by App.pm itself + # if the plugin is a + # "candidate" for a hook. App.pm around execute_hook + if ( ref( $_[0] ) =~ /^Dancer2::Plugin::/ ) { + my $plugin_self = shift @_; + my $hook_name = shift @_; + # this means it's probably our hook, let's verify + my $plugin_class = lc $class; + $plugin_class =~ s/^dancer2::plugin:://; + $hook_name =~ /^plugin\.$plugin_class/ + or Carp::croak('Unknown plugin called through other plugin'); + + # now we can't really use the app to execute it because + # the "around" modifier is the one calling us to begin + # with, so we need to call it directly ourselves + # this is okay because the modifier is there only to + # call candidates, like us (this is in fact how and + # why we were called) + return $_->( $plugin_self, @_ ) + for @{ $plugin->hooks->{$hook_name} }; + } + + return $instances{$plugin_addr}{'app'}->execute_hook(@_); }; } - return 1; #as in D1 - # The plugin is ready now. + # deprecated backwards compat: on_plugin_import() + $_->($plugin) for @{ $plugin->_DANCER2_IMPORT_TIME_SUBS() }; + + map { [ $_ => {plugin => $plugin} ] } keys %{ $plugin->keywords }; } -sub plugin_args {@_} +# turns the caller namespace into +# a D2P2 class, with exported keywords +sub _exporter_plugin { + my $caller = shift; + require Dancer2::Core::DSL; + my $keywords_list = join ' ', keys %{ Dancer2::Core::DSL->dsl_keywords }; + + eval <<"END"; ## no critic + { + package $caller; + use Moo; + use Carp (); + use Attribute::Handlers; -sub plugin_setting { - my $plugin = caller; - my $dsl = _get_dsl() - or croak 'No DSL object found'; + extends 'Dancer2::Plugin'; - ( my $plugin_name = $plugin ) =~ s/Dancer2::Plugin:://; + our \@EXPORT = ( ':app' ); - return $dsl->app->config->{'plugins'}->{$plugin_name} ||= {}; + around has => sub { + my( \$orig, \@args ) = \@_; + \$orig->( ${caller}->_p2_has( \@args) ); + }; + + sub PluginKeyword :ATTR(CODE) { + goto &Dancer2::Plugin::PluginKeyword; + } + + sub execute_plugin_hook { + goto &Dancer2::Plugin::execute_plugin_hook; + } + + my \$_keywords = {}; + sub keywords { \$_keywords } + + my \$_ClassHooks = []; + sub ClassHooks { \$_ClassHooks } + + # deprecated backwards compat + # FIXME should this throw a deprecation notice? probably yes... + sub register_plugin {1} + + sub register { + my ( \$keyword, \$sub ) = \@_; + \$_keywords->{\$keyword} = \$sub; + + \$keyword =~ /^[a-zA-Z_]+[a-zA-Z0-9_]*\$/ + or Carp::croak( + "You can't use '\$keyword', it is an invalid name" + . " (it should match ^[a-zA-Z_]+[a-zA-Z0-9_]*\\\$ )"); + + + grep +( \$keyword eq \$_ ), qw<$keywords_list> + and Carp::croak("You can't use '\$keyword', this is a reserved keyword"); + + \$Dancer2::Plugin::_keywords_by_plugin->{\$keyword} + and Carp::croak("You can't use \$keyword, " + . "this is a keyword reserved by " + . \$Dancer2::Plugin::_keywords_by_plugin->{\$keyword}); + + \$Dancer2::Plugin::_keywords_by_plugin->{\$keyword} = "$caller"; + + # Exporter::Tiny doesn't seem to generate the subs + # in the caller properly, so we have to do it manually + { + no strict 'refs'; + *{"${caller}::\$keyword"} = \$sub; + } + } + + my \@_DANCER2_IMPORT_TIME_SUBS; + sub _DANCER2_IMPORT_TIME_SUBS {\\\@_DANCER2_IMPORT_TIME_SUBS} + sub on_plugin_import (&) { + push \@_DANCER2_IMPORT_TIME_SUBS, \$_[0]; + } + + sub register_hook { goto &plugin_hooks } + + # FIXME: AUTOLOAD might pick up on this + sub dancer_app { + Carp::carp "Plugin DSL method 'dancer_app' is deprecated. " + . "Use '\\\$self->app' instead'.\n"; + + \$_[0]->app; + } + + # FIXME: AUTOLOAD might pick up on this + sub request { + Carp::carp "Plugin DSL method 'request' is deprecated. " + . "Use '\\\$self->app->request' instead'.\n"; + + \$_[0]->app->request; + } + + # FIXME: AUTOLOAD might pick up on this + sub var { + Carp::carp "Plugin DSL method 'var' is deprecated. " + . "Use '\\\$self->app->request->var' instead'.\n"; + + shift->app->request->var(\@_); + } + + sub plugin_args { + Carp::carp "Plugin DSL method 'plugin_args' is deprecated. " + . "Use '\\\@_' instead'.\n"; + + \@_; + } + + sub plugin_setting; + + # FIXME: AUTOLOAD might pick up on this + sub hook { + Carp::carp "Plugin DSL method 'hook' is deprecated. " + . "Use '\\\$self->app->add_hook' instead'.\n"; + + shift->app->add_hook( + Dancer2::Core::Hook->new( name => shift, code => shift ) ); + } + + our \$AUTOLOAD; + sub AUTOLOAD { + my ( \$self, \@args ) = \@_; + my \$method = \$AUTOLOAD; + \$method =~ s/.*:://; + my \$cb = \$self->app->name->can(\$method) + or croak("Can't locate method '\$method'"); + + Carp::carp "Using DSL in plugins is deprecated (\$method)."; + + \$cb->(\@args); + } + } +END + + die $@ if $@; + + return map { [ $_ => { class => $caller } ] } + qw/ plugin_keywords plugin_hooks /; } -sub register_hook { - my (@hooks) = @_; +sub _exporter_expand_sub { + my( $plugin, $name, $args, $global ) = @_; + my $class = $args->{class}; + + return _exported_plugin_keywords($plugin,$class) + if $name eq 'plugin_keywords'; - my $caller = caller; - my $plugin = $caller; + return _exported_plugin_hooks($class) + if $name eq 'plugin_hooks'; - $plugin =~ s/^Dancer2::Plugin:://; - $plugin =~ s/::/_/g; + # otherwise, we're exporting a keyword - my $base_name = "plugin." . lc($plugin); - for my $hook (@hooks) { - my $hook_name = "${base_name}.$hook"; - $_hooks->{$caller}->{$hook_name} = $hook; + my $p = $args->{plugin}; + my $sub = $p->keywords->{$name}; + return $name => sub(@) { $sub->($p,@_) }; +} + +# define the exported 'plugin_keywords' +sub _exported_plugin_keywords{ + my( $plugin, $class ) = @_; + + return plugin_keywords => sub(@) { + while( my $name = shift @_ ) { + ## no critic + my $sub = ref $_[0] eq 'CODE' + ? shift @_ + : eval '\&'.$class."::" . ( ref $name ? $name->[0] : $name ); + $class->keywords->{$_} = $sub for ref $name ? @$name : $name; + } } } -sub execute_hook { - my $position = shift; - my $dsl = _get_dsl(); - croak "No DSL object found" if !defined $dsl; - $dsl->execute_hook( $position, @_ ); +sub _exported_plugin_hooks { + my $class = shift; + return plugin_hooks => sub (@) { $class->add_hooks(@_) } } -# private - -my $dsl_deprecation_wrapper = 0; -sub import { - my $class = shift; - my $plugin = caller; - - # First, export Dancer2::Plugins symbols - my @export = qw( - execute_hook - register_hook - register_plugin - register - on_plugin_import - plugin_setting - plugin_args +1; + +__END__ + +=head1 SYNOPSIS + +The plugin itself: + + + package Dancer2::Plugin::Polite; + + use strict; + use warnings; + + use Dancer2::Plugin; + + has smiley => ( + is => 'ro', + default => sub { + $_[0]->config->{smiley} || ':-)' + } ); - for my $symbol (@export) { - no strict 'refs'; - *{"${plugin}::${symbol}"} = *{"Dancer2::Plugin::${symbol}"}; + plugin_keywords 'add_smileys'; + + sub BUILD { + my $plugin = shift; + + $plugin->app->add_hook( Dancer2::Core::Hook->new( + name => 'after', + code => sub { $_[0]->content( $_[0]->content . " ... please?" ) } + )); + + $plugin->app->add_route( + method => 'get', + regexp => '/goodbye', + code => sub { + my $app = shift; + 'farewell, ' . $app->request->params->{name}; + }, + ); + } - my $dsl = _get_dsl(); - return if !defined $dsl; - -# DEPRECATION NOTICE -# We expect plugin to be written with a $dsl object now, so -# this keywords will trigger a deprecation notice and will be removed in a later -# version of Dancer2. - - # Support for Dancer 1 syntax for plugin. - # Then, compile Dancer 2's DSL keywords into self-contained keywords for the - # plugin (actually, we call all the symbols by giving them $caller->dsl as - # their first argument). - # These modified versions of the DSL are then exported in the namespace of the - # plugin. - if (! grep { $_ eq ':no_dsl' } @_) { - for my $symbol ( keys %{ $dsl->keywords } ) { - - # get the original symbol from the real DSL - no strict 'refs'; - no warnings qw( redefine once ); - my $code = *{"Dancer2::Core::DSL::$symbol"}{CODE}; - - # compile it with $caller->dsl - my $compiled = sub { - carp - "DEPRECATED: $plugin calls '$symbol' instead of '\$dsl->$symbol'."; - $code->( $dsl, @_ ); - }; + sub add_smileys { + my( $plugin, $text ) = @_; - if ( $symbol eq 'dsl' ) { - $compiled = sub { $dsl }; - $dsl_deprecation_wrapper = $compiled - } + $text =~ s/ (?<= \. ) / $plugin->smiley /xeg; - # Bind the newly compiled symbol to the caller's namespace. - # As this may redefine a symbol, ensure the new coderef has - # the same prototype signature. - my $existing = *{"${plugin}::${symbol}"}; - my $prototype = prototype \&$existing; - *{"${plugin}::${symbol}"} = Scalar::Util::set_prototype( \&$compiled, $prototype ); - } + return $text; } - # Finally, make sure our caller becomes a Moo::Role - # Perl 5.8.5+ mandatory for that trick - @_ = ('Moo::Role'); - goto &Moo::Role::import; -} + 1; + +then to load into the app: + + + package MyApp; + + use strict; + use warnings; + + use Dancer2; -sub _get_dsl { - my $dsl; - my $deep = 2; - while ( my $caller = caller( $deep++ ) ) { - my $caller_dsl = $caller->can('dsl'); - next if ! $caller_dsl || $caller_dsl == $dsl_deprecation_wrapper; - $dsl = $caller->dsl; - last if defined $dsl && length( ref($dsl) ); + BEGIN { # would usually be in config.yml + set plugins => { + Polite => { + smiley => '8-D', + }, + }; } - return $dsl; -} + use Dancer2::Plugin::Polite; -1; + get '/' => sub { + add_smileys( 'make me a sandwich.' ); + }; + + 1; -__END__ =head1 DESCRIPTION -You can extend Dancer2 by writing your own plugin. A plugin is a module that -exports a bunch of symbols to the current namespace (the caller will see all -the symbols defined via C). +This is an alternate plugin basis for Dancer2. -Note that you have to C the plugin wherever you want to use its symbols. -For instance, if you have Webapp::App1 and Webapp::App2, both loaded from your -main application, they both need to C if they want to use the -symbols exported by C. +=head2 Writing the plugin -For a more gentle introduction to Dancer2 plugins, see L. +=head3 C -=method register +The plugin must begin with - register 'my_keyword' => sub { ... } => \%options; + use Dancer2::Plugin; -Allows the plugin to define a keyword that will be exported to the caller's -namespace. +which will turn the package into a L class that inherits from L. The base class provides the plugin with +two attributes: C, which is populated with the Dancer2 app object for which +the plugin is being initialized for, and C which holds the plugin +section of the application configuration. -The first argument is the symbol name, the second one the coderef to execute -when the symbol is called. +=head3 Modifying the app at building time -The coderef receives as its first argument the Dancer2::Core::DSL object. +If the plugin needs to tinker with the application -- add routes or hooks, for example -- +it can do so within its C function. -Plugins B use the DSL object to access application components and work -with them directly. + sub BUILD { + my $plugin = shift; - sub { - my $dsl = shift; - my @args = @_; + $plugin->app->add_route( ... ); + } - my $app = $dsl->app; - my $request = $app->request; +=head3 Adding keywords - if ( $app->session->read('logged_in') ) { - ... - } - }; +=head4 Via C -As an optional third argument, it's possible to give a hash ref to C -in order to set some options. +Keywords that the plugin wishes to export to the Dancer2 app can be defined via the C keyword: -The option C (boolean) is used to declare a global/non-global keyword -(by default all keywords are global). A non-global keyword must be called from -within a route handler (eg: C or C) whereas a global one can be -called from everywhere (eg: C or C). + plugin_keywords qw/ + add_smileys + add_sad_kitten + /; - register my_symbol_to_export => sub { - # ... some code - }, { is_global => 1} ; +Each of the keyword will resolve to the class method of the same name. When invoked as keyword, it'll be passed +the plugin object as its first argument. -=method on_plugin_import + sub add_smileys { + my( $plugin, $text ) = @_; -Allows the plugin to take action each time it is imported. -It is prototyped to take a single code block argument, which will be called -with the DSL object of the package importing it. + return join ' ', $text, $plugin->smiley; + } -For example, here is a way to install a hook in the importing app: + # and then in the app - on_plugin_import { - my $dsl = shift; - $dsl->app->add_hook( - Dancer2::Core::Hook->new( - name => 'before', - code => sub { ... }, - ) - ); + get '/' => sub { + add_smileys( "Hi there!" ); }; -=method register_plugin +You can also pass the functions directly to C. -A Dancer2 plugin must end with this statement. This lets the plugin register all -the symbols defined with C as exported symbols: + plugin_keywords + add_smileys => sub { + my( $plugin, $text ) = @_; - register_plugin; + $text =~ s/ (?<= \. ) / $plugin->smiley /xeg; -Register_plugin returns 1 on success and undef if it fails. + return $text; + }, + add_sad_kitten => sub { ... }; -=method plugin_args +Or a mix of both styles. We're easy that way: -Simple method to retrieve the parameters or arguments passed to a -plugin-defined keyword. Although not relevant for Dancer 1 only, or -Dancer 2 only, plugins, it is useful for universal plugins. + plugin_keywords + add_smileys => sub { + my( $plugin, $text ) = @_; - register foo => sub { - my ($dsl, @args) = plugin_args(@_); - ... - } + $text =~ s/ (?<= \. ) / $plugin->smiley /xeg; -Note that Dancer 1 will return undef as the DSL object. + return $text; + }, + 'add_sad_kitten'; -=method plugin_setting + sub add_sad_kitten { + ...; + } -If C is called inside a plugin, the appropriate configuration -will be returned. The C should be the name of the package, or, -if the plugin name is under the B namespace (which is -recommended), the remaining part of the plugin name. +If you want several keywords to be synonyms calling the same +function, you can list them in an arrayref. The first +function of the list is taken to be the "real" method to +link to the keywords. -Configuration for plugin should be structured like this in the config.yml of -the application: + plugin_keywords [qw/ add_smileys add_happy_face /]; - plugins: - plugin_name: - key: value + sub add_smileys { ... } -Enclose the remaining part in quotes if it contains ::, e.g. -for B, use: +Calls to C are cumulative. - plugins: - "Foo::Bar": - key: value +=head4 Via the C<:PluginKeyword> function attribute -=method register_hook +Keywords can also be defined by adding the C<:PluginKeyword> attribute +to the function you wish to export. -Allows a plugin to declare a list of supported hooks. Any hook declared like so -can be executed by the plugin with C. + sub foo :PluginKeyword { ... } - register_hook 'foo'; - register_hook 'foo', 'bar', 'baz'; + sub bar :PluginKeyword( baz quux ) { ... } -=method execute_hook + # equivalent to -Allows a plugin to execute the hooks attached at the given position + sub foo { ... } + sub bar { ... } - $dsl->execute_hook( 'some_hook' ); + plugin_keywords 'foo', [ qw/ baz quux / ] => \&bar; -Arguments can be passed which will be received by handlers attached to that -hook: +=head4 For an attribute - $dsl->execute_hook( 'some_hook', @some_args ); +You can also turn an attribute of the plugin into a keyword. -The hook must have been registered by the plugin first, with C. + has foo => ( + is => 'ro', + plugin_keyword => 1, # keyword will be 'foo' + ); -=head1 EXAMPLE PLUGIN + has bar => ( + is => 'ro', + plugin_keyword => 'quux', # keyword will be 'quux' + ); + + has baz => ( + is => 'ro', + plugin_keyword => [ 'baz', 'bazz' ], # keywords will be 'baz' and 'bazz' + ); -The following code is a dummy plugin that provides a keyword 'logout' that -destroys the current session and redirects to a new URL specified in -the config file as C. - package Dancer2::Plugin::Logout; - use Dancer2::Plugin; - register logout => sub { - my $dsl = shift; - my $app = $dsl->app; - my $conf = plugin_setting(); +=head3 Accessing the plugin configuration - $app->destroy_session; +The plugin configuration is available via the C method. - return $app->redirect( $conf->{after_logout} ); - }; + sub BUILD { + my $plugin = shift; - register_plugin; - 1; + if ( $plugin->config->{feeling_polite} ) { + $plugin->app->add_hook( Dancer2::Core::Hook->new( + name => 'after', + code => sub { $_[0]->content( $_[0]->content . " ... please?" ) } + )); + } + } -And in your application: +=head3 Getting default values from config file - package My::Webapp; +Since initializing a plugin with either a default or a value passed via the configuration file, +like - use Dancer2; - use Dancer2::Plugin::Logout; + has smiley => ( + is => 'ro', + default => sub { + $_[0]->config->{smiley} || ':-)' + } + ); + +C allows for a C key in the attribute definition. +Its value is the plugin configuration key that will be used to initialize the attribute. + +If it's given the value C<1>, the name of the attribute will be taken as the configuration key. + +Nested hash keys can also be refered to using a dot notation. + +If the plugin configuration has no value for the given key, the attribute default, if specified, will be honored. + +If the key is given a coderef as value, it's considered to be a C value combo: + + has foo => ( + is => 'ro', + from_config => sub { 'my default' }, + ); + + + # equivalent to + has foo => ( + is => 'ro', + from_config => 'foo', + default => sub { 'my default' }, + ); + +For example: + + # in config.yml + + plugins: + Polite: + smiley: ':-)' + greeting: + casual: Hi! + formal: How do you do? + + + # in the plugin + + has smiley => ( # will be ':-)' + is => 'ro', + from_config => 1, + default => sub { ':-(' }, + ); + + has casual_greeting => ( # will be 'Hi!' + is => 'ro', + from_config => 'greeting.casual', + ); + + has apology => ( # will be 'sorry' + is => 'ro', + from_config => 'apology', + default => sub { 'sorry' }, + ) + + has closing => ( # will be 'See ya!' + is => 'ro', + from_config => sub { 'See ya!' }, + ); + +=head3 Accessing the parent Dancer app + +If the plugin is instantiated within a Dancer app, it'll be +accessible via the method C. + + sub BUILD { + my $plugin = shift; + + $plugin->app->add_route( ... ); + } + + +=head2 Using the plugin within the app + +A plugin is loaded via + + use Dancer2::Plugin::Polite; + +The plugin will assume that it's loading within a Dancer module and will +automatically register itself against its C and export its keywords +to the local namespace. If you don't want this to happen, specify that you +don't want anything imported via empty parentheses when Cing the module: + + use Dancer2::Plugin::Polite (); + + +=head2 Plugins using plugins + +This is a (relatively) simple way for a plugin to use another plugin: + + + package Dancer2::Plugin::SourPuss; + + has polite => ( + is => 'ro', + lazy => 1, + default => sub { + # if the app already has the 'Polite' plugin loaded, it'll return + # it. If not, it'll load it in the app, and then return it. + scalar $_[0]->app->with_plugins( 'Polite' ) + }, + handles => { 'smiley' => 'smiley' }, + ); + + sub keywords { qw/ killjoy / } + + sub killjoy { + my( $plugin, $text ) = @_; + + my $smiley = $plugin->smiley; + + $text =~ s/ $smiley />:-(/xg; + + $text; + } + +=head2 Hooks + +New plugin hooks are declared via C. + + plugin_hooks 'my_hook', 'my_other_hook'; + +Hooks are prefixed with C. So the plugin +C coming from the plugin C will have the hook name +C. + +Hooks are executed within the plugin by calling them via the associated I. + + $plugin->execute_plugin_hook( 'my_hook' ); + +You can also call any other hook if you provide the full name using the +C method: + + $plugin->app->execute_hook( 'core.app.route_exception' ); + +Or using their alias: + + $plugin->app->execute_hook( 'on_route_exception' ); + +=head2 Writing Test Gotchas + +=head3 Constructor for Dancer2::Plugin::Foo has been inlined and cannot be updated + +You'll usually get this one because you are defining both the plugin and app +in your test file, and the runtime creation of Moo's attributes happens after +the compile-time import voodoo dance. + +To get around this nightmare, wrap your plugin definition in a C block. + + + BEGIN { + package Dancer2::Plugin::Foo; + + use Dancer2::Plugin; + + has bar => ( + is => 'ro', + from_config => 1, + ); + + plugin_keywords qw/ bar /; + + } + + { + package MyApp; + + use Dancer2; + use Dancer2::Plugin::Foo; + + bar(); + } + +=head3 You cannot overwrite a locally defined method (bar) with a reader + +If you set an object attribute of your plugin to be a keyword as well, you need +to call C after the attribute definition. + + package Dancer2::Plugin::Foo; + + use Dancer2::Plugin; + + has bar => ( + is => 'ro', + ); - get '/logout' => sub { logout }; + plugin_keywords 'bar'; =cut diff --git a/lib/Dancer2/Plugin2.pm b/lib/Dancer2/Plugin2.pm deleted file mode 100644 index 829c6f95c..000000000 --- a/lib/Dancer2/Plugin2.pm +++ /dev/null @@ -1,762 +0,0 @@ -package Dancer2::Plugin2; -# ABSTRACT: base class for Dancer2 plugins - -use strict; -use warnings; - -use Moo; -use Carp; -use List::Util qw/ reduce /; -use Attribute::Handlers; - -extends 'Exporter::Tiny'; - -with 'Dancer2::Core::Role::Hookable'; - -has app => ( - is => 'ro', - required => 1, -); - -has config => ( - is => 'ro', - lazy => 1, - default => sub { - my $self = shift; - my $config = $self->app->config; - my $package = ref $self; # TODO - $package =~ s/Dancer2::Plugin:://; - $config->{plugins}{$package} || {}; - }, -); - -my $_keywords = {}; -sub keywords { $_keywords } - -has '+hooks' => ( - default => sub { - my $plugin = shift; - my $name = 'plugin.' . lc ref $plugin; - $name =~ s/Dancer2::Plugin:://i; - $name =~ s/::/_/g; - - +{ - map { join( '.', $name, $_ ) => [] } - @{ $plugin->ClassHooks } - }; - }, -); - -sub add_hooks { - my $class = shift; - push @{ $class->ClassHooks }, @_; -} - -sub execute_plugin_hook { - my ( $self, $name, @args ) = @_; - my $plugin_class = ref $self; - - $plugin_class =~ s/^Dancer2::Plugin::// - or croak "Cannot call plugin hook ($name) from outside plugin"; - - my $full_name = 'plugin.' . lc($plugin_class) . ".$name"; - $full_name =~ s/::/_/g; - - $self->app->execute_hook( $full_name, @args ); -} - -# both functions are there for D2::Core::Role::Hookable -# back-compatibility. Aren't used -sub supported_hooks { [] } -sub hook_aliases { $_[0]->{'hook_aliases'} ||= {} } - -### has() STUFF ######################################## - -# our wrapping around Moo::has, done to be able to intercept -# both 'from_config' and 'plugin_keyword' -sub _p2_has { - my $class = shift; - $class->_p2_has_from_config( $class->_p2_has_keyword( @_ ) ); -}; - -sub _p2_has_from_config { - my( $class, $name, %args ) = @_; - - my $config_name = delete $args{'from_config'} - or return ( $name, %args ); - - $args{lazy} = 1; - - if ( ref $config_name eq 'CODE' ) { - $args{default} ||= $config_name; - $config_name = 1; - } - - $config_name = $name if $config_name eq '1'; - my $orig_default = $args{default} || sub{}; - $args{default} = sub { - my $plugin = shift; - my $value = reduce { eval { $a->{$b} } } $plugin->config, split '\.', $config_name; - return defined $value ? $value: $orig_default->($plugin); - }; - - return $name => %args; -} - -sub _p2_has_keyword { - my( $class, $name, %args ) = @_; - - if( my $keyword = delete $args{plugin_keyword} ) { - - $keyword = $name if $keyword eq '1'; - - $class->keywords->{$_} = sub { (shift)->$name(@_) } - for ref $keyword ? @$keyword : $keyword; - } - - return $name => %args; -} - -### ATTRIBUTE HANDLER STUFF ######################################## - -# :PluginKeyword shenanigans - -sub PluginKeyword :ATTR(CODE) { - my( $class, $sym_ref, $code, undef, $args ) = @_; - my $func_name = *{$sym_ref}{NAME}; - - $args = join '', @$args if ref $args eq 'ARRAY'; - - for my $name ( split ' ', $args || $func_name ) { - $class->keywords->{$name} = $code; - } - -} - -## EXPORT STUFF ############################################################## - -# this @EXPORT will only be taken -# into account when we do a 'use Dancer2::Plugin2' -# I.e., it'll only do its magic for the -# plugins themselves, not when they are -# called -our @EXPORT = qw/ :plugin /; - -sub _exporter_expand_tag { - my( $class, $name, $args, $global ) = @_; - - my $caller = $global->{into}; - - return _exporter_plugin($caller) - if $name eq 'plugin'; - - return _exporter_app($class,$caller,$global) - if $name eq 'app' and $caller->can('app'); - - return; - -} - -# plugin has been called within a D2 app. Modify -# the app and export keywords -sub _exporter_app { - my( $class, $caller, $global ) = @_; - - my $app = eval "${caller}::app()" or return; - - return unless $app->can('with_plugin'); - - ( my $short = $class ) =~ s/Dancer2::Plugin:://; - - my $plugin = $app->with_plugin( $short ); - $global->{plugin} = $plugin; - - return unless $class->can('keywords'); - - # deprecated backwards compat: on_plugin_import() - $_->($plugin) for @{ $plugin->_DANCER2_IMPORT_TIME_SUBS() }; - - { - ## no critic - no strict 'refs'; - *{"${class}::plugin_setting"} = sub { $plugin->config }; - } - - - # Add our hooks to the app, so they're recognized - # this is for compatibility so you can call execute_hook() - # without the fully qualified plugin name. - # The reason we need to do this here instead of when adding a - # hook is because we need to register in the app, and only now it - # exists. - # This adds a caveat that two plugins cannot register - # the same hook name, but that will be deprecated anyway. - {; - foreach my $hook ( @{ $plugin->ClassHooks } ) { - my $full_name = 'plugin.' . lc($class) . ".$hook"; - $full_name =~ s/Dancer2::Plugin:://i; - $full_name =~ s/::/_/g; - $plugin->hook_aliases->{$hook} = $full_name; - } - } - - map { [ $_ => {plugin => $plugin} ] } keys %{ $plugin->keywords }; -} - -# turns the caller namespace into -# a D2P2 class, with exported keywords -sub _exporter_plugin { - my $caller = shift; - - eval <<"END"; - { - package $caller; - use Moo; - use Carp (); - use Attribute::Handlers; - - extends 'Dancer2::Plugin2'; - - our \@EXPORT = ( ':app' ); - - around has => sub { - my( \$orig, \@args ) = \@_; - \$orig->( ${caller}->_p2_has( \@args) ); - }; - - sub PluginKeyword :ATTR(CODE) { - goto &Dancer2::Plugin2::PluginKeyword; - } - - sub execute_plugin_hook { - goto &Dancer2::Plugin2::execute_plugin_hook; - } - - my \$_keywords = {}; - sub keywords { \$_keywords } - - my \$_ClassHooks = []; - sub ClassHooks { \$_ClassHooks } - - # deprecated backwards compat - sub register_plugin {1} - - sub register { - my ( \$keyword, \$sub ) = \@_; - \$_keywords->{\$keyword} = \$sub; - - # Exporter::Tiny doesn't seem to generate the subs - # in the caller properly, so we have to do it manually - { - no strict 'refs'; - *{"${caller}::\$keyword"} = \$sub; - } - } - - my \@_DANCER2_IMPORT_TIME_SUBS; - sub _DANCER2_IMPORT_TIME_SUBS {\\\@_DANCER2_IMPORT_TIME_SUBS} - sub on_plugin_import (&) { - push \@_DANCER2_IMPORT_TIME_SUBS, \$_[0]; - } - - sub register_hook { goto &plugin_hooks } - - sub dancer_app { - Carp::carp "Plugin DSL method 'dancer_app' is deprecated. " - . "Use 'app' instead'.\n"; - - \$_[0]->app; - } - - sub request { - Carp::carp "Plugin DSL method 'request' is deprecated. " - . "Use 'app->request' instead'.\n"; - - \$_[0]->app->request; - } - - sub var { - Carp::carp "Plugin DSL method 'var' is deprecated. " - . "Use 'request->var' instead'.\n"; - - shift->app->request->var(\@_); - } - - sub plugin_args { - Carp::carp "Plugin DSL method 'plugin_args' is deprecated. " - . "Use '\\\@_' instead'.\n"; - - \@_; - } - } -END - - die $@ if $@; - - return map { [ $_ => { class => $caller } ] } - qw/ plugin_keywords plugin_hooks /; -} - -sub _exporter_expand_sub { - my( $plugin, $name, $args, $global ) = @_; - my $class = $args->{class}; - - return _exported_plugin_keywords($plugin,$class) - if $name eq 'plugin_keywords'; - - return _exported_plugin_hooks($class) - if $name eq 'plugin_hooks'; - - # otherwise, we're exporting a keyword - - my $p = $args->{plugin}; - my $sub = $p->keywords->{$name}; - return $name => sub(@) { $sub->($p,@_) }; -} - -# define the exported 'plugin_keywords' -sub _exported_plugin_keywords{ - my( $plugin, $class ) = @_; - - return plugin_keywords => sub(@) { - while( my $name = shift @_ ) { - my $sub = ref $_[0] eq 'CODE' - ? shift @_ - : eval '\&'.$class."::" . ( ref $name ? $name->[0] : $name ); - $class->keywords->{$_} = $sub for ref $name ? @$name : $name; - } - } -} - -sub _exported_plugin_hooks { - my $class = shift; - return plugin_hooks => sub (@) { $class->add_hooks(@_) } -} - -1; - -__END__ - -=head1 SYNOPSIS - -The plugin itself: - - - package Dancer2::Plugin::Polite; - - use strict; - use warnings; - - use Dancer2::Plugin2; - - has smiley => ( - is => 'ro', - default => sub { - $_[0]->config->{smiley} || ':-)' - } - ); - - plugin_keywords 'add_smileys'; - - sub BUILD { - my $plugin = shift; - - $plugin->app->add_hook( Dancer2::Core::Hook->new( - name => 'after', - code => sub { $_[0]->content( $_[0]->content . " ... please?" ) } - )); - - $plugin->app->add_route( - method => 'get', - regexp => '/goodbye', - code => sub { - my $app = shift; - 'farewell, ' . $app->request->params->{name}; - }, - ); - - } - - sub add_smileys { - my( $plugin, $text ) = @_; - - $text =~ s/ (?<= \. ) / $plugin->smiley /xeg; - - return $text; - } - - 1; - -then to load into the app: - - - package MyApp; - - use strict; - use warnings; - - use Dancer2; - - BEGIN { # would usually be in config.yml - set plugins => { - Polite => { - smiley => '8-D', - }, - }; - } - - use Dancer2::Plugin::Polite; - - get '/' => sub { - add_smileys( 'make me a sandwich.' ); - }; - - 1; - - -=head1 DESCRIPTION - -This is an alternate plugin basis for Dancer2. - -=head2 Writing the plugin - -=head3 C - -The plugin must begin with - - use Dancer2::Plugin2; - -which will turn the package into a L class that inherits from L. The base class provides the plugin with -two attributes: C, which is populated with the Dancer2 app object for which -the plugin is being initialized for, and C which holds the plugin -section of the application configuration. - -=head3 Modifying the app at building time - -If the plugin needs to tinker with the application -- add routes or hooks, for example -- -it can do so within its C function. - - sub BUILD { - my $plugin = shift; - - $plugin->app->add_route( ... ); - } - -=head3 Adding keywords - -=head4 Via C - -Keywords that the plugin wishes to export to the Dancer2 app can be defined via the C keyword: - - plugin_keywords qw/ - add_smileys - add_sad_kitten - /; - -Each of the keyword will resolve to the class method of the same name. When invoked as keyword, it'll be passed -the plugin object as its first argument. - - sub add_smileys { - my( $plugin, $text ) = @_; - - return join ' ', $text, $plugin->smiley; - } - - # and then in the app - - get '/' => sub { - add_smileys( "Hi there!" ); - }; - -You can also pass the functions directly to C. - - plugin_keywords - add_smileys => sub { - my( $plugin, $text ) = @_; - - $text =~ s/ (?<= \. ) / $plugin->smiley /xeg; - - return $text; - }, - add_sad_kitten => sub { ... }; - -Or a mix of both styles. We're easy that way: - - plugin_keywords - add_smileys => sub { - my( $plugin, $text ) = @_; - - $text =~ s/ (?<= \. ) / $plugin->smiley /xeg; - - return $text; - }, - 'add_sad_kitten'; - - sub add_sad_kitten { - ...; - } - -If you want several keywords to be synonyms calling the same -function, you can list them in an arrayref. The first -function of the list is taken to be the "real" method to -link to the keywords. - - plugin_keywords [qw/ add_smileys add_happy_face /]; - - sub add_smileys { ... } - -Calls to C are cumulative. - -=head4 Via the C<:PluginKeyword> function attribute - -Keywords can also be defined by adding the C<:PluginKeyword> attribute -to the function you wish to export. - - sub foo :PluginKeyword { ... } - - sub bar :PluginKeyword( baz quux ) { ... } - - # equivalent to - - sub foo { ... } - sub bar { ... } - - plugin_keywords 'foo', [ qw/ baz quux / ] => \&bar; - -=head4 For an attribute - -You can also turn an attribute of the plugin into a keyword. - - has foo => ( - is => 'ro', - plugin_keyword => 1, # keyword will be 'foo' - ); - - has bar => ( - is => 'ro', - plugin_keyword => 'quux', # keyword will be 'quux' - ); - - has baz => ( - is => 'ro', - plugin_keyword => [ 'baz', 'bazz' ], # keywords will be 'baz' and 'bazz' - ); - - - -=head3 Accessing the plugin configuration - -The plugin configuration is available via the C method. - - sub BUILD { - my $plugin = shift; - - if ( $plugin->config->{feeling_polite} ) { - $plugin->app->add_hook( Dancer2::Core::Hook->new( - name => 'after', - code => sub { $_[0]->content( $_[0]->content . " ... please?" ) } - )); - } - } - -=head3 Getting default values from config file - -Since initializing a plugin with either a default or a value passed via the configuration file, -like - - has smiley => ( - is => 'ro', - default => sub { - $_[0]->config->{smiley} || ':-)' - } - ); - -C allows for a C key in the attribute definition. -Its value is the plugin configuration key that will be used to initialize the attribute. - -If it's given the value C<1>, the name of the attribute will be taken as the configuration key. - -Nested hash keys can also be refered to using a dot notation. - -If the plugin configuration has no value for the given key, the attribute default, if specified, will be honored. - -If the key is given a coderef as value, it's considered to be a C value combo: - - has foo => ( - is => 'ro', - from_config => sub { 'my default' }, - ); - - - # equivalent to - has foo => ( - is => 'ro', - from_config => 'foo', - default => sub { 'my default' }, - ); - -For example: - - # in config.yml - - plugins: - Polite: - smiley: ':-)' - greeting: - casual: Hi! - formal: How do you do? - - - # in the plugin - - has smiley => ( # will be ':-)' - is => 'ro', - from_config => 1, - default => sub { ':-(' }, - ); - - has casual_greeting => ( # will be 'Hi!' - is => 'ro', - from_config => 'greeting.casual', - ); - - has apology => ( # will be 'sorry' - is => 'ro', - from_config => 'apology', - default => sub { 'sorry' }, - ) - - has closing => ( # will be 'See ya!' - is => 'ro', - from_config => sub { 'See ya!' }, - ); - -=head3 Accessing the parent Dancer app - -If the plugin is instantiated within a Dancer app, it'll be -accessible via the method C. - - sub BUILD { - my $plugin = shift; - - $plugin->app->add_route( ... ); - } - - -=head2 Using the plugin within the app - -A plugin is loaded via - - use Dancer2::Plugin::Polite; - -The plugin will assume that it's loading within a Dancer module and will -automatically register itself against its C and export its keywords -to the local namespace. If you don't want this to happen, specify that you -don't want anything imported via empty parentheses when Cing the module: - - use Dancer2::Plugin::Polite (); - - -=head2 Plugins using plugins - -This is a (relatively) simple way for a plugin to use another plugin: - - - package Dancer2::Plugin::SourPuss; - - has polite => ( - is => 'ro', - lazy => 1, - default => sub { - # if the app already has the 'Polite' plugin loaded, it'll return - # it. If not, it'll load it in the app, and then return it. - scalar $_[0]->app->with_plugins( 'Polite' ) - }, - handles => { 'smiley' => 'smiley' }, - ); - - sub keywords { qw/ killjoy / } - - sub killjoy { - my( $plugin, $text ) = @_; - - my $smiley = $plugin->smiley; - - $text =~ s/ $smiley />:-(/xg; - - $text; - } - -=head2 Hooks - -New plugin hooks are declared via C. - - plugin_hooks 'my_hook', 'my_other_hook'; - -Hooks are prefixed with C. So the plugin -C coming from the plugin C will have the hook name -C. - -Hooks are executed within the plugin by calling them via the associated I. - - $plugin->app->execute_plugin_hook( 'my_hook' ); - -You can also call any other hook if you provide the full name using the -C method: - - $plugin->app->execute_hook( 'core.app.route_exception' ); - -Or using their alias: - - $plugin->app->execute_hook( 'on_route_exception' ); - -=head2 Writing Test Gotchas - -=head3 Constructor for Dancer2::Plugin::Foo has been inlined and cannot be updated - -You'll usually get this one because you are defining both the plugin and app -in your test file, and the runtime creation of Moo's attributes happens after -the compile-time import voodoo dance. - -To get around this nightmare, wrap your plugin definition in a C block. - - - BEGIN { - package Dancer2::Plugin::Foo; - - use Dancer2::Plugin2; - - has bar => ( - is => 'ro', - from_config => 1, - ); - - plugin_keywords qw/ bar /; - - } - - { - package MyApp; - - use Dancer2; - use Dancer2::Plugin::Foo; - - bar(); - } - -=head3 You cannot overwrite a locally defined method (bar) with a reader - -If you set an object attribute of your plugin to be a keyword as well, you need -to call C after the attribute definition. - - package Dancer2::Plugin::Foo; - - use Dancer2::Plugin2; - - has bar => ( - is => 'ro', - ); - - plugin_keywords 'bar'; - -=cut