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