add files
|
1 | |
2 |
=encoding utf8 |
|
3 | ||
4 |
=head1 NAME |
|
5 | ||
6 |
Mojolicious::Guides::Growing - Growing |
|
7 | ||
8 |
=head1 OVERVIEW |
|
9 | ||
10 |
This document explains the process of starting a L<Mojolicious::Lite> |
|
11 |
prototype from scratch and growing it into a well structured L<Mojolicious> |
|
12 |
application. |
|
13 | ||
14 |
=head1 CONCEPTS |
|
15 | ||
16 |
Essentials every L<Mojolicious> developer should know. |
|
17 | ||
18 |
=head2 Model View Controller |
|
19 | ||
20 |
MVC is a software architectural pattern for graphical user interface |
|
21 |
programming originating in Smalltalk-80, that separates application logic, |
|
22 |
presentation and input. |
|
23 | ||
24 |
+------------+ +-------+ +------+ |
|
25 |
Input -> | Controller | -> | Model | -> | View | -> Output |
|
26 |
+------------+ +-------+ +------+ |
|
27 | ||
28 |
A slightly modified version of the pattern moving some application logic into |
|
29 |
the C<controller> is the foundation of pretty much every web framework these |
|
30 |
days, including L<Mojolicious>. |
|
31 | ||
32 |
+----------------+ +-------+ |
|
33 |
Request -> | | <-> | Model | |
|
34 |
| | +-------+ |
|
35 |
| Controller | |
|
36 |
| | +-------+ |
|
37 |
Response <- | | <-> | View | |
|
38 |
+----------------+ +-------+ |
|
39 | ||
40 |
The C<controller> receives a request from a user, passes incoming data to the |
|
41 |
C<model> and retrieves data from it, which then gets turned into an actual |
|
42 |
response by the C<view>. But note that this pattern is just a guideline that |
|
43 |
most of the time results in cleaner more maintainable code, not a rule that |
|
44 |
should be followed at all costs. |
|
45 | ||
46 |
=head2 REpresentational State Transfer |
|
47 | ||
48 |
REST is a software architectural style for distributed hypermedia systems such |
|
49 |
as the web. While it can be applied to many protocols it is most commonly used |
|
50 |
with HTTP these days. In REST terms, when you are opening a URL like |
|
51 |
C<http://mojolicio.us/foo> with your browser, you are basically asking the web |
|
52 |
server for the HTML C<representation> of the C<http://mojolicio.us/foo> |
|
53 |
C<resource>. |
|
54 | ||
55 |
+--------+ +--------+ |
|
56 |
| | -> http://mojolicio.us/foo -> | | |
|
57 |
| Client | | Server | |
|
58 |
| | <- <html>Mojo rocks!</html> <- | | |
|
59 |
+--------+ +--------+ |
|
60 | ||
61 |
The fundamental idea here is that all resources are uniquely addressable with |
|
62 |
URLs and every resource can have different representations such as HTML, RSS |
|
63 |
or JSON. User interface concerns are separated from data storage concerns and |
|
64 |
all session state is kept client-side. |
|
65 | ||
66 |
+---------+ +------------+ |
|
67 |
| | -> PUT /foo -> | | |
|
68 |
| | -> Hello world! -> | | |
|
69 |
| | | | |
|
70 |
| | <- 201 CREATED <- | | |
|
71 |
| | | | |
|
72 |
| | -> GET /foo -> | | |
|
73 |
| Browser | | Web Server | |
|
74 |
| | <- 200 OK <- | | |
|
75 |
| | <- Hello world! <- | | |
|
76 |
| | | | |
|
77 |
| | -> DELETE /foo -> | | |
|
78 |
| | | | |
|
79 |
| | <- 200 OK <- | | |
|
80 |
+---------+ +------------+ |
|
81 | ||
82 |
While HTTP methods such as PUT, GET and DELETE are not directly part of REST |
|
83 |
they go very well with it and are commonly used to manipulate C<resources>. |
|
84 | ||
85 |
=head2 Sessions |
|
86 | ||
87 |
HTTP was designed as a stateless protocol, web servers don't know anything |
|
88 |
about previous requests, which makes user-friendly login systems very tricky. |
|
89 |
Sessions solve this problem by allowing web applications to keep stateful |
|
90 |
information across several HTTP requests. |
|
91 | ||
92 |
GET /login?user=sri&pass=s3cret HTTP/1.1 |
|
93 |
Host: mojolicio.us |
|
94 | ||
95 |
HTTP/1.1 200 OK |
|
96 |
Set-Cookie: sessionid=987654321 |
|
97 |
Content-Length: 10 |
|
98 |
Hello sri. |
|
99 | ||
100 |
GET /protected HTTP/1.1 |
|
101 |
Host: mojolicio.us |
|
102 |
Cookie: $Version=1; sessionid=987654321 |
|
103 | ||
104 |
HTTP/1.1 200 OK |
|
105 |
Set-Cookie: sessionid=987654321 |
|
106 |
Content-Length: 16 |
|
107 |
Hello again sri. |
|
108 | ||
109 |
Traditionally all session data was stored on the server-side and only session |
|
110 |
ids were exchanged between browser and web server in the form of cookies. |
|
111 | ||
112 |
HTTP/1.1 200 OK |
|
113 |
Set-Cookie: session=hmac-sha1(base64(json($session))) |
|
114 | ||
115 |
In L<Mojolicious> however we are taking this concept one step further by |
|
116 |
storing everything C<JSON> serialized and C<Base64> encoded in C<HMAC-SHA1> |
|
117 |
signed cookies, which is more compatible with the REST philosophy and reduces |
|
118 |
infrastructure requirements. |
|
119 | ||
120 |
=head2 Test Driven Development |
|
121 | ||
122 |
TDD is a software development process where the developer starts writing |
|
123 |
failing test cases that define the desired functionality and then moves on to |
|
124 |
producing code that passes these tests. There are many advantages such as |
|
125 |
always having good test coverage and code being designed for testability, |
|
126 |
which will in turn often prevent future changes from breaking old code. Most |
|
127 |
of L<Mojolicious> was developed using TDD. |
|
128 | ||
129 |
=head1 PROTOTYPE |
|
130 | ||
131 |
One of the main differences between L<Mojolicious> and other web frameworks is |
|
132 |
that it also includes L<Mojolicious::Lite>, a micro web framework optimized |
|
133 |
for rapid prototyping. |
|
134 | ||
135 |
=head2 Differences |
|
136 | ||
137 |
You likely know the feeling, you've got a really cool idea and want to try it |
|
138 |
as quickly as possible, that's exactly why L<Mojolicious::Lite> applications |
|
139 |
don't need more than a single file. |
|
140 | ||
141 |
myapp.pl # Templates and even static files can be inlined |
|
142 | ||
143 |
Full L<Mojolicious> applications on the other hand are much closer to a well |
|
144 |
organized CPAN distribution to maximize maintainability. |
|
145 | ||
146 |
myapp # Application directory |
|
147 |
|- script # Script directory |
|
148 |
| +- myapp # Application script |
|
149 |
|- lib # Library directory |
|
150 |
| |- MyApp.pm # Application class |
|
151 |
| +- MyApp # Application namespace |
|
152 |
| +- Example.pm # Controller class |
|
153 |
|- t # Test directory |
|
154 |
| +- basic.t # Random test |
|
155 |
|- log # Log directory |
|
156 |
| +- development.log # Development mode log file |
|
157 |
|- public # Static file directory (served automatically) |
|
158 |
| +- index.html # Static HTML file |
|
159 |
+- templates # Template directory |
|
160 |
|- layouts # Template directory for layouts |
|
161 |
| +- default.html.ep # Layout template |
|
162 |
+- example # Template directory for "Example" controller |
|
163 |
+- welcome.html.ep # Template for "welcome" action |
|
164 | ||
165 |
Both application skeletons can be automatically generated. |
|
166 | ||
167 |
$ mojo generate lite_app myapp.pl |
|
168 |
$ mojo generate app MyApp |
|
169 | ||
170 |
=head2 Foundation |
|
171 | ||
172 |
We start our new application with a single executable Perl script. |
|
173 | ||
174 |
$ mkdir myapp |
|
175 |
$ cd myapp |
|
176 |
$ touch myapp.pl |
|
177 |
$ chmod 744 myapp.pl |
|
178 | ||
179 |
This will be the foundation for our login manager example application. |
|
180 | ||
181 |
#!/usr/bin/env perl |
|
182 |
use Mojolicious::Lite; |
|
183 | ||
184 |
get '/' => sub { |
|
185 |
my $self = shift; |
|
186 |
$self->render(text => 'Hello world!'); |
|
187 |
}; |
|
188 | ||
189 |
app->start; |
|
190 | ||
191 |
The built-in development web server makes working on your application a lot of |
|
192 |
fun thanks to automatic reloading. |
|
193 | ||
194 |
$ morbo myapp.pl |
|
195 |
Server available at http://127.0.0.1:3000. |
|
196 | ||
197 |
Just save your changes and they will be automatically in effect the next time |
|
198 |
you refresh your browser. |
|
199 | ||
200 |
=head2 Model |
|
201 | ||
202 |
In L<Mojolicious> we consider web applications simple frontends for existing |
|
203 |
business logic, that means L<Mojolicious> is by design entirely L<model> layer |
|
204 |
agnostic and you just use whatever Perl modules you like most. |
|
205 | ||
206 |
$ mkdir lib |
|
207 |
$ touch lib/MyUsers.pm |
|
208 |
$ chmod 644 lib/MyUsers.pm |
|
209 | ||
210 |
Our login manager will simply use a plain old Perl module abstracting away all |
|
211 |
logic related to matching usernames and passwords. |
|
212 | ||
213 |
package MyUsers; |
|
214 | ||
215 |
use strict; |
|
216 |
use warnings; |
|
217 | ||
218 |
my $USERS = { |
|
219 |
sri => 'secr3t', |
|
220 |
marcus => 'lulz', |
|
221 |
yko => 'zeecaptain' |
|
222 |
}; |
|
223 | ||
224 |
sub new { bless {}, shift } |
|
225 | ||
226 |
sub check { |
|
227 |
my ($self, $user, $pass) = @_; |
|
228 | ||
229 |
# Success |
|
230 |
return 1 if $USERS->{$user} && $USERS->{$user} eq $pass; |
|
231 | ||
232 |
# Fail |
|
233 |
return undef; |
|
234 |
} |
|
235 | ||
236 |
1; |
|
237 | ||
238 |
A simple helper can be registered with the function |
|
239 |
L<Mojolicious::Lite/"helper"> to make our C<model> available to all actions |
|
240 |
and templates. |
|
241 | ||
242 |
#!/usr/bin/env perl |
|
243 |
use Mojolicious::Lite; |
|
244 | ||
245 |
use lib 'lib'; |
|
246 |
use MyUsers; |
|
247 | ||
248 |
# Helper to lazy initialize and store our model object |
|
249 |
helper users => sub { state $users = MyUsers->new }; |
|
250 | ||
251 |
# /?user=sri&pass=secr3t |
|
252 |
any '/' => sub { |
|
253 |
my $self = shift; |
|
254 | ||
255 |
# Query parameters |
|
256 |
my $user = $self->param('user') || ''; |
|
257 |
my $pass = $self->param('pass') || ''; |
|
258 | ||
259 |
# Check password |
|
260 |
return $self->render(text => "Welcome $user.") |
|
261 |
if $self->users->check($user, $pass); |
|
262 | ||
263 |
# Failed |
|
264 |
$self->render(text => 'Wrong username or password.'); |
|
265 |
}; |
|
266 | ||
267 |
app->start; |
|
268 | ||
269 |
The method L<Mojolicious::Controller/"param"> is used to access query |
|
270 |
parameters, POST parameters, file uploads and route placeholders, all at once. |
|
271 | ||
272 |
=head2 Testing |
|
273 | ||
274 |
In L<Mojolicious> we take test driven development very serious and try to |
|
275 |
promote it wherever we can. |
|
276 | ||
277 |
$ mkdir t |
|
278 |
$ touch t/login.t |
|
279 |
$ chmod 644 t/login.t |
|
280 | ||
281 |
L<Test::Mojo> is a scriptable HTTP user agent designed specifically for |
|
282 |
testing, with many fun state of the art features such as CSS selectors based |
|
283 |
on L<Mojo::DOM>. |
|
284 | ||
285 |
use Test::More; |
|
286 |
use Test::Mojo; |
|
287 | ||
288 |
# Include application |
|
289 |
use FindBin; |
|
290 |
require "$FindBin::Bin/../myapp.pl"; |
|
291 | ||
292 |
# Allow 302 redirect responses |
|
293 |
my $t = Test::Mojo->new; |
|
294 |
$t->ua->max_redirects(1); |
|
295 | ||
296 |
# Test if the HTML login form exists |
|
297 |
$t->get_ok('/') |
|
298 |
->status_is(200) |
|
299 |
->element_exists('form input[name="user"]') |
|
300 |
->element_exists('form input[name="pass"]') |
|
301 |
->element_exists('form input[type="submit"]'); |
|
302 | ||
303 |
# Test login with valid credentials |
|
304 |
$t->post_ok('/' => form => {user => 'sri', pass => 'secr3t'}) |
|
305 |
->status_is(200)->text_like('html body' => qr/Welcome sri/); |
|
306 | ||
307 |
# Test accessing a protected page |
|
308 |
$t->get_ok('/protected')->status_is(200)->text_like('a' => qr/Logout/); |
|
309 | ||
310 |
# Test if HTML login form shows up again after logout |
|
311 |
$t->get_ok('/logout')->status_is(200) |
|
312 |
->element_exists('form input[name="user"]') |
|
313 |
->element_exists('form input[name="pass"]') |
|
314 |
->element_exists('form input[type="submit"]'); |
|
315 | ||
316 |
done_testing(); |
|
317 | ||
318 |
From now on you can always check your progress by running these unit tests |
|
319 |
against your application. |
|
320 | ||
321 |
$ ./myapp.pl test |
|
322 |
$ ./myapp.pl test t/login.t |
|
323 |
$ ./myapp.pl test -v t/login.t |
|
324 | ||
325 |
Or perform quick requests right from the command line. |
|
326 | ||
327 |
$ ./myapp.pl get / |
|
328 |
Wrong username or password. |
|
329 | ||
330 |
$ ./myapp.pl get -v '/?user=sri&pass=secr3t' |
|
331 |
GET /?user=sri&pass=secr3t HTTP/1.1 |
|
332 |
User-Agent: Mojolicious (Perl) |
|
333 |
Connection: keep-alive |
|
334 |
Accept-Encoding: gzip |
|
335 |
Content-Length: 0 |
|
336 |
Host: localhost:59472 |
|
337 | ||
338 |
HTTP/1.1 200 OK |
|
339 |
Connection: keep-alive |
|
340 |
Date: Sun, 18 Jul 2010 13:09:58 GMT |
|
341 |
Server: Mojolicious (Perl) |
|
342 |
Content-Length: 12 |
|
343 |
Content-Type: text/plain |
|
344 | ||
345 |
Welcome sri. |
|
346 | ||
347 |
=head2 State keeping |
|
348 | ||
349 |
Sessions in L<Mojolicious> pretty much just work out of the box once you start |
|
350 |
using the method L<Mojolicious::Controller/"session">, there is no setup |
|
351 |
required, but we suggest setting a more secure passphrase with |
|
352 |
L<Mojolicious/"secret">. |
|
353 | ||
354 |
app->secret('Mojolicious rocks'); |
|
355 | ||
356 |
This passphrase is used by the C<HMAC-SHA1> algorithm to make signed cookies |
|
357 |
secure and can be changed at any time to invalidate all existing sessions. |
|
358 | ||
359 |
$self->session(user => 'sri'); |
|
360 |
my $user = $self->session('user'); |
|
361 | ||
362 |
By default all sessions expire after one hour, for more control you can use |
|
363 |
the C<expiration> session value to set an expiration date in seconds from now. |
|
364 | ||
365 |
$self->session(expiration => 3600); |
|
366 | ||
367 |
And the whole session can be deleted by using the C<expires> session value to |
|
368 |
set an absolute expiration date in the past. |
|
369 | ||
370 |
$self->session(expires => 1); |
|
371 | ||
372 |
For data that should only be visible on the next request, like a confirmation |
|
373 |
message after a 302 redirect, you can use the flash, accessible through the |
|
374 |
method L<Mojolicious::Controller/"flash">. |
|
375 | ||
376 |
$self->flash(message => 'Everything is fine.'); |
|
377 |
$self->redirect_to('goodbye'); |
|
378 | ||
379 |
Just remember that all session data gets serialized with L<Mojo::JSON> and |
|
380 |
stored in C<HMAC-SHA1> signed cookies, which usually have a 4096 byte limit, |
|
381 |
depending on browser. |
|
382 | ||
383 |
=head2 Final prototype |
|
384 | ||
385 |
A final C<myapp.pl> prototype passing all of the unit tests above could look |
|
386 |
like this. |
|
387 | ||
388 |
#!/usr/bin/env perl |
|
389 |
use Mojolicious::Lite; |
|
390 | ||
391 |
use lib 'lib'; |
|
392 |
use MyUsers; |
|
393 | ||
394 |
# Make signed cookies secure |
|
395 |
app->secret('Mojolicious rocks'); |
|
396 | ||
397 |
helper users => sub { state $users = MyUsers->new }; |
|
398 | ||
399 |
# Main login action |
|
400 |
any '/' => sub { |
|
401 |
my $self = shift; |
|
402 | ||
403 |
# Query or POST parameters |
|
404 |
my $user = $self->param('user') || ''; |
|
405 |
my $pass = $self->param('pass') || ''; |
|
406 | ||
407 |
# Check password and render "index.html.ep" if necessary |
|
408 |
return $self->render unless $self->users->check($user, $pass); |
|
409 | ||
410 |
# Store username in session |
|
411 |
$self->session(user => $user); |
|
412 | ||
413 |
# Store a friendly message for the next page in flash |
|
414 |
$self->flash(message => 'Thanks for logging in.'); |
|
415 | ||
416 |
# Redirect to protected page with a 302 response |
|
417 |
$self->redirect_to('protected'); |
|
418 |
} => 'index'; |
|
419 | ||
420 |
# Make sure user is logged in for actions in this group |
|
421 |
group { |
|
422 |
under sub { |
|
423 |
my $self = shift; |
|
424 | ||
425 |
# Redirect to main page with a 302 response if user is not logged in |
|
426 |
return $self->session('user') || !$self->redirect_to('index'); |
|
427 |
}; |
|
428 | ||
429 |
# A protected page auto rendering "protected.html.ep" |
|
430 |
get '/protected'; |
|
431 |
}; |
|
432 | ||
433 |
# Logout action |
|
434 |
get '/logout' => sub { |
|
435 |
my $self = shift; |
|
436 | ||
437 |
# Expire and in turn clear session automatically |
|
438 |
$self->session(expires => 1); |
|
439 | ||
440 |
# Redirect to main page with a 302 response |
|
441 |
$self->redirect_to('index'); |
|
442 |
}; |
|
443 | ||
444 |
app->start; |
|
445 |
__DATA__ |
|
446 | ||
447 |
@@ index.html.ep |
|
448 |
% layout 'default'; |
|
449 |
%= form_for index => begin |
|
450 |
% if (param 'user') { |
|
451 |
<b>Wrong name or password, please try again.</b><br> |
|
452 |
% } |
|
453 |
Name:<br> |
|
454 |
%= text_field 'user' |
|
455 |
<br>Password:<br> |
|
456 |
%= password_field 'pass' |
|
457 |
<br> |
|
458 |
%= submit_button 'Login' |
|
459 |
% end |
|
460 | ||
461 |
@@ protected.html.ep |
|
462 |
% layout 'default'; |
|
463 |
% if (my $msg = flash 'message') { |
|
464 |
<b><%= $msg %></b><br> |
|
465 |
% } |
|
466 |
Welcome <%= session 'user' %>.<br> |
|
467 |
%= link_to Logout => 'logout' |
|
468 | ||
469 |
@@ layouts/default.html.ep |
|
470 |
<!DOCTYPE html> |
|
471 |
<html> |
|
472 |
<head><title>Login Manager</title></head> |
|
473 |
<body><%= content %></body> |
|
474 |
</html> |
|
475 | ||
476 |
A list of all built-in helpers can be found in |
|
477 |
L<Mojolicious::Plugin::DefaultHelpers> and L<Mojolicious::Plugin::TagHelpers>. |
|
478 | ||
479 |
=head1 WELL STRUCTURED APPLICATION |
|
480 | ||
481 |
Due to the flexibility of L<Mojolicious> there are many variations of the |
|
482 |
actual growing process, but this should give you a good overview of the |
|
483 |
possibilities. |
|
484 | ||
485 |
=head2 Inflating templates |
|
486 | ||
487 |
All templates and static files inlined in the C<DATA> section can be |
|
488 |
automatically turned into separate files in the C<templates> and C<public> |
|
489 |
directories. |
|
490 | ||
491 |
$ ./myapp.pl inflate |
|
492 | ||
493 |
Those directories always get priority, so inflating can also be a great way to |
|
494 |
allow your users to customize their applications. |
|
495 | ||
496 |
=head2 Simplified application class |
|
497 | ||
498 |
This is the heart of every full L<Mojolicious> application and always gets |
|
499 |
instantiated during server startup. |
|
500 | ||
501 |
$ touch lib/MyApp.pm |
|
502 |
$ chmod 644 lib/MyApp.pm |
|
503 | ||
504 |
We will start by extracting all actions from C<myapp.pl> and turn them into |
|
505 |
simplified hybrid routes in the L<Mojolicious::Routes> router, none of the |
|
506 |
actual action code needs to be changed. |
|
507 | ||
508 |
package MyApp; |
|
509 |
use Mojo::Base 'Mojolicious'; |
|
510 | ||
511 |
use MyUsers; |
|
512 | ||
513 |
sub startup { |
|
514 |
my $self = shift; |
|
515 | ||
516 |
$self->secret('Mojolicious rocks'); |
|
517 |
$self->helper(users => sub { state $users = MyUsers->new }); |
|
518 | ||
519 |
my $r = $self->routes; |
|
520 | ||
521 |
$r->any('/' => sub { |
|
522 |
my $self = shift; |
|
523 | ||
524 |
my $user = $self->param('user') || ''; |
|
525 |
my $pass = $self->param('pass') || ''; |
|
526 |
return $self->render unless $self->users->check($user, $pass); |
|
527 | ||
528 |
$self->session(user => $user); |
|
529 |
$self->flash(message => 'Thanks for logging in.'); |
|
530 |
$self->redirect_to('protected'); |
|
531 |
} => 'index'); |
|
532 | ||
533 |
my $logged_in = $r->under(sub { |
|
534 |
my $self = shift; |
|
535 |
return $self->session('user') || !$self->redirect_to('index'); |
|
536 |
}); |
|
537 |
$logged_in->get('/protected'); |
|
538 | ||
539 |
$r->get('/logout' => sub { |
|
540 |
my $self = shift; |
|
541 |
$self->session(expires => 1); |
|
542 |
$self->redirect_to('index'); |
|
543 |
}); |
|
544 |
} |
|
545 | ||
546 |
1; |
|
547 | ||
548 |
The C<startup> method gets called right after instantiation and is the place |
|
549 |
where the whole application gets set up. Since full L<Mojolicious> |
|
550 |
applications can use nested routes they have no need for C<group> blocks. |
|
551 | ||
552 |
=head2 Simplified application script |
|
553 | ||
554 |
C<myapp.pl> itself can now be turned into a simplified application script to |
|
555 |
allow running unit tests again. |
|
556 | ||
557 |
#!/usr/bin/env perl |
|
558 | ||
559 |
use strict; |
|
560 |
use warnings; |
|
561 | ||
562 |
use lib 'lib'; |
|
563 |
use Mojolicious::Commands; |
|
564 | ||
565 |
# Start command line interface for application |
|
566 |
Mojolicious::Commands->start_app('MyApp'); |
|
567 | ||
568 |
=head2 Controller class |
|
569 | ||
570 |
Hybrid routes are a nice intermediate step, but to maximize maintainability it |
|
571 |
makes sense to split our action code from its routing information. |
|
572 | ||
573 |
$ mkdir lib/MyApp |
|
574 |
$ touch lib/MyApp/Login.pm |
|
575 |
$ chmod 644 lib/MyApp/Login.pm |
|
576 | ||
577 |
Once again the actual action code does not change at all. |
|
578 | ||
579 |
package MyApp::Login; |
|
580 |
use Mojo::Base 'Mojolicious::Controller'; |
|
581 | ||
582 |
sub index { |
|
583 |
my $self = shift; |
|
584 | ||
585 |
my $user = $self->param('user') || ''; |
|
586 |
my $pass = $self->param('pass') || ''; |
|
587 |
return $self->render unless $self->users->check($user, $pass); |
|
588 | ||
589 |
$self->session(user => $user); |
|
590 |
$self->flash(message => 'Thanks for logging in.'); |
|
591 |
$self->redirect_to('protected'); |
|
592 |
} |
|
593 | ||
594 |
sub logged_in { |
|
595 |
my $self = shift; |
|
596 |
return $self->session('user') || !$self->redirect_to('index'); |
|
597 |
} |
|
598 | ||
599 |
sub logout { |
|
600 |
my $self = shift; |
|
601 |
$self->session(expires => 1); |
|
602 |
$self->redirect_to('index'); |
|
603 |
} |
|
604 | ||
605 |
1; |
|
606 | ||
607 |
All L<Mojolicious::Controller> controllers are plain old Perl classes and get |
|
608 |
instantiated on demand. |
|
609 | ||
610 |
=head2 Application class |
|
611 | ||
612 |
The application class C<lib/MyApp.pm> can now be reduced to model and routing |
|
613 |
information. |
|
614 | ||
615 |
package MyApp; |
|
616 |
use Mojo::Base 'Mojolicious'; |
|
617 | ||
618 |
use MyUsers; |
|
619 | ||
620 |
sub startup { |
|
621 |
my $self = shift; |
|
622 | ||
623 |
$self->secret('Mojolicious rocks'); |
|
624 |
$self->helper(users => sub { state $users = MyUsers->new }); |
|
625 | ||
626 |
my $r = $self->routes; |
|
627 |
$r->any('/')->to('login#index')->name('index'); |
|
628 |
my $logged_in = $r->under->to('login#logged_in'); |
|
629 |
$logged_in->get('/protected')->to('login#protected'); |
|
630 |
$r->get('/logout')->to('login#logout'); |
|
631 |
} |
|
632 | ||
633 |
1; |
|
634 | ||
635 |
L<Mojolicious::Routes> allows many route variations, choose whatever you like |
|
636 |
most. |
|
637 | ||
638 |
=head2 Templates |
|
639 | ||
640 |
Templates are usually bound to controllers, so they need to be moved into the |
|
641 |
appropriate directories. |
|
642 | ||
643 |
$ mkdir templates/login |
|
644 |
$ mv templates/index.html.ep templates/login/index.html.ep |
|
645 |
$ mv templates/protected.html.ep templates/login/protected.html.ep |
|
646 | ||
647 |
=head2 Script |
|
648 | ||
649 |
Finally C<myapp.pl> can be replaced with a proper L<Mojolicious> script. |
|
650 | ||
651 |
$ rm myapp.pl |
|
652 |
$ mkdir script |
|
653 |
$ touch script/myapp |
|
654 |
$ chmod 744 script/myapp |
|
655 | ||
656 |
Only a few small details change, since installable scripts can't use L<lib> |
|
657 |
without breaking updated dual-life modules. |
|
658 | ||
659 |
#!/usr/bin/env perl |
|
660 | ||
661 |
use strict; |
|
662 |
use warnings; |
|
663 | ||
664 |
use FindBin; |
|
665 |
BEGIN { unshift @INC, "$FindBin::Bin/../lib" } |
|
666 | ||
667 |
# Start command line interface for application |
|
668 |
require Mojolicious::Commands; |
|
669 |
Mojolicious::Commands->start_app('MyApp'); |
|
670 | ||
671 |
=head2 Simplified tests |
|
672 | ||
673 |
Normal L<Mojolicious> applications are a little easier to test, so |
|
674 |
C<t/login.t> can be simplified. |
|
675 | ||
676 |
use Test::More; |
|
677 |
use Test::Mojo; |
|
678 | ||
679 |
# Load application class |
|
680 |
my $t = Test::Mojo->new('MyApp'); |
|
681 |
$t->ua->max_redirects(1); |
|
682 | ||
683 |
$t->get_ok('/') |
|
684 |
->status_is(200) |
|
685 |
->element_exists('form input[name="user"]') |
|
686 |
->element_exists('form input[name="pass"]') |
|
687 |
->element_exists('form input[type="submit"]'); |
|
688 | ||
689 |
$t->post_ok('/' => form => {user => 'sri', pass => 'secr3t'}) |
|
690 |
->status_is(200)->text_like('html body' => qr/Welcome sri/); |
|
691 | ||
692 |
$t->get_ok('/protected')->status_is(200)->text_like('a' => qr/Logout/); |
|
693 | ||
694 |
$t->get_ok('/logout')->status_is(200) |
|
695 |
->element_exists('form input[name="user"]') |
|
696 |
->element_exists('form input[name="pass"]') |
|
697 |
->element_exists('form input[type="submit"]'); |
|
698 | ||
699 |
done_testing(); |
|
700 | ||
701 |
Test driven development takes a little getting used to, but is very well worth |
|
702 |
it! |
|
703 | ||
704 |
=head1 MORE |
|
705 | ||
706 |
You can continue with L<Mojolicious::Guides> now or take a look at the |
|
707 |
L<Mojolicious wiki|http://github.com/kraih/mojo/wiki>, which contains a lot |
|
708 |
more documentation and examples by many different authors. |
|
709 | ||
710 |
=head1 SUPPORT |
|
711 | ||
712 |
If you have any questions the documentation might not yet answer, don't |
|
713 |
hesitate to ask on the |
|
714 |
L<mailing-list|http://groups.google.com/group/mojolicious> or the official IRC |
|
715 |
channel C<#mojo> on C<irc.perl.org>. |
|
716 | ||
717 |
=cut |