add files
|
1 |
package Mojolicious::Routes; |
2 |
use Mojo::Base 'Mojolicious::Routes::Route'; |
|
3 | ||
4 |
use List::Util 'first'; |
|
5 |
use Mojo::Cache; |
|
6 |
use Mojo::Loader; |
|
7 |
use Mojo::Util 'camelize'; |
|
8 |
use Mojolicious::Routes::Match; |
|
9 |
use Scalar::Util 'weaken'; |
|
10 | ||
11 |
has base_classes => sub { [qw(Mojolicious::Controller Mojo)] }; |
|
12 |
has cache => sub { Mojo::Cache->new }; |
|
13 |
has [qw(conditions shortcuts)] => sub { {} }; |
|
14 |
has hidden => sub { [qw(attr has new tap)] }; |
|
15 |
has namespaces => sub { [] }; |
|
16 | ||
17 |
sub add_condition { shift->_add(conditions => @_) } |
|
18 |
sub add_shortcut { shift->_add(shortcuts => @_) } |
|
19 | ||
20 |
sub auto_render { |
|
21 |
my ($self, $c) = @_; |
|
22 |
my $stash = $c->stash; |
|
23 |
return if $stash->{'mojo.rendered'}; |
|
24 |
$c->render_maybe or $stash->{'mojo.routed'} or $c->render_not_found; |
|
25 |
} |
|
26 | ||
27 |
sub continue { |
|
28 |
my ($self, $c) = @_; |
|
29 | ||
30 |
my $match = $c->match; |
|
31 |
my $stack = $match->stack; |
|
32 |
my $current = $match->current; |
|
33 |
return $self->auto_render($c) unless my $field = $stack->[$current]; |
|
34 | ||
35 |
# Merge captures into stash |
|
36 |
my @keys = keys %$field; |
|
37 |
my $stash = $c->stash; |
|
38 |
@{$stash}{@keys} = @{$stash->{'mojo.captures'}}{@keys} = values %$field; |
|
39 | ||
40 |
my $continue; |
|
41 |
my $last = !$stack->[++$current]; |
|
42 |
if (my $cb = $field->{cb}) { $continue = $self->_callback($c, $cb, $last) } |
|
43 |
else { $continue = $self->_controller($c, $field, $last) } |
|
44 |
$match->current($current); |
|
45 |
$self->continue($c) if $last || $continue; |
|
46 |
} |
|
47 | ||
48 |
sub dispatch { |
|
49 |
my ($self, $c) = @_; |
|
50 | ||
51 |
# Path (partial path gets priority) |
|
52 |
my $req = $c->req; |
|
53 |
my $path = $c->stash->{path}; |
|
54 |
if (defined $path) { $path = "/$path" if $path !~ m!^/! } |
|
55 |
else { $path = $req->url->path->to_route } |
|
56 | ||
57 |
# Method (HEAD will be treated as GET) |
|
58 |
my $method = uc $req->method; |
|
59 |
$method = 'GET' if $method eq 'HEAD'; |
|
60 | ||
61 |
# Check cache |
|
62 |
my $cache = $self->cache; |
|
63 |
my $ws = $c->tx->is_websocket ? 1 : 0; |
|
64 |
my $match = Mojolicious::Routes::Match->new(root => $self); |
|
65 |
$c->match($match); |
|
66 |
if ($cache && (my $cached = $cache->get("$method:$path:$ws"))) { |
|
67 |
$match->endpoint($cached->{endpoint})->stack($cached->{stack}); |
|
68 |
} |
|
69 | ||
70 |
# Check routes |
|
71 |
else { |
|
72 |
my $options = {method => $method, path => $path, websocket => $ws}; |
|
73 |
$match->match($c => $options); |
|
74 | ||
75 |
# Cache routes without conditions |
|
76 |
if ($cache && (my $endpoint = $match->endpoint)) { |
|
77 |
my $result = {endpoint => $endpoint, stack => $match->stack}; |
|
78 |
$cache->set("$method:$path:$ws" => $result) |
|
79 |
unless $endpoint->has_conditions; |
|
80 |
} |
|
81 |
} |
|
82 | ||
83 |
return undef unless @{$c->match->stack}; |
|
84 |
$self->continue($c); |
|
85 |
return 1; |
|
86 |
} |
|
87 | ||
88 |
sub hide { push @{shift->hidden}, @_ } |
|
89 | ||
90 |
sub is_hidden { |
|
91 |
my ($self, $method) = @_; |
|
92 |
my $h = $self->{hiding} ||= {map { $_ => 1 } @{$self->hidden}}; |
|
93 |
return !!($h->{$method} || index($method, '_') == 0 || $method !~ /[a-z]/); |
|
94 |
} |
|
95 | ||
96 |
sub lookup { |
|
97 |
my ($self, $name) = @_; |
|
98 |
my $reverse = $self->{reverse} ||= {}; |
|
99 |
return $reverse->{$name} if exists $reverse->{$name}; |
|
100 |
return undef unless my $route = $self->find($name); |
|
101 |
return $reverse->{$name} = $route; |
|
102 |
} |
|
103 | ||
104 |
sub route { |
|
105 |
shift->add_child(Mojolicious::Routes::Route->new(@_))->children->[-1]; |
|
106 |
} |
|
107 | ||
108 |
sub _action { shift->plugins->emit_chain(around_action => @_) } |
|
109 | ||
110 |
sub _add { |
|
111 |
my ($self, $attr, $name, $cb) = @_; |
|
112 |
$self->$attr->{$name} = $cb; |
|
113 |
return $self; |
|
114 |
} |
|
115 | ||
116 |
sub _callback { |
|
117 |
my ($self, $c, $cb, $last) = @_; |
|
118 |
$c->stash->{'mojo.routed'}++ if $last; |
|
119 |
my $app = $c->app; |
|
120 |
$app->log->debug('Routing to a callback.'); |
|
121 |
return _action($app, $c, $cb, $last); |
|
122 |
} |
|
123 | ||
124 |
sub _class { |
|
125 |
my ($self, $c, $field) = @_; |
|
126 | ||
127 |
# Application instance |
|
128 |
return $field->{app} if ref $field->{app}; |
|
129 | ||
130 |
# Application class |
|
131 |
my @classes; |
|
132 |
my $class = $field->{controller} ? camelize($field->{controller}) : ''; |
|
133 |
if ($field->{app}) { push @classes, $field->{app} } |
|
134 | ||
135 |
# Specific namespace |
|
136 |
elsif (defined(my $namespace = $field->{namespace})) { |
|
137 |
if ($class) { push @classes, $namespace ? "${namespace}::$class" : $class } |
|
138 |
elsif ($namespace) { push @classes, $namespace } |
|
139 |
} |
|
140 | ||
141 |
# All namespaces |
|
142 |
elsif ($class) { push @classes, "${_}::$class" for @{$self->namespaces} } |
|
143 | ||
144 |
# Try to load all classes |
|
145 |
my $log = $c->app->log; |
|
146 |
for my $class (@classes) { |
|
147 | ||
148 |
# Failed |
|
149 |
unless (my $found = $self->_load($class)) { |
|
150 |
next unless defined $found; |
|
151 |
$log->debug(qq{Class "$class" is not a controller.}); |
|
152 |
return undef; |
|
153 |
} |
|
154 | ||
155 |
# Success |
|
156 |
my $new = $class->new(%$c); |
|
157 |
weaken $new->{$_} for qw(app tx); |
|
158 |
return $new; |
|
159 |
} |
|
160 | ||
161 |
# Nothing found |
|
162 |
$log->debug(qq{Controller "$classes[-1]" does not exist.}) if @classes; |
|
163 |
return @classes ? undef : 0; |
|
164 |
} |
|
165 | ||
166 |
sub _controller { |
|
167 |
my ($self, $old, $field, $last) = @_; |
|
168 | ||
169 |
# Load and instantiate controller/application |
|
170 |
my $new; |
|
171 |
unless ($new = $self->_class($old, $field)) { return !!defined $new } |
|
172 | ||
173 |
# Application |
|
174 |
my $class = ref $new; |
|
175 |
my $app = $old->app; |
|
176 |
my $log = $app->log; |
|
177 |
if (my $sub = $new->can('handler')) { |
|
178 |
$log->debug(qq{Routing to application "$class".}); |
|
179 | ||
180 |
# Try to connect routes |
|
181 |
if (my $sub = $new->can('routes')) { |
|
182 |
my $r = $new->$sub; |
|
183 |
weaken $r->parent($old->match->endpoint)->{parent} unless $r->parent; |
|
184 |
} |
|
185 |
$new->$sub($old); |
|
186 |
$old->stash->{'mojo.routed'}++; |
|
187 |
} |
|
188 | ||
189 |
# Action |
|
190 |
elsif (my $method = $field->{action}) { |
|
191 |
if (!$self->is_hidden($method)) { |
|
192 |
$log->debug(qq{Routing to controller "$class" and action "$method".}); |
|
193 | ||
194 |
if (my $sub = $new->can($method)) { |
|
195 |
$old->stash->{'mojo.routed'}++ if $last; |
|
196 |
return 1 if _action($app, $new, $sub, $last); |
|
197 |
} |
|
198 | ||
199 |
else { $log->debug('Action not found in controller.') } |
|
200 |
} |
|
201 |
else { $log->debug(qq{Action "$method" is not allowed.}) } |
|
202 |
} |
|
203 | ||
204 |
return undef; |
|
205 |
} |
|
206 | ||
207 |
sub _load { |
|
208 |
my ($self, $app) = @_; |
|
209 | ||
210 |
# Load unless already loaded |
|
211 |
return 1 if $self->{loaded}{$app}; |
|
212 |
if (my $e = Mojo::Loader->new->load($app)) { ref $e ? die $e : return undef } |
|
213 | ||
214 |
# Check base classes |
|
215 |
return 0 unless first { $app->isa($_) } @{$self->base_classes}; |
|
216 |
return ++$self->{loaded}{$app}; |
|
217 |
} |
|
218 | ||
219 |
1; |
|
220 | ||
221 |
=encoding utf8 |
|
222 | ||
223 |
=head1 NAME |
|
224 | ||
225 |
Mojolicious::Routes - Always find your destination with routes! |
|
226 | ||
227 |
=head1 SYNOPSIS |
|
228 | ||
229 |
use Mojolicious::Routes; |
|
230 | ||
231 |
# Simple route |
|
232 |
my $r = Mojolicious::Routes->new; |
|
233 |
$r->route('/')->to(controller => 'blog', action => 'welcome'); |
|
234 | ||
235 |
# More advanced routes |
|
236 |
my $blog = $r->under('/blog'); |
|
237 |
$blog->get('/list')->to('blog#list'); |
|
238 |
$blog->get('/:id' => [id => qr/\d+/])->to('blog#show', id => 23); |
|
239 |
$blog->patch(sub { shift->render(text => 'Go away!', status => 405) }); |
|
240 | ||
241 |
=head1 DESCRIPTION |
|
242 | ||
243 |
L<Mojolicious::Routes> is the core of the L<Mojolicious> web framework. |
|
244 | ||
245 |
See L<Mojolicious::Guides::Routing> for more. |
|
246 | ||
247 |
=head1 ATTRIBUTES |
|
248 | ||
249 |
L<Mojolicious::Routes> inherits all attributes from |
|
250 |
L<Mojolicious::Routes::Route> and implements the following new ones. |
|
251 | ||
252 |
=head2 base_classes |
|
253 | ||
254 |
my $classes = $r->base_classes; |
|
255 |
$r = $r->base_classes(['MyApp::Controller']); |
|
256 | ||
257 |
Base classes used to identify controllers, defaults to |
|
258 |
L<Mojolicious::Controller> and L<Mojo>. |
|
259 | ||
260 |
=head2 cache |
|
261 | ||
262 |
my $cache = $r->cache; |
|
263 |
$r = $r->cache(Mojo::Cache->new); |
|
264 | ||
265 |
Routing cache, defaults to a L<Mojo::Cache> object. |
|
266 | ||
267 |
# Disable caching |
|
268 |
$r->cache(0); |
|
269 | ||
270 |
=head2 conditions |
|
271 | ||
272 |
my $conditions = $r->conditions; |
|
273 |
$r = $r->conditions({foo => sub {...}}); |
|
274 | ||
275 |
Contains all available conditions. |
|
276 | ||
277 |
=head2 hidden |
|
278 | ||
279 |
my $hidden = $r->hidden; |
|
280 |
$r = $r->hidden([qw(attr has new)]); |
|
281 | ||
282 |
Controller attributes and methods that are hidden from router, defaults to |
|
283 |
C<attr>, C<has>, C<new> and C<tap>. |
|
284 | ||
285 |
=head2 namespaces |
|
286 | ||
287 |
my $namespaces = $r->namespaces; |
|
288 |
$r = $r->namespaces(['Foo::Bar::Controller']); |
|
289 | ||
290 |
Namespaces to load controllers from. |
|
291 | ||
292 |
# Add another namespace to load controllers from |
|
293 |
push @{$r->namespaces}, 'MyApp::Controller'; |
|
294 | ||
295 |
=head2 shortcuts |
|
296 | ||
297 |
my $shortcuts = $r->shortcuts; |
|
298 |
$r = $r->shortcuts({foo => sub {...}}); |
|
299 | ||
300 |
Contains all available shortcuts. |
|
301 | ||
302 |
=head1 METHODS |
|
303 | ||
304 |
L<Mojolicious::Routes> inherits all methods from |
|
305 |
L<Mojolicious::Routes::Route> and implements the following new ones. |
|
306 | ||
307 |
=head2 add_condition |
|
308 | ||
309 |
$r = $r->add_condition(foo => sub {...}); |
|
310 | ||
311 |
Add a new condition. |
|
312 | ||
313 |
=head2 add_shortcut |
|
314 | ||
315 |
$r = $r->add_shortcut(foo => sub {...}); |
|
316 | ||
317 |
Add a new shortcut. |
|
318 | ||
319 |
=head2 auto_render |
|
320 | ||
321 |
$r->auto_render(Mojolicious::Controller->new); |
|
322 | ||
323 |
Automatic rendering. |
|
324 | ||
325 |
=head2 continue |
|
326 | ||
327 |
$r->continue(Mojolicious::Controller->new); |
|
328 | ||
329 |
Continue dispatch chain. |
|
330 | ||
331 |
=head2 dispatch |
|
332 | ||
333 |
my $bool = $r->dispatch(Mojolicious::Controller->new); |
|
334 | ||
335 |
Match routes with L<Mojolicious::Routes::Match> and dispatch. |
|
336 | ||
337 |
=head2 hide |
|
338 | ||
339 |
$r = $r->hide(qw(foo bar)); |
|
340 | ||
341 |
Hide controller attributes and methods from router. |
|
342 | ||
343 |
=head2 is_hidden |
|
344 | ||
345 |
my $bool = $r->is_hidden('foo'); |
|
346 | ||
347 |
Check if controller attribute or method is hidden from router. |
|
348 | ||
349 |
=head2 lookup |
|
350 | ||
351 |
my $route = $r->lookup('foo'); |
|
352 | ||
353 |
Find route by name with L<Mojolicious::Routes::Route/"find"> and cache all |
|
354 |
results for future lookups. |
|
355 | ||
356 |
=head2 route |
|
357 | ||
358 |
my $route = $r->route; |
|
359 |
my $route = $r->route('/:action'); |
|
360 |
my $route = $r->route('/:action', action => qr/\w+/); |
|
361 |
my $route = $r->route(format => 0); |
|
362 | ||
363 |
Generate route matching all HTTP request methods. |
|
364 | ||
365 |
=head1 SEE ALSO |
|
366 | ||
367 |
L<Mojolicious>, L<Mojolicious::Guides>, L<http://mojolicio.us>. |
|
368 | ||
369 |
=cut |