1Catalyst::Manual::TutorUisaelr::CAodnvtarnicbeudtCCeaRdtUaDPl(ey3rs)lt:D:oMcaunmueanlt:a:tTiuotnorial::AdvancedCRUD(3)
2
3
4
6 Catalyst::Manual::Tutorial::AdvancedCRUD - Catalyst Tutorial - Part 8:
7 Advanced CRUD
8
10 This is Part 8 of 9 for the Catalyst tutorial.
11
12 Tutorial Overview
13
14 1 Introduction
15
16 2 Catalyst Basics
17
18 3 Basic CRUD
19
20 4 Authentication
21
22 5 Authorization
23
24 6 Debugging
25
26 7 Testing
27
28 8 AdvancedCRUD
29
30 9 Appendices
31
33 This part of the tutorial explores more advanced functionality for Cre‐
34 ate, Read, Update, and Delete (CRUD) than we saw in Part 3. In partic‐
35 ular, it looks at a number of techniques that can be useful for the
36 Update portion of CRUD, such as automated form generation, validation
37 of user-entered data, and automated transfer of data between forms and
38 model objects.
39
40 In keeping with the Catalyst (and Perl) spirit of flexibility, there
41 are many different ways to approach advanced CRUD operations in a Cata‐
42 lyst environment. One alternative is to use Catalyst::Helper::Con‐
43 troller::Scaffold to instantly construct a set of Controller methods
44 and templates for basic CRUD operations. Although a popular subject in
45 Quicktime movies that serve as promotional material for various frame‐
46 works, real-world applications generally require more control. Other
47 options include Data::FormValidator and HTML::FillInForm.
48
49 Here, we will make use of the HTML::Widget to not only ease form cre‐
50 ation, but to also provide validation of the submitted data. The
51 approached used by this part of the tutorial is to slowly incorporate
52 additional HTML::Widget functionality in a step-wise fashion (we start
53 with fairly simple form creation and then move on to more complex and
54 "magical" features such as validation and auto-population/auto-saving).
55
56 Note: Part 8 of the tutorial is optional. Users who do not wish to use
57 HTML::Widget may skip this part.
58
59 You can checkout the source code for this example from the catalyst
60 subversion repository as per the instructions in Catalyst::Man‐
61 ual::Tutorial::Intro
62
64 This section looks at how HTML::Widget can be used to add additional
65 functionality to the manually created form from Part 3.
66
67 Add the "HTML::Widget" Plugin
68
69 Open "lib/MyApp.pm" in your editor and add the following to the list of
70 plugins (be sure to leave the existing plugins enabled):
71
72 HTML::Widget
73
74 Add a Form Creation Helper Method
75
76 Open "lib/MyApp/Controller/Books.pm" in your editor and add the follow‐
77 ing method:
78
79 =head2 make_book_widget
80
81 Build an HTML::Widget form for book creation and updates
82
83 =cut
84
85 sub make_book_widget {
86 my ($self, $c) = @_;
87
88 # Create an HTML::Widget to build the form
89 my $w = $c->widget('book_form')->method('post');
90
91 # Get authors
92 my @authorObjs = $c->model("MyAppDB::Author")->all();
93 my @authors = map {$_->id => $_->last_name }
94 sort {$a->last_name cmp $b->last_name} @authorObjs;
95
96 # Create the form feilds
97 $w->element('Textfield', 'title' )->label('Title')->size(60);
98 $w->element('Textfield', 'rating' )->label('Rating')->size(1);
99 $w->element('Select', 'authors')->label('Authors')
100 ->options(@authors);
101 $w->element('Submit', 'submit' )->value('submit');
102
103 # Return the widget
104 return $w;
105 }
106
107 This method provides a central location that builds an HTML::Wid‐
108 get-based form with the appropriate fields. The "Get authors" code
109 uses DBIC to retrieve a list of model objects and then uses "map" to
110 create a hash where the hash keys are the database primary keys from
111 the authors table and the associated values are the last names of the
112 authors.
113
114 Add Actions to Display and Save the Form
115
116 Open "lib/MyApp/Controller/Books.pm" in your editor and add the follow‐
117 ing methods:
118
119 =head2 hw_create
120
121 Build an HTML::Widget form for book creation and updates
122
123 =cut
124
125 sub hw_create : Local {
126 my ($self, $c) = @_;
127
128 # Create the widget and set the action for the form
129 my $w = $self->make_book_widget($c);
130 $w->action($c->uri_for('hw_create_do'));
131
132 # Write form to stash variable for use in template
133 $c->stash->{widget_result} = $w->result;
134
135 # Set the template
136 $c->stash->{template} = 'books/hw_form.tt2';
137 }
138
139 =head2 hw_create_do
140
141 Build an HTML::Widget form for book creation and updates
142
143 =cut
144
145 sub hw_create_do : Local {
146 my ($self, $c) = @_;
147
148 # Retrieve the data from the form
149 my $title = $c->request->params->{title};
150 my $rating = $c->request->params->{rating};
151 my $authors = $c->request->params->{authors};
152
153 # Call create() on the book model object. Pass the table
154 # columns/field values we want to set as hash values
155 my $book = $c->model('MyAppDB::Book')->create({
156 title => $title,
157 rating => $rating
158 });
159
160 # Add a record to the join table for this book, mapping to
161 # appropriate author
162 $book->add_to_book_authors({author_id => $authors});
163
164 # Set a status message for the user
165 $c->stash->{status_msg} = 'Book created';
166
167 # Use 'hw_create' to redisplay the form. As discussed in
168 # Part 3, 'detach' is like 'forward', but it does not return
169 $c->detach('hw_create');
170 }
171
172 Note how we use "make_book_widget" to build the core parts of the form
173 in one location, but we set the action (the URL the form is sent to
174 when the user clicks the 'Submit' button) separately in "hw_create".
175 Doing so allows us to have the same form submit the data to different
176 actions (e.g., "hw_create_do" for a create operation but "hw_update_do"
177 to update an existing book object).
178
179 NOTE: If you receive an error about Catalyst not being able to find the
180 template "hw_create_do.tt2", please verify that you followed the
181 instructions in the final section of Catalyst Basics where you returned
182 to a manually-specified template. You can either use "for‐
183 ward"/"detach" OR default template names, but the two cannot be used
184 together.
185
186 Update the CSS
187
188 Edit "root/src/ttsite.css" and add the following lines to the bottom of
189 the file:
190
191 label {
192 display: block;
193 width: 10em;
194 position: relative;
195 margin: .5em 0em;
196 }
197 label input {
198 position: absolute;
199 left: 100%;
200 }
201 label select {
202 position: absolute;
203 left: 100%;
204 }
205 .submit {
206 margin-top: 2em;;
207 }
208 .error_messages {
209 color: [% site.col.error %];
210 }
211
212 These changes will display form elements vertically and also show error
213 messages in red. Note that we are pulling the color scheme settings
214 from the "root/lib/config/col" file that was created by the TTSite
215 helper. This allows us to change the color used by various error
216 styles in the CSS from a single location.
217
218 Create a Template Page To Display The Form
219
220 Open "root/src/books/hw_form.tt2" in your editor and enter the follow‐
221 ing:
222
223 [% META title = 'Create/Update Book' %]
224
225 [% widget_result.as_xml %]
226
227 <p><a href="[% Catalyst.uri_for('list') %]">Return to book list</a></p>
228
229 Add Links for Create and Update via "HTML::Widget"
230
231 Open "root/src/books/list.tt2" in your editor and add the following to
232 the bottom of the existing file:
233
234 <p>
235 HTML::Widget:
236 <a href="[% Catalyst.uri_for('hw_create') %]">Create</a>
237 </p>
238
239 Test The <HTML::Widget> Create Form
240
241 Press "Ctrl-C" to kill the previous server instance (if it's still run‐
242 ning) and restart it:
243
244 $ script/myapp_server.pl
245
246 Login as "test01". Once at the Book List page, click the HTML::Widget
247 "Create" link to display for form produced by "make_book_widget". Fill
248 out the form with the following values: Title = "Internetworking with
249 TCP/IP Vol. II", Rating = "4", and Author = "Comer". Click Submit, and
250 you will be returned to the Create/Update Book page with a "Book cre‐
251 ated" status message displayed. Click "Return to book list" to view
252 the newly created book on the main list.
253
254 Also note that this implementation allows you to can create books with
255 bogus information. Although we have constrained the authors with the
256 drop-down list, there are no restrictions on items such as the length
257 of the title (for example, you can create a one-letter title) and value
258 for the rating (you can use any number you want, and even non-numeric
259 values with SQLite). The next section will address this concern.
260
261 Note: Depending on the database you are using and how you established
262 the columns in your tables, the database could obviously provide vari‐
263 ous levels of "type enforcement" on your data. The key point being
264 made in the previous paragraph is that the web application itself is
265 not performing any validation.
266
268 Although the use of HTML::Widget in the previous section did provide an
269 automated mechanism to build the form, the real power of this module
270 stems from functionality that can automatically validate and filter the
271 user input. Validation uses constraints to be sure that users input
272 appropriate data (for example, that the email field of a form contains
273 a valid email address). Filtering can be used to remove extraneous
274 whitespace from fields or to escape meta-characters in user input.
275
276 Add Constraints and Filters to the Widget Creation Method
277
278 Open "lib/MyApp/Controller/Books.pm" in your editor and update the
279 "make_book_widget" method to match the following (new sections have
280 been marked with a "*** NEW:" comment):
281
282 sub make_book_widget {
283 my ($self, $c) = @_;
284
285 # Create an HTML::Widget to build the form
286 my $w = $c->widget('book_form')->method('post');
287
288 # Get authors
289 my @authorObjs = $c->model("MyAppDB::Author")->all();
290 my @authors = map {$_->id => $_->last_name }
291 sort {$a->last_name cmp $b->last_name} @authorObjs;
292
293 # Create the form feilds
294 $w->element('Textfield', 'title' )->label('Title')->size(60);
295 $w->element('Textfield', 'rating' )->label('Rating')->size(1);
296 # ***NEW: Convert to multi-select list
297 $w->element('Select', 'authors')->label('Authors')
298 ->options(@authors)->multiple(1)->size(3);
299 $w->element('Submit', 'submit' )->value('submit');
300
301 # ***NEW: Set constraints
302 $w->constraint(All => qw/title rating authors/)
303 ->message('Required. ');
304 $w->constraint(Integer => qw/rating/)
305 ->message('Must be an integer. ');
306 $w->constraint(Range => qw/rating/)->min(1)->max(5)
307 ->message('Must be a number between 1 and 5. ');
308 $w->constraint(Length => qw/title/)->min(5)->max(50)
309 ->message('Must be between 5 and 50 characters. ');
310
311 # ***NEW: Set filters
312 for my $column (qw/title rating authors/) {
313 $w->filter( HTMLEscape => $column );
314 $w->filter( TrimEdges => $column );
315 }
316
317 # Return the widget
318 return $w;
319 }
320
321 The main changes are:
322
323 · The "Select" element for "authors" is changed from a single-select
324 drop-down to a multi-select list by adding calls to "multiple" (set
325 to "true") and "size" (set to the number of rows to display).
326
327 · Four sets of constraints are added to provide validation of the
328 user input.
329
330 · Two filters are run on every field to remove and escape unwanted
331 input.
332
333 Rebuild the Form Submission Method to Include Validation
334
335 Edit "lib/MyApp/Controller/Books.pm" and change "hw_create_do" to match
336 the following code (enough of the code is different that you probably
337 want to cut and paste this over code the existing method):
338
339 sub hw_create_do : Local {
340 my ($self, $c) = @_;
341
342 # Retrieve the data from the form
343 my $title = $c->request->params->{title};
344 my $rating = $c->request->params->{rating};
345 my $authors = $c->request->params->{authors};
346
347 # Create the widget and set the action for the form
348 my $w = $self->make_book_widget($c);
349 $w->action($c->uri_for('hw_create_do'));
350
351 # Validate the form parameters
352 my $result = $w->process($c->req);
353
354 # Write form (including validation error messages) to
355 # stash variable for use in template
356 $c->stash->{widget_result} = $result;
357
358 # Were their validation errors?
359 if ($result->has_errors) {
360 # Warn the user at the top of the form that there were errors.
361 # Note that there will also be per-field feedback on
362 # validation errors because of '$w->process($c->req)' above.
363 $c->stash->{error_msg} = 'Validation errors!';
364 } else {
365 # Everything validated OK, so do the create
366 # Call create() on the book model object. Pass the table
367 # columns/field values we want to set as hash values
368 my $book = $c->model('MyAppDB::Book')->create({
369 title => $title,
370 rating => $rating
371 });
372
373 # Add a record to the join table for this book, mapping to
374 # appropriate author. Note that $authors will be 1 author as
375 # a scalar or ref to list of authors depending on how many the
376 # user selected; the 'ref $authors ?...' handles both cases
377 foreach my $author (ref $authors ? @$authors : $authors) {
378 $book->add_to_book_authors({author_id => $author});
379 }
380 # Set a status message for the user
381 $c->stash->{status_msg} = 'Book created';
382 }
383
384 # Set the template
385 $c->stash->{template} = 'books/hw_form.tt2';
386 }
387
388 The key changes to "hw_create_do" are:
389
390 · "hw_create_do" no longer does a "detach" to "hw_create" to redis‐
391 play the form. Now that "hw_create_do" has to process the form in
392 order to perform the validation, we go ahead and build a complete
393 set of form presentation logic into "hw_create_do" (for example,
394 "hw_create_do" now has a "$c->stash->{template}" line). Note that
395 if we process the form in "hw_create_do" and forward/detach back to
396 <hw_create>, we would end up with "make_book_widget" being called
397 twice, resulting in a duplicate set of elements being added to the
398 form. (There are other ways to address the "duplicate form render‐
399 ing" issue -- just be aware that it exists.)
400
401 · "$w->process($c->req)" is called to run the validation logic. Not
402 only does this set the "has_errors" flag if validation errors are
403 encountered, it returns a string containing any field-specific
404 warning messages.
405
406 · An "if" statement checks if any validation errors were encountered.
407 If so, "$c->stash->{error_msg}" is set and the input form is redis‐
408 played. If no errors were found, the object is created in a manner
409 similar to the prior version of the "hw_create_do" method.
410
411 Try Out the Form
412
413 Press "Ctrl-C" to kill the previous server instance (if it's still run‐
414 ning) and restart it:
415
416 $ script/myapp_server.pl
417
418 Now try adding a book with various errors: title less than 5 charac‐
419 ters, non-numeric rating, a rating of 0 or 6, etc. Also try selecting
420 one, two, and zero authors. When you click Submit, the HTML::Widget
421 "constraint" items will validate the logic and insert feedback as
422 appropriate.
423
425 In this section we will take advantage of some of the "auto-population"
426 features of "DBIx::Class::HTMLWidget". Enabling "DBIx::Class::HTMLWid‐
427 get" provides two additional methods to your DBIC model classes:
428
429 · fill_widget()
430
431 Takes data from the database and transfers it to your form widget.
432
433 · populate_from_widget()
434
435 Takes data from a form widget and uses it to update the correspond‐
436 ing records in the database.
437
438 In other words, the two methods are a mirror image of each other: one
439 reads from the database while the other writes to the database.
440
441 Add "DBIx::Class::HTMLWidget" to DBIC Model
442
443 In order to use DBIx::Class::HTMLWidget, we need to add "HTMLWidget" to
444 the "load_components" line of DBIC result source files that need to use
445 the "fill_widget" and "populate_from_widget" methods. In this case,
446 open "lib/MyAppDB/Book.pm" and update the "load_components" line to
447 match:
448
449 __PACKAGE__->load_components(qw/PK::Auto Core HTMLWidget/);
450
451 Use "populate_from_widget" in "hw_create_do"
452
453 Edit "lib/MyApp/Controller/Books.pm" and update "hw_create_do" to match
454 the following code:
455
456 =head2 hw_create_do
457
458 Build an HTML::Widget form for book creation and updates
459
460 =cut
461
462 sub hw_create_do : Local {
463 my ($self, $c) = @_;
464
465 # Create the widget and set the action for the form
466 my $w = $self->make_book_widget($c);
467 $w->action($c->uri_for('hw_create_do'));
468
469 # Validate the form parameters
470 my $result = $w->process($c->req);
471
472 # Write form (including validation error messages) to
473 # stash variable for use in template
474 $c->stash->{widget_result} = $result;
475
476 # Were their validation errors?
477 if ($result->has_errors) {
478 # Warn the user at the top of the form that there were errors.
479 # Note that there will also be per-field feedback on
480 # validation errors because of '$w->process($c->req)' above.
481 $c->stash->{error_msg} = 'Validation errors!';
482 } else {
483 my $book = $c->model('MyAppDB::Book')->new({});
484 $book->populate_from_widget($result);
485
486 # Add a record to the join table for this book, mapping to
487 # appropriate author. Note that $authors will be 1 author as
488 # a scalar or ref to list of authors depending on how many the
489 # user selected; the 'ref $authors ?...' handles both cases
490 my $authors = $c->request->params->{authors};
491 foreach my $author (ref $authors ? @$authors : $authors) {
492 $book->add_to_book_authors({author_id => $author});
493 }
494
495 # Set a status message for the user
496 $c->flash->{status_msg} = 'Book created';
497
498 # Redisplay an empty form for another
499 $c->stash->{widget_result} = $w->result;
500 }
501
502 # Set the template
503 $c->stash->{template} = 'books/hw_form.tt2';
504 }
505
506 In this version of "hw_create_do" we removed the logic that manually
507 pulled the form variables and used them to call "$c->model('MyAp‐
508 pDB::Book')->create" and replaced it with a single call to "$book->pop‐
509 ulate_from_widget". Note that we still have to call
510 "$book->add_to_book_authors" once per author because "popu‐
511 late_from_widget" does not currently handle the relationships between
512 tables. Also, we reset the form to an empty fields by adding another
513 call to "$w->result" and storing the output in the stash (if we don't
514 override the output from "$w->process($c->req)", the form values
515 already entered will be retained on redisplay -- although this could be
516 desirable for some applications, we avoid it here to help avoid the
517 creation of duplicate records).
518
519 Try Out the Form
520
521 Press "Ctrl-C" to kill the previous server instance (if it's still run‐
522 ning) and restart it:
523
524 $ script/myapp_server.pl
525
526 Try adding a book that validates. Return to the book list and the book
527 you added should be visible.
528
530 Some developers my wish to use the "old-fashioned" table style of ren‐
531 dering a form in lieu of the default "HTML::Widget" rendering that
532 assumes you will use CSS for formatting. This section demonstrates
533 some techniques that can override the default rendering with a custom
534 class.
535
536 Add a New "Element Container"
537
538 Open "lib/FormElementContainer.pm" in your editor and enter:
539
540 package FormElementContainer;
541
542 use base 'HTML::Widget::Container';
543
544 sub _build_element {
545 my ($self, $element) = @_;
546
547 return () unless $element;
548 if (ref $element eq 'ARRAY') {
549 return map { $self->_build_element($_) } @{$element};
550 }
551 my $e = $element->clone;
552 $e = new HTML::Element('span', class => 'fields_with_errors')->push_content($e)
553 if $self->error && $e->tag eq 'input';
554
555 return $e ? ($e) : ();
556 }
557
558 1;
559
560 This simply dumps the HTML code for a given form element, followed by a
561 "span" that can contain validation error message.
562
563 Enable the New Element Container When Building the Form
564
565 Open "lib/MyApp/Controller/Books.pm" in your editor. First add a "use"
566 for your element container class:
567
568 use FormElementContainer;
569
570 Note: If you forget to "use" your container class in your controller,
571 then your form will not be displayed and no error messages will be gen‐
572 erated. Don't forget this important step!
573
574 Then tell "HTML::Widget" to use that class during rendering by updating
575 "make_book_widget" to match the following:
576
577 sub make_book_widget {
578 my ($self, $c) = @_;
579
580 # Create an HTML::Widget to build the form
581 my $w = $c->widget('book_form')->method('post');
582
583 # ***New: Use custom class to render each element in the form
584 $w->element_container_class('FormElementContainer');
585
586 # Get authors
587 my @authorObjs = $c->model("MyAppDB::Author")->all();
588 my @authors = map {$_->id => $_->last_name }
589 sort {$a->last_name cmp $b->last_name} @authorObjs;
590
591 # Create the form feilds
592 $w->element('Textfield', 'title' )->label('Title')->size(60);
593 $w->element('Textfield', 'rating' )->label('Rating')->size(1);
594 # Convert to multi-select list
595 $w->element('Select', 'authors')->label('Authors')
596 ->options(@authors)->multiple(1)->size(3);
597 $w->element('Submit', 'submit' )->value('submit');
598
599 # Set constraints
600 $w->constraint(All => qw/title rating authors/)
601 ->message('Required. ');
602 $w->constraint(Integer => qw/rating/)
603 ->message('Must be an integer. ');
604 $w->constraint(Range => qw/rating/)->min(1)->max(5)
605 ->message('Must be a number between 1 and 5. ');
606 $w->constraint(Length => qw/title/)->min(5)->max(50)
607 ->message('Must be between 5 and 50 characters. ');
608
609 # Set filters
610 for my $column (qw/title rating authors/) {
611 $w->filter( HTMLEscape => $column );
612 $w->filter( TrimEdges => $column );
613 }
614
615 # Return the widget
616 return $w;
617 }
618
619 The two new lines are marked with ***New:.
620
621 Update the TT Template
622
623 Open "root/src/books/hw_form.tt2" and edit it to match:
624
625 [% META title = 'Create/Update Book' %]
626
627 [%# Comment out the auto-rendered form %]
628 [%# widget_result.as_xml %]
629
630 [%# Iterate over the form elements and display each -%]
631 <form name="book_form" action="[% widget_result.action %]" method="post">
632 <table border="0">
633 [% FOREACH element = widget_result.elements %]
634 <tr>
635 <td class="form-label">
636 [% element.label.as_text %]
637 </td>
638 <td class="form-element">
639 [% element.element_xml %]
640 <span class="form-error">
641 [% element.error_xml %]
642 </span>
643 </td>
644 </tr>
645 [% END %]
646 </table>
647 </form>
648
649 <p><a href="[% Catalyst.uri_for('list') %]">Return to book list</a></p>
650
651 [%# A little JavaScript to move the cursor to the first field %]
652 <script LANGUAGE="JavaScript">
653 document.book_form.book_form_title.focus();
654 </script>
655
656 This represents three changes:
657
658 · The existing "widget_result.as_xml" has been commented out.
659
660 · It loops through each form element, displaying the field name in
661 the first table cell along with the form element and validation
662 errors in the second field.
663
664 · JavaScript to position the user's cursor in the first field of the
665 form.
666
667 Try Out the Form
668
669 Press "Ctrl-C" to kill the previous server instance (if it's still run‐
670 ning) and restart it:
671
672 $ script/myapp_server.pl
673
674 Try adding a book that validates. Return to the book list and the book
675 you added should be visible.
676
678 Kennedy Clark, "hkclark@gmail.com"
679
680 Please report any errors, issues or suggestions to the author. The
681 most recent version of the Catalyst Tutorial can be found at
682 <http://dev.catalyst.perl.org/repos/Catalyst/trunk/Catalyst-Man‐
683 ual/lib/Catalyst/Manual/Tutorial/>.
684
685 Copyright 2006, Kennedy Clark, under Creative Commons License
686 (<http://creativecommons.org/licenses/by-nc-sa/2.5/>).
687
688
689
690perl v5.8.8 20C0a7t-a0l2y-s2t8::Manual::Tutorial::AdvancedCRUD(3)