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