more docs for action traits and additional compat wedges
[catagits/CatalystX-Declare.git] / lib / CatalystX / Declare / Keyword / Action.pm
CommitLineData
918fb36e 1use MooseX::Declare;
c2a8165b 2use MooseX::Role::Parameterized ();
918fb36e 3
8d66ec34 4class CatalystX::Declare::Keyword::Action {
918fb36e 5
6
a1dd1788 7 use Carp qw( croak );
8 use Perl6::Junction qw( any );
9 use Data::Dump qw( pp );
10 use MooseX::Types::Util qw( has_available_type_export );
c2a8165b 11 use Moose::Util qw( add_method_modifier ensure_all_roles );
a1dd1788 12 use Class::Inspector;
13 use Class::MOP;
14
918fb36e 15
e10b92dd 16 use constant STOP_PARSING => '__MXDECLARE_STOP_PARSING__';
9c11a562 17 use constant UNDER_VAR => '$CatalystX::Declare::SCOPE::UNDER';
fe864e80 18 use constant UNDER_STACK => '@CatalystX::Declare::SCOPE::UNDER_STACK';
e10b92dd 19
5fb5cef1 20 use aliased 'CatalystX::Declare::Action::CatchValidationError';
8d66ec34 21 use aliased 'CatalystX::Declare::Context::StringParsing';
918fb36e 22 use aliased 'MooseX::Method::Signatures::Meta::Method';
23 use aliased 'MooseX::MethodAttributes::Role::Meta::Method', 'AttributeRole';
c2a8165b 24 use aliased 'MooseX::MethodAttributes::Role::Meta::Role', 'AttributeMetaRole';
918fb36e 25
26
856ac9a7 27 method parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0) {
918fb36e 28
29 # somewhere to put the attributes
30 my %attributes;
31 my @populators;
918fb36e 32
33 # parse declarations
34 until (do { $ctx->skipspace; $ctx->peek_next_char } eq any qw( ; { } )) {
918fb36e 35
36 $ctx->skipspace;
37
38 # optional commas
39 if ($ctx->peek_next_char eq ',') {
40
41 my $linestr = $ctx->get_linestr;
42 substr($linestr, $ctx->offset, 1) = '';
43 $ctx->set_linestr($linestr);
44
45 next;
46 }
47
48 # next thing should be an option name
49 my $option = (
50 $skipped_declarator
51 ? $ctx->strip_name
52 : do {
53 $ctx->skip_declarator;
54 $skipped_declarator++;
55 $ctx->declarator;
56 })
57 or croak "Expected option token, not " . substr($ctx->get_linestr, $ctx->offset);
58
59 # we need to be able to handle the rest
60 my $handler = $self->can("_handle_${option}_option")
61 or croak "Unknown action option: $option";
62
63 # call the handler
e10b92dd 64 my $populator = $self->$handler($ctx, \%attributes);
65
66 if ($populator and $populator eq STOP_PARSING) {
67
68 return $ctx->shadow(sub (&) {
69 my ($body) = @_;
70 return $body->();
71 });
72 }
73
74 push @populators, $populator
75 if defined $populator;
918fb36e 76 }
77
78 croak "Need an action specification"
79 unless exists $attributes{Signature};
80
81 my $name = $attributes{Subname};
856ac9a7 82
17a275f5 83 if ($attributes{Private}) {
84 $attributes{Signature} ||= '@';
85 }
86
918fb36e 87 my $method = Method->wrap(
88 signature => qq{($attributes{Signature})},
89 package_name => $ctx->get_curstash_name,
90 name => $name,
91 );
92
a1dd1788 93 AttributeRole->meta->apply($method);
94
eb97acbb 95 my $count = $self->_count_positional_arguments($method);
96 $attributes{CaptureArgs} = $count
97 if defined $count;
98
918fb36e 99 $_->($method)
100 for @populators;
101
aae7ad1f 102 unless ($attributes{Private}) {
dd2759b0 103 $attributes{PathPart} ||= $name;
918fb36e 104
aae7ad1f 105 delete $attributes{CaptureArgs}
106 if exists $attributes{Args};
918fb36e 107
aae7ad1f 108 $attributes{CaptureArgs} = 0
109 unless exists $attributes{Args}
110 or exists $attributes{CaptureArgs};
111 }
112
113 if ($attributes{Private}) {
aae7ad1f 114 delete $attributes{ $_ }
ed4a2203 115 for qw( Args CaptureArgs Chained Signature Private );
aae7ad1f 116 }
918fb36e 117
4960c7ec 118 # inject a hashref for resolving runtime attribute values
8d66ec34 119 $self->_inject_attributes($ctx, \%attributes);
120
4960c7ec 121 # our declaration is followed by a block
918fb36e 122 if ($ctx->peek_next_char eq '{') {
123 $ctx->inject_if_block($ctx->scope_injector_call . $method->injectable_code);
124 }
4960c7ec 125
126 # there is no block, so we insert one.
918fb36e 127 else {
128 $ctx->inject_code_parts_here(
129 sprintf '{ %s%s }',
130 $ctx->scope_injector_call,
131 $method->injectable_code,
132 );
133 }
134
8d66ec34 135 my $compile_attrs = sub {
136 my $attributes = shift;;
137 my @attributes;
138
139 for my $attr (keys %$attributes) {
140 my $value = $attributes->{ $attr };
141
4960c7ec 142 # the compiletime chained attr might contain the under global var name
8d66ec34 143 next if $attr eq 'Chained' and $value eq UNDER_VAR;
144
8d66ec34 145 push @attributes,
146 map { sprintf '%s%s', $attr, defined($_) ? sprintf('(%s)', $_) : '' }
147 (ref($value) eq 'ARRAY')
148 ? @$value
149 : $value;
150 }
151
152 return \@attributes;
153 };
918fb36e 154
8d66ec34 155 return $ctx->shadow(sub {
918fb36e 156 my $class = caller;
8d66ec34 157 my $attrs = shift;
856ac9a7 158 my $body = shift;
4960c7ec 159
160 # the runtime-resolved name
dd2759b0 161 my $name = $attrs->{Subname};
918fb36e 162
4960c7ec 163 # in case no hashref was specified
8d66ec34 164 $body = $attrs and $attrs = {}
165 if ref $attrs eq 'CODE';
166
4960c7ec 167 # default path part to runtime-resolved name
dd2759b0 168 unless ($attrs->{Private}) {
169
170 $attrs->{PathPart} = $attrs->{Subname}
171 unless defined $attrs->{PathPart};
172 }
173
4960c7ec 174 # in CXD we are explicit about chained values, an undefined
175 # value means we defaulted to the outer-scope under and there
176 # was none.
8d66ec34 177 delete $attrs->{Chained}
178 unless defined $attrs->{Chained};
179
4960c7ec 180 # some attrs need to be single quoted in their stringified forms
8d66ec34 181 defined($attrs->{ $_ }) and $attrs->{ $_ } = sprintf "'%s'", $attrs->{ $_ }
182 for qw( Chained PathPart );
183
4960c7ec 184 # merge runtime and compiletime attrs
8d66ec34 185 my %full_attrs = (%attributes, %$attrs);
8d66ec34 186 my $compiled_attrs = $compile_attrs->(\%full_attrs);
8d66ec34 187
24a5fc45 188 my $real_method = $method->reify(
189 actual_body => $body,
8d66ec34 190 attributes => $compiled_attrs,
dd2759b0 191 name => $name,
24a5fc45 192 );
856ac9a7 193
4960c7ec 194 # NYI
856ac9a7 195 if ($modifier) {
196
24a5fc45 197 add_method_modifier $class, $modifier, [$name, $real_method];
856ac9a7 198 }
199 else {
200
c2a8165b 201 my $prepare_meta = sub {
202 my ($meta) = @_;
203
24a5fc45 204 $meta->add_method($name, $real_method);
8d66ec34 205 $meta->register_method_attributes($meta->name->can($real_method->name), $compiled_attrs);
c2a8165b 206 };
207
208 if ($ctx->stack->[-1] and $ctx->stack->[-1]->is_parameterized) {
209 my $real_meta = MooseX::Role::Parameterized->current_metaclass;
210
211 $real_meta->meta->make_mutable
212 if $real_meta->meta->is_immutable;
213 ensure_all_roles $real_meta->meta, AttributeMetaRole
214 if $real_meta->isa('Moose::Meta::Role');
215
216 $real_meta->$prepare_meta;
217 }
1754e3e7 218 else {
c2a8165b 219
1754e3e7 220 $class->meta->$prepare_meta;
221 }
856ac9a7 222 }
918fb36e 223 });
224 }
225
a1dd1788 226 method _handle_with_option (Object $ctx, HashRef $attrs) {
227
284b2994 228 my @roles_with_args = ();
229 push @roles_with_args, @{ $ctx->strip_names_and_args };
a1dd1788 230
231 # we need to fish for aliases here since we are still unclean
284b2994 232 my @roles = ();
233 for my $role_with_arg(@roles_with_args) {
d14acb87 234 my ($role, $params) = @{$role_with_arg};
235 if($params) {
236 my ($first, @rest) = eval $params;
34a0a1ff 237 my %params = ref $first eq 'HASH' ? %$first : ($first, @rest); # both (%opts) and {%opts}
d14acb87 238 for my $key (keys %params) {
72a81644 239 my $parameters = ref $params{$key} eq 'ARRAY' ? @{$params{$key}} : $params{$key};
240 push @{$attrs->{$key}}, $parameters;
d14acb87 241 }
72a81644 242 $attrs->{_role_attributes}->{$role} = \%params;
d14acb87 243 }
244
284b2994 245 if (defined(my $alias = $self->_check_for_available_import($ctx, $role))) {
246 $role = $alias;
247 }
248 push @roles, $role;
a1dd1788 249 }
250
284b2994 251 push @{ $attrs->{CatalystX_Declarative_ActionRoles} ||= [] }, @roles;
a1dd1788 252
253 return;
254 }
255
256 method _handle_isa_option (Object $ctx, HashRef $attrs) {
257
258 my $class = $ctx->strip_name
259 or croak "Expected bareword action class specification for action after isa";
260
261 if (defined(my $alias = $self->_check_for_available_import($ctx, $class))) {
262 $class = $alias;
263 }
264
265 $attrs->{CatalystX_Declarative_ActionClass} = $class;
266
267 return;
268 }
269
270 method _check_for_available_import (Object $ctx, Str $name) {
271
272 if (my $code = $ctx->get_curstash_name->can($name)) {
273 return $code->();
274 }
275
276 return undef;
277 }
278
918fb36e 279 method _handle_action_option (Object $ctx, HashRef $attrs) {
280
281 # action name
dd2759b0 282 my $name = $self->_strip_actionpath($ctx, interpolate => 1)
918fb36e 283 or croak "Anonymous actions not yet supported";
284
64baeca0 285 $ctx->skipspace;
286 my $populator;
287
4960c7ec 288 # shortcut under base option is basically handled by the under handler
64baeca0 289 if (substr($ctx->get_linestr, $ctx->offset, 2) eq '<-') {
290 my $linestr = $ctx->get_linestr;
291 substr($linestr, $ctx->offset, 2) = '';
292 $ctx->set_linestr($linestr);
293 $populator = $self->_handle_under_option($ctx, $attrs);
294 }
295
918fb36e 296 # signature
297 my $proto = $ctx->strip_proto || '';
298 $proto = join(', ', 'Object $self: Object $ctx', $proto || ());
299
300 $attrs->{Subname} = $name;
301 $attrs->{Signature} = $proto;
aae7ad1f 302 $attrs->{Action} = [];
918fb36e 303
5fb5cef1 304 push @{ $attrs->{CatalystX_Declarative_ActionRoles} ||= [] }, CatchValidationError;
4960c7ec 305
306 # default chained base to the global under var, to be resolved at runtime
8d66ec34 307 $attrs->{Chained} ||= UNDER_VAR;
e10b92dd 308
64baeca0 309 return unless $populator;
310 return $populator;
918fb36e 311 }
312
2dde75e7 313 method _handle_final_option (Object $ctx, HashRef $attrs) {
314
315 return $self->_build_flag_populator($ctx, $attrs, 'final');
316 }
317
918fb36e 318 method _handle_is_option (Object $ctx, HashRef $attrs) {
319
320 my $what = $ctx->strip_name
321 or croak "Expected symbol token after is symbol, not " . substr($ctx->get_linestr, $ctx->offset);
322
2dde75e7 323 return $self->_build_flag_populator($ctx, $attrs, $what);
324 }
325
326 method _build_flag_populator (Object $ctx, HashRef $attrs, Str $what) {
327
17a275f5 328 $attrs->{Private} = []
329 if $what eq 'private';
330
918fb36e 331 return sub {
332 my $method = shift;
333
334 if ($what eq any qw( end endpoint final )) {
eb97acbb 335 $attrs->{Args} = delete $attrs->{CaptureArgs};
918fb36e 336 }
337 elsif ($what eq 'private') {
aae7ad1f 338 $attrs->{Private} = [];
918fb36e 339 }
340 };
341 }
342
343 method _handle_under_option (Object $ctx, HashRef $attrs) {
344
8d66ec34 345 my $target = $self->_strip_actionpath($ctx, interpolate => 1);
e10b92dd 346 $ctx->skipspace;
347
348 if ($ctx->peek_next_char eq '{' and $self->identifier eq 'under') {
349 $ctx->inject_if_block(
8d66ec34 350 $ctx->scope_injector_call .
351 sprintf ';local %s = %s;',
e10b92dd 352 UNDER_VAR,
353 $target,
354 );
355 return STOP_PARSING;
356 }
357
8d66ec34 358 $attrs->{Chained} = $target;
918fb36e 359
360 return sub {
361 my $method = shift;
918fb36e 362 };
363 }
364
365 method _handle_chains_option (Object $ctx, HashRef $attrs) {
366
367 $ctx->skipspace;
368 $ctx->strip_name eq 'to'
369 or croak "Expected to token after chains symbol, not " . substr($ctx->get_linestr, $ctx->offset);
370
371 return $self->_handle_under_option($ctx, $attrs);
372 }
373
374 method _handle_as_option (Object $ctx, HashRef $attrs) {
375
376 $ctx->skipspace;
377
8d66ec34 378 my $path = $self->_strip_actionpath($ctx, interpolate => 1);
379 $attrs->{PathPart} = $path;
918fb36e 380
381 return;
382 }
383
384 method _count_positional_arguments (Object $method) {
c2a8165b 385 my $signature = $method->parsed_signature;
918fb36e 386
387 if ($signature->has_positional_params) {
388 my $count = @{ scalar($signature->positional_params) };
389
390 if ($count and ($signature->positional_params)[-1]->sigil eq '@') {
391 return undef;
392 }
393
394 return $count - 1;
395 }
396
397 return 0;
398 }
399
8d66ec34 400 method _inject_attributes (Object $ctx, HashRef $attrs) {
401
4960c7ec 402 # attrs that need to be runtime-resolved
dd2759b0 403 my @inject = qw( Chained PathPart Subname );
8d66ec34 404
4960c7ec 405 # turn specific attributes into a hashref
406 my $code = sprintf ' +{ %s }, sub ', # the ', sub ' turns method +{ ... } { ... } into
407 join ', ', # method +{ ... }, sub { ... }
8d66ec34 408 map { (@$_) }
8d66ec34 409 map { defined( $_->[1] ) ? $_ : [$_->[0], 'undef'] }
410 map { [pp($_), $attrs->{ $_ }] }
411 grep { defined $attrs->{ $_ } }
412 @inject;
413
4960c7ec 414 # inject the hashref code before the action body
8d66ec34 415 $ctx->inject_code_parts_here($code);
416 $ctx->inc_offset(length $code);
417 }
418
419 method _strip_actionpath (Object $ctx, :$interpolate?) {
918fb36e 420
421 $ctx->skipspace;
422 my $linestr = $ctx->get_linestr;
423 my $rest = substr($linestr, $ctx->offset);
8d66ec34 424 my $interp = sub { $interpolate ? "'$_[0]'" : $_[0] };
918fb36e 425
4960c7ec 426 # find simple barewords
918fb36e 427 if ($rest =~ /^ ( [_a-z] [_a-z0-9]* ) \b/ix) {
428 substr($linestr, $ctx->offset, length($1)) = '';
429 $ctx->set_linestr($linestr);
8d66ec34 430 return $interp->($1);
918fb36e 431 }
4960c7ec 432
433 # allow single quoted more complex barewords
a0ebba1d 434 elsif ($rest =~ /^ ' ( (?:[.:;,_a-z0-9]|\/)* ) ' /ix) {
918fb36e 435 substr($linestr, $ctx->offset, length($1) + 2) = '';
436 $ctx->set_linestr($linestr);
8d66ec34 437 return $interp->($1);
438 }
4960c7ec 439
440 # double quoted strings and variables
8d66ec34 441 elsif ($interpolate and my $str = $ctx->get_string) {
442 return $str;
918fb36e 443 }
4960c7ec 444
445 # not suitable as action path
918fb36e 446 else {
447 croak "Invalid syntax for action path: $rest";
448 }
449 }
8d66ec34 450
4960c7ec 451 # down here because it requires the parse method
8d66ec34 452 with 'MooseX::Declare::Syntax::KeywordHandling';
453
454 around context_traits { $self->$orig, StringParsing }
918fb36e 455}
456
856ac9a7 457__END__
458
459=head1 NAME
460
461CatalystX::Declare::Keyword::Action - Declare Catalyst Actions
462
463=head1 SYNOPSIS
464
465 use CatalystX::Declare;
466
467 controller MyApp::Web::Controller::Example {
468
469 # chain base action with path part setting of ''
470 # body-less actions don't do anything by themselves
6e2492a4 471 action base as '' under '/';
856ac9a7 472
473 # simple end-point action
474 action controller_class is final under base {
475 $ctx->response->body( 'controller: ' . ref $self );
476 }
477
478 # chain part actions can have arguments
479 action str (Str $string) under base {
480
481 $ctx->stash(chars => [split //, $string]);
482 }
483
484 # and end point actions too, of course
485 action uc_chars (Int $count) under str is final {
486
487 my $chars = $ctx->stash->{chars};
488 ...
489 }
490
491
492 # you can use a shortcut for multiple actions with
493 # a common base
494 under base {
495
496 # this is an endpoint after base
497 action normal is final;
498
499 # the final keyword can be used to be more
500 # visually explicit about end-points
501 final action some_action { ... }
ed4a2203 502
503 # type dispatching works
504 final action with_str (Str $x) as via_type;
505 final action with_int (Int $x) as via_type;
856ac9a7 506 }
507
508 # of course you can also chain to external actions
509 final action some_end under '/some/controller/some/action';
510 }
511
512=head1 DESCRIPTION
513
514This handler class provides the user with C<action>, C<final> and C<under>
515keywords. There are multiple ways to define actions to allow for greater
516freedom of expression. While the parts of the action declaration itself do
517not care about their order, their syntax is rather strict.
518
519You can choose to separate syntax elements via C<,> if you think it is more
520readable. The action declaration
521
522 action foo is final under base;
523
524is parsed in exactly the same way if you write it as
525
526 action foo, is final, under base;
527
528=head2 Basic Action Declaration
529
530The simplest possible declaration is
531
532 action foo;
533
6e2492a4 534This would define a chain-part action chained to nothing with the name C<foo>
856ac9a7 535and no arguments. Since it isn't followed by a block, the body of the action
536will be empty.
537
538You will automatically be provided with two variables: C<$self> is, as you
539might expect, your controller instance. C<$ctx> will be the Catalyst context
540object. Thus, the following code would stash the value returned by the
541C<get_item> method:
542
543 action foo {
544 $ctx->stash(item => $self->get_item);
545 }
546
32663314 547=head2 Why $ctx instead of $c
548
549Some might ask why the context object is called C<$ctx> instead of the usual
550C<$c>. The reason is simple: It's an opinionated best practice, since C<$ctx>
551stands out more.
552
856ac9a7 553=head2 Setting a Path Part
554
555As usual with Catalyst actions, the path part (the public name of this part of
556the URI, if you're not familiar with the term yet) will default to the name of
557the action itself (or more correctly: to whatever Catalyst defaults).
558
559To change that, use the C<as> option:
560
6e2492a4 561 under something {
562 action base as ''; # <empty>
563 action something as 'foo/bar'; # foo/bar
564 action barely as bareword; # bareword
565 }
856ac9a7 566
567=head2 Chaining Actions
568
569Currently, L<CatalystX::Declare> is completely based on the concept of
570L<chained actions|Catalyst::DispatchType::Chained>. Every action you declare is
6e2492a4 571chained or private. You can specify the action you want to chain to with the
572C<under> option:
856ac9a7 573
6e2492a4 574 action foo; # chained to nothing
856ac9a7 575 action foo under '/'; # also chained to /
576 action foo under bar; # chained to the local bar action
577 action foo under '/bar/baz'; # chained to baz in /bar
578
579C<under> is also provided as a grouping keyword. Every action inside the block
580will be chained to the specified action:
581
582 under base {
583 action foo { ... }
584 action bar { ... }
585 }
586
587You can also use the C<under> keyword for a single action. This is useful if
588you want to highlight a single action with a significant diversion from what
589is to be expected:
590
6e2492a4 591 action base under '/';
856ac9a7 592
593 under '/the/sink' is final action foo;
594
595 final action bar under base;
596
597 final action baz under base;
598
599Instead of the C<under> option declaration, you can also use a more english
600variant named C<chains to>. While C<under> might be nice and concise, some
601people might prefer this if they confuse C<under> with the specification of
602a public path part. The argument to C<chains to> is the same as to C<under>:
603
604 action foo chains to bar;
605 action foo under bar;
606
607By default all actions are chain-parts, not end-points. If you want an action
608to be picked up as end-point and available via a public path, you have to say
609so explicitely by using the C<is final> option:
610
6e2492a4 611 action base under '/';
856ac9a7 612 action foo under base is final; # /base/foo
613
614You can also drop the C<is> part of the C<is final> option if you want:
615
616 under base, final action foo { ... }
617
618You can make end-points more visually distinct by using the C<final> keyword
619instead of the option:
620
6e2492a4 621 action base under '/';
856ac9a7 622 final action foo under base; # /base/foo
623
624And of course, the C<final>, C<under> and C<action> keywords can be used in
625combination whenever needed:
626
6e2492a4 627 action base as '' under '/';
856ac9a7 628
629 under base {
630
631 final action list; # /list
632
633 action load;
634
635 under load {
636
637 final action view; # /list/load/view
638 final action edit; # /list/load/edit
639 }
640 }
641
642There is also one shorthand alternative for declaring chain targets. You can
643specify an action after a C<E<lt>-> following the action name:
644
6e2492a4 645 action base under '/';
856ac9a7 646 final action foo <- base; # /base/foo
647
648=head2 Arguments
649
650You can use signatures like you are use to from L<MooseX::Method::Signatures>
ed4a2203 651to declare action parameters. The number of positinoal arguments will be used
652during dispatching as well as their types.
856ac9a7 653
654The signature follows the action name:
655
656 # /foo/*/*/*
657 final action foo (Int $year, Int $month, Int $day);
658
659If you are using the shorthand definition, the signature follows the chain
660target:
661
662 # /foo/*
6e2492a4 663 final action foo <- base ($x) under '/' { ... }
856ac9a7 664
665Parameters may be specified on chain-parts and end-points:
666
667 # /base/*/foo/*
6e2492a4 668 action base (Str $lang) under '/';
856ac9a7 669 final action page (Int $page_num) under base;
670
671Named parameters will be populated with the values in the query parameters:
672
673 # /view/17/?page=3
6e2492a4 674 final action view (Int $id, Int :$page = 1) under '/';
856ac9a7 675
accfac7d 676If you specify a query parameter to be an C<ArrayRef>, it will be specially
677handled. For one, it will match even if there is no such value in the
678parameters. Second, it will always be wrapped as an array reference.
679
856ac9a7 680Your end-points can also take an unspecified amount of arguments by specifying
681an array as a variable:
682
683 # /find/some/deep/path/spec
6e2492a4 684 final action find (@path) under '/';
856ac9a7 685
5fb5cef1 686=head2 Validation
687
ed4a2203 688The signatures are now validated during dispatching-time, and an action with
689a non-matching signature (number of positional arguments and their types) will
690not be dispatched to. This means that
691
692 action base under '/' as '';
693
694 under base {
695
ed4a2203 696 final as double, action double_integer (Int $x) {
697 $ctx->response->body( $x * 2 );
698 }
aee1c364 699
700 final as double, action double_string (Str $x) {
701 $ctx->response->body( $x x 2 );
702 }
ed4a2203 703 }
704
705will return C<foofoo> when called as C</double/foo> and C<46> when called as
706C</double/23>.
5fb5cef1 707
856ac9a7 708=head2 Actions and Method Modifiers
709
710Method modifiers can not only be applied to methods, but also to actions. There
711is no way yet to override the attributes of an already established action via
712modifiers. However, you can modify the method underlying the action.
713
714The following code is an example role modifying the consuming controller's
715C<base> action:
716
717 use CatalystX::Declare;
718
205323ac 719 controller_role MyApp::Web::ControllerRole::RichBase {
856ac9a7 720
721 before base (Object $ctx) {
722 $ctx->stash(something => $ctx->model('Item'));
723 }
724 }
725
726Note that you have to specify the C<$ctx> argument yourself, since you are
727modifying a method, not an action.
728
729Any controller having a C<base> action (or method, for this purpose), can now
730consume the C<RichBase> role declared above:
731
732 use CatalystX::Declare;
733
734 controller MyApp::Web::Controller::Foo
735 with MyApp::Web::Controller::RichBase {
736
6e2492a4 737 action base as '' under '/';
856ac9a7 738
739 action show, final under base {
740 $ctx->response->body(
741 $ctx->stash->{something}->render,
742 );
743 }
744 }
745
72a81644 746You can consume multiple action roles similarly to the way you do with the
747class or role keyword:
748
749 action user
750 with LoggedIn
751 with isSuperUser {}
752
753Or
754
755 action User
756 with (LoggedIn, isSuperUser) {}
757
758Lastly, you can pass parameters to the underlying L<Catalyst::Action> using
759a syntax that is similar to method traits:
760
761 action myaction with hasRole(opt1=>'val1', opt2=>'val2')
762
763Where C<%opts> is a hash that is used to populate $action->attributes in the
764same way you might have done the following in classic L<Catalyst>
765
766 sub myaction :Action :Does(hasRole) :opt1(val1) :opt2(val2)
767
768Here's a more detailed example:
769
770 action User
771 with hasLogger(log_engine=>'STDOUT')
772 with hasPermissions(
773 role=>['Administrator', 'Member'],
774 ) {}
775
776Think of these are classic catalyst subroutine attributes on steriods. Unlike
777subroutine attributes, you can split and format your code across multiple lines
778and you can use deep and complex data structures such as HashRefs or ArrayRefs.
779Also, since the parameters are grouped syntactically within the C<with> keyword
780this should improve readability of your code, since it will be more clear which
781parameters belong to with roles. This should give L<CatalystX::Declare> greater
782compatibility with legacy L<Catalyst> code but offer us a way forward from
783needing subroutine attributes, which suffer from significant drawbacks.
784
785A few caveats and differences from method traits. First of all, unlike method
786traits, parameters are not passed to the L<Catalyst::Action> constructor, but
787instead used to populate the C<attributes> attribute, which is to preserve
788compatibility with how subroutine attributes work in classic L<Catalyst>.
789
790Additionally, since subroutines attributes supported a very limited syntax for
791supplying values, we follow the convention where parameter values are pushed
792onto an arrayref. In other words the following:
793
794 action User with hasLogger(engine=>'STDOUT')
795
796would create the following data structure:
797
798 $action->attributes->{engine} = ['STDOUT']
799
800The one exception is that if the value is an arrayref, those will be merged:
801
802 action User with Permissions(roles=>[qw/admin member/]) {}
803 ## Creates: $action->attributes->{roles} = ['admin','member']
804
805My feeling is that this gives better backward compatibility with classic sub
806attributes:
807
808 sub User :Action :Does(Permissions) :roles(admin) :roles(member)
809
810However, I realize this method could lead to namespace collisions. So in
811addition to putting paramters into $action->attributes, we also populate a
812special key "_role_attributes", which will preserve parameters by role:
813
814 $action->attributes->{_role_attributes}->{$role} = \%params;
815
816So the following:
817
818 action User with Permissions(roles=>[qw/admin member/]) {}
819
820Creates:
821
822 $action->attributes->{_role_attributes}->{Permissions}
823 = {roles=>[qw/admin member/]};
824
825For now you should only use this for your private code.
826
2bb54af3 827=head2 Action Classes
828
829B<This option is even more experimental>
830
831You might want to create an action with a different class than the usual
832L<Catalyst::Action>. A usual suspect here is L<Catalyst::Action::RenderView>.
833You can use the C<isa> option (did I mention it's experimental?) to specify
834what class to use:
835
836 controller MyApp::Web::Controller::Root {
837
838 $CLASS->config(namespace => '');
839
840 action end isa RenderView;
841 }
842
843The loaded class will be L<Moose>ified, so we are able to apply essential
844roles.
845
b79aeec3 846=head2 Private Actions
847
848B<This option is a bit less, but still pretty experimental>
849
850You can declare private actions with the C<is private> trait:
851
852 action end is private isa RenderView;
853
856ac9a7 854=head1 ROLES
855
856=over
857
858=item L<MooseX::Declare::Syntax::KeywordHandling>
859
860=back
861
862=head1 METHODS
863
864These methods are implementation details. Unless you are extending or
865developing L<CatalystX::Declare>, you should not be concerned with them.
866
867=head2 parse
868
869 Object->parse (Object $ctx, Str :$modifier?, Int :$skipped_declarator = 0)
870
871A hook that will be invoked by L<MooseX::Declare> when this instance is called
872to handle syntax. It will parse the action declaration, prepare attributes and
873add the actions to the controller.
874
875=head1 SEE ALSO
876
877=over
878
879=item L<CatalystX::Declare>
880
881=item L<CatalystX::Declare::Keyword::Controller>
882
883=item L<MooseX::Method::Signatures>
884
885=back
886
887=head1 AUTHOR
888
889See L<CatalystX::Declare/AUTHOR> for author information.
890
891=head1 LICENSE
892
893This program is free software; you can redistribute it and/or modify it under
894the same terms as perl itself.
918fb36e 895
856ac9a7 896=cut
918fb36e 897