X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FCatalyst%2FManual%2FTutorial%2FBasicCRUD.pod;h=d636da4039dae865799e2f397373cb001d1004ef;hb=4e1c81079691f91c7ab2c3b65ba0d981af660e04;hp=72be8e15587ef8c513cf3c64ee77c5cf25e0cfa5;hpb=7edc54841a8a242568f86b548bf05ebe2400de80;p=catagits%2FCatalyst-Manual.git diff --git a/lib/Catalyst/Manual/Tutorial/BasicCRUD.pod b/lib/Catalyst/Manual/Tutorial/BasicCRUD.pod index 72be8e1..d636da4 100644 --- a/lib/Catalyst/Manual/Tutorial/BasicCRUD.pod +++ b/lib/Catalyst/Manual/Tutorial/BasicCRUD.pod @@ -1,4 +1,4 @@ -=head1 NAME + =head1 NAME Catalyst::Manual::Tutorial::BasicCRUD - Catalyst Tutorial - Part 4: Basic CRUD @@ -209,7 +209,7 @@ Next, use your browser to enter the following URL: 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: @@ -228,15 +228,232 @@ 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 page). -Then I 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 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 in C 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" to start a chain> + +=item * + +Get arguments through C + +=item * + +Specify the path to match with C + +=back + + +=item * + +Middle + +=over 4 + +=item * + +Link to previous part of the chain with C<:Chained('_name_')> + +=item * + +Get arguments through C + +=item * + +Specify the path to match with C + +=back + + +=item * + +End + +=over 4 + +=item * + +Link to previous part of the chain with C<:Chained('_name_')> + +=item * -You should be able to click "Return to list" and now see 3 copies of -"TCP_Illustrated_Vol-2". +B," use "C" instead to end a chain> + +=item * + +Specify the path to match with C + +=back + + +=back + +In our C 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, +L, +and the 2006 advent calendar entry on the subject: +L. + + +=head2 Try the Chained Action + +If you look back at the development server startup logs from your +initial version of the C 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 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, 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 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) = @_; + + # 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 ***'); + } + +Here we print a log message and store the resultset in +C<$c-Estash-E{resultset}> so that it's automatically available +for other actions that chain off C. 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 +we are working with now), we will instead add that functionality +to a common C action shortly. + +As for C, let's modify it to first dispatch to C. +Open up C and edit the declaration for +C 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 method. Click the "Return to list" link, you should find +that there are now eight books shown. =head1 MANUALLY BUILDING A CREATE FORM @@ -257,7 +474,7 @@ Edit C and add the following method: =cut - sub form_create : Local { + sub form_create :Chained('base') :PathPart('form_create') :Args(0) { my ($self, $c) = @_; # Set the TT template to use @@ -297,7 +514,7 @@ save the form information to the database: =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 @@ -332,6 +549,21 @@ it. Then restart the server: $ 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 and enter "TCP/IP Illustrated, Vol 3" for the title, a rating of 5, and an author ID of 4. You should then see the output of the same @@ -388,15 +620,87 @@ and 2) the four lines for the Delete link near the bottom). [% # Add a link to delete a book %] - Delete + Delete [% END -%] -The additional code is obviously designed to add a new column to the -right side of the table with a C "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 "button" (for simplicity, +links will be used instead of full HTML buttons). + +Also notice that we are using a more advanced form of C than +we have seen before. Here we use C<$c-Econtroller-Eaction_for> +to automatically generate a URI appropriate for that action while +inserting the C 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. + +B 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 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 +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 doesn't mean that we can't +create another location to centralize that logic. In our case, we will +create a method called C 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 that don't +operate on an existing book can chain directly off base. + +To add the C method, edit C +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 will automatically +have the appropriate book waiting for it in +C<$c-Estash-Egt>{object}>. + +Also note that we are using different technique for setting +C<$c-Estash>. 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-Estash(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 @@ -410,12 +714,13 @@ following method: =cut - sub delete : Local { + 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."; @@ -424,12 +729,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 table. Note that C was used instead of -C: whereas C also removes the join table entries in -C, C does not (only use C if you -really need the cascading deletes... otherwise you are wasting resources). +This method first deletes the book object saved by the C method. +However, it also removes the corresponding entry from the +C table. Note that C will cascade to also delete +the related join table entries in C. Then, rather than forwarding to a "delete done" page as we did with the earlier create example, it simply sets the C to display a @@ -447,12 +750,35 @@ equivalent. If the application is still running from before, use C to kill it. Then restart the server: - $ script/myapp_server.pl + $ DBIC_TRACE=1 script/myapp_server.pl + +The C method now appears in the "Loaded Chained actions" section +of the startup debug output: + + [debug] Loaded Chained actions: + .-------------------------------------+--------------------------------------. + | 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 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 @@ -477,47 +803,50 @@ To convert the forward used in the previous section to a redirect, open C and edit the existing C method to match: - =head2 delete + =head2 delete Delete a book - + =cut - sub delete : Local { + 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')); + $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 and delete the first copy of -"TCPIP_Illustrated_Vol-2". The URL in your browser should return to -the L URL, so that is an -improvement, but notice that I. Because the stash is reset on every request (and a -redirect involves a second request), the C is cleared -before it can be displayed. +L (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 URL, so that is an +improvement, but notice that I. Because the stash is reset on every request (and a redirect +involves a second request), the C is cleared before it can +be displayed. =head2 Using C to Pass Query Parameters -There are several ways to pass information across a redirect. -In general, the best option is to use the C 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 and update the existing -C method to match the following: +There are several ways to pass information across a redirect. One +option is to use the C 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 and update the existing C +method to match the following: =head2 delete @@ -525,21 +854,22 @@ C method to match the following: =cut - sub delete : Local { + 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."})); } This modification simply leverages the ability of C to include an arbitrary number of name/value pairs in a hash reference. Next, we -need to update C to handle C as a +need to update C to handle C as a query parameter: ... @@ -561,18 +891,24 @@ Cspan class="message"E> line. =head2 Try the Delete and Redirect With Query Param Logic Restart the development server and point your browser to -L. Then delete the remaining copy -of "TCPIP_Illustrated_Vol-2". The green "Book deleted" status message +L (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 Although this did present an opportunity to show a handy -capability of C, it would be much better to use Catalyst's -C 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, -this message returns to its rightful place as a service-side -mechanism (we will migrate this code to C in the next part -of the tutorial). +B Another popular method for maintaining server-side +information across a redirect is to use the C technique we +discuss in the next part of the tutorial, +L. While +C is a "slicker" mechanism in that it's all handled by the +server and doesn't "pollute" your URLs, B 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 @@ -585,4 +921,3 @@ L). -