File Coverage

blib/lib/Catalyst/ActionRole/Methods.pm
Criterion Covered Total %
statement 9 9 100.0
branch 2 2 100.0
condition 1 3 33.3
subroutine 2 2 100.0
pod 1 1 100.0
total 15 17 88.2


line stmt bran cond sub pod time code
1             package Catalyst::ActionRole::Methods;
2              
3 1     1   2737901 use Moose::Role;
  1         3  
  1         11  
4              
5             our $VERSION = '0.104';
6              
7             around 'list_extra_info' => sub {
8             my $orig = shift;
9             my $self = shift;
10             my $info = $self->$orig( @_ );
11             $info->{'HTTP_METHOD'} = [ $self->get_allowed_methods( $self->class, undef, $self->name ) ];
12             $info;
13             };
14              
15             around 'dispatch', sub {
16             my $orig = shift;
17             my $self = shift;
18             my $c = shift;
19              
20             my $return = $self->$orig($c, @_);
21              
22             my $class = $self->class;
23             my $controller = $c->component( $class );
24             my $method_name = $self->name;
25             my $req_method = $c->request->method;
26             my $suffix = uc $req_method;
27             my ( $rest_method, $code );
28              
29             {
30             $rest_method = $method_name . '_' . $suffix;
31              
32             if ( $code = $controller->action_for( $rest_method ) ) {
33             my $sub_return = $c->forward( $code, $c->request->args );
34             return defined $sub_return ? $sub_return : $return;
35             } elsif ( $code = $controller->can( $rest_method ) ) {
36             # nothing to do
37             } elsif ( 'OPTIONS' eq $suffix ) {
38             $c->response->status( 204 );
39             } elsif ( 'HEAD' eq $suffix ) {
40             $suffix = 'GET';
41             redo;
42             } elsif ( 'not_implemented' eq $suffix ) {
43             ( my $enc_req_method = $req_method ) =~ s[(["'&<>])]{ '&#'.(ord $1).';' }ge;
44             $c->response->status( 405 );
45             $c->response->content_type( 'text/html' );
46             $c->response->body(
47             '<!DOCTYPE html><title>405 Method Not Allowed</title>'
48             . "<p>The requested method $enc_req_method is not allowed for this URL.</p>"
49             );
50             } else {
51             $suffix = 'not_implemented';
52             redo;
53             }
54             }
55              
56             if ( not $code ) {
57             my @allowed = $self->get_allowed_methods( $class, $c, $method_name );
58             $c->response->header( Allow => @allowed ? \@allowed : '' );
59             }
60              
61             # localise stuff so we can dispatch the action 'as normal, but get
62             # different stats shown, and different code run.
63             # Also get the full path for the action, and make it look like a forward
64             local $self->{'code'} = $code || sub {};
65             ( local $self->{'reverse'} = "-> $self->{'reverse'}" ) =~ s{[^/]+\z}{$rest_method};
66              
67             my $sub_return = $c->execute( $class, $self, @{ $c->request->args } );
68             defined $sub_return ? $sub_return : $return;
69             };
70              
71             sub get_allowed_methods {
72 2     2 1 24 my ( $self, $controller, $c, $name ) = @_;
73 2   33     13 my $class = ref $controller || $controller; # backcompat
74 2         53 my %methods = map /^\Q$name\E\_(.+)()$/, $class->meta->get_all_method_names;
75 2 100       5234 $methods{'HEAD'} = 1 if exists $methods{'GET'};
76 2         4 delete $methods{'not_implemented'};
77 2         14 sort keys %methods;
78             }
79              
80             1;
81              
82             __END__
83              
84             =pod
85              
86             =encoding UTF-8
87              
88             =head1 NAME
89              
90             Catalyst::ActionRole::Methods - Dispatch by HTTP Methods
91              
92             =head1 SYNOPSIS
93              
94             sub foo : Local Does('Methods') {
95             my ($self, $c, $arg) = @_;
96             # called first, regardless of HTTP request method
97             }
98              
99             sub foo_GET : Action {
100             my ($self, $c, $arg) = @_;
101             # called next, but only for GET requests
102             # this is passed the same @_ as its generic action
103             }
104              
105             sub foo_POST { # does not need to be an action
106             my ($self, $c, $arg) = @_;
107             # likewise for POST requests
108             }
109              
110             sub foo_not_implemented { # fallback
111             my ($self, $c, $arg) = @_;
112             # only needed if you want to override the default 405 response
113             }
114              
115             =head1 DESCRIPTION
116              
117             This is a L<Catalyst> extension which adds additional dispatch based on the
118             HTTP method, in the same way L<Catalyst::Action::REST> does:
119              
120             An action which does this role will be matched and run as usual. But after it
121             returns, a sub-action will also run, which will be identified by taking the
122             name of the main action and appending an underscore and the HTTP request method
123             name. This sub-action is passed the same captures and args as the main action.
124              
125             You can also write the sub-action as a plain method without declaring it as an
126             action. Probably the only advantage of declaring it as an action is that other
127             action roles can then be applied to it.
128              
129             There are several fallbacks if a sub-action for the current request method does
130             not exist:
131              
132             =over 3
133              
134             =item 1.
135              
136             C<HEAD> requests will try to use the sub-action for C<GET>.
137              
138             =item 2.
139              
140             C<OPTIONS> requests will set up a 204 (No Content) response.
141              
142             =item 3.
143              
144             The C<not_implemented> sub-action is tried as a last resort.
145              
146             =item 4.
147              
148             Finally, a 405 (Method Not Found) response is set up.
149              
150             =back
151              
152             Both fallback responses include an C<Allow> header which will be populated from
153             the available sub-actions.
154              
155             Note that this action role only I<adds> dispatch. It does not affect matching!
156             The main action will always run if it otherwise matches the request, even if no
157             suitable sub-action exists and a 405 is generated. Nor does it affect chaining.
158             All subsequent actions in a chain will still run, along with their sub-actions.
159              
160             =head1 INTERACTION WITH CHAINED DISPATCH
161              
162             The fact that this is an action role which is attached to individual actions
163             has some odd and unintuitive consequences when combining it with Chained
164             dispatch, particularly when it is used in multiple actions in the same chain.
165             This example will not work well at all:
166              
167             sub foo : Chained(/) CaptureArgs(1) Does('Methods') { ... }
168             sub foo_GET { ... }
169              
170             sub bar : Chained(foo) Args(0) { ... }
171             sub bar_POST { ... }
172              
173             Because each action does its own isolated C<Methods> sub-dispatch, a C<GET>
174             request to this chain will run C<foo>, then C<foo_GET>, then C<bar>, then
175             set up a 405 response due to the absence of C<bar_GET>. And because C<bar> only
176             has a sub-action for C<POST>, that is all the C<Allow> header will contain.
177              
178             Worse (maybe), a C<POST> will run C<foo>, then set up a 405 response with an
179             C<Allow> list of just C<GET>, but then still run C<bar> and C<bar_POST>.
180              
181             This means it is never useful for an action which is further along a chain to
182             have I<more> sub-actions than any earlier action.
183              
184             Having I<fewer> sub-actions can be useful: if the earlier part of the chain is
185             shared with other chains then each chain can handle a different set of request
186             methods:
187              
188             sub foo : Chained(/) CaptureArgs(1) Does('Methods') { ... }
189             sub foo_GET { ... }
190             sub foo_POST { ... }
191              
192             sub bar : Chained(foo) Args(0) { ... }
193             sub bar_GET { ... }
194              
195             sub quux : Chained(foo) Args(0) { ... }
196             sub quux_POST { ... }
197              
198             In this example, the C</foo/bar> chain will handle only C<GET> while the
199             C</foo/quux> chain will handle only C<POST>. If you later wanted to make
200             C</foo/quux> also handle C<GET> then you would only need to add C<quux_GET>
201             because there is already a C<foo_GET>. But to make C</foo/bar> handle C<PUT>,
202             you would need to add both C<foo_PUT> I<and> C<bar_PUT>.
203              
204             =head1 VERSUS Catalyst::Action::REST
205              
206             L<Catalyst::Action::REST> works fine doesn't it? Why offer a new approach? There's
207             a few reasons:
208              
209             First, when L<Catalyst::Action::REST> was written we did not have
210             L<Moose> and the only way to augment functionality was via inheritance. Now that
211             L<Moose> is common we instead say that it is typically better to use a L<Moose::Role>
212             to augment a class function rather to use a subclass. The role approach is a smaller
213             hammer and it plays nicer when you need to combine several roles to augment a class
214             (as compared to multiple inheritance approaches.). This is why we brought support for
215             action roles into core L<Catalyst::Controller> several years ago. Letting you have
216             this functionality via a role should lead to more flexible systems that play nice
217             with other roles. One nice side effect of this 'play nice with others' is that we
218             were able to hook into the 'list_extra_info' method of the core action class so that
219             you can now see in your developer mode debug output the matched http methods, for
220             example:
221              
222             .-------------------------------------+----------------------------------------.
223             | Path Spec | Private |
224             +-------------------------------------+----------------------------------------+
225             | /myaction/*/next_action_in_chain | GET, HEAD, POST /myaction (1) |
226             | | => /next_action_in_chain (0) |
227             '-------------------------------------+----------------------------------------'
228              
229             This is not to say its never correct to use an action class, but now you have the
230             choice.
231              
232             Second, L<Catalyst::Action::REST> has the behavior as noted of altering the core
233             L<Catalyst::Request> class. This might not be desired and has always struck the
234             author as a bit too much side effect / action at a distance.
235              
236             Last, L<Catalyst::Action::REST> is actually a larger distribution with a bunch of
237             other features and dependencies that you might not want. The intention is to offer
238             those bits of functionality as standalone, modern components and allow one to assemble
239             the parts needed, as needed.
240              
241             This action role is for the most part a 1-1 port of the action class, with one minor
242             change to reduce the dependency count. Additionally, it does not automatically
243             apply the L<Catalyst::Request::REST> action class to your global L<Catalyst>
244             action class. This feature is left off because its easy to set this yourself if
245             desired via the global L<Catalyst> configuration and we want to follow and promote
246             the idea of 'do one thing well and nothing surprising'.
247              
248             B<NOTE> There is an additional minor change in how we handle return values from actions. In
249             general L<Catalyst> does nothing with an action return value (unless in an auto action).
250             However this might not always be the future case, and you might have used that return value
251             for something in your custom code. In L<Catalyst::Action::REST> the return value was
252             always the return of the dispatched sub action (if any). We tweaked this so that we use
253             the sub action return value, BUT if that value is undefined, we use the parent action
254             return value instead.
255              
256             We also dropped saying 'REST' when all we are doing is dispatching on HTTP method.
257             Since the time that the first version of L<Catalysts::Action::REST> was released to
258             CPAN our notion of what 'REST' means has greatly evolved so I think its correct to
259             change the name to be functionality specific and to not confuse people that are new
260             to the REST discipline.
261              
262             This action role is intended to be used in all the places
263             you used to use the action class and have the same results, with the exception
264             of the already mentioned 'not messing with the global request class'. However
265             L<Catalyst::Action::REST> has been around for a long time and is well vetted in
266             production so I would caution care with changing your mission critical systems
267             very quickly.
268              
269             =head1 VERSUS NATIVE METHOD ATTRIBUTES
270              
271             L<Catalyst> since version 5.90030 has offered a core approach to dispatch on the
272             http method (via L<Catalyst::ActionRole::HTTPMethods>). Why still use this action role
273             versus the core functionality? ALthough it partly comes down to preference and the
274             author's desire to give current users of L<Catalyst::Action::REST> a path forward, there
275             is some functionality differences beetween the two which may recommend one over the
276             other. For example the core method matching does not offer an automatic default
277             'Not Implemented' response that correctly sets the OPTIONS header. Also the dispatch
278             flow between the two approaches is different and when using chained actions one
279             might be a better choice over the other depending on how your chains are arranged and
280             your desired flow of action.
281              
282             =head1 METHODS
283            
284             This role contains the following methods.
285              
286             =head2 get_allowed_methods
287              
288             Returns a list of the allowed methods.
289              
290             =head2 dispatch
291            
292             This method overrides the default dispatch mechanism to the re-dispatching
293             mechanism described above.
294              
295             =head1 CONTRIBUTORS
296              
297             This module is based on code, tests and documentation extracted out of
298             L<Catalyst::Action::REST>, which was originally developed by Adam Jacob
299             with lots of help from mst and jrockway, while being paid by Marchex, Inc
300             (http://www.marchex.com).
301              
302             The following people also contributed to parts copied from that package:
303            
304             Tomas Doran (t0m) E<lt>bobtfish@bobtfish.netE<gt>
305            
306             Dave Rolsky E<lt>autarch@urth.orgE<gt>
307            
308             Arthur Axel "fREW" Schmidt E<lt>frioux@gmail.comE<gt>
309            
310             J. Shirley E<lt>jshirley@gmail.comE<gt>
311            
312             Wallace Reis E<lt>wreis@cpan.orgE<gt>
313            
314             =head1 AUTHOR
315              
316             Aristotle Pagaltzis <pagaltzis@gmx.de>
317              
318             John Napiorkowski <jjnapiork@cpan.org>
319              
320             =head1 COPYRIGHT AND LICENSE
321              
322             This software is copyright (c) 2024 by Aristotle Pagaltzis.
323              
324             This is free software; you can redistribute it and/or modify it under
325             the same terms as the Perl 5 programming language system itself.
326              
327             =cut