The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

Name

Forward::Routes - restful routes for web framework developers

Description

Instead of letting a web server like Apache decide which files to serve based on the provided URL, the whole work can be done by your framework using the Forward::Routes module.

1. Routes setup

Think of routes as kind of simplified regular expressions!

Each route represents a URL path pattern and holds a set of default values.

    # create a routes root object
    my $routes = Forward::Routes->new;

    # add a new route with a :city placeholder and controller and action defaults
    $routes->add_route('/towns/:city')->defaults(controller => 'world', action => 'cities');

2. Search for a matching route

After the setup has been done, the method and path of a current HTTP request can be passed to the routes root object using the "match" method to search for a matching route.

The match method returns an array ref of Forward::Routes::Match objects in case of a match, or undef if there is no match.

    # get request path and method (e.g. from a Plack::Request object)
    my $path   = $req->path_info;
    my $method = $req->method;

    # search routes
    my $matches = $routes->match($method => $path);

Unless advanced techniques such as bridges are used, the array ref contains no more than one match object ($matches->[0]).

The search ends as soon as a matching route has been found. As a result, if there are multiple routes that might match, the route that has been defined first wins.

If the passed path and method do not match against a defined route, an undefined value is returned. Frameworks might render a 404 not found page in such cases.

    # $matches is undef
    my $matches = $routes->match(get => '/hello_world');

3. Parameters

The match object holds two types of parameters:

  • default values of the matching route as defined earlier via the "defaults" method

  • placeholder values extracted from the passed URL path

Controller and action parameters can be used by your framework to execute the desired controller method, while making default and placeholder values of the matching route available to that method for further use.

    # $matches is an array ref
    my $matches = $routes->match(get => '/towns/paris');

    # $match is a Forward::Routes::Match object
    my $match = $matches->[0]

    # $match->params->{controller} is "world"
    # $match->params->{action}     is "cities"
    # $match->params->{city}       is "paris"

Features and Methods

Add new routes

The add_route method adds a new route to the parent route object (in simple use cases, to the routes root object) and returns the new route object.

The passed parameter is the URL path pattern of the new route object. The URL path pattern is kind of a simplified reqular expression for the path part of a URL and is transformed to a real regular expression internally. It is used later on to check whether the passed request path matches the route.

    $root = Forward::Routes->new;
    my $new_route = $root->add_route('foo/bar');

    my $m = $root->match(get => 'foo/bar');
    # $m->[0]->params is {}

    my $m = $r->match(get => 'foo/hello');
    # $m is undef;

Placeholders

Placeholders start with a colon and match everything except slashes. If the route matches against the passed request method and path, placeholder values can be retrieved from the returned match object.

    $r = Forward::Routes->new;
    $r->add_route(':foo/:bar');

    $m = $r->match(get => 'hello/there');
    # $m->[0]->params is {foo => 'hello', bar => 'there'};

    $m = $r->match(get => 'hello/there/you');
    # $m is undef

Optional Placeholders

Placeholders can be marked as optional by surrounding them with brackets and a trailing question mark.

    $r = Forward::Routes->new;
    $r->add_route(':year(/:month/:day)?');

    $m = $r->match(get => '2009');
    # $m->[0]->params is {year => 2009}

    $m = $r->match(get => '2009/12');
    # $m is undef

    $m = $r->match(get => '2009/12/10');
    # $m->[0]->params is {year => 2009, month => 12, day => 10}


    $r = Forward::Routes->new;
    $r->add_route('/hello/world(-:city)?');

    $m = $r->match(get => 'hello/world');
    # $m->[0]->params is {}

    $m = $r->match(get => 'hello/world-paris');
    # $m->[0]->params is {city => 'paris'}

Grouping

Placeholders have to be surrounded with brackets if more than one placeholder is put between slashes (grouping).

    $r = Forward::Routes->new;
    $r->add_route('world/(:country)-(:cities)');

    $m = $r->match(get => 'world/us-new_york');
    # $m->[0]->params is {country => 'us', cities => 'new_york'}

Constraints

