File Coverage

blib/lib/Catalyst/Plugin/CachedUriForAction.pm
Criterion Covered Total %
statement 84 86 97.6
branch 36 40 90.0
condition 16 19 84.2
subroutine 9 9 100.0
pod 1 2 50.0
total 146 156 93.5


line stmt bran cond sub pod time code
1 4     4   4406213 use 5.008005; use strict; use warnings;
  4     4   34  
  4     4   22  
  4         9  
  4         72  
  4         17  
  4         8  
  4         263  
2              
3              
4             our $VERSION = '1.002';
5              
6             use mro;
7 4     4   29 use Carp ();
  4         7  
  4         29  
8 4     4   93 use URI::Encode::XS 'uri_encode_utf8';
  4         16  
  4         83  
9 4     4   1433  
  4         1778  
  4         3588  
10              
11             my $c = shift;
12             $c->maybe::next::method( @_ );
13              
14 3     3 0 1644227 my $dispatcher = $c->dispatcher;
15 3         28 my $cache = \%{ $dispatcher->{(CACHE_KEY)} };
16             for my $action ( values %{ $c->dispatcher->_action_hash } ) {
17 3         197 my $xa = $dispatcher->expand_action( $action );
18 3         53 my $n_caps = $xa->number_of_captures;
  3         13  
19 3         9  
  3         11  
20 174         19313 # not an action that a request can be dispatched to?
21 174         33377 next if not defined $dispatcher->uri_for_action( $action, [ ('dummy') x $n_caps ] );
22              
23             my $n_args = $xa->number_of_args; # might be undef to mean "any number"
24 174 100       18647 my $tmpl = $c->uri_for( $action, [ ("\0\0\0\0") x $n_caps ], ("\0\0\0\0") x ( $n_args || 0 ) );
25             my @part = split /%00%00%00%00/, $tmpl, -1;
26 48         15245 $cache->{ '/' . $action->reverse } = [ $n_caps, $n_args, ( shift @part ), \@part ];
27 48   100     5494 }
28 48         51526 }
29 48         1328  
30             my $c = shift;
31              
32             my $dispatcher = $c->dispatcher;
33             my $cache = $dispatcher && $dispatcher->{(CACHE_KEY)}
34 57     57 1 32160 or return $c->next::method( @_ ); # fall back if called too early
35              
36 57         174 my $action = shift;
37 57 50 33     1495 my $captures = @_ && 'ARRAY' eq ref $_[0] ? shift : [];
38             my $fragment = @_ && 'SCALAR' eq ref $_[-1] ? pop : undef;
39             my $params = @_ && 'HASH' eq ref $_[-1] ? pop : undef;
40 57         100  
41 57 100 100     217 $action = '/' . $dispatcher->get_action_by_path( $action )->reverse
42 57 100 100     172 if ref $action
43 57 100 100     158 and do { local $@; eval { $action->isa( 'Catalyst::Action' ) } };
44              
45             my $info = $cache->{ $action }
46             or Carp::croak "Can't find action for path '$action' in uri_for_action";
47 57 100 100     119  
  3         5  
  3         6  
  3         27  
