3 Catalyst::Manual::Tutorial::AdvancedCRUD - Catalyst Tutorial - Part 8: Advanced CRUD
8 This is B<Part 8 of 9> 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<Basic CRUD|Catalyst::Manual::Tutorial_BasicCRUD>
28 L<Authentication|Catalyst::Manual::Tutorial::Authentication>
32 L<Authorization|Catalyst::Manual::Tutorial::Authorization>
36 L<Debugging|Catalyst::Manual::Tutorial::Debugging>
40 L<Testing|Catalyst::Manual::Tutorial::Testing>
48 L<Appendices|Catalyst::Manual::Tutorial::Appendices>
54 This part of the tutorial explores more advanced functionality for
55 Create, Read, Update, and Delete (CRUD) than we saw in Part 3. In
56 particular, it looks at a number of techniques that can be useful for
57 the Update portion of CRUD, such as automated form generation,
58 validation of user-entered data, and automated transfer of data between
59 forms and model objects.
61 In keeping with the Catalyst (and Perl) spirit of flexibility, there are
62 many different ways to approach advanced CRUD operations in a Catalyst
63 environment. One alternative is to use
64 L<Catalyst::Helper::Controller::Scaffold|Catalyst::Helper::Controller::Scaffold>
65 to instantly construct a set of Controller methods and templates for
66 basic CRUD operations. Although a popular subject in Quicktime
67 movies that serve as promotional material for various frameworks,
68 real-world applications generally require more control. Other
69 options include L<Data::FormValidator|Data::FormValidator> and
70 L<HTML::FillInForm|HTML::FillInForm>.
72 Note that HTML::Widget is no longer maintained.
73 L<HTML::FormFu|HTML::FormFu> was developed as a replacement. There is
74 an example HTML::FormFu application at
75 L<http://dev.catalyst.perl.org/repos/Catalyst/examples/Advent07FormFu/final/Fu/Fu-0.01.tar.gz>.
76 Another popular alternative for HTML FormFu is
77 L<Catalyst::Controller::Formbuilder|Catalyst::Controller::Formbuilder>
78 which is used in the L<Catalyst
79 Book|http://www.packtpub.com/catalyst-perl-web-application/book>.
81 Here, we will make use of the
82 L<HTML::Widget|HTML::Widget> to not only ease form creation, but to
83 also provide validation of the submitted data. The approached used by
84 this part of the tutorial is to slowly incorporate additional
85 L<HTML::Widget|HTML::Widget> functionality in a step-wise fashion (we
86 start with fairly simple form creation and then move on to more
87 complex and "magical" features such as validation and
88 auto-population/auto-saving).
90 B<Note:> Part 8 of the tutorial is optional. Users who do not wish to
91 use L<HTML::Widget|HTML::Widget> may skip this part.
93 You can checkout the source code for this example from the catalyst subversion repository as per the instructions in L<Catalyst::Manual::Tutorial::Intro>
95 =head1 C<HTML::WIDGET> FORM CREATION
97 This section looks at how L<HTML::Widget|HTML::Widget> can be used to
98 add additional functionality to the manually created form from Part 3.
100 =head2 Add the C<HTML::Widget> Plugin
102 Open C<lib/MyApp.pm> in your editor and add the following to the list of
103 plugins (be sure to leave the existing plugins enabled):
107 =head2 Add a Form Creation Helper Method
109 Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
112 =head2 make_book_widget
114 Build an HTML::Widget form for book creation and updates
118 sub make_book_widget {
121 # Create an HTML::Widget to build the form
122 my $w = $c->widget('book_form')->method('post');
125 my @authorObjs = $c->model("MyAppDB::Author")->all();
126 my @authors = map {$_->id => $_->last_name }
127 sort {$a->last_name cmp $b->last_name} @authorObjs;
129 # Create the form feilds
130 $w->element('Textfield', 'title' )->label('Title')->size(60);
131 $w->element('Textfield', 'rating' )->label('Rating')->size(1);
132 $w->element('Select', 'authors')->label('Authors')
134 $w->element('Submit', 'submit' )->value('submit');
140 This method provides a central location that builds an
141 HTML::Widget-based form with the appropriate fields. The "Get authors"
142 code uses DBIC to retrieve a list of model objects and then uses C<map>
143 to create a hash where the hash keys are the database primary keys from
144 the authors table and the associated values are the last names of the
147 =head2 Add Actions to Display and Save the Form
149 Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
154 Build an HTML::Widget form for book creation and updates
158 sub hw_create : Local {
161 # Create the widget and set the action for the form
162 my $w = $self->make_book_widget($c);
163 $w->action($c->uri_for('hw_create_do'));
165 # Write form to stash variable for use in template
166 $c->stash->{widget_result} = $w->result;
169 $c->stash->{template} = 'books/hw_form.tt2';
175 Build an HTML::Widget form for book creation and updates
179 sub hw_create_do : Local {
182 # Retrieve the data from the form
183 my $title = $c->request->params->{title};
184 my $rating = $c->request->params->{rating};
185 my $authors = $c->request->params->{authors};
187 # Call create() on the book model object. Pass the table
188 # columns/field values we want to set as hash values
189 my $book = $c->model('MyAppDB::Book')->create({
194 # Add a record to the join table for this book, mapping to
196 $book->add_to_book_authors({author_id => $authors});
198 # Set a status message for the user
199 $c->stash->{status_msg} = 'Book created';
201 # Use 'hw_create' to redisplay the form. As discussed in
202 # Part 3, 'detach' is like 'forward', but it does not return
203 $c->detach('hw_create');
206 Note how we use C<make_book_widget> to build the core parts of the form
207 in one location, but we set the action (the URL the form is sent to when
208 the user clicks the 'Submit' button) separately in C<hw_create>. Doing
209 so allows us to have the same form submit the data to different actions
210 (e.g., C<hw_create_do> for a create operation but C<hw_update_do> to
211 update an existing book object).
213 B<NOTE:> If you receive an error about Catalyst not being able to find
214 the template C<hw_create_do.tt2>, please verify that you followed the
215 instructions in the final section of
216 L<Catalyst Basics|Catalyst::Manual::Tutorial::CatalystBasics> where
217 you returned to a manually-specified template. You can either use
218 C<forward>/C<detach> B<OR> default template names, but the two cannot
222 =head2 Update the CSS
224 Edit C<root/src/ttsite.css> and add the following lines to the bottom of
245 color: [% site.col.error %];
248 These changes will display form elements vertically and also show error
249 messages in red. Note that we are pulling the color scheme settings
250 from the C<root/lib/config/col> file that was created by the TTSite
251 helper. This allows us to change the color used by various error styles
252 in the CSS from a single location.
254 =head2 Create a Template Page To Display The Form
256 Open C<root/src/books/hw_form.tt2> in your editor and enter the following:
258 [% META title = 'Create/Update Book' %]
260 [% widget_result.as_xml %]
262 <p><a href="[% Catalyst.uri_for('list') %]">Return to book list</a></p>
264 =head2 Add Links for Create and Update via C<HTML::Widget>
266 Open C<root/src/books/list.tt2> in your editor and add the following to
267 the bottom of the existing file:
271 <a href="[% Catalyst.uri_for('hw_create') %]">Create</a>
275 =head2 Test The <HTML::Widget> Create Form
277 Press C<Ctrl-C> to kill the previous server instance (if it's still
278 running) and restart it:
280 $ script/myapp_server.pl
282 Login as C<test01>. Once at the Book List page, click the HTML::Widget
283 "Create" link to display for form produced by C<make_book_widget>. Fill
284 out the form with the following values: Title = "Internetworking with
285 TCP/IP Vol. II", Rating = "4", and Author = "Comer". Click Submit, and
286 you will be returned to the Create/Update Book page with a "Book
287 created" status message displayed. Click "Return to book list" to view
288 the newly created book on the main list.
290 Also note that this implementation allows you to can create books with
291 bogus information. Although we have constrained the authors with the
292 drop-down list, there are no restrictions on items such as the length of
293 the title (for example, you can create a one-letter title) and value for
294 the rating (you can use any number you want, and even non-numeric values
295 with SQLite). The next section will address this concern.
297 B<Note:> Depending on the database you are using and how you established
298 the columns in your tables, the database could obviously provide various
299 levels of "type enforcement" on your data. The key point being made in
300 the previous paragraph is that the I<web application> itself is not
301 performing any validation.
303 =head1 C<HTML::WIDGET> VALIDATION AND FILTERING
305 Although the use of L<HTML::Widget|HTML::Widget> in the previous section
306 did provide an automated mechanism to build the form, the real power of
307 this module stems from functionality that can automatically validate and
308 filter the user input. Validation uses constraints to be sure that
309 users input appropriate data (for example, that the email field of a
310 form contains a valid email address). Filtering can be used to remove
311 extraneous whitespace from fields or to escape meta-characters in user
314 =head2 Add Constraints and Filters to the Widget Creation Method
316 Open C<lib/MyApp/Controller/Books.pm> in your editor and update the
317 C<make_book_widget> method to match the following (new sections have
318 been marked with a C<*** NEW:> comment):
320 sub make_book_widget {
323 # Create an HTML::Widget to build the form
324 my $w = $c->widget('book_form')->method('post');
327 my @authorObjs = $c->model("MyAppDB::Author")->all();
328 my @authors = map {$_->id => $_->last_name }
329 sort {$a->last_name cmp $b->last_name} @authorObjs;
331 # Create the form feilds
332 $w->element('Textfield', 'title' )->label('Title')->size(60);
333 $w->element('Textfield', 'rating' )->label('Rating')->size(1);
334 # ***NEW: Convert to multi-select list
335 $w->element('Select', 'authors')->label('Authors')
336 ->options(@authors)->multiple(1)->size(3);
337 $w->element('Submit', 'submit' )->value('submit');
339 # ***NEW: Set constraints
340 $w->constraint(All => qw/title rating authors/)
341 ->message('Required. ');
342 $w->constraint(Integer => qw/rating/)
343 ->message('Must be an integer. ');
344 $w->constraint(Range => qw/rating/)->min(1)->max(5)
345 ->message('Must be a number between 1 and 5. ');
346 $w->constraint(Length => qw/title/)->min(5)->max(50)
347 ->message('Must be between 5 and 50 characters. ');
349 # ***NEW: Set filters
350 for my $column (qw/title rating authors/) {
351 $w->filter( HTMLEscape => $column );
352 $w->filter( TrimEdges => $column );
359 The main changes are:
365 The C<Select> element for C<authors> is changed from a single-select
366 drop-down to a multi-select list by adding calls to C<multiple> (set to
367 C<true>) and C<size> (set to the number of rows to display).
371 Four sets of constraints are added to provide validation of the user input.
375 Two filters are run on every field to remove and escape unwanted input.
379 =head2 Rebuild the Form Submission Method to Include Validation
381 Edit C<lib/MyApp/Controller/Books.pm> and change C<hw_create_do> to
382 match the following code (enough of the code is different that you
383 probably want to cut and paste this over code the existing method):
385 sub hw_create_do : Local {
388 # Retrieve the data from the form
389 my $title = $c->request->params->{title};
390 my $rating = $c->request->params->{rating};
391 my $authors = $c->request->params->{authors};
393 # Create the widget and set the action for the form
394 my $w = $self->make_book_widget($c);
395 $w->action($c->uri_for('hw_create_do'));
397 # Validate the form parameters
398 my $result = $w->process($c->req);
400 # Write form (including validation error messages) to
401 # stash variable for use in template
402 $c->stash->{widget_result} = $result;
404 # Were their validation errors?
405 if ($result->has_errors) {
406 # Warn the user at the top of the form that there were errors.
407 # Note that there will also be per-field feedback on
408 # validation errors because of '$w->process($c->req)' above.
409 $c->stash->{error_msg} = 'Validation errors!';
411 # Everything validated OK, so do the create
412 # Call create() on the book model object. Pass the table
413 # columns/field values we want to set as hash values
414 my $book = $c->model('MyAppDB::Book')->create({
419 # Add a record to the join table for this book, mapping to
420 # appropriate author. Note that $authors will be 1 author as
421 # a scalar or ref to list of authors depending on how many the
422 # user selected; the 'ref $authors ?...' handles both cases
423 foreach my $author (ref $authors ? @$authors : $authors) {
424 $book->add_to_book_authors({author_id => $author});
426 # Set a status message for the user
427 $c->stash->{status_msg} = 'Book created';
431 $c->stash->{template} = 'books/hw_form.tt2';
434 The key changes to C<hw_create_do> are:
440 C<hw_create_do> no longer does a C<detach> to C<hw_create> to redisplay
441 the form. Now that C<hw_create_do> has to process the form in order to
442 perform the validation, we go ahead and build a complete set of form
443 presentation logic into C<hw_create_do> (for example, C<hw_create_do>
444 now has a C<$c-E<gt>stash-E<gt>{template}> line). Note that if we
445 process the form in C<hw_create_do> I<and> forward/detach back to
446 <hw_create>, we would end up with C<make_book_widget> being called
447 twice, resulting in a duplicate set of elements being added to the form.
448 (There are other ways to address the "duplicate form rendering" issue --
449 just be aware that it exists.)
453 C<$w-E<gt>process($c-E<gt>req)> is called to run the validation logic.
454 Not only does this set the C<has_errors> flag if validation errors are
455 encountered, it returns a string containing any field-specific warning
460 An C<if> statement checks if any validation errors were encountered. If
461 so, C<$c-E<gt>stash-E<gt>{error_msg}> is set and the input form is
462 redisplayed. If no errors were found, the object is created in a manner
463 similar to the prior version of the C<hw_create_do> method.
467 =head2 Try Out the Form
469 Press C<Ctrl-C> to kill the previous server instance (if it's still
470 running) and restart it:
472 $ script/myapp_server.pl
474 Now try adding a book with various errors: title less than 5 characters,
475 non-numeric rating, a rating of 0 or 6, etc. Also try selecting one,
476 two, and zero authors. When you click Submit, the HTML::Widget
477 C<constraint> items will validate the logic and insert feedback as
481 =head1 Enable C<DBIx::Class::HTMLWidget> Support
483 In this section we will take advantage of some of the "auto-population"
484 features of C<DBIx::Class::HTMLWidget>. Enabling
485 C<DBIx::Class::HTMLWidget> provides two additional methods to your DBIC
494 Takes data from the database and transfers it to your form widget.
498 populate_from_widget()
500 Takes data from a form widget and uses it to update the corresponding
501 records in the database.
505 In other words, the two methods are a mirror image of each other: one
506 reads from the database while the other writes to the database.
508 =head2 Add C<DBIx::Class::HTMLWidget> to DBIC Model
510 In order to use L<DBIx::Class::HTMLWidget|DBIx::Class::HTMLWidget>, we
511 need to add C<HTMLWidget> to the C<load_components> line of DBIC result
512 source files that need to use the C<fill_widget> and
513 C<populate_from_widget> methods. In this case, open
514 C<lib/MyAppDB/Book.pm> and update the C<load_components> line to match:
516 __PACKAGE__->load_components(qw/PK::Auto Core HTMLWidget/);
518 =head2 Use C<populate_from_widget> in C<hw_create_do>
520 Edit C<lib/MyApp/Controller/Books.pm> and update C<hw_create_do> to
521 match the following code:
525 Build an HTML::Widget form for book creation and updates
529 sub hw_create_do : Local {
532 # Create the widget and set the action for the form
533 my $w = $self->make_book_widget($c);
534 $w->action($c->uri_for('hw_create_do'));
536 # Validate the form parameters
537 my $result = $w->process($c->req);
539 # Write form (including validation error messages) to
540 # stash variable for use in template
541 $c->stash->{widget_result} = $result;
543 # Were their validation errors?
544 if ($result->has_errors) {
545 # Warn the user at the top of the form that there were errors.
546 # Note that there will also be per-field feedback on
547 # validation errors because of '$w->process($c->req)' above.
548 $c->stash->{error_msg} = 'Validation errors!';
550 my $book = $c->model('MyAppDB::Book')->new({});
551 $book->populate_from_widget($result);
553 # Add a record to the join table for this book, mapping to
554 # appropriate author. Note that $authors will be 1 author as
555 # a scalar or ref to list of authors depending on how many the
556 # user selected; the 'ref $authors ?...' handles both cases
557 my $authors = $c->request->params->{authors};
558 foreach my $author (ref $authors ? @$authors : $authors) {
559 $book->add_to_book_authors({author_id => $author});
562 # Set a status message for the user
563 $c->flash->{status_msg} = 'Book created';
565 # Redisplay an empty form for another
566 $c->stash->{widget_result} = $w->result;
570 $c->stash->{template} = 'books/hw_form.tt2';
573 In this version of C<hw_create_do> we removed the logic that manually
574 pulled the form variables and used them to call
575 C<$c-E<gt>model('MyAppDB::Book')-E<gt>create> and replaced it with a
576 single call to C<$book-E<gt>populate_from_widget>. Note that we still
577 have to call C<$book-E<gt>add_to_book_authors> once per author because
578 C<populate_from_widget> does not currently handle the relationships
579 between tables. Also, we reset the form to an empty fields by adding
580 another call to C<$w-E<gt>result> and storing the output in the stash
581 (if we don't override the output from C<$w-E<gt>process($c-E<gt>req)>,
582 the form values already entered will be retained on redisplay --
583 although this could be desirable for some applications, we avoid it
584 here to help avoid the creation of duplicate records).
587 =head2 Try Out the Form
589 Press C<Ctrl-C> to kill the previous server instance (if it's still
590 running) and restart it:
592 $ script/myapp_server.pl
594 Try adding a book that validates. Return to the book list and the book
595 you added should be visible.
599 =head1 Rendering C<HTMLWidget> Forms in a Table
601 Some developers my wish to use the "old-fashioned" table style of
602 rendering a form in lieu of the default C<HTML::Widget> rendering that
603 assumes you will use CSS for formatting. This section demonstrates
604 some techniques that can override the default rendering with a
608 =head2 Add a New "Element Container"
610 Open C<lib/FormElementContainer.pm> in your editor and enter:
612 package FormElementContainer;
614 use base 'HTML::Widget::Container';
617 my ($self, $element) = @_;
619 return () unless $element;
620 if (ref $element eq 'ARRAY') {
621 return map { $self->_build_element($_) } @{$element};
623 my $e = $element->clone;
624 $e = new HTML::Element('span', class => 'fields_with_errors')->push_content($e)
625 if $self->error && $e->tag eq 'input';
627 return $e ? ($e) : ();
632 This simply dumps the HTML code for a given form element, followed by a
633 C<span> that can contain validation error message.
636 =head2 Enable the New Element Container When Building the Form
638 Open C<lib/MyApp/Controller/Books.pm> in your editor. First add a
639 C<use> for your element container class:
641 use FormElementContainer;
643 B<Note:> If you forget to C<use> your container class in your
644 controller, then your form will not be displayed and no error messages
645 will be generated. Don't forget this important step!
647 Then tell C<HTML::Widget> to use that class during rendering by updating
648 C<make_book_widget> to match the following:
650 sub make_book_widget {
653 # Create an HTML::Widget to build the form
654 my $w = $c->widget('book_form')->method('post');
656 # ***New: Use custom class to render each element in the form
657 $w->element_container_class('FormElementContainer');
660 my @authorObjs = $c->model("MyAppDB::Author")->all();
661 my @authors = map {$_->id => $_->last_name }
662 sort {$a->last_name cmp $b->last_name} @authorObjs;
664 # Create the form feilds
665 $w->element('Textfield', 'title' )->label('Title')->size(60);
666 $w->element('Textfield', 'rating' )->label('Rating')->size(1);
667 # Convert to multi-select list
668 $w->element('Select', 'authors')->label('Authors')
669 ->options(@authors)->multiple(1)->size(3);
670 $w->element('Submit', 'submit' )->value('submit');
673 $w->constraint(All => qw/title rating authors/)
674 ->message('Required. ');
675 $w->constraint(Integer => qw/rating/)
676 ->message('Must be an integer. ');
677 $w->constraint(Range => qw/rating/)->min(1)->max(5)
678 ->message('Must be a number between 1 and 5. ');
679 $w->constraint(Length => qw/title/)->min(5)->max(50)
680 ->message('Must be between 5 and 50 characters. ');
683 for my $column (qw/title rating authors/) {
684 $w->filter( HTMLEscape => $column );
685 $w->filter( TrimEdges => $column );
692 The two new lines are marked with C<***New:>.
695 =head2 Update the TT Template
697 Open C<root/src/books/hw_form.tt2> and edit it to match:
699 [% META title = 'Create/Update Book' %]
701 [%# Comment out the auto-rendered form %]
702 [%# widget_result.as_xml %]
705 [%# Iterate over the form elements and display each -%]
706 <form name="book_form" action="[% widget_result.action %]" method="post">
708 [% FOREACH element = widget_result.elements %]
710 <td class="form-label">
711 [% element.label.as_text %]
713 <td class="form-element">
714 [% element.element_xml %]
715 <span class="form-error">
716 [% element.error_xml %]
725 <p><a href="[% Catalyst.uri_for('list') %]">Return to book list</a></p>
728 [%# A little JavaScript to move the cursor to the first field %]
729 <script LANGUAGE="JavaScript">
730 document.book_form.book_form_title.focus();
733 This represents three changes:
739 The existing C<widget_result.as_xml> has been commented out.
743 It loops through each form element, displaying the field name in the
744 first table cell along with the form element and validation errors in
749 JavaScript to position the user's cursor in the first field of the form.
754 =head2 Try Out the Form
756 Press C<Ctrl-C> to kill the previous server instance (if it's still
757 running) and restart it:
759 $ script/myapp_server.pl
761 Try adding a book that validates. Return to the book list and the book
762 you added should be visible.
767 Kennedy Clark, C<hkclark@gmail.com>
769 Please report any errors, issues or suggestions to the author. The
770 most recent version of the Catalyst Tutorial can be found at
771 L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/Catalyst-Manual/lib/Catalyst/Manual/Tutorial/>.
773 Copyright 2006, Kennedy Clark, under Creative Commons License
774 (L<http://creativecommons.org/licenses/by-nc-sa/2.5/>).