By default, placeholders match everything except slashes. The constraints method allows to make placeholders more restrictive. The first passed parameter is the name of the placeholder, the second parameter is a Perl regular expression.

    $r = Forward::Routes->new;

    # placeholder only matches integers
    $r->add_route('articles/:id')->constraints(id => qr/\d+/);
    
    $m = $r->match(get => 'articles/abc');
    # $m is undef
    
    $m = $r->match(get => 'articles/123');
    # $m->[0]->params is {id => 123}

Defaults

The defaults method allows to add default values to a route. If the route matches against the passed request method and path, default values can be retrieved from the returned match object.

    $r = Forward::Routes->new;
    $r->add_route('articles')
      ->defaults(first_name => 'Kevin', last_name => 'Smith');

    $m = $r->match(get => 'articles');
    # $m->[0]->params is {first_name => 'Kevin', last_name => 'Smith'}

Optional Placeholders and Defaults

Placeholders are automatically filled with default values if the route would not match otherwise.

    $r = Forward::Routes->new;
    $r->add_route(':year(/:month)?/:day')->defaults(month => 1);

    $m = $r->match(get => '2009');
    # $m is undef

    $m = $r->match(get => '2009/12');
    # $m->[0]->params is {year => 2009, month => 1, day => 12}

    $m = $r->match(get => '2009/2/3');
    # $m->[0]->params is {year => 2009, month => 2, day => 3};

Shortcut for Action and Controller Defaults

The to method provides a shortcut for action and controller defaults.

    $r = Forward::Routes->new;

    $r->add_route('articles')
      ->to('foo#bar');

    # is a shortcut for
    $r->add_route('articles')
      ->defaults(controller => 'foo', action => 'bar');

    $m = $r->match(get => 'articles');
    # $m->[0]->params is {controller => 'foo', action => 'bar'}

Request Method Constraints

The via method sets the HTTP request method required for a route to match. If no method is set, the request method has no influence on the search for a matching route.

    $r = Forward::Routes->new;
    $r->add_route('logout')->via('post');

    my $m = $r->match(get => 'logout');
    # $m is undef
    
    my $m = $r->match(post => 'logout');
    # $m->[0] is {}

Format Constraints

The format method restricts the allowed formats of a URL path. If the route matches against the passed request method and path, the format value can be retrieved from the returned match object.

    $r = Forward::Routes->new;
    $r->add_route(':foo/:bar')->format('html','xml');

    $m = $r->match(get => 'hello/there.html');
    # $m->[0]->params is {foo => 'hello', bar => 'there', format => 'html'}

    $m = $r->match(get => 'hello/there.xml');
    # $m->[0]->params is {foo => 'hello', bar => 'there', format => 'xml'}

    $m = $r->match(get => 'hello/there.jpeg');
    # $m is undef

Once a format constraint has been defined, all child routes inherit the behaviour of their parents, unless they get format constraints themselves. For example, adding a format constraint to the route root object affects all child routes added via add_route.

    my $root = Forward::Routes->new->format('html');
    $root->add_route('foo')->format('xml');
    $root->add_route('baz');

    $m = $root->match(get => 'foo.html');
    # $m is undef;
    
    $m = $root->match(get => 'foo.xml');
    # $m->[0]->params is {format => 'xml'};

    $m = $root->match(get => 'baz.html');
    # $m->[0]->params is {format => 'html'};

    $m = $root->match(get => 'baz.xml');
    # $m is undef;

If no format constraint is added to a route and the route's parents also have no format constraints, there is also no format validation taking place. This might cause kind of unexpected behaviour when dealing with placeholders:

    $r = Forward::Routes->new;
    $r->add_route(':foo/:bar');

    $m = $r->match(get => 'hello/there.html');
    # $m->[0]->params is {foo => 'hello', bar => 'there.html'}

If this is not what you want, an empty format constraint can be passed explicitly:

    $r = Forward::Routes->new->format('');
    $r->add_route(':foo/:bar');

    $m = $r->match(get => 'hello/there.html');
    # $m->[0] is undef

    $m = $r->match(get => 'hello/there');
    # $m->[0]->params is {foo => 'hello', bar => 'there'}

Naming

Each route can get a name through the name method. Names are required to make routes reversible (see build_path).

    $r = Forward::Routes->new;
    $r->add_route('logout')->name('foo');

Path Building

