[32205] | 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
|
---|