Newer Older
369 lines | 9.157kb
add files
Yuki Kimoto authored on 2014-03-26
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