Routes are reversible, i.e. paths can be generated through the build_path method. The first parameter is the name of the route. If the route consists of placeholders which are not optional, placeholder values have to be passed as well to generate the path, otherwise an exception is thrown. The build_path method returns a hash ref with the keys "method" and "path".

    $r = Forward::Routes->new;
    $r->add_route('world/(:country)-(:cities)')->name('hello')->via('post');

    my $path = $r->build_path('hello', country => 'us', cities => 'new_york')
    # $path->{path}   is 'world/us-new_york';
    # $path->{method} is 'post';

Path building is useful to build tag helpers that can be used in templates. For example, a link_to helper might generate a link with the help of a route name: link_to('route_name', placeholder => 'value'). In contrast to hard coding the URL in templates, routes could be changed an all links in your templates would get adjusted automatically.

Chaining

All methods can be chained.

    $r = Forward::Routes->new;
    my $articles = $r->add_route('articles/:id')
      ->defaults(first_name => 'foo', last_name => 'bar')
      ->format('html')
      ->constraints(id => qr/\d+/)
      ->name('hot')
      ->to('hello#world')
      ->via('get','post');

Nested Routes

New routes cannot only be added to the routes root object, but to any route. Building deep routes trees might result in performance gains in larger projects with many routes, as the amount of regular expression searches can be reduced this way.

    # nested routes
    $root = Forward::Routes->new;
    $nested1 = $root->add_route('foo1');
    $nested1->add_route('bar1');
    $nested1->add_route('bar2');
    $nested1->add_route('bar3');
    $nested1->add_route('bar4');
    $nested1->add_route('bar5');

    $nested2 = $root->add_route('foo2');
    $nested2->add_route('bar5');

    $m = $r->match(get => 'foo2/bar5');
    # 3 regular expression searches performed

    # alternative:
    $root = Forward::Routes->new;
    $root->add_route('foo1/bar1');
    $root->add_route('foo1/bar2');
    $root->add_route('foo1/bar3');
    $root->add_route('foo1/bar4');
    $root->add_route('foo1/bar5');
    $root->add_route('foo2/bar5');
    # 6 regular expression searches performed

Resources

The resources method allows to generate Rails like resources.

Please look at Forward::Guides::Routes::Resources for more in depth documentation on restful resources.

    $r = Forward::Routes->new;
    $r->add_resources('users', 'photos', 'tags');

    $m = $r->match(get => 'photos');
    # $m->[0]->params is {controller => 'photos', action => 'index'}

    $m = $r->match(get => 'photos/1');
    # $m->[0]->params is {controller => 'photos', action => 'show', id => 1}

    $m = $r->match(put => 'photos/1');
    # $m->[0]->params is {controller => 'photos', action => 'update', id => 1}

Path Building and Resources

    $r = Forward::Routes->new;
    $r->add_resources('users', 'photos', 'tags');

    # $r->build_path('photos_update', id => 987)->{path} is 'photos/987'

Nested Resources

Please look at Forward::Guides::Routes::NestedResources for more in depth documentation on nested resources.

    $r = Forward::Routes->new;
    my $magazines = $r->add_resources('magazines');
    $magazines->add_resources('ads');

    $m = $r->match(get => 'magazines/1/ads/4');
    # $m->[0]->params is
    # {controller => 'ads', action => 'show', magazines_id => 1, ads_id => 4}

Bridges

    $r = Forward::Routes->new;
    my $bridge = $r->bridge('admin')->to('check#authentication');
    $bridge->add_route('foo')->to('my#stuff');

    $m = $r->match(get => 'admin/foo');
    # $m->[0]->params is {controller => 'check', action => 'authentication'}
    # $m->[1]->params is {controller => 'my', action => 'stuff'}

Author

ForwardEver

Copyright and License

Copyright (C) 2011, ForwardEver

This program is free software, you can redistribute it and/or modify it under the terms of the Artistic License version 2.0.

Credits

Path matching and path building inspired by Viacheslav Tykhanovskyi's Router module https://github.com/vti/router

Concept of nested routes and bridges inspired by Sebastian Riedel's Mojolicious::Routes module https://github.com/kraih/mojo/tree/master/lib/Mojolicious/Routes

Concept of restful resources inspired by Ruby on Rails