1Dancer2::Tutorial(3) User Contributed Perl Documentation Dancer2::Tutorial(3)
2
3
4
6 Dancer2::Tutorial - An example to get you dancing
7
9 version 0.301004
10
12 This tutorial is has three parts. Since they build on one another, each
13 part is meant to be gone through in sequential order.
14
15 Part I, the longest part of this tutorial, will focus on the basics of
16 Dancer2 development by building a simple yet functional blog app,
17 called "dancr", that you can use to impress your friends, mates, and
18 family.
19
20 In Part II, you'll learn about the preferred way to get your own web
21 apps up and running by using the "dancer2" utility. We will take the
22 script written in Part I and convert it into a proper Dancer2 app,
23 called "Dancr2", to help you gain an understanding of what the
24 "dancer2" utility does for you.
25
26 Finally, in Part III, we give you a taste of the power of plugins that
27 other developers have written and will show you how to modify the
28 "Dancr2" app to use a database plugin.
29
30 This tutorial assumes you have some familiarity with Perl and that you
31 know how to create and execute a Perl script on your computer. Some
32 experience with web development is also greatly helpful but not
33 entirely necessary. This tutorial is mostly geared toward developers
34 but website designers can get something out of it as well since the
35 basics of templating are covered plus it might be good for a designer
36 to have a decent idea of how Dancer2 works.
37
39 Part I covers many of the basic concepts you'll need to know to lay a
40 good foundation for your future development work with Dancer2 by
41 building a simple micro-blogging app.
42
43 What is Dancer2?
44 Dancer2 is a micro-web framework, written in the Perl programming
45 language, and is modeled after a Ruby web application framework called
46 Sinatra <http://www.sinatrarb.com>.
47
48 When we say "micro" framework, we mean that Dancer2 aims to maximize
49 your freedom and control by getting out of your way. "Micro" doesn't
50 mean Dancer2 is only good for creating small apps. Instead, it means
51 that Dancer2's primary focus is on taking care of a lot of the boring,
52 technical details of your app for you and by creating an easy, clean
53 routing layer on top of your app's code. It also means you have almost
54 total control over the app's functionality and how you create and
55 present your content. You will not confined to someone else's approach
56 to creating a website or app.
57
58 With Dancer2, you can build anything from a specialized content
59 management system to providing a simple API for querying a database
60 over the web. But you don't have to reinvent the wheel, either. Dancer2
61 has hundreds of plugins that you can take advantage of. You can add
62 only the capabilities your app needs to keep complexity to a minimum.
63
64 As a framework, Dancer2 provides you with the tools and infrastructure
65 you can leverage to deliver content on the web quickly, easily and
66 securely. The tools, Dancer2 provides, called "keywords," are commands
67 that you use to build your app, access the data inside of it, and
68 deliver it on the internet in many different formats.
69
70 Dancer2's keywords provide what is called a Domain Specific Language
71 (DSL) designed specifically for the task of building apps. But don't
72 let the technical jargon scare you off. Things will become clearer in
73 our first code example which we will look at shortly.
74
75 Getting Dancer2 installed
76
77 First, we need to make sure you have Dancer2 installed. Typically, you
78 will do that with one of the following two commands:
79
80 cpan Dancer2 # requires the cpan command to be installed and configured
81 cpanm Dancer2 # requires you have cpanminus installed
82
83 If you aren't familiar with installing Perl modules on your machine,
84 you should read this guide <https://www.cpan.org/modules/INSTALL.html>.
85 You may also want to consult your OS's documentation or a knowledgeable
86 expert. And, of course, your search engine of choice is always there
87 for you, as well.
88
89 Your first Dancer2 "Hello World!" app
90
91 Now that you have Dancer2 installed, open up your favorite text editor
92 and copy and paste the following lines of Perl code into it and save it
93 to a file called "dancr.pl":
94
95 #!/usr/bin/env perl
96 use Dancer2;
97
98 get '/' => sub {
99 return 'Hello World!';
100 };
101
102 start;
103
104 If you make this script executable and run it, it will fire up a
105 simple, standalone web server that will display "Hello World!" when you
106 point your browser to <http://localhost:3000>. Cool!
107
108 Important note: We want to emphasize that writing a script file like
109 this with a "start" command is not how you would typically begin
110 writing a Dancer2 app. Part II of this tutorial will show you the
111 recommended approach using the "dancer2" utility. For now, we want to
112 stay focused on the fundamentals.
113
114 So, though our example app is very simple, there is a lot going on
115 under the hood when we invoke "use Dancer2;" in our first line of code.
116 We won't go into the gory details of how it all works. For now, it's
117 enough for you to know that the Dancer2 module infuses your script with
118 the ability to use Dancer2 keywords for building apps. Getting
119 comfortable with the concept of keywords is probably the most important
120 step you can take as a budding Dancer2 developer and this tutorial will
121 do its best to help foster your understanding of them.
122
123 The next line of code in our example (which spans three lines to make
124 it more readable) is the route handler. Let's examine this line
125 closely, because route handlers are at the core of how to build an app
126 with Dancer2.
127
128 The syntax of a Dancer2 "route handler" has three parts:
129
130 • an http method or http verb; in this example, we use the "get"
131 keyword to tell Dancer2 that this route should apply to GET http
132 requests. "get" is the first of many keywords that Dancer2 provides
133 that we will cover in this tutorial. Those familiar with web
134 development will know that a GET request is what we use to fetch
135 information from a website.
136
137 • the route pattern; this is the bit of code that appears immediately
138 after our "get" keyword. In this example it is a forward slash
139 ("/"), wrapped in single quotes, and it represents the pattern we
140 wish to match against the URL that the browser, or client, has
141 requested. Web developers will immediately recognize that the
142 forward slash symbolizes the root directory of our website.
143 Experienced Perl programmers will pick up on the fact that the
144 route pattern is nothing more than an argument for our "get"
145 keyword.
146
147 • the route action; this is the subroutine that returns our data.
148 More precisely, it is a subroutine reference. The route action in
149 our example returns a simple string, "Hello World!". Like the route
150 pattern, the route action is nothing more than an argument to our
151 "get" keyword.
152
153 Note that convention has us use the fat comma ("=>") operator
154 between the route pattern and the action to to make our code more
155 readable. But we could just as well have used a regular old comma
156 to separate these argument to our "get" method. Gotta love Perl for
157 its flexibility.
158
159 So to put our route pattern in the example into plain English, we are
160 telling our app, "If the root directory is requested with the GET http
161 method, send the string 'Hello World!' back in our response." Of
162 course, since this is a web app, we also have to send back headers with
163 our response. This is quitely taken care of for us by Dancer2 so we
164 don't have to think about it.
165
166 The syntax for route handlers might seem a bit foreign for newer Perl
167 developers. But rest assured there is nothing magical about it and it
168 is all just plain old Perl under the hood. If you keep in mind that the
169 keyword is a subroutine (or more precisely, a method) and that the
170 pattern and action are arguments to the keyword, you'll pick it up in
171 no time. Thinking of these keywords as "built-ins" to the Dancer2
172 framework might also eliminate any initial confusion about them.
173
174 The most important takeaway here is that we build our app by adding
175 route handlers which are nothing more than a collection of, HTTP verbs,
176 URL patterns, and actions.
177
178 How about a little more involved example?
179 While investigating some Python web frameworks like Flask
180 <http://flask.pocoo.org/> or Bottle <https://bottlepy.org/docs/dev/>, I
181 enjoyed the way they explained step-by-step how to build an example
182 application which was a little more involved than a trivial example.
183 This tutorial is modeled after them.
184
185 Using the Flaskr <https://github.com/pallets/flask> sample application
186 as my inspiration (OK, shamelessly plagiarised) I translated that
187 application to the Dancer2 framework so I could better understand how
188 Dancer2 worked. (I'm learning it too!)
189
190 So "dancr" was born.
191
192 dancr is a simple "micro" blog which uses the SQLite
193 <http://www.sqlite.org> database engine for simplicity's sake. You'll
194 need to install sqlite on your server if you don't have it installed
195 already. Consult your OS documentation for getting SQLite installed on
196 your machine.
197
198 Required Perl modules
199
200 Obviously you need Dancer2 installed. You'll also need the Template
201 Toolkit, File::Slurper, and DBD::SQLite modules. These all can be
202 installed using your CPAN client with the following command:
203
204 cpan Template File::Slurper DBD::SQLite
205
206 The database code
207 We're not going to spend a lot of time on the database, as it's not
208 really the point of this particular tutorial. Try not to dwell on this
209 section too much if you don't understand all of it.
210
211 Open your favorite text editor <http://www.vim.org> and create a schema
212 definition called 'schema.sql' with the following content:
213
214 create table if not exists entries (
215 id integer primary key autoincrement,
216 title string not null,
217 text string not null
218 );
219
220 Here we have a single table with three columns: id, title, and text.
221 The 'id' field is the primary key and will automatically get an ID
222 assigned by the database engine when a row is inserted.
223
224 We want our application to initialize the database automatically for us
225 when we start it. So, let's edit the 'dancr.pl' file we created earlier
226 and give it the ability to talk to our database with the following
227 subroutines: (Or, if you prefer, you can copy and paste the finished
228 dancr.pl script, found near the end of Part I in this tutorial, into
229 the file all at once and then just follow along with the tutorial.)
230
231 sub connect_db {
232 my $dbh = DBI->connect("dbi:SQLite:dbname=".setting('database'))
233 or die $DBI::errstr;
234
235 return $dbh;
236 }
237
238 sub init_db {
239 my $db = connect_db();
240 my $schema = read_text('./schema.sql');
241 $db->do($schema)
242 or die $db->errstr;
243 }
244
245 Nothing too fancy in here, I hope. It's standard DBI except for the
246 "setting('database')" thing, more on that in a bit. For now, just
247 assume that the expression evaluates to the location of the database
248 file.
249
250 In Part III of the tutorial, we will show you how to use the
251 Dancer2::Plugin::Database module for an easier way to configure and
252 manage database connections for your Dancer2 apps.
253
254 Our first route handler
255 Ok, let's get back to the business of learning Dancer2 by creating our
256 app's first route handler for the root URL. Replace the route handler
257 in our simple example above with this one:
258
259 get '/' => sub {
260 my $db = connect_db();
261 my $sql = 'select id, title, text from entries order by id desc';
262
263 my $sth = $db->prepare($sql)
264 or die $db->errstr;
265
266 $sth->execute
267 or die $sth->errstr;
268
269 template 'show_entries.tt', {
270 msg => get_flash(),
271 add_entry_url => uri_for('/add'),
272 entries => $sth->fetchall_hashref('id'),
273 };
274 };
275
276 Our new route handler is the same as the one in our first example
277 except that our route action does a lot more work.
278
279 Something you might not have noticed right away is the semicolon at the
280 end of the route handler. This might confuse newer Perl coders and is a
281 source of bugs for more experienced ones who forget to add it. We need
282 the semicolon there because we are creating a reference to a subroutine
283 and because that's just what the Perl compiler demands and we must obey
284 if we want our code to run.
285
286 Alright, let's take a closer look at this route's action. The first few
287 lines are standard DBI. The important bit related to Dancer2 is the
288 "template" keyword at the end of the action. That tells Dancer2 to
289 process the output through one of its templating engines. There are
290 many template engines available for use with Dancer2. In this tutorial,
291 we're using Template Toolkit which offers a lot more flexibility than
292 the simple default Dancer2 template engine.
293
294 Templates all go into a "views/" directory which located in the same
295 directory as our dancr.pl script. Optionally, you can create a "layout"
296 template which provides a consistent look and feel for all of your
297 views. We'll construct our own layout template, cleverly named main.tt,
298 a little later in this tutorial.
299
300 So what's going on with the hashref as the second argument to the
301 template directive? Those are all of the parameters we want to pass
302 into our template. We have a "msg" field which displays a message to
303 the user when an event happens like a new entry is posted, or the user
304 logs in or out. It's called a "flash" message because we only want to
305 display it one time, not every time the "/" URL is rendered.
306
307 The "uri_for" directive tells Dancer2 to provide a URI for that
308 specific route, in this case, it is the route to post a new entry into
309 the database. You might ask why we don't simply hardcode the "/add"
310 URI in our application or templates. The best reason not to do that is
311 because it removes a layer of flexibility as to where to "mount" the
312 web application. Although the application is coded to use the root URL
313 "/" it might be better in the future to locate it under its own URL
314 route (maybe "/dancr"?) - at that point we'd have to go through our
315 application and the templates and update the URLs and hope we didn't
316 miss any of them. By using the "uri_for" Dancer2 method, we can easily
317 load the application wherever we like and not have to modify the
318 application at all.
319
320 Finally, the "entries" field contains a hashref with the results from
321 our database query. Those results will be rendered in the template
322 itself, so we just pass them in.
323
324 So what does the show_entries.tt template look like? This:
325
326 [% IF session.logged_in %]
327 <form action="[% add_entry_url %]" method=post class=add-entry>
328 <dl>
329 <dt>Title:
330 <dd><input type=text size=30 name=title>
331 <dt>Text:
332 <dd><textarea name=text rows=5 cols=40></textarea>
333 <dd><input type=submit value=Share>
334 </dl>
335 </form>
336 [% END %]
337 <ul class=entries>
338 [% IF entries.size %]
339 [% FOREACH id IN entries.keys.nsort %]
340 <li><h2>[% entries.$id.title | html %]</h2>[% entries.$id.text | html %]
341 [% END %]
342 [% ELSE %]
343 <li><em>Unbelievable. No entries here so far</em>
344 [% END %]
345 </ul>
346
347 Go ahead and create a "views/" directory in the same directory as the
348 script and add this file to it.
349
350 Again, since this isn't a tutorial about Template Toolkit, we'll gloss
351 over the syntax here and just point out the section which starts with
352 "<ul class=entries>". This is the section where the database query
353 results are displayed. You can also see at the very top some discussion
354 about a session, more on that soon.
355
356 The only other Template Toolkit related thing that has to be mentioned
357 here is the "| html" in "[% entries.$id.title | html %]". That's a
358 filter <http://www.template-
359 toolkit.org/docs/manual/Filters.html#section_html> to convert
360 characters like "<" and ">" to "<" and ">". This way they will be
361 displayed by the browser as content on the page rather than just
362 included. If we did not do this, the browser might interpret content as
363 part of the page, and a malicious user could smuggle in all kinds of
364 bad code that would then run in another user's browser. This is called
365 Cross Site Scripting <https://en.wikipedia.org/wiki/Cross-
366 site_scripting> or XSS and you should make sure to avoid it by always
367 filtering data that came in from the web when you display it in a
368 template.
369
370 Other HTTP verbs
371 There are 8 defined HTTP verbs defined in RFC 2616
372 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9>: OPTIONS,
373 GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT. Of these, the majority
374 of web applications focus on the verbs which closely map to the CRUD
375 (Create, Retrieve, Update, Delete) operations most database-driven
376 applications need to implement.
377
378 In addition, the "PATCH" verb was defined in RFC5789
379 <http://tools.ietf.org/html/rfc5789>, and is intended as a "partial
380 PUT", sending just the changes required to the entity in question. How
381 this would be handled is down to your app, it will vary depending on
382 the type of entity in question and the serialization in use.
383
384 Dancer2's keywords currently supports GET, PUT/PATCH, POST, DELETE,
385 OPTIONS which map to Retrieve, Update, Create, Delete respectively.
386 Let's take a look now at the "/add" route handler which handles a POST
387 operation.
388
389 post '/add' => sub {
390 if ( not session('logged_in') ) {
391 send_error("Not logged in", 401);
392 }
393
394 my $db = connect_db();
395 my $sql = 'insert into entries (title, text) values (?, ?)';
396 my $sth = $db->prepare($sql)
397 or die $db->errstr;
398
399 $sth->execute(
400 body_parameters->get('title'),
401 body_parameters->get('text')
402 ) or die $sth->errstr;
403
404 set_flash('New entry posted!');
405 redirect '/';
406 };
407
408 As before, the HTTP verb begins the handler, followed by the route, and
409 a subroutine to do something; in this case it will insert a new entry
410 into the database.
411
412 The first check in the subroutine is to make sure the user sending the
413 data is logged in. If not, the application returns an error and stops
414 processing. Otherwise, we have standard DBI stuff. Let me insert (heh,
415 heh) a blatant plug here for always, always using parameterized INSERTs
416 in your application SQL statements. It's the only way to be sure your
417 application won't be vulnerable to SQL injection. (See
418 <http://www.bobby-tables.com> for correct INSERT examples in multiple
419 languages.) Here we're using the "body_parameters" convenience method
420 to pull in the parameters in the current HTTP request. (You can see the
421 'title' and 'text' form parameters in the show_entries.tt template
422 above.) Those values are inserted into the database, then we set a
423 flash message for the user and redirect her back to the root URL.
424
425 It's worth mentioning that the "flash message" is not part of Dancer2,
426 but a part of this specific application. We need to implement it
427 ourself.
428
429 Logins and sessions
430 Dancer2 comes with a simple in-memory session manager out of the box.
431 It supports a bunch of other session engines including YAML, memcached,
432 browser cookies and others. We'll just stick with the in-memory model
433 which works great for development and tutorials, but won't persist
434 across server restarts or scale very well in "real world" production
435 scenarios.
436
437 Configuration options
438
439 To use sessions in our application, we have to tell Dancer2 to activate
440 the session handler and initialize a session manager. To do that, we
441 add some configuration directives toward the top of our 'dancr.pl'
442 file. But there are more options than just the session engine we want
443 to set.
444
445 set 'database' => File::Spec->catfile(File::Spec->tmpdir(), 'dancr.db');
446 set 'session' => 'Simple';
447 set 'template' => 'template_toolkit';
448 set 'logger' => 'console';
449 set 'log' => 'debug';
450 set 'show_errors' => 1;
451 set 'startup_info' => 1;
452
453 Hopefully these are fairly self-explanatory. We want the Simple session
454 engine, the Template Toolkit template engine, logging enabled (at the
455 'debug' level with output to the console instead of a file), we want to
456 show errors to the web browser and prints a banner at the server start
457 with information such as versions and the environment.
458
459 Dancer2 doesn't impose any limits on what parameters you can set using
460 the "set" syntax. For this application we're going to embed our single
461 username and password into the application itself:
462
463 set 'username' => 'admin';
464 set 'password' => 'password';
465
466 Hopefully no one will ever guess our clever password! Obviously, you
467 will want a more sophisticated user authentication scheme in any sort
468 of non-tutorial application but this is good enough for our purposes.
469
470 In Part II of our tutorial, we will show you how to use Dancer2's
471 configuration files to manage these options and set up different
472 environments for your app using different configuration files. For now,
473 we're going to keep it simple and leave that discussion for later.
474
475 Logging in
476
477 Now that dancr is configured to handle sessions, let's take a look at
478 the URL handler for the "/login" route.
479
480 any ['get', 'post'] => '/login' => sub {
481 my $err;
482
483 if ( request->method() eq "POST" ) {
484 # process form input
485 if ( body_parameters->get('username') ne setting('username') ) {
486 $err = "Invalid username";
487 }
488 elsif ( body_parameters->get('password') ne setting('password') ) {
489 $err = "Invalid password";
490 }
491 else {
492 session 'logged_in' => true;
493 set_flash('You are logged in.');
494 return redirect '/';
495 }
496 }
497
498 # display login form
499 template 'login.tt', {
500 err => $err,
501 };
502 };
503
504 This is the first handler which accepts two different verb types, a GET
505 for a human browsing to the URL and a POST for the browser to submit
506 the user's input to the web application. Since we're handling two
507 different verbs, we check to see what verb is in the request. If it's
508 not a POST, we drop down to the "template" directive and display the
509 login.tt template:
510
511 <h2>Login</h2>
512 [% IF err %]<p class=error><strong>Error:</strong> [% err %][% END %]
513 <form action="[% login_url %]" method=post>
514 <dl>
515 <dt>Username:
516 <dd><input type=text name=username>
517 <dt>Password:
518 <dd><input type=password name=password>
519 <dd><input type=submit value=Login>
520 </dl>
521 </form>
522
523 This is even simpler than our show_entries.tt template–but wait–
524 there's a "login_url" template parameter and we're only passing in the
525 "err" parameter. Where's the missing parameter? It's being generated
526 and sent to the template in a "before_template_render" directive, we'll
527 come back to that in a moment or two.
528
529 So the user fills out the login.tt template and submits it back to the
530 "/login" route handler. We now check the user input against our
531 application settings and if the input is incorrect, we alert the user,
532 otherwise the application starts a session and sets the "logged_in"
533 session parameter to the "true()" value. Dancer2 exports both a
534 "true()" and "false()" convenience method which we use here. After
535 that, it's another flash message and back to the root URL handler.
536
537 Logging out
538
539 And finally, we need a way to clear our user's session with the
540 customary logout procedure.
541
542 get '/logout' => sub {
543 app->destroy_session;
544 set_flash('You are logged out.');
545 redirect '/';
546 };
547
548 "app->destroy_session;" is Dancer2's way to remove a stored session.
549 We notify the user she is logged out and route her back to the root URL
550 once again.
551
552 You might wonder how we can then set a value in the session in
553 "set_flash", because we just destroyed the session.
554
555 Destroying the session has removed the data from the persistence layer
556 (which is the memory of our running application, because we are using
557 the "simple" session engine). If we write to the session now, it will
558 actually create a completely new session for our user. This new, empty
559 session will have a new session ID, which Dancer2 tells the user's
560 browser about in the response. When the browser requests the root URL,
561 it will send this new session ID to our application.
562
563 Layout and static files
564 We still have a missing puzzle piece or two. First, how can we use
565 Dancer2 to serve our CSS stylesheet? Second, where are flash messages
566 displayed? Third, what about the "before_template_render" directive?
567
568 Serving static files
569
570 In Dancer2, static files should go into the "public/" directory, but in
571 the application itself be sure to omit the "public/" element from the
572 path. For example, the stylesheet for dancr lives in
573 "dancr/public/css/style.css" but is served from
574 <http://localhost:3000/css/style.css>.
575
576 If you wanted to build a mostly static web site you could simply write
577 route handlers like this one:
578
579 get '/' => sub {
580 send_file 'index.html';
581 };
582
583 where index.html would live in your "public/" directory.
584
585 "send_file" does exactly what it says: it loads a static file, then
586 sends the contents of that file to the user.
587
588 Let's go ahead and create our style sheet. In the same directory as
589 your dancr.pl script, issue the following commands:
590
591 mkdir public && mkdir public/css && touch public/css/style.css
592
593 Next add the following css to the "public/css/style.css" file you just
594 created:
595
596 body { font-family: sans-serif; background: #eee; }
597 a, h1, h2 { color: #377ba8; }
598 h1, h2 { font-family: 'Georgia', serif; margin: 0; }
599 h1 { border-bottom: 2px solid #eee; }
600 h2 { font-size: 1.2em; }
601
602 .page { margin: 2em auto; width: 35em; border: 5px solid #ccc;
603 padding: 0.8em; background: white; }
604 .entries { list-style: none; margin: 0; padding: 0; }
605 .entries li { margin: 0.8em 1.2em; }
606 .entries li h2 { margin-left: -1em; }
607 .add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; }
608 .add-entry dl { font-weight: bold; }
609 .metanav { text-align: right; font-size: 0.8em; padding: 0.3em;
610 margin-bottom: 1em; background: #fafafa; }
611 .flash { background: #cee5F5; padding: 0.5em;
612 border: 1px solid #aacbe2; }
613 .error { background: #f0d6d6; padding: 0.5em; }
614
615 Be sure to save the file.
616
617 Layouts
618
619 I mentioned earlier in the tutorial that it is possible to create a
620 "layout" template. In dancr, that layout is called "main" and it's set
621 up by putting in a directive like this:
622
623 set layout => 'main';
624
625 near the top of your web application. This tells Dancer2's template
626 engine that it should look for a file called main.tt in
627 "views/layouts/" and insert the calls from the "template" directive
628 into a template parameter called "content".
629
630 Here is the simple layout file we will use for this web application. Go
631 ahead and add this the main.tt file to the "views/layouts/" directory.
632
633 <!doctype html>
634 <html>
635 <head>
636 <title>dancr</title>
637 <link rel=stylesheet type=text/css href="[% css_url %]">
638 </head>
639 <body>
640 <div class=page>
641 <h1>dancr</h1>
642 <div class=metanav>
643 [% IF not session.logged_in %]
644 <a href="[% login_url %]">log in</a>
645 [% ELSE %]
646 <a href="[% logout_url %]">log out</a>
647 [% END %]
648 </div>
649 [% IF msg %]
650 <div class=flash> [% msg %] </div>
651 [% END %]
652 [% content %]
653 </div>
654 </body>
655 </html>
656
657 Aha! You now see where the flash message "msg" parameter gets rendered.
658 You can also see where the content from the specific route handlers is
659 inserted (the fourth line from the bottom in the "content" template
660 parameter).
661
662 But what about all those other *_url template parameters?
663
664 Using "before_template_render"
665
666 Dancer2 has a way to manipulate the template parameters before they're
667 passed to the engine for processing. It's "before_template_render".
668 Using this keyword, you can generate and set the URIs for the "/login"
669 and "/logout" route handlers and the URI for the stylesheet. This is
670 handy for situations like this where there are values which are re-used
671 consistently across all (or most) templates. This cuts down on code-
672 duplication and makes your app easier to maintain over time since you
673 only need to update the values in this one place instead of everywhere
674 you render a template.
675
676 hook before_template_render => sub {
677 my $tokens = shift;
678
679 $tokens->{'css_url'} = request->base . 'css/style.css';
680 $tokens->{'login_url'} = uri_for('/login');
681 $tokens->{'logout_url'} = uri_for('/logout');
682 };
683
684 Here again I'm using "uri_for" instead of hardcoding the routes. This
685 code block is executed before any of the templates are processed so
686 that the template parameters have the appropriate values before being
687 rendered.
688
689 Putting it all together
690 Here's the complete 'dancr.pl' script from start to finish.
691
692 use Dancer2;
693 use DBI;
694 use File::Spec;
695 use File::Slurper qw/ read_text /;
696 use Template;
697
698 set 'database' => File::Spec->catfile(File::Spec->tmpdir(), 'dancr.db');
699 set 'session' => 'Simple';
700 set 'template' => 'template_toolkit';
701 set 'logger' => 'console';
702 set 'log' => 'debug';
703 set 'show_errors' => 1;
704 set 'startup_info' => 1;
705 set 'username' => 'admin';
706 set 'password' => 'password';
707 set 'layout' => 'main';
708
709 sub set_flash {
710 my $message = shift;
711
712 session flash => $message;
713 }
714
715 sub get_flash {
716 my $msg = session('flash');
717 session->delete('flash');
718
719 return $msg;
720 }
721
722 sub connect_db {
723 my $dbh = DBI->connect("dbi:SQLite:dbname=".setting('database'))
724 or die $DBI::errstr;
725
726 return $dbh;
727 }
728
729 sub init_db {
730 my $db = connect_db();
731 my $schema = read_text('./schema.sql');
732 $db->do($schema)
733 or die $db->errstr;
734 }
735
736 hook before_template_render => sub {
737 my $tokens = shift;
738
739 $tokens->{'css_url'} = request->base . 'css/style.css';
740 $tokens->{'login_url'} = uri_for('/login');
741 $tokens->{'logout_url'} = uri_for('/logout');
742 };
743
744 get '/' => sub {
745 my $db = connect_db();
746 my $sql = 'select id, title, text from entries order by id desc';
747
748 my $sth = $db->prepare($sql)
749 or die $db->errstr;
750
751 $sth->execute
752 or die $sth->errstr;
753
754 template 'show_entries.tt', {
755 msg => get_flash(),
756 add_entry_url => uri_for('/add'),
757 entries => $sth->fetchall_hashref('id'),
758 };
759 };
760
761 post '/add' => sub {
762 if ( not session('logged_in') ) {
763 send_error("Not logged in", 401);
764 }
765
766 my $db = connect_db();
767 my $sql = 'insert into entries (title, text) values (?, ?)';
768
769 my $sth = $db->prepare($sql)
770 or die $db->errstr;
771
772 $sth->execute(
773 body_parameters->get('title'),
774 body_parameters->get('text')
775 ) or die $sth->errstr;
776
777 set_flash('New entry posted!');
778 redirect '/';
779 };
780
781 any ['get', 'post'] => '/login' => sub {
782 my $err;
783
784 if ( request->method() eq "POST" ) {
785 # process form input
786 if ( body_parameters->get('username') ne setting('username') ) {
787 $err = "Invalid username";
788 }
789 elsif ( body_parameters->get('password') ne setting('password') ) {
790 $err = "Invalid password";
791 }
792 else {
793 session 'logged_in' => true;
794 set_flash('You are logged in.');
795 return redirect '/';
796 }
797 }
798
799 # display login form
800 template 'login.tt', {
801 err => $err,
802 };
803
804 };
805
806 get '/logout' => sub {
807 app->destroy_session;
808 set_flash('You are logged out.');
809 redirect '/';
810 };
811
812 init_db();
813 start;
814
815 Advanced route moves
816
817 There's a lot more to route matching than shown here. For example, you
818 can match routes with regular expressions, or you can match pieces of a
819 route like "/hello/:name" where the ":name" piece magically turns into
820 a named parameter in your handler for manipulation.
821
822 You can explore this and other advanced concepts by reading the
823 Dancer2::Manual.
824
826 In Part I, we took an ordinary Perl script and turned it into a simple
827 web app to teach you basic Dancer2 concepts. While starting with a
828 simple script like this helped make it easier to teach these concepts,
829 it did not demonstrate how a typical app is built by a Dancer2
830 developer. So let's show you how things really get done.
831
832 Creating a new app
833 So now that you have a better idea of what goes into building an app
834 with Dancer2, it's time to cha-cha with the "dancer2" utility which
835 will save you a lot of time and effort by setting up directories,
836 files, and default configuration settings for you.
837
838 The "dancer2" utility was installed on your machine when you installed
839 the Dancer2 distribution. Hop over to the command line into a directory
840 you have permission to write to and issue the following command:
841
842 dancer2 gen -a Dancr2
843
844 That command should output something like the following to the console:
845
846 + Dancr2
847 + Dancr2/config.yml
848 + Dancr2/Makefile.PL
849 + Dancr2/MANIFEST.SKIP
850 + Dancr2/.dancer
851 + Dancr2/cpanfile
852 + Dancr2/bin
853 + Dancr2/bin/app.psgi
854 + Dancr2/environments
855 + Dancr2/environments/development.yml
856 + Dancr2/environments/production.yml
857 + Dancr2/lib
858 + Dancr2/lib/Dancr2.pm
859 + Dancr2/public
860 + Dancr2/public/favicon.ico
861 + Dancr2/public/500.html
862 + Dancr2/public/dispatch.cgi
863 + Dancr2/public/404.html
864 + Dancr2/public/dispatch.fcgi
865 + Dancr2/public/css
866 + Dancr2/public/css/error.css
867 + Dancr2/public/css/style.css
868 + Dancr2/public/images
869 + Dancr2/public/images/perldancer.jpg
870 + Dancr2/public/images/perldancer-bg.jpg
871 + Dancr2/public/javascripts
872 + Dancr2/public/javascripts/jquery.js
873 + Dancr2/t
874 + Dancr2/t/001_base.t
875 + Dancr2/t/002_index_route.t
876 + Dancr2/views
877 + Dancr2/views/index.tt
878 + Dancr2/views/layouts
879 + Dancr2/views/layouts/main.tt
880
881 What you just did was create a fully functional app in Dancer2 with
882 just one command! The new app, named "Dancr2," won't do anything
883 particularly useful until you add your own routes to it, but it does
884 take care of many of the tedious tasks of setting up an app for you.
885
886 The files and folders that were generated and that you see listed above
887 provide a convenient scaffolding, or skeleton, upon which you can build
888 your app. The default skelelton provides you with basic error pages,
889 css, javascript, graphics, tests, templates and other files which you
890 are free to modify and customize to your liking.
891
892 If you don't like the default skeleton provided to you by Dancer, the
893 "dancer2" command allows you to generate your own custom skeletons.
894 Consult "BOOTSTRAPPING-A-NEW-APP" in Dancer2::Manual for further
895 details on this and other capabilities of the "dancer2") utility.
896
897 Getting the new app up and running with Plack
898 In Part I, we used the "start" command in our script to launch a server
899 to serve our app. Things are a little different when using "dancer2",
900 however. You'll notice that the "dancer2" utility created a "bin/"
901 directory with a file in it called "app.psgi". This is the file we use
902 to get our app up and running.
903
904 Let's see how to to do that by first changing into the Dancr2 directory
905 and then starting the server using the "plackup" command:
906
907 cd Dancr2;
908 plackup -p 5000 bin/app.psgi
909
910 If all went well, you'll be able to see the Dancr2 home page by
911 visiting:
912
913 http://localhost:5000
914
915 The web page you see there gives you some very basic advice for tuning
916 and modifying your app and where you can go for more information to
917 learn about developing apps with Dancer2 (like this handy tutorial!).
918
919 Our Dancr2 app is served on a simple web server provided by Plack.
920 Plack is PSGI compliant software, hence the "psgi" extension for our
921 file in the "bin/" directory. Plack and PSGI is beyond the scope of
922 this tutorial but you can learn more by visiting the Plack website
923 <http://plackperl.org/>.
924
925 For now, all you need to know is that if you are deploying an app for
926 use by just yourself or a handful of people on a local network, Plack
927 alone may do the trick. More typically, you would use Plack in
928 conjunction with other server software to make your app much more
929 robust. But in the early stages of your app's development, a simple
930 Plack server is more than likely all you need.
931
932 To learn more about the different ways for deploying your app, see the
933 Dancer2 Deployment Manual
934
935 Porting dancr.pl over to the new Dancr2 app
936 Ok, so now that we've got our new Dancr2 app up and running, it's time
937 to learn how to take advantage of what the "dancer2" utility set up for
938 us by porting our dancr.pl script created in Part I into Dancr2.
939
940 The "lib/" directory
941
942 The "lib/" directory in our Dancr2 app is where our "app.psgi" file
943 will expect our code to live. So let's take a peek at the file
944 generated for us in there:
945
946 cat lib/Dancr2.pm
947
948 You'll see something like the following bit of code which provides a
949 single route to our app's home page and loads the index template:
950
951 package Dancr2;
952 use Dancer2;
953
954 our $VERSION = '0.1';
955
956 get '/' => sub {
957 template 'index' => { title => 'Dancr2' };
958 };
959
960 true;
961
962 The first thing you'll notice is that instead of a script, we are using
963 a module, "Dancr2" to package our code. Modules make it easer to pull
964 off many powerful tricks like packaging our app across several discrete
965 modules. We'll let the manual explain this more advanced technique.
966
967 Updating the Dancr2 module
968
969 Now that we know where to put our code, let's update the "Dancr2.pm"
970 module with our original "dancr.pl" code. Remove the existing sample
971 route in "Dancr2.pm" and replace it with the code from our "dancr.pl"
972 file. You'll have to make a couple of adjustments to the "dancr.pl"
973 code like removing the "use Dancer2;" line since it's already provided
974 by our module. You'll also want to be sure to remove the "start;" line
975 as well from the end of the file.
976
977 When you're done, "Dancr2.pm" should look something close to this:
978
979 package Dancr2;
980 use Dancer2;
981
982 our $VERSION = '0.1';
983
984 # Our original dancr.pl code with some minor tweaks
985 use DBI;
986 use File::Spec;
987 use File::Slurper qw/ read_text /;
988 use Template;
989
990 set 'database' => File::Spec->catfile(File::Spec->tmpdir(), 'dancr.db');
991 set 'session' => 'YAML';
992 ...
993
994 <snip> # The rest of the stuff </snip>
995
996 ...
997
998 sub init_db {
999 my $schema = read_text('./schema.sql');
1000 $db->do($schema)
1001 or die $db->errstr;
1002 }
1003
1004 get '/logout' => sub {
1005 app->destroy_session;
1006 set_flash('You are logged out.');
1007 redirect '/';
1008 };
1009
1010 init_db();
1011
1012 Finally, to avoid getting an error in the "init_db") subroutine when it
1013 tries to load our schema file, copy over the "schema.db" file to the
1014 root directory of the Dancr2 app:
1015
1016 cp /path/to/dancr.pl/schema.db /path/to/Dancr2;
1017
1018 Ok, now that we've got the code moved over, let's move the assets from
1019 dancr.pl to our new app.
1020
1021 The "public/" directory
1022
1023 As mentioned in Part I, our static assets go into our "public/"
1024 directory. If you followed along with the tutorial in Part I, you
1025 should have a "public/" directory with a "public/css" subdirectory and
1026 a file called "style.css" within that.
1027
1028 Dancer2 has conveniently generated the "public/css" directory for us
1029 which has a default css file. Let's copy the style sheet from our
1030 original app so our new app can use it:
1031
1032 # Note: This command overwrites the default style sheet. Move it or copy
1033 # it if you wish to preserve it.
1034
1035 cp /path/to/dancr.pl/public/css/style.css /path/to/Dancr2/public/css;
1036
1037 The "views" directory
1038
1039 Along with our "public/" directory, Dancer has also provided a "views/"
1040 directory, which as we covered, serves as the a home for our templates.
1041 Let's get those copied over now:
1042
1043 # NOTE: This command will overwrite the default main.tt tempalte file. Move
1044 # it or copy it if you wish to preserve it.
1045
1046 cp -r /path/to/dancr.pl/views/* /path/to/Dancr2/views;
1047
1048 Does it work?
1049
1050 If you followed the instructions here closely, your Dancr2 app should
1051 be working. Shut down any running Plack servers and then issue the
1052 same plackup command to see if it runs:
1053
1054 cd /path/to/Dancr2
1055 plackup -p 5000 bin/app.psgi
1056
1057 If you see any errors, get them resolved until the app loads.
1058
1059 Configuring Your App
1060 In Part I, you configured your app with a series of "set" statements
1061 near the top of your file. Now we will show you a better way to
1062 configure your app using Dancer2's configuration files.
1063
1064 Your skeleton provides your app with three different configuration
1065 files. The first two files we'll discuss, found in the "environments/"
1066 folder of your app, are "development.yml" and "production.yml". As you
1067 can probably guess, the "development.yml" file has settings intended to
1068 be used while developing the app. The "production.yml" file has
1069 settings more appropriate for running your app when used by others. The
1070 third configuration file is found in the root directory of your app and
1071 is named "config.yml". This file has the settings that are common to
1072 all environments but that can be overridden by the environment
1073 configuration files. You can still override any configuration file
1074 settings in your modules using the "set" command.
1075
1076 We will take a look at the "development.yml" file first. Open that file
1077 in your text editor and take a look inside. It has a bunch of helpful
1078 comments and the following five settings sprinkled throughout:
1079
1080 logger: "console"
1081 log: "core"
1082 show_errors: 1
1083 startup_info: 1
1084
1085 The first four settings duplicate many of the settings in our new
1086 Dancr2 app. So in the spirit of DRY (don't repeat yourself), edit your
1087 Dancr2 module and delete the four lines that correspond to these four
1088 settings.
1089
1090 Then, in the configuration file, be sure to change the value for the
1091 "log" setting from "core" to "debug" so it matches the value we had in
1092 our module.
1093
1094 We will leave it up to you what you want to do with the fourth setting,
1095 "startup_info". You can read about that setting, along with all the
1096 other settings, in the configuration manual.
1097
1098 Finally, let's add a new setting to the configuration file for
1099 "session" with the following line:
1100
1101 session: "Simple"
1102
1103 Then delete the corresponding setting from your Dancr2 module.
1104
1105 Alright, our Dancr2 app is a little leaner and meaner. Now open the
1106 main "config.yml" file and look for the settings in there that are also
1107 duplicated in our app's module. There are two:
1108
1109 layout: "main"
1110 template: "simple"
1111
1112 Leave "layout" as is but change the template setting to
1113 "template_toolkit". Then edit your Dancr2 module file and delete these
1114 two settings.
1115
1116 Finally, add the following configuration settings to the .yml file:
1117
1118 username: "admin"
1119 password: "password"
1120
1121 Then you delete these two settings from the Dancr2 module, as well.
1122
1123 So, if you have been following along, you now have only the following
1124 "set" command in your Dancr2 module, related to the database
1125 configuration:
1126
1127 set 'database' => File::Spec->catfile(File::Spec->tmpdir(), 'dancr.db');
1128
1129 We will get rid of this setting in Part III of the tutorial. All the
1130 rest of the settings have been transferred to our configuration files.
1131 Nice!
1132
1133 We still have a little more cleanup we can do. Now that Dancer2 knows
1134 we are using Template::Toolkit, we can delete the "use Template;" line
1135 from our module.
1136
1137 Now start the app "plackup" command and check to see that everything
1138 works. By default, Dancer2 will load the development environment
1139 configuration. When it comes time to put your app into production, you
1140 can load the "production.yml" file configuration with plackup's "--env"
1141 switch like so:
1142
1143 plackup -p 5000 --env production bin/app.psgi
1144
1145 Keep on Dancing!
1146 This concludes Part II of our tutorial where we showed you how to take
1147 advantage of the "dancer2" utility to set up a app skeleton to make it
1148 really easy to get started developing your own apps.
1149
1150 Part III will refine our app a little further by showing you how to use
1151 plugins so you can start capitalizing on all the great work contributed
1152 by other Dancer2 developers.
1153
1155 Dancer2 takes advantage of the open source software revolution by
1156 making it exceedingly easy to use plugins that you can mix into your
1157 app to give it new functionality. In Part III of this tutorial, we will
1158 update our new Dancr2 app to use the Dancer2::Plugin::Database to give
1159 you enough skills to go out and explore other plugins on your own.
1160
1161 Installing plugins
1162 Like Dancer2 itself, Dancer2 plugins can be found on the CPAN. Use your
1163 favorite method for downloading and installing the
1164 Dancer2::Plugin::Database module on your machine. We recommend using
1165 "cpanminus" like so:
1166
1167 cpanm Dancer2::Plugin::Database
1168
1169 Using plugins
1170 Using a plugin couldn't be easier. Simply add the following line to
1171 your Dancr2 module below the "use Dancer2;" line in your module:
1172
1173 use Dancer2::Plugin::Database;
1174
1175 Configuring plugins
1176 Plugins can be configured with the YAML configuration files mentioned
1177 in Part II of this tutorial. Let's edit the "development.yml" file and
1178 add our database configuration there. Below the last line in that file,
1179 add the following lines, being careful to keep the indentation as you
1180 see it here:
1181
1182 plugins: ; all plugin configuration settings go in this section
1183 Database: ; the name of our plugin
1184 driver: "SQLite" ; driver we want to use
1185 database: "dancr.db" ; where the database will go in our app
1186 ; run a query when connecting to the datbase:
1187 on_connect_do: [ "create table if not exists entries (id integer primary key autoincrement, title string not null, text string not null)" ]
1188
1189 Here, we direct our database plugin to use the "SQLite" driver and to
1190 place the database in the root directory of our Dancr2. The
1191 "on_connect_db" setting tells the plugin to run an SQL query when it
1192 connects with the database to create a table for us if it doesn't
1193 already exist.
1194
1195 Modifying our database code in the Dancr2 module
1196 Now it's time to modify our Dancr2 module so it will use the plugin to
1197 query the database instead of our own code. There are a few things to
1198 do. First, we will delete the code we no longer need.
1199
1200 Since our configuration file tells the plugin where our database is, we
1201 can delete this line:
1202
1203 set 'database' => File::Spec->catfile(File::Spec->tmpdir(), 'dancr.db');
1204
1205 And since the database plugin will create our database connection and
1206 initialize our database for us, we can scrap the following two
1207 subroutines and line from our module:
1208
1209 sub connect_db {
1210 my $dbh = DBI->connect("dbi:SQLite:dbname=".setting('database'))
1211 or die $DBI::errstr;
1212
1213 return $dbh;
1214 }
1215
1216 sub init_db {
1217 my $db = connect_db();
1218 my $schema = read_text('./schema.sql');
1219 $db->do($schema)
1220 or die $db->errstr;
1221 }
1222
1223 init_db(); # Found at the bottom of our file
1224
1225 With that done, let's now take advantage of a hook the plugin provides
1226 us that we can use to handle certain events by adding the following
1227 command to our module to handle database errors:
1228
1229 hook 'database_error' => sub {
1230 my $error = shift;
1231 die $error;
1232 };
1233
1234 Now let's make a few adjustments to the bits of code that make the
1235 database queries. In our "get '/'" route, change all instances of $db
1236 with "database" and remove all the "die" calls since we now have a hook
1237 to handle the errors for us. When you are done, your route should look
1238 something like this:
1239
1240 get '/' => sub {
1241 my $sql = 'select id, title, text from entries order by id desc';
1242 my $sth = database->prepare($sql);
1243 $sth->execute;
1244 template 'show_entries.tt', {
1245 msg => get_flash(),
1246 add_entry_url => uri_for('/add'),
1247 entries => $sth->fetchall_hashref('id'),
1248 };
1249 };
1250
1251 Make the same changes to the "post '/add'" route to transform it into
1252 this:
1253
1254 post '/add' => sub {
1255 if ( not session('logged_in') ) {
1256 send_error("Not logged in", 401);
1257 }
1258
1259 my $sql = 'insert into entries (title, text) values (?, ?)';
1260 my $sth = database->prepare($sql);
1261 $sth->execute(
1262 body_parameters->get('title'),
1263 body_parameters->get('text')
1264 );
1265
1266 set_flash('New entry posted!');
1267 redirect '/';
1268 };
1269
1270 Our last step is to get rid of the following lines which we no longer
1271 need, thanks to our plugin:
1272
1273 use DBI;
1274 use File::Spec;
1275 use File::Slurper qw/ read_text /;
1276
1277 That's it! Now start your app with "plackup" to make sure you don't get
1278 any errors and then point your browser to test the app to make sure it
1279 works as expected. If it doesn't, double and triple check your
1280 configuration settings and your module's code which should now look
1281 like this:
1282
1283 package Dancr2;
1284 use Dancer2;
1285 use Dancer2::Plugin::Database;
1286
1287 our $VERSION = '0.1';
1288
1289 my $flash;
1290
1291 sub set_flash {
1292 my $message = shift;
1293
1294 $flash = $message;
1295 }
1296
1297 sub get_flash {
1298 my $msg = $flash;
1299 $flash = "";
1300
1301 return $msg;
1302 }
1303
1304 hook before_template_render => sub {
1305 my $tokens = shift;
1306
1307 $tokens->{'css_url'} = request->base . 'css/style.css';
1308 $tokens->{'login_url'} = uri_for('/login');
1309 $tokens->{'logout_url'} = uri_for('/logout');
1310 };
1311
1312 hook 'database_error' => sub {
1313 my $error = shift;
1314 die $error;
1315 };
1316
1317 get '/' => sub {
1318 my $sql = 'select id, title, text from entries order by id desc';
1319 my $sth = database->prepare($sql);
1320 $sth->execute;
1321 template 'show_entries.tt', {
1322 msg => get_flash(),
1323 add_entry_url => uri_for('/add'),
1324 entries => $sth->fetchall_hashref('id'),
1325 };
1326 };
1327
1328 post '/add' => sub {
1329 if ( not session('logged_in') ) {
1330 send_error("Not logged in", 401);
1331 }
1332
1333 my $sql = 'insert into entries (title, text) values (?, ?)';
1334 my $sth = database->prepare($sql);
1335 $sth->execute(
1336 body_parameters->get('title'),
1337 body_parameters->get('text')
1338 );
1339
1340 set_flash('New entry posted!');
1341 redirect '/';
1342 };
1343
1344 any ['get', 'post'] => '/login' => sub {
1345 my $err;
1346
1347 if ( request->method() eq "POST" ) {
1348 # process form input
1349 if ( params->{'username'} ne setting('username') ) {
1350 $err = "Invalid username";
1351 }
1352 elsif ( params->{'password'} ne setting('password') ) {
1353 $err = "Invalid password";
1354 }
1355 else {
1356 session 'logged_in' => true;
1357 set_flash('You are logged in.');
1358 return redirect '/';
1359 }
1360 }
1361
1362 # display login form
1363 template 'login.tt', {
1364 err => $err,
1365 };
1366
1367 };
1368
1369 get '/logout' => sub {
1370 app->destroy_session;
1371 set_flash('You are logged out.');
1372 redirect '/';
1373 };
1374
1375 true;
1376
1377 Next steps
1378 Congrats! You are now using the database plugin like a boss. The
1379 database plugin does a lot more than what we showed you here. We'll
1380 leave it up to you to consult the Dancer2::Plugin::Database to unlock
1381 its full potential.
1382
1383 There are many more plugins for you to explore. You now know enough to
1384 install and experiment with them. Some of the more popular and useful
1385 plugins are listed at Dancer2::Plugins. You can also search CPAN with
1386 "Dancer2::Plugin" for a more comprehensive listing.
1387
1388 If you are feeling really inspired, you can learn how to extend Dancer2
1389 with your own plugins by reading Dancer2::Plugin.
1390
1392 I hope these tutorials have been helpful and interesting enough to get
1393 you exploring Dancer2 on your own. The framework is still under
1394 development but it's definitely mature enough to use in a production
1395 project.
1396
1397 Happy dancing!
1398
1399 One more thing: Test!
1400 Before we go, we want to mention that Dancer2 makes it very easy to run
1401 automated tests on your app to help you find bugs. If you are new to
1402 testing, we encourage you to start learning how. Your future self will
1403 thank you. The effort you put into creating tests for your app will
1404 save you many hours of frustration in the long run. Unfortunately,
1405 until we get Part IV of this tutorial written, you'll have to consult
1406 the Dancer2 testing documentation for more details on how to test your
1407 app.
1408
1409 Enjoy!
1410
1412 • <http://perldancer.org>
1413
1414 • <http://github.com/PerlDancer/Dancer2>
1415
1416 • Dancer2::Plugins
1417
1419 The CSS stylesheet is copied verbatim from the Flaskr example
1420 application and is subject to their license:
1421
1422 Copyright (c) 2010, 2013 by Armin Ronacher and contributors.
1423
1424 Some rights reserved.
1425
1426 Redistribution and use in source and binary forms of the software as
1427 well as documentation, with or without modification, are permitted
1428 provided that the following conditions are met:
1429
1430 • Redistributions of source code must retain the above copyright
1431 notice, this list of conditions and the following disclaimer.
1432
1433 • Redistributions in binary form must reproduce the above copyright
1434 notice, this list of conditions and the following disclaimer in the
1435 documentation and/or other materials provided with the
1436 distribution.
1437
1438 • The names of the contributors may not be used to endorse or promote
1439 products derived from this software without specific prior written
1440 permission.
1441
1443 Dancer Core Developers
1444
1446 This software is copyright (c) 2021 by Alexis Sukrieh.
1447
1448 This is free software; you can redistribute it and/or modify it under
1449 the same terms as the Perl 5 programming language system itself.
1450
1451
1452
1453perl v5.34.0 2021-07-22 Dancer2::Tutorial(3)