capabilities, including full Update functionality, will be addressed in
Part 9.
+Although this part of the tutorial will show you how to build CRUD
+functionality yourself, another option is to use a "CRUD builder" type
+of tool to automate the process. You get less control, but it's quick
+and easy. For example, see
+L<CatalystX::ListFramework::Builder|CatalystX::ListFramework::Builder>,
+L<CatalystX::CRUD|CatalystX::CRUD>, and
+L<CatalystX::CRUD::YUI|CatalystX::CRUD::YUI>.
+
You can checkout the source code for this example from the catalyst
subversion repository as per the instructions in
-L<Catalyst::Manual::Tutorial::Intro>
+L<Catalyst::Manual::Tutorial::Intro|Catalyst::Manual::Tutorial::Intro>.
=head1 FORMLESS SUBMISSION
[% # Provide a link back to the list page -%]
[% # 'uri_for()' builds a full URI; e.g., 'http://localhost:3000/books/list' -%]
- <p><a href="[% Catalyst.uri_for('/books/list') %]">Return to list</a></p>
+ <p><a href="[% c.uri_for('/books/list') %]">Return to list</a></p>
[% # Try out the TT Dumper (for development only!) -%]
<pre>
[% Dumper.dump(book) %]
</pre>
-The TT C<USE> directive allows access to a variety of plugin modules (TT
-plugins, that is, not Catalyst plugins) to add extra functionality to
-the base TT capabilities. Here, the plugin allows L<Data::Dumper>
-"pretty printing" of objects and variables. Other than that, the rest
-of the code should be familiar from the examples in Part 3.
+The TT C<USE> directive allows access to a variety of plugin modules
+(TT plugins, that is, not Catalyst plugins) to add extra functionality
+to the base TT capabilities. Here, the plugin allows
+L<Data::Dumper|Data::Dumper> "pretty printing" of objects and
+variables. Other than that, the rest of the code should be familiar
+from the examples in Part 3.
-B<IMPORTANT NOTE> As mentioned earlier, the C<MyApp::View::TT.pm> view
-class created by TTSite redefines the name used to access the Catalyst
-context object in TT templates from the usual C<c> to C<Catalyst>.
=head2 Try the C<url_create> Feature
If the application is still running from before, use C<Ctrl-C> to kill
it. Then restart the server:
- $ script/myapp_server.pl
+ $ DBIC_TRACE=1 script/myapp_server.pl
Note that new path for C</books/url_create> appears in the startup debug
output.
http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
-Your browser should display " Added book 'TCPIP_Illustrated_Vol-2' by
+Your browser should display "Added book 'TCPIP_Illustrated_Vol-2' by
'Stevens' with a rating of 5." along with a dump of the new book model
object. You should also see the following DBIC debug messages displayed
in the development server log messages if you have DBIC_TRACE set:
the existing record for Richard Stevens. The C<SELECT> statement results
from DBIC automatically fetching the book for the C<Dumper.dump(book)>.
-If you then click the "Return to list" link, you should find that there
-are now six books shown (if necessary, Shift-Reload your browser at the
-C</books/list> page).
+If you then click the "Return to list" link, you should find that
+there are now six books shown (if necessary, Shift+Reload or
+Ctrl+Reload your browser at the C</books/list> page).
-Then I<add 2 more copies of the same book> so that we have some extras for
-our delete logic that will be coming up soon. Enter the same URL above
-two more times (or refresh your browser twice if it still contains this
-URL):
- http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
+=head1 CONVERT TO A CHAINED ACTION
+
+Although the example above uses the same C<Local> action type for the
+method that we saw in the previous part of the tutorial, there is an
+alternate approach that allows us to be more specific while also
+paving the way for more advanced capabilities. Change the method
+declaration for C<url_create> in C<lib/MyApp/Controller/Books.pm> you
+entered above to match the following:
+
+ sub url_create :Chained('/') :PathPart('books/url_create') :Args(3) {
+
+This converts the method to take advantage of the Chained
+action/dispatch type. Chaining let's you have a single URL
+automatically dispatch to several controller methods, each of which
+can have precise control over the number of arguments that it will
+receive. A chain can essentially be thought of having three parts --
+a beginning, a middle and an end. The bullets below summarize the key
+points behind each of these parts of a chain:
+
+
+=over 4
+
+
+=item *
+
+Beginning
+
+=over 4
+
+=item *
+
+B<Use "C<:Chained('/')>" to start a chain>
+
+=item *
+
+Get arguments through C<CaptureArgs()>
+
+=item *
+
+Specify the path to match with C<PathPart()>
+
+=back
+
+
+=item *
+
+Middle
+
+=over 4
+
+=item *
+
+Link to previous part of the chain with C<:Chained('_name_')>
+
+=item *
+
+Get arguments through C<CaptureArgs()>
-You should be able to click "Return to list" and now see 3 copies of
-"TCP_Illustrated_Vol-2".
+=item *
+
+Specify the path to match with C<PathPart()>
+
+=back
+
+
+=item *
+
+End
+
+=over 4
+
+=item *
+
+Link to previous part of the chain with C<:Chained('_name_')>
+
+=item *
+
+B<Do NOT get arguments through "C<CaptureArgs()>," use "C<Args()>" instead to end a chain>
+
+=item *
+
+Specify the path to match with C<PathPart()>
+
+=back
+
+
+=back
+
+In our C<url_create> method above, we have combined all 3 parts into a
+single method: C<:Chained('/')> to start the chain,
+C<:PathPart('books/url_create')> to specify the base URL to match,
+along with C<:Args(3)> to capture exactly 3 arguments and also end the
+chain.
+
+As we will see shortly, a chain can consist of as many "links" as you
+wish, with each part capturing some arguments and doing some work
+along the way. We will continue to use the Chained action type in this
+part of the tutorial and explore slightly more advanced capabilities
+with the base method and delete feature below. But Chained dispatch
+is capable of far more. For additional information, see
+L<Catalyst::Manual::Intro/Action types>,
+L<Catalyst::DispatchType::Chained|Catalyst::DispatchType::Chained>,
+and the 2006 advent calendar entry on the subject:
+L<http://www.catalystframework.org/calendar/2006/10>.
+
+
+=head2 Try the Chained Action
+
+If you look back at the development server startup logs from your
+initial version of the C<url_create> method (the one using the
+C<:Local> attribute), you will notice that it produced output similar
+to the following:
+
+ [debug] Loaded Path actions:
+ .-------------------------------------+--------------------------------------.
+ | Path | Private |
+ +-------------------------------------+--------------------------------------+
+ | / | /default |
+ | / | /index |
+ | /books | /books/index |
+ | /books/list | /books/list |
+ | /books/url_create | /books/url_create |
+ '-------------------------------------+--------------------------------------'
+
+Now start the development server with our basic chained method in
+place and the startup debug output should change to something along
+the lines of the following:
+
+ [debug] Loaded Path actions:
+ .-------------------------------------+--------------------------------------.
+ | Path | Private |
+ +-------------------------------------+--------------------------------------+
+ | / | /default |
+ | / | /index |
+ | /books | /books/index |
+ | /books/list | /books/list |
+ '-------------------------------------+--------------------------------------'
+
+ [debug] Loaded Chained actions:
+ .-------------------------------------+--------------------------------------.
+ | Path Spec | Private |
+ +-------------------------------------+--------------------------------------+
+ | /books/url_create/*/*/* | /books/url_create |
+ '-------------------------------------+--------------------------------------'
+
+C<url_create> has disappeared form the "Loaded Path actions" section
+but it now shows up under the newly created "Loaded Chained actions"
+section. And, the "/*/*/*" portion clearly shows that we have
+specified that 3 arguments are required.
+
+As with our non-chained version of C<url_create>, use your browser to
+enter the following URL:
+
+ http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
+
+You should see the same "Added book 'TCPIP_Illustrated_Vol-2' by
+'Stevens' with a rating of 5." along with a dump of the new book model
+object. Click the "Return to list" link, you should find that there
+are now seven books shown (two copies of TCPIP_Illustrated_Vol-2).
+
+
+=head2 Refactor to Use a "Base" Method to Start The Chains
+
+Let's make a quick update to our initial Chained action to show a
+little more of the power of chaining. First, open
+C<lib/MyApp/Controller/Books.pm> in your editor and add the following
+method:
+
+ =head2 base
+
+ Can place common logic to start chained dispatch here
+
+ =cut
+
+ sub base :Chained('/') :PathPart('books') :CaptureArgs(0) {
+ my ($self, $c) = @_;
+
+ $c->log->debug('*** INSIDE BASE METHOD ***');
+ }
+
+Although we only use the C<base> method to create a log message, we
+could obviously do any number of things here. For example, if your
+controller always needs a book ID as it's first argument, you could
+have the base method capture that argument (with C<:CaptureArgs(1)>)
+and use it to pull the book object with that ID from the database and
+leave it in the stash for later parts of your chains to then act upon.
+
+In our case, let's modify our C<url_create> method to first call
+C<base>. Open up C<lib/MyApp/Controller/Books.pm> and edit the
+declaration for C<url_create> to match the following:
+
+ sub url_create :Chained('base') :PathPart('url_create') :Args(3) {
+
+Next, let's try out our refactored chain. Restart the development
+server and notice that our "Loaded Chained actions" section has
+changed slightly:
+
+ [debug] Loaded Chained actions:
+ .-------------------------------------+--------------------------------------.
+ | Path Spec | Private |
+ +-------------------------------------+--------------------------------------+
+ | /books/url_create/*/*/* | /books/base (0) |
+ | | => /books/url_create |
+ '-------------------------------------+--------------------------------------'
+
+The "Path Spec" is the same, but now it maps to two Private actions as
+we would expect.
+
+Once again, enter the following URL into your browser:
+
+ http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4
+
+The same "Added book 'TCPIP_Illustrated_Vol-2' by 'Stevens' with a
+rating of 5." and dump of the new book object should appear. Also
+notice the extra debug message in the development server output from
+the C<base> method. Click the "Return to list" link, you should find
+that there are now eight books shown.
=head1 MANUALLY BUILDING A CREATE FORM
=cut
- sub form_create : Local {
+ sub form_create :Chained('base') :PathPart('form_create') :Args(0) {
my ($self, $c) = @_;
# Set the TT template to use
This action simply invokes a view containing a book creation form.
+
=head2 Add a Template for the Form
Open C<root/src/books/form_create.tt2> in your editor and enter:
[% META title = 'Manual Form Book Create' -%]
- <form method="post" action="[% Catalyst.uri_for('form_create_do') %]">
+ <form method="post" action="[% c.uri_for('form_create_do') %]">
<table>
<tr><td>Title:</td><td><input type="text" name="title"></td></tr>
<tr><td>Rating:</td><td><input type="text" name="rating"></td></tr>
Note that we have specified the target of the form data as
C<form_create_do>, the method created in the section that follows.
+
=head2 Add a Method to Process Form Values and Update Database
Edit C<lib/MyApp/Controller/Books.pm> and add the following method to
=cut
- sub form_create_do : Local {
+ sub form_create_do :Chained('base') :PathPart('form_create_do') :Args(0) {
my ($self, $c) = @_;
# Retrieve the values from the form
$ script/myapp_server.pl
+Notice that the server startup log reflects the two new chained
+methods that we added:
+
+ [debug] Loaded Chained actions:
+ .-------------------------------------+--------------------------------------.
+ | Path Spec | Private |
+ +-------------------------------------+--------------------------------------+
+ | /books/form_create | /books/base (0) |
+ | | => /books/form_create |
+ | /books/form_create_do | /books/base (0) |
+ | | => /books/form_create_do |
+ | /books/url_create/*/*/* | /books/base (0) |
+ | | => /books/url_create |
+ '-------------------------------------+--------------------------------------'
+
Point your browser to L<http://localhost:3000/books/form_create> and
enter "TCP/IP Illustrated, Vol 3" for the title, a rating of 5, and an
-author ID of 4. You should then be forwarded to the same
+author ID of 4. You should then see the output of the same
C<create_done.tt2> template seen in earlier examples. Finally, click
"Return to list" to view the full list of books.
</td>
<td>
[% # Add a link to delete a book %]
- <a href="[% Catalyst.uri_for('delete/') _ book.id %]">Delete</a>
+ <a href="[% c.uri_for('delete', book.id) %]">Delete</a>
</td>
</tr>
[% END -%]
</table>
-The additional code is obviously designed to add a new column to the
-right side of the table with a C<Delete> "button" (for simplicity, links
-will be used instead of full HTML buttons).
+The additional code is obviously designed to add a new column to the
+right side of the table with a C<Delete> "button" (for simplicity,
+links will be used instead of full HTML buttons).
+
+B<Note:> You should use more than just a simple link with your
+applications. Consider using some sort of of confirmation page
+(typically with unique actions in your controller for both the
+confirmation and the actual delete operation). Also, you should try
+to use an HTTP POST operation (versus the GET used here) for
+operations that change the state of your application (e.g., the
+database).
+
=head2 Add a Delete Action to the Controller
Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
following method:
- =head2 delete
+ =head2 delete
Delete a book
=cut
- sub delete : Local {
+ sub delete :Chained('base') :PathPart('delete') :Args(1) {
# $id = primary key of book to delete
my ($self, $c, $id) = @_;
$ script/myapp_server.pl
+The C<delete> method now appears in the "Loaded Chained actions" section
+of the startup debug output:
+
+ [debug] Loaded Chained actions:
+ .-------------------------------------+--------------------------------------.
+ | Path Spec | Private |
+ +-------------------------------------+--------------------------------------+
+ | /books/delete/* | /books/base (0) |
+ | | => /books/delete |
+ | /books/form_create | /books/base (0) |
+ | | => /books/form_create |
+ | /books/form_create_do | /books/base (0) |
+ | | => /books/form_create_do |
+ | /books/url_create/*/*/* | /books/base (0) |
+ | | => /books/url_create |
+ '-------------------------------------+--------------------------------------'
+
Then point your browser to L<http://localhost:3000/books/list> and click
the "Delete" link next to the first "TCPIP_Illustrated_Vol-2". A green
"Book deleted" status message should display at the top of the page,
=cut
- sub delete : Local {
+ sub delete :Chained('base') :PathPart('delete') :Args(1) {
# $id = primary key of book to delete
my ($self, $c, $id) = @_;
=head2 Try the Delete and Redirect Logic
Restart the development server and point your browser to
-L<http://localhost:3000/books/list> and delete the first copy of
-"TCPIP_Illustrated_Vol-2". The URL in your browser should return to
-the L<http://localhost:3000/books/list> URL, so that is an
-improvement, but notice that I<no green "Book deleted" status message
-is displayed>. Because the stash is reset on every request (and a
-redirect involves a second request), the C<status_msg> is cleared
-before it can be displayed.
+L<http://localhost:3000/books/list> and delete the first copy of the
+remaining two "TCPIP_Illustrated_Vol-2" books. The URL in your
+browser should return to the L<http://localhost:3000/books/list> URL,
+so that is an improvement, but notice that I<no green "Book deleted"
+status message is displayed>. Because the stash is reset on every
+request (and a redirect involves a second request), the C<status_msg>
+is cleared before it can be displayed.
=head2 Using C<uri_for> to Pass Query Parameters
-There are several ways to pass information across a redirect.
-In general, the best option is to use the C<flash> technique that we
-will see in Part 5 of the tutorial; however, here we will pass the
-information via query parameters on the redirect itself. Open
-C<lib/MyApp/Controller/Books.pm> and update the existing
-C<sub delete> method to match the following:
+There are several ways to pass information across a redirect. One
+option is to use the C<flash> technique that we will see in Part 5 of
+the tutorial; however, here we will pass the information via query
+parameters on the redirect itself. Open
+C<lib/MyApp/Controller/Books.pm> and update the existing C<sub delete>
+method to match the following:
=head2 delete
=cut
- sub delete : Local {
+ sub delete :Chained('base') :PathPart('delete') :Args(1) {
# $id = primary key of book to delete
my ($self, $c, $id) = @_;
This modification simply leverages the ability of C<uri_for> to include
an arbitrary number of name/value pairs in a hash reference. Next, we
-need to update C<root/lib/site/layout> to handle C<status_msg> as a
+need to update C<root/src/wrapper.tt2> to handle C<status_msg> as a
query parameter:
- <div id="header">[% PROCESS site/header %]</div>
-
+ ...
<div id="content">
- <span class="message">[% status_msg || Catalyst.request.params.status_msg %]</span>
- <span class="error">[% error_msg %]</span>
- [% content %]
- </div>
-
- <div id="footer">[% PROCESS site/footer %]</div>
+ [%# Status and error messages %]
+ <span class="message">[% status_msg || c.request.params.status_msg %]</span>
+ <span class="error">[% error_msg %]</span>
+ [%# This is where TT will stick all of your template's contents. -%]
+ [% content %]
+ </div><!-- end content -->
+ ...
+
+Although the sample above only shows the C<content> div, leave the
+rest of the file intact -- the only change we made to the C<wrapper.tt2>
+was to add "C<|| c.request.params.status_msg>" to the
+C<E<lt>span class="message"E<gt>> line.
=head2 Try the Delete and Redirect With Query Param Logic
of "TCPIP_Illustrated_Vol-2". The green "Book deleted" status message
should return.
-B<NOTE:> Although this did present an opportunity to show a handy
-capability of C<uri_for>, it would be much better to use Catalyst's
-C<flash> feature in this situation. Although the technique here is
-less dangerous than leaving the delete URL in the client's browser,
-we have still exposed the status message to the user. With C<flash>,
-this message returns to its rightful place as a service-side
-mechanism (we will migrate this code to C<flash> in the next part
-of the tutorial).
+B<NOTE:> Another popular method for maintaining server-side
+information across a redirect is to use the C<flash> technique we
+discuss in the next part of the tutorial,
+L<Authentication|Catalyst::Manual::Tutorial::Authentication>. While
+C<flash> is a "slicker" mechanism in that it's all handled by the
+server and doesn't "pollute" your URLs, B<it is important to note that
+C<flash> can lead to situations where the wrong information shows up
+in the wrong browser window if the user has multiple windows or
+browser tabs open.> (For example, Window A causes something to be
+placed in the stash, but before that window performs a redirect,
+Window B makes a request to the server and gets the status information
+that should really go to Window A.) For this reason, you may wish
+to use the "query param" technique shown here in your applications.
=head1 AUTHOR
Please report any errors, issues or suggestions to the author. The
most recent version of the Catalyst Tutorial can be found at
-L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/Catalyst-Manual/lib/Catalyst/Manual/Tutorial/>.
+L<http://dev.catalyst.perl.org/repos/Catalyst/Catalyst-Manual/5.70/trunk/lib/Catalyst/Manual/Tutorial/>.
Copyright 2006-2008, Kennedy Clark, under Creative Commons License
-(L<http://creativecommons.org/licenses/by-nc-sa/2.5/>).
-
+(L<http://creativecommons.org/licenses/by-sa/3.0/us/>).