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   4352778 use 5.008005; use strict; use warnings;
  4     4   88  
  4     4   22  
  4         8  
  4         82  
  4         19  
  4         12  
  4         267  
2              
3              
4             our $VERSION = '1.003';
5              
6             use mro;
7 4     4   30 use Carp ();
  4         9  
  4         33  
8 4     4   75 use URI::Encode::XS 'uri_encode_utf8';
  4         9  
  4         99  
9 4     4   1553  
  4         2156  
  4         3591  
10              
11             my $c = shift;
12             $c->maybe::next::method( @_ );
13              
14 3     3 0 1699123 my $dispatcher = $c->dispatcher;
15 3         33 my $cache = \%{ $dispatcher->{(CACHE_KEY)} };
16             for my $action ( values %{ $c->dispatcher->_action_hash } ) {
17 3         200 my $xa = $dispatcher->expand_action( $action );
18 3         55 my $n_caps = $xa->number_of_captures;
  3         14  
19 3         8  
  3         13  
20 174         19586 # not an action that a request can be dispatched to?
21 174         34562 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       19095 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         16054 $cache->{ '/' . $action->reverse } = [ $n_caps, $n_args, ( shift @part ), \@part ];
27 48   100     5851 }
28 48         54056 }
29 48         1367  
30             my $c = shift;
31              
32             my $dispatcher = $c->dispatcher;
33             my $cache = $dispatcher && $dispatcher->{(CACHE_KEY)}
34 57     57 1 34606 or return $c->next::method( @_ ); # fall back if called too early
35              
36 57         205 my $action = shift;
37 57 50 33     1583 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         109  
41 57 100 100     217 $action = '/' . $dispatcher->get_action_by_path( $action )->reverse
42 57 100 100     191 if ref $action
43 57 100 100     173 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     130  
  3         7  
  3         6  
  3         30  
48             my ( $n_caps, $n_args, $path, $extra_parts ) = @$info;
49 57 100       681  
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         147 # 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       104 Carp::croak "Wrong number of captures for path '$action' (need $n_caps) in uri_for_action"
58 29 100       348 if @$captures != $n_caps;
59             }
60              
61 26 100       275 # 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         74 # and now since @$extra_parts is exactly the same length as @$captures and @_ combined
70 51 100       97 # iterate over those arrays and use a cursor into @$extra_parts to interleave its elements
71 32 100       571 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         56 # and as we checked above @$captures alone has at least length $n_caps
  49         179  
76 26         54 # so we will need all of @$captures to cover @$extra_parts, and may then still have some of it left over
  21         58  
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         33 }
  0         0  
83              
84 19         50 $path =~ s/%2B/+/g;
  0         0  
85 19         38  
  17         59  
86             my $uri_obj = ref $c ? do {
87             my $base = $c->request->base;
88 45         115 ( my $uri = $$base ) =~ s!/?\z!$path!;
89             bless \$uri, ref $base;
90             } : do { # fallback if called as class method
91 35         912 bless \$path, 'URI::_generic';
92 35         1365 };
93 35         105  
94 45 100       96 if ( defined $params ) {
95 10         32 my $query = '';
96             my $delim = $URI::DEFAULT_QUERY_FORM_DELIMITER || '&';
97             my ( $v, $enc_key );
98 45 100       104 for my $key ( sort keys %$params ) {
99 20         36 $v = $params->{ $key };
100 20   50     68 if ( 'ARRAY' ne ref $v ) {
101 20         37 ( $query .= $delim ) .= uri_encode_utf8 $key;
102 20         71 ( $query .= '=' ) .= uri_encode_utf8 $v if defined $v;
103 21         36 } elsif ( @$v ) {
104 21 100       49 $enc_key = $delim . uri_encode_utf8 $key;
    50          
105 20         64 for ( @$v ) {
106 20 100       78 $query .= $enc_key;
107             ( $query .= '=' ) .= uri_encode_utf8 $_ if defined;
108 1         4 }
109 1         4 }
110 2         5 }
111 2 50       9 if ( '' ne $query ) {
112             $query =~ s/%20/+/g;
113             ( $$uri_obj .= '?' ) .= substr $query, length $delim;
114             }
115 20 50       55 }
116 20         72  
117 20         97 if ( defined $fragment ) {
118             ( $$uri_obj .= '#' ) .= uri_encode_utf8 $$fragment;
119             }
120              
121 45 100       91 $uri_obj;
122 2         9 }
123              
124             BEGIN { delete $Catalyst::Plugin::CachedUriForAction::{'uri_encode_utf8'} }
125 45         258  
126             1;
127              
128 4     4   113  
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