1Mojolicious::Guides::GrUosweirngC(o3n)tributed Perl DocuMmoejnotlaitciioonus::Guides::Growing(3)
2
3
4

NAME

6       Mojolicious::Guides::Growing - Growing Mojolicious applications
7

OVERVIEW

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

CONCEPTS

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

PROTOTYPE

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

WELL-STRUCTURED APPLICATION

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

MORE

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

SUPPORT

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)
Impressum