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