File Coverage

blib/lib/Catalyst/Plugin/CachedUriForAction.pm
Criterion Covered Total %
statement 24 24 100.0
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 33 33 100.0


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