Initial Commit
This commit is contained in:
771
database/perl/vendor/lib/Mojolicious/Guides/Growing.pod
vendored
Normal file
771
database/perl/vendor/lib/Mojolicious/Guides/Growing.pod
vendored
Normal file
@@ -0,0 +1,771 @@
|
||||
|
||||
=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
|
||||
Reference in New Issue
Block a user