NAME
Mojolicious::Guides::Growing - Growing
OVERVIEW
Starting a Mojolicious::Lite prototype from scratch and growing it into a well structured Mojolicious application.
CONCEPTS
Essentials every Mojolicious developer should know.
Model View Controller
MVC is a software architectual pattern for graphical user iterface programming, originating in Smalltalk-80 that separates application logic, presentation and input.
.------------. .-------. .------.
Request -> | Controller | -> | Model | -> | View | -> Response
'------------' '-------' '------'
A slightly modified version of the pattern moving some application logic into the controller
is the foundation of pretty much every web framework these days.
.----------------. .-------.
Request -> | | <-> | Model |
| | '-------'
| Controller |
| | .-------.
Response <- | | <-> | View |
'----------------' '-------'
The controller
receives a request from a user, passes incoming data to the model
and retrieves outgoing data from it, which then gets turned into an actual response by the view
. It's important to consider that this pattern is just a guideline that most of the time results in cleaner more maintainable code, but not a rule that should be followed at all costs.
PROTOTYPE
One of the main differences between Mojolicious and other web frameworks is that it also includes Mojolicious::Lite, a micro web framework optimized for rapid prototyping.
Foundation
A Mojolicious::Lite protoype often consists of just a single executable Perl script like myapp.pl
.
% mkdir myapp
% cd myapp
% touch myapp.pl
% chmod 744 myapp.pl
This will be the foundation for our login manager example application.
use Mojolicious::Lite;
get '/' => sub {
my $self = shift;
$self->render(text => 'Hello world!');
};
app->start;
The built in web server makes working on your application a lot of fun thanks to automatic reloading.
% ./myapp.pl daemon --reload
Server available at http://*:3000.
Just save your changes and they will be automatically in effect the next time you refresh your browser.
Model
In Mojolicious we consider web applications simple frontends for existing application logic, that means Mojolicious is entirely model layer agnostic and you just use whatever Perl modules you like most.
% mkdir lib
% touch lib/MyUser.pm
% chmod 644 lib/MyUser.pm
Our login manager will simply use a normal Perl module abstracting away all logic related to matching user names and passwords.
package MyUsers;
use strict;
use warnings;
my $USERS = {
sri => 'secr3t',
vti => 'lulz',
yko => 'zeecaptain'
};
sub new { bless {}, shift }
sub check {
my ($self, $user, $pass) = @_;
# Success
return 1 if $USERS->{$user} && $USERS->{$user} eq $pass;
# Fail
return;
}
1;
The defaults
method of the application can be used to make our model
available to all actions through the stash
.
use Mojolicious::Lite;
use lib 'lib';
use MyUsers;
app->defaults(users => MyUsers->new);
# GET /?user=sri&pass=secr3t
any '/' => sub {
my $self = shift;
# Query parameters
my $user = $self->param('user');
my $pass = $self->param('pass');
# Check password
return $self->render(text => "Welcome $user!")
if $self->stash('users')->check($user, $pass);
# Failed
$self->render(text => 'Wrong username or password!');
};
app->start;
The param
method of our Mojolicious::Controller instance is used to access query parameters, POST parameters and route placeholders, all at once.
Testing
Test driven development is a very powerful tool for designing good APIs, because you are building a use case before the actual implementation.
% mkdir t
% touch t/login.t
% chmod 644 t/login.t
Test::Mojo is a scriptable user agent designed specifically for testing with many fun state of the art features such as CSS3 selectors.
use Test::More tests => 16;
use Test::Mojo;
# Include application
use FindBin;
$ENV{MOJO_HOME} = "$FindBin::Bin/../";
require "$ENV{MOJO_HOME}/myapp.pl";
my $t = Test::Mojo->new(max_redirects => 1);
# GET / (login form)
$t->get_ok('/')->status_is(200)
->element_exists('form input[name="user"]')
->element_exists('form input[name="pass"]')
->element_exists('form input[type="submit"]');
# POST / (submit login form)
$t->post_form_ok('/' => {user => 'sri', pass => 'secr3t'})
->status_is(200)->text_like('html body' => qr/Welcome sri/);
# GET /protected (member only page)
$t->get_ok('/protected')->status_is(200)->text_like('a' => qr/Logout/);
# GET /logout (back to the login form)
$t->get_ok('/logout')->status_is(200)
->element_exists('form input[name="user"]')
->element_exists('form input[name="pass"]')
->element_exists('form input[type="submit"]');
From now on you can always check your progress by running these unit tests against your application.
% ./myapp.pl test
...
% perl t/login.pl
...
To make the test less noisy and limit log output to just error
messages you could add a line like this.
$t->app->log->level('error');
You can also make quick GET
requests from the command line.
% ./myapp.pl get /
Wrong username or password!
% ./myapp.pl get -v /?user=sri&pass=secr3t
HTTP/1.1 200 OK
Connection: Keep-Alive
Date: Sun, 18 Jul 2010 13:09:58 GMT
Server: Mojolicious (Perl)
Content-Length: 12
Content-Type: text/plain
Welcome sri!
Final Prototype
A final prototype for our login manager could look like this.
use Mojolicious::Lite;
use lib 'lib';
use MyUsers;
app->defaults(users => MyUsers->new);
any '/' => sub {
my $self = shift;
# Query or POST parameters
my $user = $self->param('user') || '';
my $pass = $self->param('pass') || '';
# Check password
return $self->render
unless $self->stash('users')->check($user, $pass);
# Store user name in session
$self->session(user => $user);
# Store a friendly message for the next page in flash
$self->flash(message => 'Thanks for logging in!');
# Redirect to protected page
$self->redirect_to('protected');
} => 'index';
get '/protected' => sub {
my $self = shift;
# Redirect to main page if user is not logged in
return $self->redirect_to('index') unless $self->session('user');
} => 'protected';
get '/logout' => sub {
my $self = shift;
# Expire and in turn clear session automatically
$self->session(expires => 1);
# Redirect to main page
$self->redirect_to('index');
} => 'logout';
app->start;
__DATA__
@@ layouts/default.html.ep
<!doctype html><html>
<head><title>Login Manager</title></head>
<body><%= content %></body>
</html>
@@ index.html.ep
% layout 'default';
<%= form_for index => {%>
<% if (param 'user') { %>
<b>Wrong name or password, please try again.</b><br />
<% } %>
Name:<br />
<%= input user => (type => 'text') %><br />
Password:<br />
<%= input pass => (type => 'text') %><br />
<input type="submit" value="Login" />
<%}%>
@@ protected.html.ep
% layout 'default';
<% if (my $message = flash 'message' ) { %>
<b><%= $message %></b><br />
<% } %>
Welcome <%= session 'user' %>!<br />
<%= link_to logout => {%>
Logout
<%}%>