package Mojolicious::Routes; use Mojo::Base 'Mojolicious::Routes::Route'; use List::Util 'first'; use Mojo::Cache; use Mojo::Loader 'load_class'; use Mojo::Util 'camelize'; use Mojolicious::Routes::Match; use Scalar::Util 'weaken'; has base_classes => sub { [qw(Mojolicious::Controller Mojolicious)] }; has cache => sub { Mojo::Cache->new }; has [qw(conditions shortcuts)] => sub { {} }; has types => sub { {num => qr/[0-9]+/} }; has hidden => sub { [qw(attr has new tap)] }; has namespaces => sub { [] }; sub add_condition { $_[0]->conditions->{$_[1]} = $_[2] and return $_[0] } sub add_shortcut { $_[0]->shortcuts->{$_[1]} = $_[2] and return $_[0] } sub add_type { $_[0]->types->{$_[1]} = $_[2] and return $_[0] } sub continue { my ($self, $c) = @_; my $match = $c->match; my $stack = $match->stack; my $position = $match->position; return _render($c) unless my $field = $stack->[$position]; # Merge captures into stash my $stash = $c->stash; @{$stash->{'mojo.captures'} //= {}}{keys %$field} = values %$field; @$stash{keys %$field} = values %$field; my $continue; my $last = !$stack->[++$position]; if (my $cb = $field->{cb}) { $continue = $self->_callback($c, $cb, $last) } else { $continue = $self->_controller($c, $field, $last) } $match->position($position); $self->continue($c) if $last || $continue; } sub dispatch { my ($self, $c) = @_; $self->match($c); @{$c->match->stack} ? $self->continue($c) : return undef; return 1; } sub hide { push @{shift->hidden}, @_ } sub is_hidden { my ($self, $method) = @_; my $h = $self->{hiding} ||= {map { $_ => 1 } @{$self->hidden}}; return !!($h->{$method} || $method =~ /^_/ || $method =~ /^[A-Z_]+$/); } sub lookup { ($_[0]{reverse} //= $_[0]->_index)->{$_[1]} } sub match { my ($self, $c) = @_; # Path (partial path gets priority) my $req = $c->req; my $path = $c->stash->{path}; if (defined $path) { $path = "/$path" if $path !~ m!^/! } else { $path = $req->url->path->to_route } # Method (HEAD will be treated as GET) my $method = uc($req->url->query->clone->param('_method') || $req->method); $method = 'GET' if $method eq 'HEAD'; # Check cache my $ws = $c->tx->is_websocket ? 1 : 0; my $match = Mojolicious::Routes::Match->new(root => $self); $c->match($match); my $cache = $self->cache; if (my $result = $cache->get("$method:$path:$ws")) { return $match->endpoint($result->{endpoint})->stack($result->{stack}); } # Check routes $match->find($c => {method => $method, path => $path, websocket => $ws}); return unless my $route = $match->endpoint; $cache->set( "$method:$path:$ws" => {endpoint => $route, stack => $match->stack}); } sub _action { shift->plugins->emit_chain(around_action => @_) } sub _callback { my ($self, $c, $cb, $last) = @_; $c->stash->{'mojo.routed'} = 1 if $last; my $app = $c->app; $app->log->debug('Routing to a callback'); return _action($app, $c, $cb, $last); } sub _class { my ($self, $c, $field) = @_; # Application instance return $field->{app} if ref $field->{app}; # Application class my @classes; my $class = $field->{controller} ? camelize $field->{controller} : ''; if ($field->{app}) { push @classes, $field->{app} } # Specific namespace elsif (defined(my $ns = $field->{namespace})) { if ($class) { push @classes, $ns ? "${ns}::$class" : $class } elsif ($ns) { push @classes, $ns } } # All namespaces elsif ($class) { push @classes, "${_}::$class" for @{$self->namespaces} } # Try to load all classes my $log = $c->app->log; for my $class (@classes) { # Failed next unless defined(my $found = $self->_load($class)); return !$log->debug(qq{Class "$class" is not a controller}) unless $found; # Success my $new = $class->new(%$c); weaken $new->{$_} for qw(app tx); return $new; } # Nothing found $log->debug(qq{Controller "$classes[-1]" does not exist}) if @classes; return @classes ? undef : 0; } sub _controller { my ($self, $old, $field, $last) = @_; # Load and instantiate controller/application my $new; unless ($new = $self->_class($old, $field)) { return defined $new } # Application my $class = ref $new; my $app = $old->app; my $log = $app->log; if ($new->isa('Mojolicious')) { $log->debug(qq{Routing to application "$class"}); # Try to connect routes if (my $sub = $new->can('routes')) { my $r = $new->$sub; weaken $r->parent($old->match->endpoint)->{parent} unless $r->parent; } $new->handler($old); $old->stash->{'mojo.routed'} = 1; } # Action elsif (my $method = $field->{action}) { if (!$self->is_hidden($method)) { $log->debug(qq{Routing to controller "$class" and action "$method"}); if (my $sub = $new->can($method)) { $old->stash->{'mojo.routed'} = 1 if $last; return 1 if _action($app, $new, $sub, $last); } else { $log->debug('Action not found in controller') } } else { $log->debug(qq{Action "$method" is not allowed}) } } return undef; } sub _load { my ($self, $app) = @_; # Load unless already loaded return 1 if $self->{loaded}{$app}; if (my $e = load_class $app) { ref $e ? die $e : return undef } # Check base classes return 0 unless first { $app->isa($_) } @{$self->base_classes}; return $self->{loaded}{$app} = 1; } sub _render { my $c = shift; my $stash = $c->stash; return if $stash->{'mojo.rendered'}; $c->render_maybe or $stash->{'mojo.routed'} or $c->helpers->reply->not_found; } 1; =encoding utf8 =head1 NAME Mojolicious::Routes - Always find your destination with routes =head1 SYNOPSIS use Mojolicious::Routes; # Simple route my $r = Mojolicious::Routes->new; $r->route('/')->to(controller => 'blog', action => 'welcome'); # More advanced routes my $blog = $r->under('/blog'); $blog->get('/list')->to('blog#list'); $blog->get('/:id' => [id => qr/\d+/])->to('blog#show', id => 23); $blog->patch(sub { shift->render(text => 'Go away!', status => 405) }); =head1 DESCRIPTION L is the core of the L web framework. See L for more. =head1 TYPES These placeholder types are available by default. =head2 num $r->get('/article/'); Placeholder value needs to be a non-fractional number, similar to the regular expression C<([0-9]+)>. =head1 ATTRIBUTES L inherits all attributes from L and implements the following new ones. =head2 base_classes my $classes = $r->base_classes; $r = $r->base_classes(['MyApp::Controller']); Base classes used to identify controllers, defaults to L and L. =head2 cache my $cache = $r->cache; $r = $r->cache(Mojo::Cache->new); Routing cache, defaults to a L object. =head2 conditions my $conditions = $r->conditions; $r = $r->conditions({foo => sub {...}}); Contains all available conditions. =head2 hidden my $hidden = $r->hidden; $r = $r->hidden(['attr', 'has', 'new']); Controller attributes and methods that are hidden from router, defaults to C, C, C and C. =head2 namespaces my $namespaces = $r->namespaces; $r = $r->namespaces(['MyApp::Controller', 'MyApp']); Namespaces to load controllers from. # Add another namespace to load controllers from push @{$r->namespaces}, 'MyApp::MyController'; =head2 shortcuts my $shortcuts = $r->shortcuts; $r = $r->shortcuts({foo => sub {...}}); Contains all available shortcuts. =head2 types my $types = $r->types; $r = $r->types({lower => qr/[a-z]+/}); Registered placeholder types, by default only L is already defined. =head1 METHODS L inherits all methods from L and implements the following new ones. =head2 add_condition $r = $r->add_condition(foo => sub {...}); Register a condition. $r->add_condition(foo => sub { my ($route, $c, $captures, $arg) = @_; ... return 1; }); =head2 add_shortcut $r = $r->add_shortcut(foo => sub {...}); Register a shortcut. $r->add_shortcut(foo => sub { my ($route, @args) = @_; ... }); =head2 add_type $r = $r->add_type(foo => qr/\w+/); $r = $r->add_type(foo => ['bar', 'baz']); Register a placeholder type. $r->add_type(lower => qr/[a-z]+/); =head2 continue $r->continue(Mojolicious::Controller->new); Continue dispatch chain and emit the hook L for every action. =head2 dispatch my $bool = $r->dispatch(Mojolicious::Controller->new); Match routes with L and dispatch with L. =head2 hide $r = $r->hide('foo', 'bar'); Hide controller attributes and methods from router. =head2 is_hidden my $bool = $r->is_hidden('foo'); Check if controller attribute or method is hidden from router. =head2 lookup my $route = $r->lookup('foo'); Find route by name with L and cache all results for future lookups. =head2 match $r->match(Mojolicious::Controller->new); Match routes with L. =head1 SEE ALSO L, L, L. =cut