-
Notifications
You must be signed in to change notification settings - Fork 582
Routes for non lite apps
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 a Mojolicious application.
For example, a URL like
http://www.example.com/cities/paris.html
can provide content that has been retrieved from a database, or also content that has been fetched from another website in "real-time", or even continuously updating content retrieved from other users sitting in front of their computers.
So, Mojolicious allows you to display dynamic content in a search engine friendly way!
In order to achieve this, Mojolicious decides on it's own how to handle URLs and what to deliver as a result.
This is where routes come into play. Routes are kind of "rules" how to handle URLs. Mojolicious checks whether a specific URL matches a certain pattern (as defined in the route), and determines what happens if a match occurs (as also defined in the route).
For example, you can define Apache to handle all URLs, except requests for http://yourdomain.com/myapp, so that URLs like
http://yourdomain.com/myapp/users.html
http://yourdomain.com/myapp/data.html
are handled by Mojolicious.
Routes themselves are very dynamic (think of them as kind of simplified regular expressions), so another advantage of routes is that an infinite amount of URLs can be handled, without the need to place a file for each URL on your server .
For example, a route in the form
/:cities/
could be responsible to deliver content if a user requests
http://yourdomain.com/myapp/new_york.html
http://yourdomain.com/myapp/paris.html or
http://yourdomain.com/myapp/any_other_city.html
Finally, routes are reversible. Instead of hard copying URLs in your templates, you can use route names in templates, forms and redirects. If you decide to relocate the content (provide content under a different URL), you just need to modifiy the route, not the templates, forms etc.
The following guide is kind of a step by step introduction to Mojolicious not-lite apps, with an emphasis on routes. It is recommended that you understand Mojolicious lite-applications first, before starting with none-lite Mojolicious applications!
For a more complete guide that demonstrates the full power of routes, also read the official Mojolicious Routing Guide!
When you create a mojolicious (non lite) app through
mojolicious generate app hello
a hello.pm file is created in the /hello/lib folder.
The hello.pm file contains the following route
$r->route('/:controller/:action/:id')->to('example#welcome', id => 1);
which acts as kind of a fallback default route.
You can now start the server
perl hello/script/hello daemon --reload
Server available at http://*:3000
and start playing around. The reload option makes sure that changes in your files are recognized by the server eliminating the need to restart.
Enter
http://localhost:3000/
in your browser and a "Welcome to the Mojolicious Web Framework!!" message should appear on your screen!
Calling this URL actually calls the welcome method of the example controller which is located at
hello/lib/hello/Example.pm
Now lets change the URL a bit
http://localhost:3000/test/me
and a website not found (404) error should appear.
In order to get some content beeing displayed, we have to create a "Test" controller that contains the "me" method.
So create a file
hello/lib/hello/Test.pm
which should look like this:
package hello::Test;
use strict;
use warnings;
use base 'Mojolicious::Controller';
sub me {
my $self = shift;
$self->render_text('The me method from controller test is called!');
}
Hit the reload button or enter
http://localhost:3000/test/me
again and you shoud see the message "The me method from controller test is called!".
Let's look at the route again:
$r->route('/:controller/:action/:id')->to('example#welcome', id => 1);
Using routes that way, when you add a new controller "Foo" with an action "bar" it will always be available as /foo/bar right away.
Also check out the ->to
part. to
is just a set of default values.
So when you enter
http://localhost:3000/example/
the default "welcome" method (as defined in to) will be called.
"example#welcome" is just a shortcut for
controller=>'example', action=>'welcome'
so one could also write
$r->route('/:controller/:action/:id')
->to(controller=>'example', action=>'welcome', id => 1);
Or enter
http://localhost:3000/
and the default "welcome" method (as defined in to) of the default controller "example" (as also defined in to) will be called.
Now you can start to define more specific routes ahead of the existing route, e.g.
$r->route('/test/you/')->to('example#welcome');
$r->route('/:controller/:action/:id')->to('example#welcome', id => 1);
The request for
http://localhost:3000/test/you
would always use the same controller (example) and action (welcome).
Finally lets look at the "id" part of the route:
$r->route('/:controller/:action/:id')->to('example#welcome', id => 1);
Including an "id" in the URL has no effect on which controller or action is finally called. However, the "id" value is saved in the so called "stash".
To make things a bit more clear, we change the Test.pm controller a bit:
sub me {
my $self = shift;
$self->render_text(
'The me method from controller test is called, '.
'the passed id is:'.$self->stash('id')
);
}
Now http://localhost:3000/test/me
should output
"The me method from controller test is called, the passed id is:1"
As we haven't passed an id value, the default as defined in the "to" part of the route is used (value: 1).
http://localhost:3000/test/me/2
results in
"The me method from controller test is called, the passed id is:2"
Let's create an app that provides you with information on the worlds biggest cities.
First, create a route
$r->route('/cities/:id/')->to('cities#show');
As explained in the previous example, routes for the "hello" sample app are defined in
/hello/lib/hello.pm
Also delete the routes created in the previous example, to avoid any conflicts, as we start with a fresh new example!
Now add a controller that can handle the route (at /hello/lib/hello/Cities.pm):
package hello::Cities;
use strict;
use warnings;
use base 'Mojolicious::Controller';
sub show {
my $self = shift;
my $city_info = $self->stash('id');
$self->render_text($city_info);
}
1;
Note: make sure that the second part of the package name starts with a capital letter:
package hello::Cities
not: package hello::cities.
as the controller file name "Cities.pm" also starts with a capital letter.
Now enter
http://localhost:3000/cities/paris
and the word "paris" should appear on your screen!
The route
$r->route('/cities/:id/')->to('cities#show');
dispatches all requests to the "show" method in the "Cities" controller as defined in the "to" part of the route.
In contrast to :controller and :action (used earlier directly in the route), :id is NOT a reserved word and as a result, has no effect on which action or controller is called and also has no other side effects.
The :id placeholder in the route is saved in the so called stash and can be used in the "show" method of the "Cities" controller:
my $city_info = $self->stash('id');
$self->render_text($city_info);
In our example, the value contained in :id is just saved in $city_info
and than printed via render_text.
Enter
http://localhost:3000/cities/newyork
and the string "newyork" is delivered to your browser!
Lets modify the request a bit, add .html (dot html) to the end of the URL.
Reload or enter
http://localhost:3000/cities/newyork.html
again.
Surprise, the request still works and "newyork" is printed on the screen again.
Mojolicious is able to detect file extensions like .html and .txt at the end of a route automatically. Even better, the file extension is also saved in the stash and can be accessed in the controller methods.
For testing purposes, we modify the "show" method in the "Cities" controller
my $city_info = $self->stash('id');
my $format = $self->stash('format') || 'no special format';
$self->render_text($city_info.' format:'.$format);
Now the name of the city and the format should be displayed on your screen:
http://localhost:3000/cities/newyork.html
outputs: newyork format:html
http://localhost:3000/cities/newyork.txt
outputs: newyork format:txt
http://localhost:3000/cities/berlin
outputs: berlin format:no special format
Now we want to display information on each city. As we do not want to deal with real databases like MySQL at this point, we save all data in a Perl hash. Our controller should look like this now:
package hello::Cities;
use strict;
use warnings;
use base 'Mojolicious::Controller';
my %cities = (
new_york => 'New York is famous for its Statue of Liberty.',
paris => 'Paris is famous for its Eiffel Tower.'
);
sub show {
my $self = shift;
my $city_info = $cities{ $self->stash('id') };
if ( $city_info ){
$self->render_text($city_info);
}
else {
$self->render_text('City not found!');
}
}
1;
A bit offtopic:
What are we doing here? We just use $self->stash('id')
(e.g. paris) as the hash key to get a corresponding hash value from the %cities
hash. If the hash key exists and contains a true value, the hash value with information on the city is returned and rendered. It only works if the cities hash is defined as a "class hash". So after the "Cities" controller has been compiled and as long as the "Cities" controller exists in memory, the data is save. All this won't work if you define the cities hash directly in one of the methods (e.g. the "show" method), as the hash would only be alive for the time the method is executed. In real live, you should use a database, of course, to save data permenantly!
Enter:
http://localhost:3000/cities/new_york.html
and "New York is famous for its Statue of Liberty." should appear on your screen!
As the render_text method is really "boring", we will set up templates first, before we learn more about routes.
We will modify our "Cities" controller file once again, now it looks like this:
package hello::Cities;
use strict;
use warnings;
use base 'Mojolicious::Controller';
my %cities = (
new_york => 'New York is famous for its Statue of Liberty.',
paris => 'Paris is famous for its Eiffel Tower.'
);
sub show {
my $self = shift;
my $city_info = $cities{ $self->stash('id') };
$self->render_text('City not found') unless $city_info;
$self->stash( our_data => $city_info );
}
1;
Instead of using the render_text method (in case that city information exists), we now just put our $city_info
as a hash into the stash (hash key is "our_data", hash value is $city_info
). Do not try to load the URL right now! To get the whole thing working, we first have to create a template file.
So switch to the /hello/templates folder and create a new sub folder called "cities" (the name of our controller, but all lowercase).
Now create a template file called
show.html.ep
Full path is /hello/templates/cities/show.html.ep
"show" is the name of our action, "html" is the desired format, and "ep" is the templating language, in this case embedded perl (mojolious default templating language).
The template file should look like this:
<%= $our_data %>
(Called from our HTML template!)
Thats it! Now reload or enter
http://localhost:3000/cities/new_york.html
You should see: "New York is famous for its Statue of Liberty. (Called from our HTML template!)"
Earlier, we have saved $cities_info
to the stash, using "our_data" as the hash key. Now we can access this data from our templates via
$our_data
We put this variable between <%= %>
tags to tell Mojolicious to print this perl var to the screen!
But Mojolicous is smart. We now add .txt instead of .html to the end!
http://localhost:3000/cities/new_york.txt
Loading the page results in a 404 error. This is because we have only created a template for the "html" format, but not for the "text" format!
Create a template for "txt" format, full path is
/hello/templates/cities/show.txt.ep
with similar content:
<%= $our_data %>
(Called from our TXT template!)
Enter
http://localhost:3000/cities/new_york.txt
again and the displayed message should look like this:
New York is famous for its Statue of Liberty.
(Called from our TXT template!)
You can enter a URL with a specific file extension (like txt or html) and the format is automatically detected! Even more important, the template name is automatically detected as well, based on the names of the controller, the action and the format! If you add the "html" extension, an html template is delivered (if it exists), and if you add the "txt" extension, than the text template is delivered, if it exists, and this also works with all kind of file extensions, like xml, jpeg and so on!
Mojolicious is smart: depending on what extension you request, it renders the correct template file!
Finally, let's add another route:
$r->route('/cities')->to('cities#index');
as well as a new "Cities" controller method called "index"
sub index {
my $self = shift;
$self->stash(cities => \%cities );
}
and another template:
/hello/templates/cities/index.html.ep
with the content:
List of all city IDs (names)<br />
% foreach my $key (keys %$cities) {
%= link_to "/cities/$key" => begin
%= $key
% end
<br />
% }
Now enter
http://localhost:3000/cities
or
http://localhost:3000/cities.html
and you should see a list of all cities with links to detailed information on the city.
We will now not only display existing data, but create new data, of course with a tight focus on routes.
As we not only want to display information, but also create some, we create another route:
$r->route('/cities/new')->to('cities#create_form');
This route has to appear before
$r->route('/cities/:id/')->to('cities#show');
otherwise it would never match. The URL
http://localhost:3000/cities/new/
would (if done in wrong order) look for a city with the name "create", which is not what we want! So the order of routes is extremly important.
Now lets define a "create_form" method in our "Cities" controller:
sub create_form {
my $self = shift;
}
and a template file, full path is:
/hello/templates/cities/create_form.html.ep
Put
Create a new city:
in this file.
Enter
http://localhost:3000/cities/new
in your browser and "Create a new city:" should appear on your screen.
Actually, the create_form method does nothing! So the question is: does all this still works if we remove the "create_form" method completely from our "Cities" controller? Let's do that. Remove the "create_form" method!
Enter
http://localhost:3000/cities/new
in your browser and an exception is thrown! Oh no.
So it doesn't work? It works. However, because of certain Perl limitations, the --reload option of our build-in server does not support this.
Second trial: restart the development server manually, that means stop the server via CRTL+C, than
perl hello/script/hello daemon --reload
again.
Now
http://localhost:3000/cities/new
and "Create a new city:" should appear on your screen again.
So basically, it's enough to define a route and a template, a controller method is only required if you want to change something!
Our goal is to display a form that allows us to enter the name of a new city as well as additional information on the city.
The data of the new city than should be send back to the path
http://localhost:3000/cities
and saved to our database (in our case, actually to our hash, see chapter before)!
However, there is a problem: the path
http://localhost:3000/cities
is already used to show a list of all cities (dispatched to the "index" method in our Cities controller)!
So how is it possible to use the same URL for just showing a list of cities and for adding a new city at the same time?
Answer: by implementing different behaviour based on the request method!
If you enter
http://localhost:3000/cities
a so called "get request" is processed. In this case, we just want to display a list of cities. Get requests are useful if you do NOT want to change the state of your data (RESTful behaviour).
So let's modifiy the route:
$r->route('/cities')->via('get')->to('cities#index');
The route will only match if http://localhost:3000/cities is requested via GET, e.g. by just typing in your browser:
http://localhost:3000/cities
The route will not match, if http://localhost:3000/cities is requested via POST (which will be the case when we submit our form data).
Try it out, everything should still work.
Now let's change the "create_form" template a bit. We want to add two input fields and a submit button, and in order to do this, we use the Mojolicious tag helper. The create_form template located at
/hello/templates/cities/create_form.html.ep
should now look like this:
Create a new city:
<%= form_for '/cities' => (method => 'post') => begin %>
Name: <%= input 'name' %>
Info: <%= input 'info' %>
<%= submit_button 'Submit' %>
<% end %>
"form_for" creates a HTML form tag, submitted data is send to 'http://localhost:3000/cities', and the information is send via the "POST" method.
Enter
http://localhost:3000/cities/new
and you should see two input fields and a submit button!
Press the submit button and you will see a 404 not found error, even though the URL
http://localhost:3000/cities
is requested by your browser.
This is because so far we do not have a route that matches our POST request for http://localhost:3000/cities correctly (just for get requests).
So let's create a fourth route:
$r->route('/cities')->via('post')->to('cities#create');
From now on, all post requests are dispatched to controller "Cities" and method "create".
Now, we finally need a "create" method in the "Cities" controller, otherwise we would have a route, but still get a 404 not found error:
sub create {
my $self = shift;
my $new_city_name = $self->param('name');
my $new_city_info = $self->param('info');
$cities{$new_city_name} = $new_city_info;
$self->redirect_to('/cities/'.$self->param('name'));
}
Enter
http://localhost:3000/cities/new
and enter
"berlin" (in the name field),
"Berlin is famous for its Brandenburg Gate" (in the info field),
and press the "Submit" button!
You should then be redirected to
http://localhost:3000/cities/berlin
and "Berlin is famous for its Brandenburg Gate" should appear on your screen!
In the create method, we use the submitted value from the first form field "name" as the hash key ("berlin"), and the value from the second form field "info" ("Berlin is famous for its Brandenburg Gate") as the hash value for the %cities hash. That way, new data is added. Finally, a user is redirected to http://localhost:3000/cities/berlin after the data has been saved.
So, first of all, information on berlin has been saved to our %cities hash, the user is than redirected and a second request prints this information to our screen!
The value "berlin" has been saved into the cities hash successfully. However, it will only be there as long as your server is running and as long as files are not reloaded. If you restart your server, the cities hash will be set to it's initial state (containing just paris and new_york)! If you modify the "Cities" controller and the mojolicious daemon is started with the --reload option, the data is lost as well. Use a real database to save data permanently.
The via method allows to dispatch requests based on the request method. "GET" and "POST" are most common and supported by virtually all browsers. Use GET, if you just want to retrieve data (without changing it on the server side), use POST if you want to add data on the server (to your database).
Each route can be assigned a name via the "name" method.
Our route file contains the following routes:
$r->route('/cities')->via('get')->to('cities#index');
$r->route('/cities')->via('post')->to('cities#create');
$r->route('/cities/new')->to('cities#create_form');
$r->route('/cities/:id/')->to('cities#show');
Now, we give every route a name:
$r->route('/cities')->name('cities_index')
->via('get')->to('cities#index');
$r->route('/cities')->name('cities_create')
->via('post')->to('cities#create');
$r->route('/cities/new')->name('cities_create_form')
->to('cities#create_form');
$r->route('/cities/:id/')->name('cities_show')
->to('cities#show');
One of the main advantages of routes is that they are easily reversible, so with the help of a route, placeholders can be turned back into a path at any time.
Let's look at our create_form template:
Create a new city:
<%= form_for '/cities' => (method => 'post') => begin %>
Name: <%= input 'name' %>
Info: <%= input 'info' %>
<%= submit_button 'Submit' %>
<% end %>
Now that we have a name for our route (that matches the path "/cities"), we include it in the form_for helper (replacing the path /cities/create ) and our template file looks like this:
Create a new city:
<%= form_for 'cities_create' => (method => 'post') => begin %>
Name: <%= input 'name' %>
Info: <%= input 'info' %>
<%= submit_button 'Submit' %>
<% end %>
Also look at the "create" method in the "Cities" controller:
sub create {
my $self = shift;
my $new_city_name = $self->param('name');
my $new_city_info = $self->param('info');
$cities{$new_city_name} = $new_city_info;
$self->redirect_to('/cities/'.$self->param('name'));
}
The redirect_to method makes sure that we are redirected to "/cities/new_town" after the data has been saved.
Lets modify the method a bit:
$self->redirect_to('cities_show', id => $self->param('name') );
So instead of passing a path, we now pass the name of the route along with a value for the :id placeholder (that is part of that route).
Finally, lets look at the index.html.ep template:
List of all city IDs (names)<br />
% foreach my $key (keys %$cities) {
%= link_to "/cities/$key" => begin
%= $key
% end
<br />
% }
Just replace the part after the link_to helper:
List of all city IDs (names)<br />
% foreach my $key (keys %$cities) {
%= link_to "cities_show" => { id => $key } => begin
%= $key
% end
<br />
% }
Still everything works fine! Try it!
Suddenly, you decide that you no longer want your data to be located under the "/cities" path, but under "/towns" !
Now, change all the routes:
$r->route('/towns')->name('cities_index')
->via('get')->to('cities#index');
$r->route('/towns')->name('cities_create')
->via('post')->to('cities#create');
$r->route('/towns/new')->name('cities_create_form')
->to('cities#create_form');
$r->route('/towns/:id/')->name('cities_show')
->to('cities#show');
and check out if everything still works fine, e.g. enter
http://localhost:3000/towns/new
and save some data, e.g. name: "hamburg", info: "Hamburg is famous for its harbour promenade!" Press "Submit" and watch at the output and the paths...
Everything still works fine! You should get redirected to
http://localhost:3000/towns/hamburg
So by just changing the routes, the whole app is now available under http://localhost:3000/towns.
Without changing the create_form template, data is send to the correct path.
Without changing the "create" method in our "Cities" controller, we are still redirected to the correct path (/towns/new_city).
So avoiding absolute paths, and giving your routes names, makes a lot of sense!
But be careful: route names have to be unique (per application).
So if you give names to routes, they have to be individual enough to not get in conflict with route names within the same application that might be added in the future.
E.g. if you start to give names to routes, "create_form" could be too general as there might be lots of forms in the future that allow to create something, so cities_create_form might be more appropriate!
Also keep in mind: routes don't have anything to do with controllers per se. For example, one route can have multiple controllers (when using :controller as a placeholder).