Changed reference to "user base" into "use parent" to match code
[catagits/Catalyst-Manual.git] / lib / Catalyst / Manual / Tutorial / BasicCRUD.pod
index f2565f2..b6afd45 100644 (file)
@@ -1,4 +1,4 @@
-=head1 NAME
+ =head1 NAME
 
 Catalyst::Manual::Tutorial::BasicCRUD - Catalyst Tutorial - Part 4: Basic CRUD
 
@@ -390,7 +390,7 @@ 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
+=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 @@ method:
        
        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.
-
-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:
+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.
+
+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 @@ and 2) the four lines for the Delete link near the bottom).
         </td>
         <td>
           [% # Add a link to delete a book %]
-          <a href="[% c.uri_for('delete', book.id) %]">Delete</a>
+          <a href="[% c.uri_for(c.controller.action_for('delete'), [book.id]) %]">Delete</a>
         </td>
       </tr>
     [% END -%]
@@ -622,6 +630,33 @@ 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).
 
+Also notice that we are using a more advanced form of C<uri_for> than 
+we have seen before.  Here we use C<$c-E<gt>controller-
+E<gt>action_for> to automatically generate a URI appropriate for that 
+action based on the method we want to link to while inserting the 
+C<book.id> value into the appropriate place.  Now, if you ever change 
+C<:PathPart('delete')> in your controller method to 
+C<:PathPart('kill')>, then your links will automatically update 
+without any changes to your .tt2 template file.  As long as the name 
+of your method does not changed ("delete" here), then your links will 
+still be correct.  There are a few shortcuts and options when using
+C<action_for()>:
+
+=over 4
+
+=item *
+
+If you are referring to a method in the current controller, you can
+use C<$self-E<gt>action_for('_method_name_')>.
+
+=item *
+
+If you are referring to a method in a different controller, you need
+to include that as an argument to C<controller()>, as in
+C<$c-E<gt>controller('_controller_name_')-E<gt>action_for('_method_name_')>.
+
+=back
+
 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 
@@ -631,6 +666,62 @@ operations that change the state of your application (e.g., the
 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:
+
+    =head2 object
+    
+    Fetch the specified book object based on the book ID and store
+    it in the stash
+    
+    =cut
+    
+    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 +733,13 @@ following method:
         
     =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 +748,10 @@ following method:
         $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,29 +769,35 @@ equivalent.
 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
@@ -726,37 +822,41 @@ To convert the forward used in the previous section to a redirect,
 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.";
     
-        # Redirect the user back to the list page
-        $c->response->redirect($c->uri_for('/books/list'));
+        # Redirect the user back to the list page.  Note the use
+        # of $self->action_for as earlier in this section (BasicCRUD)
+        $c->response->redirect($c->uri_for($self->action_for('list'));
     }
 
 
 =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,15 +874,16 @@ method to match the following:
         
     =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', 
+        $c->response->redirect($c->uri_for($self->action_for('list'), 
             {status_msg => "Book deleted."}));
     }
 
@@ -810,8 +911,9 @@ C<E<lt>span class="message"E<gt>> line.
 =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 +924,10 @@ 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 
+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.