File Coverage

blib/lib/Catalyst/ControllerRole/At.pm
Criterion Covered Total %
statement 70 86 81.4
branch 32 34 94.1
condition 10 10 100.0
subroutine 5 9 55.5
pod n/a
total 117 139 84.1


line stmt bran cond sub pod time code
1             package Catalyst::ControllerRole::At;
2              
3 1     1   2450 use Moose::Role;
  1         3  
  1         8  
4             our $VERSION = '0.008';
5              
6             sub _parse_Get_attr {
7 1     1   5530 my ($self, $app, $action_subname, $value) = @_;
8 1         4 my %attributes = $self->_parse_At_attr($app, $action_subname, $value);
9 1         5 $attributes{Method} = 'GET';
10 1         6 return %attributes;
11             }
12              
13             sub _parse_Post_attr {
14 1     1   39039 my ($self, $app, $action_subname, $value) = @_;
15 1         5 my %attributes = $self->_parse_At_attr($app, $action_subname, $value);
16 1         7 $attributes{Method} = 'POST';
17 1         7 return %attributes;
18             }
19              
20             sub _parse_Put_attr {
21 1     1   5588 my ($self, $app, $action_subname, $value) = @_;
22 1         4 my %attributes = $self->_parse_At_attr($app, $action_subname, $value);
23 1         4 $attributes{Method} = 'PUT';
24 1         6 return %attributes;
25             }
26              
27             sub _parse_Delete_attr {
28 0     0   0 my ($self, $app, $action_subname, $value) = @_;
29 0         0 my %attributes = $self->_parse_At_attr($app, $action_subname, $value);
30 0         0 $attributes{Method} = 'DELETE';
31 0         0 return %attributes;
32             }
33              
34             sub _parse_Head_attr {
35 0     0   0 my ($self, $app, $action_subname, $value) = @_;
36 0         0 my %attributes = $self->_parse_At_attr($app, $action_subname, $value);
37 0         0 $attributes{Method} = 'HEAD';
38 0         0 return %attributes;
39             }
40              
41             sub _parse_Options_attr {
42 0     0   0 my ($self, $app, $action_subname, $value) = @_;
43 0         0 my %attributes = $self->_parse_At_attr($app, $action_subname, $value);
44 0         0 $attributes{Method} = 'OPTIONS';
45 0         0 return %attributes;
46             }
47              
48             sub _parse_Patch_attr {
49 0     0   0 my ($self, $app, $action_subname, $value) = @_;
50 0         0 my %attributes = $self->_parse_At_attr($app, $action_subname, $value);
51 0         0 $attributes{Method} = 'PATCH';
52 0         0 return %attributes;
53             }
54              
55             sub _parse_At_attr {
56 20     20   684681 my ($self, $app, $action_subname, $value) = @_;
57 20         64 my ($chained, $path_part, $arg_type, $args, %extra_proto) = ('/','','Args',0, ());
58            
59 20         88 my @controller_path_parts = split('/', $self->path_prefix($app));
60 20         2275 my @parent_controller_path_parts = @controller_path_parts;
61 20         43 my $affix = pop @parent_controller_path_parts;
62              
63 20   100     218 my %expansions = (
64             '$up' => '/' . join('/', @parent_controller_path_parts),
65             '$parent' => '/' . join('/', @parent_controller_path_parts, $action_subname),
66             '$name' => $action_subname,
67             '$controller' => '/' . join('/', @controller_path_parts),
68             '$action' => '/' . join('/', @controller_path_parts, $action_subname),
69             '$affix' => '/' . ($affix||''),
70             );
71              
72 20         55 $expansions{'$path_prefix'} = $expansions{'$controller'}; # Backwards compatibility
73 20         45 $expansions{'$path_end'} = $expansions{'$affix'}; # Backwards compatibility
74              
75 20   100     66 $value = ($value||'') . '';
76 20         109 my ($path, $query) = ($value=~/^([^?]*)\??(.*)$/);
77 20 100 100     92 my (@path_parts) = map { $expansions{$_} ? $expansions{$_} :$_ } split('/', ($path||''));
  41         136  
78              
79 20         43 my @arg_proto;
80             my @named_fields;
81              
82 20 100       48 if($query) {
83 1         10 my @q = ($query=~m/{(.+?)}/g);
84 1         5 $extra_proto{QueryParam} = \@q;
85 1         3 foreach my $q (@q) {
86 2         9 my ($q_part, $type) = split(':', $q);
87 2 50       8 if(defined($q_part)) {
88 2 100       7 if($q_part=~m/=/) {
89 1         4 ($q_part) = split('=', $q_part); # Discard any=default
90             }
91 2         6 $q_part=~s/^[!?]//;
92             $extra_proto{Field} = $extra_proto{Field} ?
93 2 100       17 "$extra_proto{Field},$q_part=>\$query{$q_part}" : "$q_part=>\$query{$q_part}"
94             }
95             }
96             }
97              
98 20 100 100     65 if(($path_parts[-1]||'') eq '...') {
99 3         6 $arg_type = 'CaptureArgs';
100 3         6 pop @path_parts;
101             }
102              
103 20   100     111 while(my ($spec) = (($path_parts[-1]||'') =~m/^{(.*)}$/)) {
104 17 100       70 if($spec) {
105 15         42 my ($name, $constraint) = split(':', $spec);
106 15 100       45 unshift @arg_proto, $constraint if $constraint;
107 15 100       33 if($name) {
108 11 100       29 if($name eq '*') {
109 2         5 $args = undef;
110             } else {
111 9         20 unshift @named_fields, $name;
112             }
113             } else {
114 4         8 unshift @named_fields, undef;
115             }
116             }
117 17 100       38 $args++ if defined $args;
118             } continue {
119 17         81 pop @path_parts;
120             }
121              
122             {
123 20         31 my $cnt = 0;
  20         38  
124 20         42 foreach my $name (@named_fields) {
125 13 100       58 if(defined($name)) {
126             $extra_proto{Field} = $extra_proto{Field} ?
127 9 100       43 "$extra_proto{Field},$name=>\$args[$cnt]" : "$name=>\$args[$cnt]"
128             }
129 13         42 $cnt++;
130             }
131             }
132              
133 20 100       33 if(
134 10         97 my ($key, $value) = map { $_ =~ /^(.*?)(?:\(\s*['"]?(.+?)['"]?\s*\))?$/ } grep { $_ =~m/^Via\(.+\)$/ }
  30         1892  
135 20 50       87 @{$self->meta->get_method($action_subname)->attributes||[]})
136             {
137 10 100       33 $chained = join '/', grep { defined $_ } map { $expansions{$_} ? $expansions{$_} : $_ } split('\/',$value);
  12         42  
  12         50  
138 10         30 $chained =~s[//][/]g;
139             }
140              
141 20         54 $path_part = join('/', @path_parts);
142 20         70 $path_part =~s/^\///;
143              
144 20 100       146 my %attributes = (
145             Chained => $chained,
146             PathPart => $path_part,
147             Does => [qw/NamedFields QueryParameter/],
148             $arg_type => (@arg_proto ? (join(',',@arg_proto)) : $args),
149             %extra_proto,
150             );
151              
152 20         181 return %attributes;
153             }
154              
155             1;
156              
157             =head1 NAME
158              
159             Catalyst::ControllerRole::At - A new approach to building Catalyst actions
160              
161             =head1 SYNOPSIS
162              
163             package MyApp::Controller::User;
164              
165             use Moose;
166             use MooseX::MethodAttributes;
167             use Types::Standard qw/Int Str/;
168              
169             extends 'Catalyst::Controller';
170             with 'Catalyst::ControllerRole::At';
171              
172             # Define your actions, for example:
173            
174             sub global :At(/global/{}/{}) { ... } # http://localhost/global/$arg/$arg
175              
176             sub list :At($action?{q:Str}) { ... } # http://localhost/user/list?q=$string
177              
178             sub find :At($controller/{id:Int}) { ... } # http://localhost/user/$integer
179              
180             # Define an action with an HTTP Method match at the same time
181              
182             sub update :Get($controller/{id:Int}) { ... } # GET http://localhost/user/$integer
183              
184             __PACKAGE__->meta->make_immutable;
185              
186             =head1 DESCRIPTION
187              
188             The way L<Catalyst> uses method attributes to annote a subroutine with meta
189             information used to map that action to an incoming request has sometimes been difficult
190             for newcomers to the framework. Partly this is due to how the system evolved and was
191             augmented, with more care towards backwards compatibility (for example with L<Maypole>, its
192             architectural anscestor) than with designing a forward system that is easy to grasp.
193             Additionally aspects of the system such as chained dispatch are very useful in the
194             hands of an expert but the interface leaves a lot to be desired. For example it is
195             possible to craft actions that mix chaining syntax with 'classic' syntax in ways that
196             are confusing. And having more than one way to do the same thing without clear and
197             obvious benefits is confusing to newcomers.
198              
199             Lastly, the core L<Catalyst::Controller> syntax has confusing defaults that are not readily guessed.
200             For example do you know the difference (if any) between Args and Args()? Or the difference
201             between Path, Path(''), and Path()? In many cases defaults are applied that were not
202             intended and things that you might think are the same turn out to have different effects. All
203             this conspires to worsen the learning curve.
204              
205             This role defines an alternative syntax that we hope is easier to understand and for the most
206             part eliminates defaults and guessed intentions. It only defines two method attributes, "At()"
207             and "Via()", which have no defaults and one of which is always required. It also smooths
208             over differences between 'classic' route matching using :Local and :Path and the newer
209             syntax based on Chaining by providing a single approach that bridges between the two
210             styles. One can mix and match the two without being required to learn a new syntax or to
211             rearchitect the system.
212              
213             The "At()" syntax more closely resembles the type of URL you are trying to match, which should
214             make code creation and maintainance easier by reducing the mental mismatch that happens with
215             the core syntax.
216              
217             Ultimately this ControllerRole is an attempt to layer some sugar on top of the existing
218             interface with the hope to establishing a normalized, easy approach that doesn't have the
219             learning curve or confusion of the existing system.
220              
221             I also recommend reading L<Catalyst::RouteMatching> for general notes and details on
222             how dispatching and matching works.
223              
224             =head1 URL Templating
225              
226             The following are examples and specification for how to map a URL to an action or to
227             a chain of actions in L<Catalyst>. All examples assume the application is running at
228             the root of your website domain (https://localhost/, not https://localhost/somepath)
229              
230             =head2 Matching a Literal Path
231              
232             The action 'global_path' will respond to 'https://localhost/foo/bar/baz'.
233              
234             package MyApp::Controller::Example;
235              
236             use Moose;
237             use MooseX::MethodAttributes;
238              
239             extends 'Catalyst::Controller';
240             with 'Catalyst::ControllerRole::At';
241              
242             sub global_path :At(/foo/bar/baz) { ... }
243              
244             __PACKAGE__->meta->make_immutable;
245              
246             The main two parts are consuming the role c< with 'Catalyst::ControllerRole::At'>
247             and using the C<At> method attribute. This attribute can only appear once in your
248             action and should be string that matches a specification as to be described in the
249             following examples.
250              
251             =head2 Arguments in a Path specification
252              
253             Often you wish to parameterize your URL template such that instead of matching a full
254             literal path, you may instead place slots for placeholders, which get passed to the
255             action during a request. For example:
256              
257             package MyApp::Controller::Example;
258              
259             use Moose;
260             use MooseX::MethodAttributes;
261              
262             extends 'Catalyst::Controller';
263             with 'Catalyst::ControllerRole::At';
264              
265             sub args :At(/example/{}) {
266             my ($self, $c, $arg) = @_;
267             }
268              
269             __PACKAGE__->meta->make_immutable;
270              
271             In the above controller we'd match a URL like 'https://localhost/example/100' and
272             'https://localhost/example/whatever'. The parameterized argument is passed as '$arg'
273             into the action when a request is matched.
274              
275             You may have as many argument placeholders as you wish, or you may specific an open
276             ended number of placeholders:
277              
278             sub arg2 :At(/example/{}/{}) { ... } # https://localhost/example/foo/bar
279             sub args :At(/example/{*} { ... } # https://localhost/example/1/2/3/4/...
280              
281             In this case action 'arg2' matches its path with 2 arguments, while 'args' will match
282             'any number of arguments', subject to operating system limitations.
283              
284             B<NOTE> Since the open ended argument specification can catch lots of URLs, this type
285             of argument specification is run as a special 'low priorty' match. For example (using
286             the above two actions) should the request be 'https://localhost/example/foo/bar', then
287             the first action 'arg2' would match since its a better match for that request given it
288             has a more constrained specification. In general I recommend using '{*}' sparingly.
289              
290             B<NOTE> Placeholder must come after path part literals or expansion variables as discussed
291             below. For example "At(/bar/{}/bar)" is not valid. This type of match is possible with
292             chained actions (see more examples below).
293              
294             =head2 Naming your Arguments
295              
296             You may name your argument placeholders. If you do so you can access your argument
297             placeholder values via the %_ hash. For example:
298              
299             sub args :At(/example/{id}) {
300             my ($self, $c, $id) = @_;
301             $c->response->body("The requested ID is $_{id}");
302             }
303              
304             Note that regardless of whether you name your arguments or not, they will get passed to
305             your actions at request via @_, as in core L<Catalyst>. So in the above example '$id'
306             is equal to '$_{id}'. You may use whichever makes the most sense for your task, or
307             standardize a project on one form or the other. You might also find naming the arguments
308             to be a useful form of documentation.
309              
310             =head2 Type constraints on your Arguments
311              
312             You may leverage the built in support for applying type constraints on your arguments:
313              
314             package MyApp::Controller::Example;
315              
316             use Moose;
317             use MooseX::MethodAttributes;
318             use Types::Standard qw/Int/;
319              
320             extends 'Catalyst::Controller';
321             with 'Catalyst::ControllerRole::At';
322              
323             sub args :At(/example/{id:Int}) {
324             my ($self, $c, $id) = @_;
325             }
326              
327             __PACKAGE__->meta->make_immutable;
328              
329             Would match 'http://localhost/example/100' but not 'http://localhost/example/string'
330              
331             All the same rules that apply to L<Catalyst> regarding use of type constraints apply. Most
332             importantly you must remember to inport your type constraints, as in the above example. You
333             should consider reviewing L<Catalyst::RouteMatching> for more general help.
334              
335             You may declare a type constraint on an argument but not name it, as in the following
336             example:
337              
338             sub args :At(/example/{:Int}) {
339             my ($self, $c, $id) = @_;
340             }
341              
342             Note the ':' prepended to the type constraint name is NOT optional.
343              
344             B<NOTE> Using type constraints in your route matching can have performance implications.
345              
346             B<NOTE> If you have more than one argument placeholder and you apply a type constraint to
347             one, you must apply constraints to all. You may use an open type constraint like C<Any>
348             as defined in L<Types::Standard> for placeholders where you don't care what the value is. For
349             example:
350              
351             use Types::Standard qw/Any Int/;
352              
353             sub args :At(/example/{:Any}/{:Int}) {
354             my ($self, $c, $id) = @_;
355             }
356              
357             =head2 Expansion Variables in your Path
358              
359             B<NOTE> Over the years since this role was first written I have found in general that
360             these expansions seem to add more confusion then they are worth. I find I really don't
361             need them. Your results may vary. I won't remove them for back compat reasons, but
362             I recommend using them sparingly. '$affix' appears to have some value but the name isn't
363             very good. I Added an alias '$path_end' which is slightly better I think. Recommendations
364             welcomed.
365              
366             Generally you would prefer not to hardcode the full path of your actions, as in the
367             examples given so far. General Catalyst best practice is to have your actions live
368             under the namespace of the controller in which they are defined. That makes things
369             more organized and easier to find as your application grows in complexity. In order
370             to make this and other common action template patterns easier, we support the following
371             variable expansions in your URL template specification:
372              
373             $controller: Your controller namespace (as an absolute path)
374             $path_prefix: Alias for $controller
375             $action: The action namespace (same as $controller/$name)
376             $up: The namespace of the controller containing this controller
377             $name The name of your action (the subroutine name)
378             $affix: The last part of the controller namespace.
379              
380             For example if your controller is 'MyApp::Controller::User::Details' then:
381              
382             $controller => /user/details
383             $up => /user
384             $affix => /details
385              
386             And if 'MyApp::Controller::User::Details' contained an action like:
387              
388             sub list :At() { ... }
389              
390             then:
391              
392             $name => /list
393             $action => /user/details/list
394              
395             You use these variable expansions the same way as literal paths:
396              
397             package MyApp::Controller::Example;
398              
399             use Moose;
400             use MooseX::MethodAttributes;
401             use Types::Standard qw/Int/;
402              
403             extends 'Catalyst::Controller';
404             with 'Catalyst::ControllerRole::At';
405              
406             sub args :At($controller/{id:Int}) {
407             my ($self, $c, $id) = @_;
408             }
409              
410             sub list :At($action) { ... }
411              
412             __PACKAGE__->meta->make_immutable;
413              
414             In this example the action 'args' would match 'https://localhost/example/100' (with '100' being
415             considered an argument) while action 'list' would match 'https::/localhost/example/list'.
416              
417             You can use expansion variables in your base controllers or controller roles to more
418             easily make shared actions.
419              
420             B<NOTE> Your controller namespace is typically based on its package name, unless you
421             have overridden it by setting an alternative in the configuation value 'namespace', or
422             your have in some way overridden the logic that produces a namespace. The default
423             behavior is to produce a namespace like the following:
424              
425             package MyApp::Controller::User => /user
426             package MyApp::Controller::User::name => /user/name
427              
428             Changing the way a controller defines its namespace will also change how actions that are
429             defined in that controller defines thier namespaces.
430              
431             B<NOTE> WHen using expansions, you should not place a '/' at the start of your
432             template URI.
433              
434             =head2 Matching GET parameters
435              
436             You can match GET (query) parameters in your URL template definitions:
437              
438             package MyApp::Controller::Example;
439              
440             use Moose;
441             use MooseX::MethodAttributes;
442             use Types::Standard qw/Int Str/;
443              
444             extends 'Catalyst::Controller';
445             with 'Catalyst::ControllerRole::At';
446              
447             sub query :At($action?{name:Str}{age:Int}) {
448             my ($self, $c, $id) = @_;
449             }
450              
451             __PACKAGE__->meta->make_immutable;
452              
453             This would match 'https://example/query?name=john;age=47'.
454              
455             Your query keys will appear in the %_ in the same way as all your named arguments.
456              
457             You do not need to use a type constraint on the query parameters. If you do not do so
458             all that is required is that the requested query parameters exist.
459              
460             This uses the ActionRole L<Catalyst::ActionRole::QueryParameter> under the hood, which
461             you may wish to review for more details.
462              
463             =head2 Chaining Actions inside a Controller
464              
465             L<Catalyst> action chaining allows you to spread the logic associated with a given URL
466             across a set of actions which all are responsible for handling a part of the URL
467             template. The idea is to allow you to better decompose your logic to promote clarity
468             and reuse. However the built-in syntax for declaring action chains is difficult for
469             many people to use. Here's how you do it with L<Catalyst::ControllerRole::At>
470              
471             Starting a Chain of actions is straightforward. you just add '/...' to the end of your
472             path specification. This is to indicate that the action expects more parts 'to follow'.
473             For example:
474              
475             package MyApp::Controller::Example;
476              
477             use Moose;
478             use MooseX::MethodAttributes;
479             use Types::Standard qw/Int Str/;
480              
481             extends 'Catalyst::Controller';
482             with 'Catalyst::ControllerRole::At';
483              
484             sub init :At($controller/...) { ... }
485            
486             __PACKAGE__->meta->make_immutable;
487              
488             The action 'init' starts a new chain of actions and declares the first part of the
489             definition, 'https://localhost/example/...'. You continue a chain in the same way,
490             but you need to specify the parent action that is being continued using the 'Via'
491             attribute. You terminate a chain when you define an action that doesn't declare '...'
492             as the last path. For example:
493              
494             sub init :At($controller/...) {
495             my ($self, $c) = @_;
496             }
497              
498             sub next :Via(init) At({}/...) {
499             my ($self, $c, $arg) = @_;
500             }
501              
502             sub last :Via(next) At({}) {
503             my ($self, $c, $arg) = @_;
504             }
505              
506             This defines an action chain with three 'stops' which matches a URL like (for example)
507             'https://localhost/$controller/arg1/arg2'. Each action will get executed for the matching
508             part, and will get arguments as defined in their match specification.
509              
510             B<NOTE> The 'Via' attribute must contain a value.
511            
512             When chaining you can use (or not) any mix of type constraints on your arguments, named
513             arguments, and query parameter matching. Here's a full example:
514              
515             package MyApp::Controller::Example;
516              
517             use Moose;
518             use MooseX::MethodAttributes;
519             use Types::Standard qw/Int/;
520              
521             extends 'Catalyst::Controller';
522             with 'Catalyst::ControllerRole::At';
523              
524             sub init :At($controller/...) { ... }
525              
526             sub next :Via(init) At({id:Int}/...) {
527             my ($self, $c, $int_id) = @_;
528             }
529              
530             sub last :Via(next) At({id:Int}?{q}) {
531             my ($self, $c, $int_id) = @_;
532             }
533              
534             __PACKAGE__->meta->make_immutable;
535              
536             =head2 Actions in a Chain with no match template
537              
538             Sometimes for the purposes of organizing code you will have an action that is a
539             midpoint in a chain that does not match any part of a URL template. For that
540             case you can omit the path and argument match specification. For example:
541              
542             package MyApp::Controller::Example;
543              
544             use Moose;
545             use MooseX::MethodAttributes;
546             use Types::Standard qw/Int/;
547              
548             extends 'Catalyst::Controller';
549             with 'Catalyst::ControllerRole::At';
550              
551             sub init :At($controller/...) { ... }
552              
553             sub middle :Via(init) At(...) {
554             my ($self, $c) = @_;
555             }
556              
557             sub last :Via(next) At({id:Int}) {
558             my ($self, $c, $id) = @_;
559             }
560              
561             __PACKAGE__->meta->make_immutable;
562              
563             This will match a URL like 'https://localhost/example/100'.
564              
565             B<NOTE> If you declare a Via but not At, this is an error. You must
566             always provide an At(), even in the case of a terminal action with no
567             match parts of it own. For example:
568              
569             package MyApp::Controller::Example;
570              
571             use Moose;
572             use MooseX::MethodAttributes;
573              
574             extends 'Catalyst::Controller';
575             with 'Catalyst::ControllerRole::At';
576              
577             sub first :At($controller/...) { ... }
578              
579             sub second :Via(first) At(...) {
580             my ($self, $c) = @_;
581             }
582              
583             sub third :Via(second) At(...) {
584             my ($self, $c) = @_;
585             }
586              
587             sub last :Via(third) At() {
588             my ($self, $c, $id) = @_;
589             }
590              
591             __PACKAGE__->meta->make_immutable;
592              
593             This creates a chained action that matches 'http://localhost/example' but calls
594             each of the three actions in the chain in order. Although it might seem odd to
595             create an action that is not connected to a path part of a URL request, you might find
596             cases where this results in well factored and reusable controllers.
597              
598             B<NOTE> For the purposes of executing code, we treat 'At' and 'At()' as the same. However
599             We highly recommend At() as a best practice since it more clearly represents the idea
600             of 'no match template'.
601              
602             =head2 Chaining Actions across Controllers
603              
604             The method attributes 'Via()' contains a pointer to the action being continued. In
605             standard practice this is almost always the name of an action in the same controller
606             as the one declaring it. This could be said to be a 'relative' (as in relative to
607             the current controller) action. However you don't have to use a relative name. You
608             can use any action's absolute private name, as long as it is an action that declares itself
609             to be a link in a chain.
610              
611             However in practice it is not alway a good idea to spread your chained acions across
612             across controllers in a manner that is not easy to follow. We recommend you try
613             to limit youself to chains that follow the controller hierarchy, which should be
614             easier for your code maintainers.
615              
616             For this common, best practice case when you are continuing your chained actions across
617             controllers, following a controller hierarchy, we provide some template expansions you can
618             use in the 'Via' attribute. These are useful to enforce this best practice as well as
619             promote reusability by decoupling hard coded private action namespaces from your controller.
620              
621             $up: The controller whose namespace contains the current controller
622             $name The name of the current actions subroutine
623             $parent: Expands to $up/$subname
624              
625             For example:
626              
627             package MyApp::Controller::ThingsTodo;
628              
629             use Moose;
630             use MooseX::MethodAttributes;
631              
632             extends 'Catalyst::Controller';
633             with 'Catalyst::ControllerRole::At';
634              
635             sub init :At($controller/...) {
636             my ($self, $c) = @_;
637             }
638              
639             sub list :Via(init) At($name) {
640             my ($self, $c) = @_;
641             }
642              
643             __PACKAGE__->meta->make_immutable;
644              
645             package MyApp::Controller::ThingsTodo::Item;
646              
647             use Moose;
648             use MooseX::MethodAttributes;
649              
650             extends 'Catalyst::Controller';
651             with 'Catalyst::ControllerRole::At';
652              
653             sub init :Via($parent) At({id:Int}/...) {
654             my ($self, $c) = @_;
655             }
656              
657             sub show :Via(init) At($name) { ... }
658             sub update :Via(init) At($name) { ... }
659             sub delete :Via(init) At($name) { ... }
660              
661             __PACKAGE__->meta->make_immutable;
662              
663             This creates four (4) URL templates:
664              
665             https://localhost/thingstodo/list
666             https://localhost/thingstodo/:id/show
667             https://localhost/thingstodo/:id/update
668             https://localhost/thingstodo/:id/delete
669              
670             With an action execution flow as follows:
671              
672             https://localhost/thingstodo/list =>
673             /thingstodo/init
674             /thingstodo/list
675              
676             https://localhost/thingstodo/:id/show
677             /thingstodo/init
678             /thingstodo/item/init
679             /thingstodo/item/show
680              
681             https://localhost/thingstodo/:id/update
682             /thingstodo/init
683             /thingstodo/item/init
684             /thingstodo/item/update
685              
686             https://localhost/thingstodo/:id/delete
687             /thingstodo/init
688             /thingstodo/item/init
689             /thingstodo/item/delete
690              
691             =head2 Method Shortcuts
692              
693             Its common today to want to be able to match a URL to a specific HTTP method. For example
694             you might want to match a GET request to one action and a POST request to another. L<Catalyst>
695             offers the C<Method> attribute as well as shortcuts: C<GET>, C<POST>, C<PUT>, C<DELETE>, C<HEAD>,
696             C<OPTIONS>. To tidy your method declarations you can use C<Get>, C<Post>, C<Put>, C<Delete>, C<Head>,
697             C<Options> in place of C<At>:
698              
699             package MyApp::Controller::Example;
700              
701             use Moose;
702             use MooseX::MethodAttributes;
703              
704             extends 'Catalyst::Controller';
705             with 'Catalyst::ControllerRole::At';
706              
707             sub get :Get($controller/...) { ... }
708             sub post :Post($controller/...) { ... }
709             sub put :Put($controller/...) { ... }
710             sub delete :Delete($controller/...) { ... }
711             sub head :Head($controller/...) { ... }
712             sub options :Options($controller/...) { ... }
713              
714             __PACKAGE__->meta->make_immutable;
715              
716             Basically:
717              
718             sub get :Get($controller/...) { ... }
719              
720             Is the same as:
721              
722             sub get :GET At($controller/...) { ... }
723              
724             You may find the few characters saved worth it or not. The choice is yours.
725              
726             =head1 COOKBOOK
727              
728             One thing I like to do is create a base controller for my project
729             so that I can make my controllers more concise:
730              
731             package Myapp::Controller;
732              
733             use Moose;
734             extends 'Catalyst::Controller';
735             with 'Catalyst::ControllerRole::At';
736            
737             __PACKAGE__->meta->make_immutable;
738              
739             You can of course doa lot more here if you want but I usually recommend
740             the lightest touch possible in your base controllers since the more you customize
741             the harder it might be for people new to the code to debug the system.
742              
743             =head1 TODO
744              
745             - HTTP Methods
746             - Incoming Content type matching
747             - ??Content Negotiation??
748              
749             =head1 AUTHOR
750            
751             John Napiorkowski L<email:jjnapiork@cpan.org>
752            
753             =head1 SEE ALSO
754            
755             L<Catalyst>, L<Catalyst::Controller>.
756            
757             =head1 COPYRIGHT & LICENSE
758            
759             Copyright 2016, John Napiorkowski L<email:jjnapiork@cpan.org>
760            
761             This library is free software; you can redistribute it and/or modify it under
762             the same terms as Perl itself.
763              
764             =cut