File Coverage

blib/lib/Catalyst/Plugin/CachedUriForAction.pm
Criterion Covered Total %
statement 87 89 97.7
branch 38 42 90.4
condition 16 19 84.2
subroutine 9 9 100.0
pod 1 2 50.0
total 151 161 93.7


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