3 Catalyst::Manual::Tutorial::AdvancedCRUD - Catalyst Tutorial - Part 9: Advanced CRUD
8 This is B<Part 9 of 10> for the Catalyst tutorial.
10 L<Tutorial Overview|Catalyst::Manual::Tutorial>
16 L<Introduction|Catalyst::Manual::Tutorial::Intro>
20 L<Catalyst Basics|Catalyst::Manual::Tutorial::CatalystBasics>
24 L<More Catalyst Basics|Catalyst::Manual::Tutorial::MoreCatalystBasics>
28 L<Basic CRUD|Catalyst::Manual::Tutorial::BasicCRUD>
32 L<Authentication|Catalyst::Manual::Tutorial::Authentication>
36 L<Authorization|Catalyst::Manual::Tutorial::Authorization>
40 L<Debugging|Catalyst::Manual::Tutorial::Debugging>
44 L<Testing|Catalyst::Manual::Tutorial::Testing>
52 L<Appendices|Catalyst::Manual::Tutorial::Appendices>
59 This part of the tutorial explores more advanced functionality for
60 Create, Read, Update, and Delete (CRUD) than we saw in Part 3. In
61 particular, it looks at a number of techniques that can be useful for
62 the Update portion of CRUD, such as automated form generation,
63 validation of user-entered data, and automated transfer of data between
64 forms and model objects.
66 In keeping with the Catalyst (and Perl) spirit of flexibility, there are
67 many different ways to approach advanced CRUD operations in a Catalyst
68 environment. One alternative is to use
69 L<Catalyst::Helper::Controller::Scaffold|Catalyst::Helper::Controller::Scaffold>
70 to instantly construct a set of Controller methods and templates for
71 basic CRUD operations. Although a popular subject in Quicktime
72 movies that serve as promotional material for various frameworks,
73 real-world applications generally require more control. Other
74 options include L<Data::FormValidator|Data::FormValidator> and
75 L<HTML::FillInForm|HTML::FillInForm>.
77 Note that HTML::Widget is no longer maintained.
78 L<HTML::FormFu|HTML::FormFu> was developed as a replacement. There is
79 an example HTML::FormFu application at
80 L<http://dev.catalyst.perl.org/repos/Catalyst/examples/Advent07FormFu/final/Fu/Fu-0.01.tar.gz>.
81 Another popular alternative for HTML FormFu is
82 L<Catalyst::Controller::Formbuilder|Catalyst::Controller::Formbuilder>
83 which is used in the L<Catalyst
84 Book|http://www.packtpub.com/catalyst-perl-web-application/book>.
86 Here, we will make use of the
87 L<HTML::Widget|HTML::Widget> to not only ease form creation, but to
88 also provide validation of the submitted data. The approached used by
89 this part of the tutorial is to slowly incorporate additional
90 L<HTML::Widget|HTML::Widget> functionality in a step-wise fashion (we
91 start with fairly simple form creation and then move on to more
92 complex and "magical" features such as validation and
93 auto-population/auto-saving).
95 B<Note:> Part 8 of the tutorial is optional. Users who do not wish to
96 use L<HTML::Widget|HTML::Widget> may skip this part.
98 You can checkout the source code for this example from the catalyst subversion repository as per the instructions in L<Catalyst::Manual::Tutorial::Intro>
100 =head1 C<HTML::WIDGET> FORM CREATION
102 This section looks at how L<HTML::Widget|HTML::Widget> can be used to
103 add additional functionality to the manually created form from Part 3.
105 =head2 Add the C<HTML::Widget> Plugin
107 Open C<lib/MyApp.pm> in your editor and add the following to the list of
108 plugins (be sure to leave the existing plugins enabled):
112 =head2 Add a Form Creation Helper Method
114 Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
117 =head2 make_book_widget
119 Build an HTML::Widget form for book creation and updates
123 sub make_book_widget {
126 # Create an HTML::Widget to build the form
127 my $w = $c->widget('book_form')->method('post');
130 my @authorObjs = $c->model("MyAppDB::Author")->all();
131 my @authors = map {$_->id => $_->last_name }
132 sort {$a->last_name cmp $b->last_name} @authorObjs;
134 # Create the form feilds
135 $w->element('Textfield', 'title' )->label('Title')->size(60);
136 $w->element('Textfield', 'rating' )->label('Rating')->size(1);
137 $w->element('Select', 'authors')->label('Authors')
139 $w->element('Submit', 'submit' )->value('submit');
145 This method provides a central location that builds an
146 HTML::Widget-based form with the appropriate fields. The "Get authors"
147 code uses DBIC to retrieve a list of model objects and then uses C<map>
148 to create a hash where the hash keys are the database primary keys from
149 the authors table and the associated values are the last names of the
152 =head2 Add Actions to Display and Save the Form
154 Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
159 Build an HTML::Widget form for book creation and updates
163 sub hw_create : Local {
166 # Create the widget and set the action for the form
167 my $w = $self->make_book_widget($c);
168 $w->action($c->uri_for('hw_create_do'));
170 # Write form to stash variable for use in template
171 $c->stash->{widget_result} = $w->result;
174 $c->stash->{template} = 'books/hw_form.tt2';
180 Build an HTML::Widget form for book creation and updates
184 sub hw_create_do : Local {
187 # Retrieve the data from the form
188 my $title = $c->request->params->{title};
189 my $rating = $c->request->params->{rating};
190 my $authors = $c->request->params->{authors};
192 # Call create() on the book model object. Pass the table
193 # columns/field values we want to set as hash values
194 my $book = $c->model('MyAppDB::Book')->create({
199 # Add a record to the join table for this book, mapping to
201 $book->add_to_book_authors({author_id => $authors});
203 # Set a status message for the user
204 $c->stash->{status_msg} = 'Book created';
206 # Use 'hw_create' to redisplay the form. As discussed in
207 # Part 3, 'detach' is like 'forward', but it does not return
208 $c->detach('hw_create');
211 Note how we use C<make_book_widget> to build the core parts of the form
212 in one location, but we set the action (the URL the form is sent to when
213 the user clicks the 'Submit' button) separately in C<hw_create>. Doing
214 so allows us to have the same form submit the data to different actions
215 (e.g., C<hw_create_do> for a create operation but C<hw_update_do> to
216 update an existing book object).
218 B<NOTE:> If you receive an error about Catalyst not being able to find
219 the template C<hw_create_do.tt2>, please verify that you followed the
220 instructions in the final section of
221 L<Catalyst Basics|Catalyst::Manual::Tutorial::CatalystBasics> where
222 you returned to a manually-specified template. You can either use
223 C<forward>/C<detach> B<OR> default template names, but the two cannot
227 =head2 Update the CSS
229 Edit C<root/src/ttsite.css> and add the following lines to the bottom of
250 color: [% site.col.error %];
253 These changes will display form elements vertically and also show error
254 messages in red. Note that we are pulling the color scheme settings
255 from the C<root/lib/config/col> file that was created by the TTSite
256 helper. This allows us to change the color used by various error styles
257 in the CSS from a single location.
259 =head2 Create a Template Page To Display The Form
261 Open C<root/src/books/hw_form.tt2> in your editor and enter the following:
263 [% META title = 'Create/Update Book' %]
265 [% widget_result.as_xml %]
267 <p><a href="[% Catalyst.uri_for('list') %]">Return to book list</a></p>
269 =head2 Add Links for Create and Update via C<HTML::Widget>
271 Open C<root/src/books/list.tt2> in your editor and add the following to
272 the bottom of the existing file:
276 <a href="[% Catalyst.uri_for('hw_create') %]">Create</a>
280 =head2 Test The <HTML::Widget> Create Form
282 Press C<Ctrl-C> to kill the previous server instance (if it's still
283 running) and restart it:
285 $ script/myapp_server.pl
287 Login as C<test01>. Once at the Book List page, click the HTML::Widget
288 "Create" link to display for form produced by C<make_book_widget>. Fill
289 out the form with the following values: Title = "Internetworking with
290 TCP/IP Vol. II", Rating = "4", and Author = "Comer". Click Submit, and
291 you will be returned to the Create/Update Book page with a "Book
292 created" status message displayed. Click "Return to book list" to view
293 the newly created book on the main list.
295 Also note that this implementation allows you to can create books with
296 bogus information. Although we have constrained the authors with the
297 drop-down list, there are no restrictions on items such as the length of
298 the title (for example, you can create a one-letter title) and value for
299 the rating (you can use any number you want, and even non-numeric values
300 with SQLite). The next section will address this concern.
302 B<Note:> Depending on the database you are using and how you established
303 the columns in your tables, the database could obviously provide various
304 levels of "type enforcement" on your data. The key point being made in
305 the previous paragraph is that the I<web application> itself is not
306 performing any validation.
308 =head1 C<HTML::WIDGET> VALIDATION AND FILTERING
310 Although the use of L<HTML::Widget|HTML::Widget> in the previous section
311 did provide an automated mechanism to build the form, the real power of
312 this module stems from functionality that can automatically validate and
313 filter the user input. Validation uses constraints to be sure that
314 users input appropriate data (for example, that the email field of a
315 form contains a valid email address). Filtering can be used to remove
316 extraneous whitespace from fields or to escape meta-characters in user
319 =head2 Add Constraints and Filters to the Widget Creation Method
321 Open C<lib/MyApp/Controller/Books.pm> in your editor and update the
322 C<make_book_widget> method to match the following (new sections have
323 been marked with a C<*** NEW:> comment):
325 sub make_book_widget {
328 # Create an HTML::Widget to build the form
329 my $w = $c->widget('book_form')->method('post');
332 my @authorObjs = $c->model("MyAppDB::Author")->all();
333 my @authors = map {$_->id => $_->last_name }
334 sort {$a->last_name cmp $b->last_name} @authorObjs;
336 # Create the form feilds
337 $w->element('Textfield', 'title' )->label('Title')->size(60);
338 $w->element('Textfield', 'rating' )->label('Rating')->size(1);
339 # ***NEW: Convert to multi-select list
340 $w->element('Select', 'authors')->label('Authors')
341 ->options(@authors)->multiple(1)->size(3);
342 $w->element('Submit', 'submit' )->value('submit');
344 # ***NEW: Set constraints
345 $w->constraint(All => qw/title rating authors/)
346 ->message('Required. ');
347 $w->constraint(Integer => qw/rating/)
348 ->message('Must be an integer. ');
349 $w->constraint(Range => qw/rating/)->min(1)->max(5)
350 ->message('Must be a number between 1 and 5. ');
351 $w->constraint(Length => qw/title/)->min(5)->max(50)
352 ->message('Must be between 5 and 50 characters. ');
354 # ***NEW: Set filters
355 for my $column (qw/title rating authors/) {
356 $w->filter( HTMLEscape => $column );
357 $w->filter( TrimEdges => $column );
364 The main changes are:
370 The C<Select> element for C<authors> is changed from a single-select
371 drop-down to a multi-select list by adding calls to C<multiple> (set to
372 C<true>) and C<size> (set to the number of rows to display).
376 Four sets of constraints are added to provide validation of the user input.
380 Two filters are run on every field to remove and escape unwanted input.
384 =head2 Rebuild the Form Submission Method to Include Validation
386 Edit C<lib/MyApp/Controller/Books.pm> and change C<hw_create_do> to
387 match the following code (enough of the code is different that you
388 probably want to cut and paste this over code the existing method):
390 sub hw_create_do : Local {
393 # Retrieve the data from the form
394 my $title = $c->request->params->{title};
395 my $rating = $c->request->params->{rating};
396 my $authors = $c->request->params->{authors};
398 # Create the widget and set the action for the form
399 my $w = $self->make_book_widget($c);
400 $w->action($c->uri_for('hw_create_do'));
402 # Validate the form parameters
403 my $result = $w->process($c->req);
405 # Write form (including validation error messages) to
406 # stash variable for use in template
407 $c->stash->{widget_result} = $result;
409 # Were their validation errors?
410 if ($result->has_errors) {
411 # Warn the user at the top of the form that there were errors.
412 # Note that there will also be per-field feedback on
413 # validation errors because of '$w->process($c->req)' above.
414 $c->stash->{error_msg} = 'Validation errors!';
416 # Everything validated OK, so do the create
417 # Call create() on the book model object. Pass the table
418 # columns/field values we want to set as hash values
419 my $book = $c->model('MyAppDB::Book')->create({
424 # Add a record to the join table for this book, mapping to
425 # appropriate author. Note that $authors will be 1 author as
426 # a scalar or ref to list of authors depending on how many the
427 # user selected; the 'ref $authors ?...' handles both cases
428 foreach my $author (ref $authors ? @$authors : $authors) {
429 $book->add_to_book_authors({author_id => $author});
431 # Set a status message for the user
432 $c->stash->{status_msg} = 'Book created';
436 $c->stash->{template} = 'books/hw_form.tt2';
439 The key changes to C<hw_create_do> are:
445 C<hw_create_do> no longer does a C<detach> to C<hw_create> to redisplay
446 the form. Now that C<hw_create_do> has to process the form in order to
447 perform the validation, we go ahead and build a complete set of form
448 presentation logic into C<hw_create_do> (for example, C<hw_create_do>
449 now has a C<$c-E<gt>stash-E<gt>{template}> line). Note that if we
450 process the form in C<hw_create_do> I<and> forward/detach back to
451 <hw_create>, we would end up with C<make_book_widget> being called
452 twice, resulting in a duplicate set of elements being added to the form.
453 (There are other ways to address the "duplicate form rendering" issue --
454 just be aware that it exists.)
458 C<$w-E<gt>process($c-E<gt>req)> is called to run the validation logic.
459 Not only does this set the C<has_errors> flag if validation errors are
460 encountered, it returns a string containing any field-specific warning
465 An C<if> statement checks if any validation errors were encountered. If
466 so, C<$c-E<gt>stash-E<gt>{error_msg}> is set and the input form is
467 redisplayed. If no errors were found, the object is created in a manner
468 similar to the prior version of the C<hw_create_do> method.
472 =head2 Try Out the Form
474 Press C<Ctrl-C> to kill the previous server instance (if it's still
475 running) and restart it:
477 $ script/myapp_server.pl
479 Now try adding a book with various errors: title less than 5 characters,
480 non-numeric rating, a rating of 0 or 6, etc. Also try selecting one,
481 two, and zero authors. When you click Submit, the HTML::Widget
482 C<constraint> items will validate the logic and insert feedback as
486 =head1 Enable C<DBIx::Class::HTMLWidget> Support
488 In this section we will take advantage of some of the "auto-population"
489 features of C<DBIx::Class::HTMLWidget>. Enabling
490 C<DBIx::Class::HTMLWidget> provides two additional methods to your DBIC
499 Takes data from the database and transfers it to your form widget.
503 populate_from_widget()
505 Takes data from a form widget and uses it to update the corresponding
506 records in the database.
510 In other words, the two methods are a mirror image of each other: one
511 reads from the database while the other writes to the database.
513 =head2 Add C<DBIx::Class::HTMLWidget> to DBIC Model
515 In order to use L<DBIx::Class::HTMLWidget|DBIx::Class::HTMLWidget>, we
516 need to add C<HTMLWidget> to the C<load_components> line of DBIC result
517 source files that need to use the C<fill_widget> and
518 C<populate_from_widget> methods. In this case, open
519 C<lib/MyAppDB/Book.pm> and update the C<load_components> line to match:
521 __PACKAGE__->load_components(qw/PK::Auto Core HTMLWidget/);
523 =head2 Use C<populate_from_widget> in C<hw_create_do>
525 Edit C<lib/MyApp/Controller/Books.pm> and update C<hw_create_do> to
526 match the following code:
530 Build an HTML::Widget form for book creation and updates
534 sub hw_create_do : Local {
537 # Create the widget and set the action for the form
538 my $w = $self->make_book_widget($c);
539 $w->action($c->uri_for('hw_create_do'));
541 # Validate the form parameters
542 my $result = $w->process($c->req);
544 # Write form (including validation error messages) to
545 # stash variable for use in template
546 $c->stash->{widget_result} = $result;
548 # Were their validation errors?
549 if ($result->has_errors) {
550 # Warn the user at the top of the form that there were errors.
551 # Note that there will also be per-field feedback on
552 # validation errors because of '$w->process($c->req)' above.
553 $c->stash->{error_msg} = 'Validation errors!';
555 my $book = $c->model('MyAppDB::Book')->new({});
556 $book->populate_from_widget($result);
558 # Add a record to the join table for this book, mapping to
559 # appropriate author. Note that $authors will be 1 author as
560 # a scalar or ref to list of authors depending on how many the
561 # user selected; the 'ref $authors ?...' handles both cases
562 my $authors = $c->request->params->{authors};
563 foreach my $author (ref $authors ? @$authors : $authors) {
564 $book->add_to_book_authors({author_id => $author});
567 # Set a status message for the user
568 $c->flash->{status_msg} = 'Book created';
570 # Redisplay an empty form for another
571 $c->stash->{widget_result} = $w->result;
575 $c->stash->{template} = 'books/hw_form.tt2';
578 In this version of C<hw_create_do> we removed the logic that manually
579 pulled the form variables and used them to call
580 C<$c-E<gt>model('MyAppDB::Book')-E<gt>create> and replaced it with a
581 single call to C<$book-E<gt>populate_from_widget>. Note that we still
582 have to call C<$book-E<gt>add_to_book_authors> once per author because
583 C<populate_from_widget> does not currently handle the relationships
584 between tables. Also, we reset the form to an empty fields by adding
585 another call to C<$w-E<gt>result> and storing the output in the stash
586 (if we don't override the output from C<$w-E<gt>process($c-E<gt>req)>,
587 the form values already entered will be retained on redisplay --
588 although this could be desirable for some applications, we avoid it
589 here to help avoid the creation of duplicate records).
592 =head2 Try Out the Form
594 Press C<Ctrl-C> to kill the previous server instance (if it's still
595 running) and restart it:
597 $ script/myapp_server.pl
599 Try adding a book that validates. Return to the book list and the book
600 you added should be visible.
604 =head1 Rendering C<HTMLWidget> Forms in a Table
606 Some developers my wish to use the "old-fashioned" table style of
607 rendering a form in lieu of the default C<HTML::Widget> rendering that
608 assumes you will use CSS for formatting. This section demonstrates
609 some techniques that can override the default rendering with a
613 =head2 Add a New "Element Container"
615 Open C<lib/FormElementContainer.pm> in your editor and enter:
617 package FormElementContainer;
619 use base 'HTML::Widget::Container';
622 my ($self, $element) = @_;
624 return () unless $element;
625 if (ref $element eq 'ARRAY') {
626 return map { $self->_build_element($_) } @{$element};
628 my $e = $element->clone;
629 $e = new HTML::Element('span', class => 'fields_with_errors')->push_content($e)
630 if $self->error && $e->tag eq 'input';
632 return $e ? ($e) : ();
637 This simply dumps the HTML code for a given form element, followed by a
638 C<span> that can contain validation error message.
641 =head2 Enable the New Element Container When Building the Form
643 Open C<lib/MyApp/Controller/Books.pm> in your editor. First add a
644 C<use> for your element container class:
646 use FormElementContainer;
648 B<Note:> If you forget to C<use> your container class in your
649 controller, then your form will not be displayed and no error messages
650 will be generated. Don't forget this important step!
652 Then tell C<HTML::Widget> to use that class during rendering by updating
653 C<make_book_widget> to match the following:
655 sub make_book_widget {
658 # Create an HTML::Widget to build the form
659 my $w = $c->widget('book_form')->method('post');
661 # ***New: Use custom class to render each element in the form
662 $w->element_container_class('FormElementContainer');
665 my @authorObjs = $c->model("MyAppDB::Author")->all();
666 my @authors = map {$_->id => $_->last_name }
667 sort {$a->last_name cmp $b->last_name} @authorObjs;
669 # Create the form feilds
670 $w->element('Textfield', 'title' )->label('Title')->size(60);
671 $w->element('Textfield', 'rating' )->label('Rating')->size(1);
672 # Convert to multi-select list
673 $w->element('Select', 'authors')->label('Authors')
674 ->options(@authors)->multiple(1)->size(3);
675 $w->element('Submit', 'submit' )->value('submit');
678 $w->constraint(All => qw/title rating authors/)
679 ->message('Required. ');
680 $w->constraint(Integer => qw/rating/)
681 ->message('Must be an integer. ');
682 $w->constraint(Range => qw/rating/)->min(1)->max(5)
683 ->message('Must be a number between 1 and 5. ');
684 $w->constraint(Length => qw/title/)->min(5)->max(50)
685 ->message('Must be between 5 and 50 characters. ');
688 for my $column (qw/title rating authors/) {
689 $w->filter( HTMLEscape => $column );
690 $w->filter( TrimEdges => $column );
697 The two new lines are marked with C<***New:>.
700 =head2 Update the TT Template
702 Open C<root/src/books/hw_form.tt2> and edit it to match:
704 [% META title = 'Create/Update Book' %]
706 [%# Comment out the auto-rendered form %]
707 [%# widget_result.as_xml %]
710 [%# Iterate over the form elements and display each -%]
711 <form name="book_form" action="[% widget_result.action %]" method="post">
713 [% FOREACH element = widget_result.elements %]
715 <td class="form-label">
716 [% element.label.as_text %]
718 <td class="form-element">
719 [% element.element_xml %]
720 <span class="form-error">
721 [% element.error_xml %]
730 <p><a href="[% Catalyst.uri_for('list') %]">Return to book list</a></p>
733 [%# A little JavaScript to move the cursor to the first field %]
734 <script LANGUAGE="JavaScript">
735 document.book_form.book_form_title.focus();
738 This represents three changes:
744 The existing C<widget_result.as_xml> has been commented out.
748 It loops through each form element, displaying the field name in the
749 first table cell along with the form element and validation errors in
754 JavaScript to position the user's cursor in the first field of the form.
759 =head2 Try Out the Form
761 Press C<Ctrl-C> to kill the previous server instance (if it's still
762 running) and restart it:
764 $ script/myapp_server.pl
766 Try adding a book that validates. Return to the book list and the book
767 you added should be visible.
772 Kennedy Clark, C<hkclark@gmail.com>
774 Please report any errors, issues or suggestions to the author. The
775 most recent version of the Catalyst Tutorial can be found at
776 L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/Catalyst-Manual/lib/Catalyst/Manual/Tutorial/>.
778 Copyright 2006, Kennedy Clark, under Creative Commons License
779 (L<http://creativecommons.org/licenses/by-nc-sa/2.5/>).