[Catalyst-commits] r9387 -
Catalyst-Manual/5.70/trunk/lib/Catalyst/Manual/Tutorial
hkclark at dev.catalyst.perl.org
hkclark at dev.catalyst.perl.org
Mon Feb 23 22:07:30 GMT 2009
Author: hkclark
Date: 2009-02-23 22:07:30 +0000 (Mon, 23 Feb 2009)
New Revision: 9387
Modified:
Catalyst-Manual/5.70/trunk/lib/Catalyst/Manual/Tutorial/BasicCRUD.pod
Log:
Misc updates to factor out find()'ing an object into a common 'object' method and store the resultset inside 'base'
Modified: Catalyst-Manual/5.70/trunk/lib/Catalyst/Manual/Tutorial/BasicCRUD.pod
===================================================================
--- Catalyst-Manual/5.70/trunk/lib/Catalyst/Manual/Tutorial/BasicCRUD.pod 2009-02-23 19:13:29 UTC (rev 9386)
+++ Catalyst-Manual/5.70/trunk/lib/Catalyst/Manual/Tutorial/BasicCRUD.pod 2009-02-23 22:07:30 UTC (rev 9387)
@@ -390,7 +390,7 @@
are now seven books shown (two copies of TCPIP_Illustrated_Vol-2).
-=head2 Refactor to Use a "Base" Method to Start The Chains
+=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
@@ -405,20 +405,28 @@
sub base :Chained('/') :PathPart('books') :CaptureArgs(0) {
my ($self, $c) = @_;
+
+ # Store the resultset in stash so it's available for other methods
+ $c->stash->{resultset} = $c->model('DB::Books');
+ # Print a message to the debug log
$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.
+Here we print a log message and store the resultset in
+C<$c-E<gt>stash-E<gt>{resultset}> so that it's automatically available
+for other actions that chain off C<base>. 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. Because we have several
+actions that don't need to retrieve a book (such as the C<url_create>
+we are working with now), we will instead add that functionality
+to a common C<object> action shortly.
-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:
+As for C<url_create>, let's modify it to first dispatch to 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) {
@@ -612,7 +620,7 @@
</td>
<td>
[% # Add a link to delete a book %]
- <a href="[% c.uri_for('delete', book.id) %]">Delete</a>
+ <a href="[% c.uri_for('id', book.id, 'delete') %]">Delete</a>
</td>
</tr>
[% END -%]
@@ -631,6 +639,55 @@
database).
+=head2 Add a Common Method to Retrieve a Book for the Chain
+
+As mentioned earlier, since we have a mixture of actions that operate on
+a single book ID and others that do no, we should not have C<base>
+capture the book ID, find the corresponding book in the database and
+save it in the stash for later links in the chain. However, just
+because that logic does not belong in C<base> doesn't mean that we can't
+create another location to centralize that logic. In our case, we will
+create a method called C<object> that will store the specific book in
+the stash. Chains that always operate on a single existing book can
+chain off this method, but methods such as C<url_create> that don't
+operate on an existing book can chain directly off base.
+
+To add the C<object> method, edit C<lib/MyApp/Controller/Books.pm>
+and add the following code:
+
+ sub object :Chained('base') :PathPart('id') :CaptureArgs(1) {
+ my ($self, $c, $id) = @_;
+
+ # Find the book object and store it in the stash
+ $c->stash(object => $c->stash->{resultset}->find($id));
+
+ # Make sure the lookup was successful. You would probably
+ # want to do something like this in a real app:
+ # $c->detach('/error_404') if !$c->stash->{object};
+ die "Book $id not found!" if !$c->stash->{object};
+ }
+
+Now, any other method that chains off C<object> will automatically
+have the appropriate book waiting for it in
+C<$c-E<gt>stash-Egt>{object}>.
+
+Also note that we are using different technique for setting
+C<$c-E<gt>stash>. The advantage of this style is that it let's you
+set multiple stash variables at a time. For example:
+
+ $c->stash(object => $c->stash->{resultset}->find($id),
+ another_thing => 1);
+
+or as a hashref:
+
+ $c->stash({object => $c->stash->{resultset}->find($id),
+ another_thing => 1});
+
+Either format works, but the C<$c-E<gt>stash(name => value);>
+style is growing in popularity -- you may which to use it all
+the time (even when you are only setting a single value).
+
+
=head2 Add a Delete Action to the Controller
Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
@@ -642,12 +699,13 @@
=cut
- sub delete :Chained('base') :PathPart('delete') :Args(1) {
+ sub delete :Chained('object') :PathPart('delete') :Args(0) {
# $id = primary key of book to delete
- my ($self, $c, $id) = @_;
+ my ($self, $c) = @_;
- # Search for the book and then delete it
- $c->model('DB::Books')->search({id => $id})->delete_all;
+ # Use the book object saved by 'object' and delete it along
+ # with related 'book_authors' entries
+ $c->stash->{object}->delete;
# Set a status message to be displayed at the top of the view
$c->stash->{status_msg} = "Book deleted.";
@@ -656,12 +714,10 @@
$c->forward('list');
}
-This method first deletes the book with the specified primary key ID.
-However, it also removes the corresponding entry from the
-C<book_authors> table. Note that C<delete_all> was used instead of
-C<delete>: whereas C<delete_all> also removes the join table entries in
-C<book_authors>, C<delete> does not (only use C<delete_all> if you
-really need the cascading deletes... otherwise you are wasting resources).
+This method first deletes the book object saved by the C<object> method.
+However, it also removes the corresponding entry from the
+C<book_authors> table. Note that C<delete> will cascade to also delete
+the related join table entries in C<book_authors>.
Then, rather than forwarding to a "delete done" page as we did with the
earlier create example, it simply sets the C<status_msg> to display a
@@ -679,31 +735,37 @@
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
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 |
- '-------------------------------------+--------------------------------------'
+ .-------------------------------------+--------------------------------------.
+ | Path Spec | Private |
+ +-------------------------------------+--------------------------------------+
+ | /books/id/*/delete | /books/base (0) |
+ | | -> /books/object (1) |
+ | | => /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,
-along with a list of the eight remaining books.
+along with a list of the eight remaining books. You will also see the
+cascading delete operation via the DBIC_TRACE output:
+ DELETE FROM books WHERE ( id = ? ): '6'
+ SELECT me.book_id, me.author_id FROM book_authors me WHERE ( me.book_id = ? ): '6'
+ DELETE FROM book_authors WHERE ( author_id = ? AND book_id = ? ): '4', '6'
+
=head2 Fixing a Dangerous URL
Note the URL in your browser once you have performed the deletion in the
@@ -726,18 +788,19 @@
open C<lib/MyApp/Controller/Books.pm> and edit the existing
C<sub delete> method to match:
- =head2 delete
+ =head2 delete
Delete a book
-
+
=cut
- sub delete :Chained('base') :PathPart('delete') :Args(1) {
+ sub delete :Chained('object') :PathPart('delete') :Args(0) {
# $id = primary key of book to delete
my ($self, $c, $id) = @_;
- # Search for the book and then delete it
- $c->model('DB::Books')->search({id => $id})->delete_all;
+ # Use the book object saved by 'object' and delete it along
+ # with related 'book_authors' entries
+ $c->stash->{object}->delete;
# Set a status message to be displayed at the top of the view
$c->stash->{status_msg} = "Book deleted.";
@@ -750,13 +813,15 @@
=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 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.
+L<http://localhost:3000/books/list> (don't just hit "Refresh" in your
+browser since we left the URL in an invalid state in the previous
+section!) 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
@@ -774,12 +839,13 @@
=cut
- sub delete :Chained('base') :PathPart('delete') :Args(1) {
+ sub delete :Chained('object') :PathPart('delete') :Args(0) {
# $id = primary key of book to delete
my ($self, $c, $id) = @_;
- # Search for the book and then delete it
- $c->model('DB::Books')->search({id => $id})->delete_all;
+ # Use the book object saved by 'object' and delete it along
+ # with related 'book_authors' entries
+ $c->stash->{object}->delete;
# Redirect the user back to the list page with status msg as an arg
$c->response->redirect($c->uri_for('/books/list',
@@ -810,8 +876,9 @@
=head2 Try the Delete and Redirect With Query Param Logic
Restart the development server and point your browser to
-L<http://localhost:3000/books/list>. Then delete the remaining copy
-of "TCPIP_Illustrated_Vol-2". The green "Book deleted" status message
+L<http://localhost:3000/books/list> (you should now be able to safely
+hit "refresh" in your browser). Then delete the remaining copy of
+"TCPIP_Illustrated_Vol-2". The green "Book deleted" status message
should return.
B<NOTE:> Another popular method for maintaining server-side
@@ -822,10 +889,10 @@
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
+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
+that should really go to Window A. For this reason, you may wish
to use the "query param" technique shown here in your applications.
More information about the Catalyst-commits
mailing list