48             my ( $n_caps, $n_args, $path, $extra_parts ) = @$info;
49 57 100       496  
50             # this is not very sensical but it has to be like this because it is what Catalyst does:
51             # the :Args() case (i.e. any number of args) is grouped with the :Args(0) case (i.e. no args)
52 55         143 # instead of being grouped with with the :Args(N) case (i.e. a fixed non-zero number of args)
53             if ( $n_args ) {
54             Carp::croak "Not enough captures for path '$action' (need $n_caps) in uri_for_action"
55             if @$captures < $n_caps;
56             } else {
57 55 100       89 Carp::croak "Wrong number of captures for path '$action' (need $n_caps) in uri_for_action"
58 29 100       207 if @$captures != $n_caps;
59             }
60              
61 26 100       238 # the following is carefully written to
62             # - loop over every input array exactly once
63             # - avoid any conditionals inside each loop body
64             # - use only simple loop forms that are specially optimised by the perl interpreter
65             my $i = -1;
66             if ( defined $n_args ) { # the non-slurpy case
67             Carp::croak "Wrong number of args+captures for path '$action' (need ".@$extra_parts.") in uri_for_action"
68             if ( @$captures + @_ ) != @$extra_parts;
69 51         79 # and now since @$extra_parts is exactly the same length as @$captures and @_ combined
70 51 100       81 # iterate over those arrays and use a cursor into @$extra_parts to interleave its elements
71 32 100       470 for ( @$captures ) { ( $path .= uri_encode_utf8 $_ ) .= $extra_parts->[ ++$i ] }
72             for ( @_ ) { ( $path .= uri_encode_utf8 $_ ) .= $extra_parts->[ ++$i ] }
73             } else {
74             # in the slurpy case, the size of @$extra_parts is determined by $n_caps alone since $n_args was undef
75 26         53 # and as we checked above @$captures alone has at least length $n_caps
  49         154  
76 26         48 # so we will need all of @$captures to cover @$extra_parts, and may then still have some of it left over
  21         51  
77             # so iterate over @$extra_parts and use a cursor into @$captures to interleave its elements
78             for ( @$extra_parts ) { ( $path .= uri_encode_utf8 $captures->[ ++$i ] ) .= $_ }
79             # and then append the rest of @$captures, and then everything from @_ after that
80             for ( ++$i .. $#$captures ) { ( $path .= '/' ) .= uri_encode_utf8 $captures->[ $_ ] }
81             for ( @_ ) { ( $path .= '/' ) .= uri_encode_utf8 $_ }
82 19         36 }
  0         0  
83              
84 19         48 $path =~ s/%2B/+/g;
  0         0  
85 19         34  
  17         64  
86             my $uri_obj = ref $c ? do {
87             my $base = $c->request->base;
88 45         100 ( my $uri = $$base ) =~ s!/?\z!$path!;
89             bless \$uri, ref $base;
90             } : do { # fallback if called as class method
91 35         858 bless \$path, 'URI::_generic';
92 35         1221 };
93 35         103  
94 45 100       85 if ( defined $params ) {
95 10         26 my $query = '';
96             my $delim = $URI::DEFAULT_QUERY_FORM_DELIMITER || '&';
97             my ( $v, $enc_key );
98 45 100       94 for my $key ( sort keys %$params ) {
99 20         35 $v = $params->{ $key };
100 20   50     62 if ( 'ARRAY' ne ref $v ) {
101 20         32 ( $query .= $delim ) .= uri_encode_utf8 $key;
102 20         66 ( $query .= '=' ) .= uri_encode_utf8 $v if defined $v;
103 21         41 } elsif ( @$v ) {
104 21 100       45 $enc_key = $delim . uri_encode_utf8 $key;
    50          
105 20         58 for ( @$v ) {
106 20 100       71 $query .= $enc_key;
107             ( $query .= '=' ) .= uri_encode_utf8 $_ if defined;
108 1         5 }
109 1         4 }
110 2         4 }
111 2 50       8 if ( '' ne $query ) {
112             $query =~ s/%20/+/g;
113             ( $$uri_obj .= '?' ) .= substr $query, length $delim;
114             }
115 20 50       52 }
116 20         35  
117 20         92 if ( defined $fragment ) {
118             ( $$uri_obj .= '#' ) .= uri_encode_utf8 $$fragment;
119             }
120              
121 45 100       86 $uri_obj;
122 2         8 }
123              
124             BEGIN { delete $Catalyst::Plugin::CachedUriForAction::{'uri_encode_utf8'} }
125 45         199  
126             1;
127              
128 4     4   122  
129             =pod
130              
131             =encoding UTF-8
132              
133             =head1 NAME
134              
135             Catalyst::Plugin::CachedUriForAction - drop-in supercharger for uri_for_action
136              
137             =head1 SYNOPSIS
138              
139             use Catalyst qw( CachedUriForAction );
140              
141             =head1 DESCRIPTION
142              
143             This provides a (mostly) drop-in replacement version of C<uri_for_action>.
144              
145             The stock Catalyst C<uri_for_action> method is a thin wrapper around C<uri_for>.
146             Every time you pass C<uri_for> an action to create a parametrized URL for it, it introspects the dispatcher.
147             This is expensive, and on views that generate a lot of URLs, it can add up to a substantial cost.
148             Doing this introspection repeatedly can only possibly be useful if the set of controllers and actions in the application can change at runtime.
149             Even then it is still wasted time on any view that generates many URLs for the same action.
150              
151             This plugin scans the dispatch table once during startup and pregenerates templates for all possible output URLs.
152             The only work then left in C<uri_for_action> is the string manipulation to assemble a URL from its template.
153              
154             =head1 LIMITATIONS
155              
156             The following things are unsupported in this plugin:
157              
158             =over 3
159              
160             =item * Controller and action addition/removal at runtime
161              
162             This is by design and not likely to ever change.
163              
164             B<If you need this then you will not be able to use this plugin.>
165              
166             =item * Incorrect C<uri_for_action> inputs
167              
168             The stock method returns undef when given an unknown action path or the wrong number of captures or args.
169             This has never been useful to me but has been a cause of some annoying debugging sessions.
170             This plugin puts an end to that by throwing an exception instead.
171              
172             If you run into this, you can use C<eval> or fall back to C<uri_for> for those calls.
173              
174             =item * Setting the URL fragment as part of the args
175              
176             This plugin does not handle args in the sloppy/DWIM fashion C<uri_for> tries to offer.
177             Setting a URL fragment is supported, but only by passing it as a trailing scalar ref.
178             Plain parameters are always treated as args and therefore encoded.
179              
180             If you run into this, you can fall back to C<uri_for> for those calls.
181              
182             =item * Arg constraints (such as C<:CaptureArgs(Int,Str)>)
183              
184             Note that this plugin does not affect request dispatch so constraints will still apply there.
185             They will merely not be validated when generating URLs.
186              
187             This may be possible to support but demand would have to justify an attempt at it.
188              
189             =item * C<"\0\0\0\0"> in the PathPart of any action
190              
191             This string is internally used as a marker for placeholder values.
192             The dispatch table scanner will generate bogus templates for such actions.
193             This is mentioned here just for completeness as it seems unlikely to bite anyone in practice.
194              
195             If you do run into this, you can fall back to C<uri_for> for those actions.
196              
197             =back
198              
199             =head1 AUTHOR
200              
201             Aristotle Pagaltzis <pagaltzis@gmx.de>
202              
203             =head1 COPYRIGHT AND LICENSE
204              
205             This software is copyright (c) 2021 by Aristotle Pagaltzis.
206              
207             This is free software; you can redistribute it and/or modify it under
208             the same terms as the Perl 5 programming language system itself.
209              
210             =cut