1Catalyst::Manual::TutorUisaelr::CAodnvtarnicbeudtCCeaRdtUaDPl(ey3rs)lt:D:oMcaunmueanlt:a:tTiuotnorial::AdvancedCRUD(3)
2
3
4

NAME

6       Catalyst::Manual::Tutorial::AdvancedCRUD - Catalyst Tutorial - Part 8:
7       Advanced CRUD
8

OVERVIEW

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

DESCRIPTION

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

"HTML::WIDGET" FORM CREATION

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

"HTML::WIDGET" VALIDATION AND FILTERING

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

Enable "DBIx::Class::HTMLWidget" Support

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

Rendering "HTMLWidget" Forms in a Table

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

AUTHOR

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