772 lines
23 KiB
Plaintext
772 lines
23 KiB
Plaintext
|
|
=encoding utf8
|
|
|
|
=head1 NAME
|
|
|
|
Mojolicious::Guides::Growing - Growing Mojolicious applications
|
|
|
|
=head1 OVERVIEW
|
|
|
|
This document explains the process of starting a L<Mojolicious::Lite> prototype from scratch and growing it into a
|
|
well-structured L<Mojolicious> application.
|
|
|
|
=head1 CONCEPTS
|
|
|
|
Essentials every L<Mojolicious> developer should know.
|
|
|
|
=head2 Model View Controller
|
|
|
|
MVC is a software architectural pattern for graphical user interface programming originating in Smalltalk-80, that
|
|
separates application logic, presentation and input.
|
|
|
|
+------------+ +-------+ +------+
|
|
Input -> | Controller | -> | Model | -> | View | -> Output
|
|
+------------+ +-------+ +------+
|
|
|
|
A slightly modified version of the pattern moving some application logic into the I<controller> is the foundation of
|
|
pretty much every web framework these days, including L<Mojolicious>.
|
|
|
|
+----------------+ +-------+
|
|
Request -> | | <-> | Model |
|
|
| | +-------+
|
|
| Controller |
|
|
| | +-------+
|
|
Response <- | | <-> | View |
|
|
+----------------+ +-------+
|
|
|
|
The I<controller> receives a request from a user, passes incoming data to the I<model> and retrieves data from it,
|
|
which then gets turned into an actual response by the I<view>. But note that this pattern is just a guideline that most
|
|
of the time results in cleaner more maintainable code, not a rule that should be followed at all costs.
|
|
|
|
=head2 REpresentational State Transfer
|
|
|
|
REST is a software architectural style for distributed hypermedia systems such as the web. While it can be applied to
|
|
many protocols it is most commonly used with HTTP these days. In REST terms, when you are opening a URL like
|
|
C<http://mojolicious.org/foo> with your browser, you are basically asking the web server for the HTML I<representation>
|
|
of the C<http://mojolicious.org/foo> I<resource>.
|
|
|
|
+--------+ +--------+
|
|
| | -> http://mojolicious.org/foo -> | |
|
|
| Client | | Server |
|
|
| | <- <html>Mojo rocks!</html> <- | |
|
|
+--------+ +--------+
|
|
|
|
The fundamental idea here is that all resources are uniquely addressable with URLs and every resource can have
|
|
different representations such as HTML, RSS or JSON. User interface concerns are separated from data storage concerns
|
|
and all session state is kept client-side.
|
|
|
|
+---------+ +------------+
|
|
| | -> PUT /foo -> | |
|
|
| | -> Hello World! -> | |
|
|
| | | |
|
|
| | <- 201 CREATED <- | |
|
|
| | | |
|
|
| | -> GET /foo -> | |
|
|
| Browser | | Web Server |
|
|
| | <- 200 OK <- | |
|
|
| | <- Hello World! <- | |
|
|
| | | |
|
|
| | -> DELETE /foo -> | |
|
|
| | | |
|
|
| | <- 200 OK <- | |
|
|
+---------+ +------------+
|
|
|
|
While HTTP methods such as C<PUT>, C<GET> and C<DELETE> are not directly part of REST they go very well with it and are
|
|
commonly used to manipulate I<resources>.
|
|
|
|
=head2 Sessions
|
|
|
|
HTTP was designed as a stateless protocol, web servers don't know anything about previous requests, which makes
|
|
user-friendly login systems very tricky. Sessions solve this problem by allowing web applications to keep stateful
|
|
information across several HTTP requests.
|
|
|
|
GET /login?user=sebastian&pass=s3cret HTTP/1.1
|
|
Host: mojolicious.org
|
|
|
|
HTTP/1.1 200 OK
|
|
Set-Cookie: sessionid=987654321
|
|
Content-Length: 10
|
|
Hello sebastian.
|
|
|
|
GET /protected HTTP/1.1
|
|
Host: mojolicious.org
|
|
Cookie: sessionid=987654321
|
|
|
|
HTTP/1.1 200 OK
|
|
Set-Cookie: sessionid=987654321
|
|
Content-Length: 16
|
|
Hello again sebastian.
|
|
|
|
Traditionally all session data was stored on the server-side and only session ids were exchanged between browser and
|
|
web server in the form of cookies.
|
|
|
|
Set-Cookie: session=hmac-sha1(base64(json($session)))
|
|
|
|
In L<Mojolicious> however we are taking this concept one step further by storing everything JSON serialized and Base64
|
|
encoded in HMAC-SHA1 signed cookies, which is more compatible with the REST philosophy and reduces infrastructure
|
|
requirements.
|
|
|
|
=head2 Test-Driven Development
|
|
|
|
TDD is a software development process where the developer starts writing failing test cases that define the desired
|
|
functionality and then moves on to producing code that passes these tests. There are many advantages such as always
|
|
having good test coverage and code being designed for testability, which will in turn often prevent future changes from
|
|
breaking old code. Much of L<Mojolicious> was developed using TDD.
|
|
|
|
=head1 PROTOTYPE
|
|
|
|
One of the main differences between L<Mojolicious> and other web frameworks is that it also includes
|
|
L<Mojolicious::Lite>, a micro web framework optimized for rapid prototyping.
|
|
|
|
=head2 Differences
|
|
|
|
You likely know the feeling, you've got a really cool idea and want to try it as quickly as possible, that's exactly
|
|
why L<Mojolicious::Lite> applications don't need more than a single file.
|
|
|
|
myapp.pl # Templates and even static files can be inlined
|
|
|
|
Full L<Mojolicious> applications on the other hand are much closer to a well organized CPAN distribution to maximize
|
|
maintainability.
|
|
|
|
myapp # Application directory
|
|
|- script # Script directory
|
|
| +- my_app # Application script
|
|
|- lib # Library directory
|
|
| |- MyApp.pm # Application class
|
|
| +- MyApp # Application namespace
|
|
| +- Controller # Controller namespace
|
|
| +- Example.pm # Controller class
|
|
|- my_app.yml # Configuration file
|
|
|- t # Test directory
|
|
| +- basic.t # Random test
|
|
|- log # Log directory
|
|
| +- development.log # Development mode log file
|
|
|- public # Static file directory (served automatically)
|
|
| +- index.html # Static HTML file
|
|
+- templates # Template directory
|
|
|- layouts # Template directory for layouts
|
|
| +- default.html.ep # Layout template
|
|
+- example # Template directory for "Example" controller
|
|
+- welcome.html.ep # Template for "welcome" action
|
|
|
|
Both application skeletons can be automatically generated with the commands
|
|
L<Mojolicious::Command::Author::generate::lite_app> and L<Mojolicious::Command::Author::generate::app>.
|
|
|
|
$ mojo generate lite-app myapp.pl
|
|
$ mojo generate app MyApp
|
|
|
|
Feature-wise both are almost equal, the only real differences are organizational, so each one can be gradually
|
|
transformed into the other.
|
|
|
|
=head2 Foundation
|
|
|
|
We start our new application with a single executable Perl script.
|
|
|
|
$ mkdir myapp
|
|
$ cd myapp
|
|
$ touch myapp.pl
|
|
$ chmod 744 myapp.pl
|
|
|
|
This will be the foundation for our login manager example application.
|
|
|
|
#!/usr/bin/env perl
|
|
use Mojolicious::Lite -signatures;
|
|
|
|
get '/' => sub ($c) {
|
|
$c->render(text => 'Hello World!');
|
|
};
|
|
|
|
app->start;
|
|
|
|
The built-in development web server makes working on your application a lot of fun thanks to automatic reloading.
|
|
|
|
$ morbo ./myapp.pl
|
|
Web application available at http://127.0.0.1:3000
|
|
|
|
Just save your changes and they will be automatically in effect the next time you refresh your browser.
|
|
|
|
=head2 A bird's-eye view
|
|
|
|
It all starts with an HTTP request like this, sent by your browser.
|
|
|
|
GET / HTTP/1.1
|
|
Host: localhost:3000
|
|
|
|
Once the request has been received by the web server through the event loop, it will be passed on to L<Mojolicious>,
|
|
where it will be handled in a few simple steps.
|
|
|
|
=over 2
|
|
|
|
=item 1.
|
|
|
|
Check if a static file exists that would meet the requirements.
|
|
|
|
=item 2.
|
|
|
|
Try to find a route that would meet the requirements.
|
|
|
|
=item 3.
|
|
|
|
Dispatch the request to this route, usually reaching one or more actions.
|
|
|
|
=item 4.
|
|
|
|
Process the request, maybe generating a response with the renderer.
|
|
|
|
=item 5.
|
|
|
|
Return control to the web server, and if no response has been generated yet, wait for a non-blocking operation to do so
|
|
through the event loop.
|
|
|
|
=back
|
|
|
|
With our application the router would have found an action in step 2, and rendered some text in step 4, resulting in an
|
|
HTTP response like this being sent back to the browser.
|
|
|
|
HTTP/1.1 200 OK
|
|
Content-Length: 12
|
|
Hello World!
|
|
|
|
=head2 Model
|
|
|
|
In L<Mojolicious> we consider web applications simple frontends for existing business logic, that means L<Mojolicious>
|
|
is by design entirely I<model> layer agnostic and you just use whatever Perl modules you like most.
|
|
|
|
$ mkdir -p lib/MyApp/Model
|
|
$ touch lib/MyApp/Model/Users.pm
|
|
$ chmod 644 lib/MyApp/Model/Users.pm
|
|
|
|
Our login manager will simply use a plain old Perl module abstracting away all logic related to matching usernames and
|
|
passwords. The name C<MyApp::Model::Users> is an arbitrary choice, and is simply used to make the separation of
|
|
concerns more visible.
|
|
|
|
package MyApp::Model::Users;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use experimental qw(signatures);
|
|
|
|
use Mojo::Util qw(secure_compare);
|
|
|
|
my $USERS = {
|
|
joel => 'las3rs',
|
|
marcus => 'lulz',
|
|
sebastian => 'secr3t'
|
|
};
|
|
|
|
sub new { bless {}, shift }
|
|
|
|
sub check ($self, $user, $pass) {
|
|
|
|
# Success
|
|
return 1 if $USERS->{$user} && secure_compare $USERS->{$user}, $pass;
|
|
|
|
# Fail
|
|
return undef;
|
|
}
|
|
|
|
1;
|
|
|
|
A simple helper can be registered with the function L<Mojolicious::Lite/"helper"> to make our model available to all
|
|
actions and templates.
|
|
|
|
#!/usr/bin/env perl
|
|
use Mojolicious::Lite -signatures;
|
|
|
|
use lib qw(lib);
|
|
use MyApp::Model::Users;
|
|
|
|
# Helper to lazy initialize and store our model object
|
|
helper users => sub { state $users = MyApp::Model::Users->new };
|
|
|
|
# /?user=sebastian&pass=secr3t
|
|
any '/' => sub ($c) {
|
|
|
|
# Query parameters
|
|
my $user = $c->param('user') || '';
|
|
my $pass = $c->param('pass') || '';
|
|
|
|
# Check password
|
|
return $c->render(text => "Welcome $user.") if $c->users->check($user, $pass);
|
|
|
|
# Failed
|
|
$c->render(text => 'Wrong username or password.');
|
|
};
|
|
|
|
app->start;
|
|
|
|
The method L<Mojolicious::Controller/"param"> is used to access query parameters, C<POST> parameters, file uploads and
|
|
route placeholders, all at once.
|
|
|
|
=head2 Testing
|
|
|
|
In L<Mojolicious> we take testing very serious and try to make it a pleasant experience.
|
|
|
|
$ mkdir t
|
|
$ touch t/login.t
|
|
$ chmod 644 t/login.t
|
|
|
|
L<Test::Mojo> is a scriptable HTTP user agent designed specifically for testing, with many fun state of the art
|
|
features such as CSS selectors based on L<Mojo::DOM>.
|
|
|
|
use Test::More;
|
|
use Test::Mojo;
|
|
|
|
# Include application
|
|
use Mojo::File qw(curfile);
|
|
require(curfile->dirname->sibling('myapp.pl'));
|
|
|
|
# Allow 302 redirect responses
|
|
my $t = Test::Mojo->new;
|
|
$t->ua->max_redirects(1);
|
|
|
|
# Test if the HTML login form exists
|
|
$t->get_ok('/')
|
|
->status_is(200)
|
|
->element_exists('form input[name="user"]')
|
|
->element_exists('form input[name="pass"]')
|
|
->element_exists('form input[type="submit"]');
|
|
|
|
# Test login with valid credentials
|
|
$t->post_ok('/' => form => {user => 'sebastian', pass => 'secr3t'})
|
|
->status_is(200)
|
|
->text_like('html body' => qr/Welcome sebastian/);
|
|
|
|
# Test accessing a protected page
|
|
$t->get_ok('/protected')->status_is(200)->text_like('a' => qr/Logout/);
|
|
|
|
# Test if HTML login form shows up again after logout
|
|
$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"]');
|
|
|
|
done_testing();
|
|
|
|
Your application won't pass these tests, but from now on you can use them to check your progress.
|
|
|
|
$ prove -l
|
|
$ prove -l t/login.t
|
|
$ prove -l -v t/login.t
|
|
|
|
Or perform quick requests right from the command line with L<Mojolicious::Command::get>.
|
|
|
|
$ ./myapp.pl get /
|
|
Wrong username or password.
|
|
|
|
$ ./myapp.pl get -v '/?user=sebastian&pass=secr3t'
|
|
GET /?user=sebastian&pass=secr3t HTTP/1.1
|
|
User-Agent: Mojolicious (Perl)
|
|
Accept-Encoding: gzip
|
|
Content-Length: 0
|
|
Host: localhost:59472
|
|
|
|
HTTP/1.1 200 OK
|
|
Date: Sun, 18 Jul 2010 13:09:58 GMT
|
|
Server: Mojolicious (Perl)
|
|
Content-Length: 12
|
|
Content-Type: text/plain
|
|
|
|
Welcome sebastian.
|
|
|
|
=head2 State keeping
|
|
|
|
Sessions in L<Mojolicious> pretty much just work out of the box once you start using the method
|
|
L<Mojolicious::Controller/"session">, there is no setup required, but we suggest setting a more secure passphrase with
|
|
L<Mojolicious/"secrets">.
|
|
|
|
$app->secrets(['Mojolicious rocks']);
|
|
|
|
This passphrase is used by the HMAC-SHA1 algorithm to make signed cookies tamper resistant and can be changed at any
|
|
time to invalidate all existing sessions.
|
|
|
|
$c->session(user => 'sebastian');
|
|
my $user = $c->session('user');
|
|
|
|
By default all sessions expire after one hour, for more control you can use the C<expiration> session value to set an
|
|
expiration date in seconds from now.
|
|
|
|
$c->session(expiration => 3600);
|
|
|
|
And the whole session can be deleted by using the C<expires> session value to set an absolute expiration date in the
|
|
past.
|
|
|
|
$c->session(expires => 1);
|
|
|
|
For data that should only be visible on the next request, like a confirmation message after a C<302> redirect performed
|
|
with L<Mojolicious::Plugin::DefaultHelpers/"redirect_to">, you can use the flash, accessible through
|
|
L<Mojolicious::Plugin::DefaultHelpers/"flash">.
|
|
|
|
$c->flash(message => 'Everything is fine.');
|
|
$c->redirect_to('goodbye');
|
|
|
|
Just remember that all session data gets serialized with L<Mojo::JSON> and stored in HMAC-SHA1 signed cookies, which
|
|
usually have a C<4096> byte (4KiB) limit, depending on browser.
|
|
|
|
=head2 Final prototype
|
|
|
|
A final C<myapp.pl> prototype passing all of the tests above could look like this.
|
|
|
|
#!/usr/bin/env perl
|
|
use Mojolicious::Lite -signatures;
|
|
|
|
use lib qw(lib);
|
|
use MyApp::Model::Users;
|
|
|
|
# Make signed cookies tamper resistant
|
|
app->secrets(['Mojolicious rocks']);
|
|
|
|
helper users => sub { state $users = MyApp::Model::Users->new };
|
|
|
|
# Main login action
|
|
any '/' => sub ($c) {
|
|
|
|
# Query or POST parameters
|
|
my $user = $c->param('user') || '';
|
|
my $pass = $c->param('pass') || '';
|
|
|
|
# Check password and render "index.html.ep" if necessary
|
|
return $c->render unless $c->users->check($user, $pass);
|
|
|
|
# Store username in session
|
|
$c->session(user => $user);
|
|
|
|
# Store a friendly message for the next page in flash
|
|
$c->flash(message => 'Thanks for logging in.');
|
|
|
|
# Redirect to protected page with a 302 response
|
|
$c->redirect_to('protected');
|
|
} => 'index';
|
|
|
|
# Make sure user is logged in for actions in this group
|
|
group {
|
|
under sub ($c) {
|
|
|
|
# Redirect to main page with a 302 response if user is not logged in
|
|
return 1 if $c->session('user');
|
|
$c->redirect_to('index');
|
|
return undef;
|
|
};
|
|
|
|
# A protected page auto rendering "protected.html.ep"
|
|
get '/protected';
|
|
};
|
|
|
|
# Logout action
|
|
get '/logout' => sub ($c) {
|
|
|
|
# Expire and in turn clear session automatically
|
|
$c->session(expires => 1);
|
|
|
|
# Redirect to main page with a 302 response
|
|
$c->redirect_to('index');
|
|
};
|
|
|
|
app->start;
|
|
__DATA__
|
|
|
|
@@ index.html.ep
|
|
% layout 'default';
|
|
%= form_for index => begin
|
|
% if (param 'user') {
|
|
<b>Wrong name or password, please try again.</b><br>
|
|
% }
|
|
Name:<br>
|
|
%= text_field 'user'
|
|
<br>Password:<br>
|
|
%= password_field 'pass'
|
|
<br>
|
|
%= submit_button 'Login'
|
|
% end
|
|
|
|
@@ protected.html.ep
|
|
% layout 'default';
|
|
% if (my $msg = flash 'message') {
|
|
<b><%= $msg %></b><br>
|
|
% }
|
|
Welcome <%= session 'user' %>.<br>
|
|
%= link_to Logout => 'logout'
|
|
|
|
@@ layouts/default.html.ep
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head><title>Login Manager</title></head>
|
|
<body><%= content %></body>
|
|
</html>
|
|
|
|
And the directory structure should be looking like this now.
|
|
|
|
myapp
|
|
|- myapp.pl
|
|
|- lib
|
|
| +- MyApp
|
|
| +- Model
|
|
| +- Users.pm
|
|
+- t
|
|
+- login.t
|
|
|
|
Our templates are using quite a few features of the renderer, L<Mojolicious::Guides::Rendering> explains them all in
|
|
great detail.
|
|
|
|
=head1 WELL-STRUCTURED APPLICATION
|
|
|
|
Due to the flexibility of L<Mojolicious> there are many variations of the actual growing process, but this should give
|
|
you a good overview of the possibilities.
|
|
|
|
=head2 Inflating templates
|
|
|
|
All templates and static files inlined in the C<DATA> section can be automatically turned into separate files in the
|
|
C<templates> and C<public> directories with the command L<Mojolicious::Command::Author::inflate>.
|
|
|
|
$ ./myapp.pl inflate
|
|
|
|
Those directories have a higher precedence, so inflating can also be a great way to allow your users to customize their
|
|
applications.
|
|
|
|
=head2 Simplified application class
|
|
|
|
This is the heart of every full L<Mojolicious> application and always gets instantiated during server startup.
|
|
|
|
$ touch lib/MyApp.pm
|
|
$ chmod 644 lib/MyApp.pm
|
|
|
|
We will start by extracting all actions from C<myapp.pl> and turn them into simplified hybrid routes in the
|
|
L<Mojolicious::Routes> router, none of the actual action code needs to be changed.
|
|
|
|
package MyApp;
|
|
use Mojo::Base 'Mojolicious', -signatures;
|
|
|
|
use MyApp::Model::Users;
|
|
|
|
sub startup ($self) {
|
|
|
|
$self->secrets(['Mojolicious rocks']);
|
|
$self->helper(users => sub { state $users = MyApp::Model::Users->new });
|
|
|
|
my $r = $self->routes;
|
|
|
|
$r->any('/' => sub ($c) {
|
|
|
|
my $user = $c->param('user') || '';
|
|
my $pass = $c->param('pass') || '';
|
|
return $c->render unless $c->users->check($user, $pass);
|
|
|
|
$c->session(user => $user);
|
|
$c->flash(message => 'Thanks for logging in.');
|
|
$c->redirect_to('protected');
|
|
} => 'index');
|
|
|
|
my $logged_in = $r->under(sub ($c) {
|
|
return 1 if $c->session('user');
|
|
$c->redirect_to('index');
|
|
return undef;
|
|
});
|
|
$logged_in->get('/protected');
|
|
|
|
$r->get('/logout' => sub ($c) {
|
|
$c->session(expires => 1);
|
|
$c->redirect_to('index');
|
|
});
|
|
}
|
|
|
|
1;
|
|
|
|
The C<startup> method gets called right after instantiation and is the place where the whole application gets set up.
|
|
Since full L<Mojolicious> applications can use nested routes they have no need for C<group> blocks.
|
|
|
|
=head2 Simplified application script
|
|
|
|
C<myapp.pl> itself can now be turned into a simplified application script to allow running tests again.
|
|
|
|
#!/usr/bin/env perl
|
|
|
|
use Mojo::Base -strict;
|
|
use lib qw(lib);
|
|
use Mojolicious::Commands;
|
|
|
|
# Start command line interface for application
|
|
Mojolicious::Commands->start_app('MyApp');
|
|
|
|
And the directory structure of our hybrid application should be looking like this.
|
|
|
|
myapp
|
|
|- myapp.pl
|
|
|- lib
|
|
| |- MyApp.pm
|
|
| +- MyApp
|
|
| +- Model
|
|
| +- Users.pm
|
|
|- t
|
|
| +- login.t
|
|
+- templates
|
|
|- layouts
|
|
| +- default.html.ep
|
|
|- index.html.ep
|
|
+- protected.html.ep
|
|
|
|
=head2 Controller class
|
|
|
|
Hybrid routes are a nice intermediate step, but to maximize maintainability it makes sense to split our action code
|
|
from its routing information.
|
|
|
|
$ mkdir lib/MyApp/Controller
|
|
$ touch lib/MyApp/Controller/Login.pm
|
|
$ chmod 644 lib/MyApp/Controller/Login.pm
|
|
|
|
Once again the actual action code does not need to change, we just rename C<$c> to C<$self> since the controller is now
|
|
the invocant.
|
|
|
|
package MyApp::Controller::Login;
|
|
use Mojo::Base 'Mojolicious::Controller', -signatures;
|
|
|
|
sub index ($self) {
|
|
my $user = $self->param('user') || '';
|
|
my $pass = $self->param('pass') || '';
|
|
return $self->render unless $self->users->check($user, $pass);
|
|
|
|
$self->session(user => $user);
|
|
$self->flash(message => 'Thanks for logging in.');
|
|
$self->redirect_to('protected');
|
|
}
|
|
|
|
sub logged_in ($self) {
|
|
return 1 if $self->session('user');
|
|
$self->redirect_to('index');
|
|
return undef;
|
|
}
|
|
|
|
sub logout ($self) {
|
|
$self->session(expires => 1);
|
|
$self->redirect_to('index');
|
|
}
|
|
|
|
1;
|
|
|
|
All L<Mojolicious::Controller> controllers are plain old Perl classes and get instantiated on demand.
|
|
|
|
=head2 Application class
|
|
|
|
The application class C<lib/MyApp.pm> can now be reduced to model and routing information.
|
|
|
|
package MyApp;
|
|
use Mojo::Base 'Mojolicious', -signatures;
|
|
|
|
use MyApp::Model::Users;
|
|
|
|
sub startup ($self) {
|
|
|
|
$self->secrets(['Mojolicious rocks']);
|
|
$self->helper(users => sub { state $users = MyApp::Model::Users->new });
|
|
|
|
my $r = $self->routes;
|
|
$r->any('/')->to('login#index')->name('index');
|
|
|
|
my $logged_in = $r->under('/')->to('login#logged_in');
|
|
$logged_in->get('/protected')->to('login#protected');
|
|
|
|
$r->get('/logout')->to('login#logout');
|
|
}
|
|
|
|
1;
|
|
|
|
The router allows many different route variations, L<Mojolicious::Guides::Routing> explains them all in great detail.
|
|
|
|
=head2 Templates
|
|
|
|
Templates are our views, and usually bound to controllers, so they need to be moved into the appropriate directories.
|
|
|
|
$ mkdir templates/login
|
|
$ mv templates/index.html.ep templates/login/index.html.ep
|
|
$ mv templates/protected.html.ep templates/login/protected.html.ep
|
|
|
|
=head2 Script
|
|
|
|
Finally C<myapp.pl> can be moved into a C<script> directory and renamed to C<my_app> to follow the CPAN standard.
|
|
|
|
$ mkdir script
|
|
$ mv myapp.pl script/my_app
|
|
|
|
Just a few small details change, instead of a relative path to L<lib> we now use L<Mojo::File> to get an absolute path,
|
|
allowing us to start the application from outside its home directory.
|
|
|
|
#!/usr/bin/env perl
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Mojo::File qw(curfile);
|
|
use lib curfile->dirname->sibling('lib')->to_string;
|
|
use Mojolicious::Commands;
|
|
|
|
# Start command line interface for application
|
|
Mojolicious::Commands->start_app('MyApp');
|
|
|
|
=head2 Simplified tests
|
|
|
|
Full L<Mojolicious> applications are a little easier to test, so C<t/login.t> can be simplified.
|
|
|
|
use Test::More;
|
|
use Test::Mojo;
|
|
|
|
# Load application class
|
|
my $t = Test::Mojo->new('MyApp');
|
|
$t->ua->max_redirects(1);
|
|
|
|
$t->get_ok('/')
|
|
->status_is(200)
|
|
->element_exists('form input[name="user"]')
|
|
->element_exists('form input[name="pass"]')
|
|
->element_exists('form input[type="submit"]');
|
|
|
|
$t->post_ok('/' => form => {user => 'sebastian', pass => 'secr3t'})
|
|
->status_is(200)
|
|
->text_like('html body' => qr/Welcome sebastian/);
|
|
|
|
$t->get_ok('/protected')->status_is(200)->text_like('a' => qr/Logout/);
|
|
|
|
$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"]');
|
|
|
|
done_testing();
|
|
|
|
And our final directory structure should be looking like this.
|
|
|
|
myapp
|
|
|- script
|
|
| +- my_app
|
|
|- lib
|
|
| |- MyApp.pm
|
|
| +- MyApp
|
|
| |- Controller
|
|
| | +- Login.pm
|
|
| +- Model
|
|
| +- Users.pm
|
|
|- t
|
|
| +- login.t
|
|
+- templates
|
|
|- layouts
|
|
| +- default.html.ep
|
|
+- login
|
|
|- index.html.ep
|
|
+- protected.html.ep
|
|
|
|
Test-driven development takes a little getting used to, but can be a very powerful tool.
|
|
|
|
=head1 MORE
|
|
|
|
You can continue with L<Mojolicious::Guides> now or take a look at the L<Mojolicious
|
|
wiki|https://github.com/mojolicious/mojo/wiki>, which contains a lot more documentation and examples by many different
|
|
authors.
|
|
|
|
=head1 SUPPORT
|
|
|
|
If you have any questions the documentation might not yet answer, don't hesitate to ask in the
|
|
L<Forum|https://forum.mojolicious.org> or the official IRC channel C<#mojo> on C<chat.freenode.net>
|
|
(L<chat now!|https://webchat.freenode.net/#mojo>).
|
|
|
|
=cut
|